Add static files - CSS and JS
Copied from app/static/: - dashboard.css (4.7KB) - dashboard.js (41KB) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 0 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
h1 { color: #ffffff; margin-bottom: 20px; font-size: 24px; }
|
||||||
|
h2 { color: #ffffff; margin: 4px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
h2:first-child { margin-top: 0; }
|
||||||
|
#topSection {
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
#topSection.fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0 10px 4px 10px;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
#topSection.fixed.hidden {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
#topSectionSpacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#topSectionSpacer.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#inputCard { margin: 10px 0; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
||||||
|
.grid-narrow { display: inline-grid; grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 115px; gap: 10px; margin-bottom: 0; }
|
||||||
|
.card {
|
||||||
|
background: #151515;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
.card.clickable:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #0af;
|
||||||
|
}
|
||||||
|
.card.clickable:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.card.clickable.active {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #0af;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 170, 255, 0.3);
|
||||||
|
}
|
||||||
|
.stat-value { font-weight: bold; color: #ffffff; text-align: center; }
|
||||||
|
.stat-label { color: #888; text-transform: uppercase; text-align: center; }
|
||||||
|
.stat-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #202020; color: #ffffff; }
|
||||||
|
.stat-row:last-child { border-bottom: none; }
|
||||||
|
.progress-bar {
|
||||||
|
background: #202020;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
background: linear-gradient(90deg, #00aa55, #00cc66);
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; color: #ffffff; }
|
||||||
|
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #202020; }
|
||||||
|
th { color: #ffffff; font-size: 11px; text-transform: uppercase; }
|
||||||
|
td { font-size: 13px; color: #ffffff; }
|
||||||
|
.type-rss { color: #f90; }
|
||||||
|
.type-atom { color: #09f; }
|
||||||
|
.type-unknown { color: #ffffff; }
|
||||||
|
.url {
|
||||||
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
.time { color: #ffffff; font-size: 12px; }
|
||||||
|
.updated { color: #ffffff; font-size: 11px; text-align: right; margin-top: 20px; }
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
#searchInput:focus { outline: none; border-color: #0af; }
|
||||||
|
#searchInput::placeholder { color: #555; }
|
||||||
|
.search-host { margin-bottom: 10px; }
|
||||||
|
.search-feed:hover { background: #1a1a1a; }
|
||||||
|
|
||||||
|
/* Command buttons */
|
||||||
|
.cmd-btn {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #0af;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.cmd-btn:hover {
|
||||||
|
background: #252525;
|
||||||
|
border-color: #0af;
|
||||||
|
}
|
||||||
|
.cmd-btn:active {
|
||||||
|
background: #0af;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visit link */
|
||||||
|
.visit-link:hover {
|
||||||
|
color: #0af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TLD Grid */
|
||||||
|
.domain-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.tld-section {
|
||||||
|
width: 135px;
|
||||||
|
background: #151515;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.tld-section:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #0af;
|
||||||
|
}
|
||||||
|
.tld-section.expanded {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #0af;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 170, 255, 0.3);
|
||||||
|
}
|
||||||
|
.tld-section .tld-header {
|
||||||
|
padding: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.tld-section .tld-name {
|
||||||
|
color: #0af;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.tld-section .tld-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* Expanded content shown in separate container */
|
||||||
|
#expandedTLDContent {
|
||||||
|
margin-top: 10px;
|
||||||
|
background: #151515;
|
||||||
|
border: 1px solid #0af;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
#expandedTLDContent .tld-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#expandedTLDContent .tld-toggle {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
#expandedTLDContent .tld-name {
|
||||||
|
color: #0af;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,874 @@
|
|||||||
|
function initDashboard() {
|
||||||
|
function commaFormat(n) {
|
||||||
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State
|
||||||
|
let infiniteScrollState = null;
|
||||||
|
let isLoadingMore = false;
|
||||||
|
let searchQuery = '';
|
||||||
|
let domainFilter = 'all'; // all, pass, skip, hold, dead
|
||||||
|
// Feed filter: multi-select with ALL as exclusion toggle
|
||||||
|
// When allSelected=true, selected items are EXCLUDED; when false, selected items are INCLUDED
|
||||||
|
let feedFilter = { allSelected: false, statuses: [], types: [] };
|
||||||
|
let currentOpenTLD = null; // Track which TLD is currently open
|
||||||
|
|
||||||
|
// Smart sticky header - scroll normally, show fixed on scroll up
|
||||||
|
let lastScrollY = 0;
|
||||||
|
const topSection = document.getElementById('topSection');
|
||||||
|
const spacer = document.getElementById('topSectionSpacer');
|
||||||
|
let headerHeight = topSection.offsetHeight;
|
||||||
|
let isFixed = false;
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const currentScrollY = window.scrollY;
|
||||||
|
|
||||||
|
// If at top, return to normal flow
|
||||||
|
if (currentScrollY <= 0) {
|
||||||
|
topSection.classList.remove('fixed', 'hidden');
|
||||||
|
spacer.classList.remove('active');
|
||||||
|
isFixed = false;
|
||||||
|
lastScrollY = currentScrollY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only activate fixed mode after scrolling past the header
|
||||||
|
if (currentScrollY > headerHeight) {
|
||||||
|
if (currentScrollY < lastScrollY) {
|
||||||
|
// Scrolling up - show fixed header
|
||||||
|
if (!isFixed) {
|
||||||
|
spacer.style.height = headerHeight + 'px';
|
||||||
|
spacer.classList.add('active');
|
||||||
|
topSection.classList.add('fixed');
|
||||||
|
// Start hidden, then show
|
||||||
|
topSection.classList.add('hidden');
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
topSection.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
isFixed = true;
|
||||||
|
} else {
|
||||||
|
topSection.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} else if (currentScrollY > lastScrollY && isFixed) {
|
||||||
|
// Scrolling down while fixed - hide it
|
||||||
|
topSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollY = currentScrollY;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
// Stat card click handler
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const card = e.target.closest('.card.clickable');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const filterType = card.dataset.filter;
|
||||||
|
const status = card.dataset.status;
|
||||||
|
const type = card.dataset.type;
|
||||||
|
|
||||||
|
if (filterType === 'domain') {
|
||||||
|
// Remove active from domain cards only
|
||||||
|
document.querySelectorAll('.card.clickable[data-filter="domain"]').forEach(c => c.classList.remove('active'));
|
||||||
|
card.classList.add('active');
|
||||||
|
domainFilter = status || 'all';
|
||||||
|
|
||||||
|
// Update placeholder
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
searchInput.placeholder = domainFilter === 'all' ? 'Search domains...' : `Showing ${domainFilter} domains...`;
|
||||||
|
|
||||||
|
// Reload TLD list with new filter
|
||||||
|
loadFeeds(searchQuery);
|
||||||
|
} else if (filterType === 'feed') {
|
||||||
|
const wasActive = card.classList.contains('active');
|
||||||
|
|
||||||
|
if (status === 'all') {
|
||||||
|
// ALL card toggles exclusion mode
|
||||||
|
if (wasActive) {
|
||||||
|
card.classList.remove('active');
|
||||||
|
feedFilter.allSelected = false;
|
||||||
|
} else {
|
||||||
|
card.classList.add('active');
|
||||||
|
feedFilter.allSelected = true;
|
||||||
|
}
|
||||||
|
} else if (status) {
|
||||||
|
// Status card (pass, skip, hold, dead) - multi-select
|
||||||
|
if (wasActive) {
|
||||||
|
card.classList.remove('active');
|
||||||
|
feedFilter.statuses = feedFilter.statuses.filter(s => s !== status);
|
||||||
|
} else {
|
||||||
|
card.classList.add('active');
|
||||||
|
feedFilter.statuses.push(status);
|
||||||
|
}
|
||||||
|
} else if (type) {
|
||||||
|
// Type card (rss, atom, json, unknown, empty) - multi-select
|
||||||
|
if (wasActive) {
|
||||||
|
card.classList.remove('active');
|
||||||
|
feedFilter.types = feedFilter.types.filter(t => t !== type);
|
||||||
|
} else {
|
||||||
|
card.classList.add('active');
|
||||||
|
feedFilter.types.push(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload TLD list with feed filter
|
||||||
|
loadFeeds(searchQuery);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh only expanded TLD sections with new domain filter
|
||||||
|
function refreshExpandedTLDs() {
|
||||||
|
const expandedContainer = document.getElementById('expandedTLDContent');
|
||||||
|
if (expandedContainer && expandedContainer.style.display !== 'none' && expandedContainer.dataset.tld) {
|
||||||
|
// Mark as needing reload and reload
|
||||||
|
expandedContainer.dataset.loaded = 'false';
|
||||||
|
loadTLDDomains(expandedContainer, searchQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply feed filter to currently visible feeds
|
||||||
|
function applyFeedFilter() {
|
||||||
|
document.querySelectorAll('.inline-feed-block').forEach(block => {
|
||||||
|
const feedStatus = block.dataset.status || 'hold';
|
||||||
|
const feedType = block.dataset.type || 'unknown';
|
||||||
|
|
||||||
|
let show = true;
|
||||||
|
if (feedFilter.status !== 'all' && feedStatus !== feedFilter.status) {
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
|
if (feedFilter.type && feedType !== feedFilter.type) {
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
block.style.display = show ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event delegation for domain-spacer clicks (toggle feeds)
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const spacer = e.target.closest('.domain-spacer');
|
||||||
|
if (spacer) {
|
||||||
|
const block = spacer.closest('.domain-block');
|
||||||
|
if (block) {
|
||||||
|
const feedsDiv = block.querySelector('.domain-feeds');
|
||||||
|
if (feedsDiv) {
|
||||||
|
const isVisible = feedsDiv.style.display !== 'none';
|
||||||
|
feedsDiv.style.display = isVisible ? 'none' : 'block';
|
||||||
|
if (!isVisible) {
|
||||||
|
feedsDiv.querySelectorAll('.inline-feed-block').forEach(feedBlock => {
|
||||||
|
const itemsDiv = feedBlock.querySelector('.feed-items');
|
||||||
|
if (itemsDiv && !itemsDiv.dataset.loaded) {
|
||||||
|
itemsDiv.dataset.loaded = 'true';
|
||||||
|
loadFeedItems(feedBlock.dataset.url, itemsDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event delegation for feed-url-toggle clicks (toggle feed info)
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const urlToggle = e.target.closest('.feed-url-toggle');
|
||||||
|
if (urlToggle) {
|
||||||
|
const feedBlock = urlToggle.closest('.inline-feed-block');
|
||||||
|
if (feedBlock) {
|
||||||
|
const infoDiv = feedBlock.querySelector('.feed-info');
|
||||||
|
if (infoDiv) {
|
||||||
|
const isVisible = infoDiv.style.display !== 'none';
|
||||||
|
infoDiv.style.display = isVisible ? 'none' : 'block';
|
||||||
|
if (!isVisible && !infoDiv.dataset.loaded) {
|
||||||
|
infoDiv.dataset.loaded = 'true';
|
||||||
|
loadFeedInfo(feedBlock.dataset.url, infoDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event delegation for feed-title-toggle and feed-filler-toggle clicks (toggle items)
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const titleToggle = e.target.closest('.feed-title-toggle');
|
||||||
|
const fillerToggle = e.target.closest('.feed-filler-toggle');
|
||||||
|
if (titleToggle || fillerToggle) {
|
||||||
|
const feedBlock = (titleToggle || fillerToggle).closest('.inline-feed-block');
|
||||||
|
if (feedBlock) {
|
||||||
|
const itemsDiv = feedBlock.querySelector('.feed-items');
|
||||||
|
if (itemsDiv) {
|
||||||
|
const isVisible = itemsDiv.style.display !== 'none';
|
||||||
|
itemsDiv.style.display = isVisible ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load feed info
|
||||||
|
async function loadFeedInfo(feedUrl, infoDiv) {
|
||||||
|
infoDiv.innerHTML = '<span style="color: #666;">Loading...</span>';
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/feedInfo?url=${encodeURIComponent(feedUrl)}`);
|
||||||
|
if (!resp.ok) throw new Error('Failed to load');
|
||||||
|
const f = await resp.json();
|
||||||
|
|
||||||
|
let html = '<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; color: #888;">';
|
||||||
|
const fields = [
|
||||||
|
['URL', f.url],
|
||||||
|
['Title', f.title],
|
||||||
|
['Description', f.description],
|
||||||
|
['Type', f.type],
|
||||||
|
['Language', f.language],
|
||||||
|
['Site URL', f.siteUrl],
|
||||||
|
['Status', f.status],
|
||||||
|
['Last Error', f.lastError],
|
||||||
|
['Item Count', f.itemCount],
|
||||||
|
['Oldest Item', f.oldestItemDate],
|
||||||
|
['Newest Item', f.newestItemDate],
|
||||||
|
['Discovered', f.discoveredAt],
|
||||||
|
['Last Checked', f.lastCheckedAt],
|
||||||
|
['Next Check', f.nextCheckAt],
|
||||||
|
['Publish Status', f.publishStatus],
|
||||||
|
['Publish Account', f.publishAccount],
|
||||||
|
];
|
||||||
|
fields.forEach(([label, value]) => {
|
||||||
|
if (value != null && value !== '' && value !== 0) {
|
||||||
|
html += `<span style="color: #666;">${escapeHtml(label)}:</span><span style="color: #fff;">${escapeHtml(String(value))}</span>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
infoDiv.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
infoDiv.innerHTML = `<span style="color: #f66;">Error: ${escapeHtml(err.message)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load feed items
|
||||||
|
async function loadFeedItems(feedUrl, itemsDiv) {
|
||||||
|
itemsDiv.innerHTML = '<span style="color: #666;">Loading...</span>';
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/feedItems?url=${encodeURIComponent(feedUrl)}&limit=50`);
|
||||||
|
if (!resp.ok) throw new Error('Failed to load');
|
||||||
|
const items = await resp.json();
|
||||||
|
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
// Just clear the items area, keep the feed visible
|
||||||
|
itemsDiv.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
items.forEach(item => {
|
||||||
|
const date = item.pub_date ? new Date(item.pub_date).toLocaleDateString() : '';
|
||||||
|
html += `<div style="padding: 2px 0; border-bottom: 1px solid #1a1a1a; overflow: hidden;">`;
|
||||||
|
html += `<div style="float: left; width: 6em; white-space: nowrap; margin-right: 6px; color: #666; text-align: right;">${escapeHtml(date)} </div>`;
|
||||||
|
if (item.link) {
|
||||||
|
html += `<a href="${escapeHtml(item.link)}" target="_blank" style="color: #0af; text-decoration: none;">${escapeHtml(item.title || item.link)}</a>`;
|
||||||
|
} else {
|
||||||
|
html += `<span style="color: #fff;">${escapeHtml(item.title || '(no title)')}</span>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
itemsDiv.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
itemsDiv.innerHTML = `<span style="color: #f66;">Error: ${escapeHtml(err.message)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
const statusConfig = {
|
||||||
|
hold: { color: '#f90', bg: '#330', border: '#550' },
|
||||||
|
skip: { color: '#f66', bg: '#400', border: '#600' },
|
||||||
|
pass: { color: '#0f0', bg: '#040', border: '#060' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render status buttons
|
||||||
|
function renderStatusBtns(currentStatus, type, id) {
|
||||||
|
const order = ['pass', 'hold', 'skip'];
|
||||||
|
let html = '<div class="status-btn-group" style="display: inline-flex; margin-right: 10px;">';
|
||||||
|
order.forEach((s, i) => {
|
||||||
|
const cfg = statusConfig[s];
|
||||||
|
const isActive = s === currentStatus;
|
||||||
|
const bg = isActive ? cfg.bg : '#1a1a1a';
|
||||||
|
const border = isActive ? cfg.border : '#333';
|
||||||
|
const color = isActive ? cfg.color : '#ccc';
|
||||||
|
html += `<button class="status-btn" data-type="${type}" data-id="${escapeHtml(id)}" data-status="${s}"
|
||||||
|
style="padding: 2px 6px; background: ${bg}; border: 1px solid ${border}; border-radius: 3px;
|
||||||
|
color: ${color}; cursor: pointer; margin-left: ${i > 0 ? '1px' : '0'};">${s}</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render TLD section header
|
||||||
|
function renderTLDHeader(tld) {
|
||||||
|
return `<div class="tld-section" data-tld="${escapeHtml(tld)}">
|
||||||
|
<div class="tld-header" style="display: flex; align-items: center; padding: 10px; background: #1a1a1a; border-bottom: 1px solid #333; cursor: pointer; user-select: none;">
|
||||||
|
<span style="color: #0af; font-weight: bold; font-size: 1.1em;">.${escapeHtml(tld)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tld-content" style="display: block;">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTLDFooter(tld) {
|
||||||
|
return `<div class="tld-footer" style="display: flex; align-items: center; justify-content: flex-start; padding: 6px 10px; background: #1a1a1a; border-top: 1px solid #333; cursor: pointer; user-select: none;">
|
||||||
|
<span style="color: #666; font-size: 0.9em;">▲ .${escapeHtml(tld)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTLDSection(container, tld) {
|
||||||
|
const tldContent = container.querySelector(`.tld-section[data-tld="${tld}"] .tld-content`);
|
||||||
|
if (tldContent) {
|
||||||
|
tldContent.insertAdjacentHTML('beforeend', renderTLDFooter(tld));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event delegation for TLD clicks (toggle section)
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const tldHeader = e.target.closest('.tld-header');
|
||||||
|
const tldFooter = e.target.closest('.tld-footer');
|
||||||
|
const expandedContainer = document.getElementById('expandedTLDContent');
|
||||||
|
|
||||||
|
// Handle clicks in expanded container header
|
||||||
|
if (tldHeader && tldHeader.closest('#expandedTLDContent')) {
|
||||||
|
// Close the expanded content
|
||||||
|
const currentSection = document.querySelector('.tld-section.expanded');
|
||||||
|
if (currentSection) {
|
||||||
|
currentSection.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
expandedContainer.style.display = 'none';
|
||||||
|
expandedContainer.innerHTML = '';
|
||||||
|
currentOpenTLD = null;
|
||||||
|
// Show TLD list again
|
||||||
|
const domainList = document.querySelector('.domain-list');
|
||||||
|
if (domainList) domainList.style.display = '';
|
||||||
|
updateStats(); // Revert to search or all stats
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clicks on TLD cards
|
||||||
|
if (tldHeader || tldFooter) {
|
||||||
|
const section = (tldHeader || tldFooter).closest('.tld-section');
|
||||||
|
if (section) {
|
||||||
|
const tld = section.dataset.tld;
|
||||||
|
const isExpanded = section.classList.contains('expanded');
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Closing this TLD
|
||||||
|
section.classList.remove('expanded');
|
||||||
|
expandedContainer.style.display = 'none';
|
||||||
|
expandedContainer.innerHTML = '';
|
||||||
|
currentOpenTLD = null;
|
||||||
|
// Show TLD list again
|
||||||
|
const domainList = document.querySelector('.domain-list');
|
||||||
|
if (domainList) domainList.style.display = '';
|
||||||
|
updateStats(); // Revert to search or all stats
|
||||||
|
} else {
|
||||||
|
// Close any other open TLD first
|
||||||
|
document.querySelectorAll('.tld-section.expanded').forEach(s => {
|
||||||
|
s.classList.remove('expanded');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opening this TLD
|
||||||
|
section.classList.add('expanded');
|
||||||
|
currentOpenTLD = tld;
|
||||||
|
// Hide TLD list
|
||||||
|
const domainList = document.querySelector('.domain-list');
|
||||||
|
if (domainList) domainList.style.display = 'none';
|
||||||
|
// Show TLD stats (filtered by search if active)
|
||||||
|
const currentSearch = document.getElementById('searchInput').value.trim();
|
||||||
|
updateStatsForTLD(tld, currentSearch);
|
||||||
|
|
||||||
|
// Set up expanded container with header
|
||||||
|
expandedContainer.innerHTML = `
|
||||||
|
<div class="tld-header">
|
||||||
|
<span class="tld-name">.${escapeHtml(tld)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tld-content">
|
||||||
|
<div class="tld-loading" style="padding: 10px; color: #666;">Loading...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
expandedContainer.style.display = 'block';
|
||||||
|
expandedContainer.dataset.tld = tld;
|
||||||
|
expandedContainer.dataset.loaded = 'false';
|
||||||
|
|
||||||
|
// Load domains
|
||||||
|
loadTLDDomains(expandedContainer, searchQuery);
|
||||||
|
|
||||||
|
// Scroll to expanded container
|
||||||
|
expandedContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update stats for a specific TLD (optionally filtered by search)
|
||||||
|
async function updateStatsForTLD(tld, search = '') {
|
||||||
|
try {
|
||||||
|
let url = `/api/tldStats?tld=${encodeURIComponent(tld)}`;
|
||||||
|
if (search) {
|
||||||
|
url += `&search=${encodeURIComponent(search)}`;
|
||||||
|
}
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const stats = await resp.json();
|
||||||
|
|
||||||
|
document.getElementById('totalDomains').textContent = commaFormat(stats.total_domains || 0);
|
||||||
|
document.getElementById('passDomains').textContent = commaFormat(stats.pass_domains || 0);
|
||||||
|
document.getElementById('skipDomains').textContent = commaFormat(stats.skip_domains || 0);
|
||||||
|
document.getElementById('holdDomains').textContent = commaFormat(stats.hold_domains || 0);
|
||||||
|
document.getElementById('deadDomains').textContent = commaFormat(stats.dead_domains || 0);
|
||||||
|
|
||||||
|
document.getElementById('totalFeeds').textContent = commaFormat(stats.total_feeds || 0);
|
||||||
|
document.getElementById('aliveFeeds').textContent = commaFormat(stats.alive_feeds || 0);
|
||||||
|
document.getElementById('publishFeeds').textContent = commaFormat(stats.publish_feeds || 0);
|
||||||
|
document.getElementById('skipFeeds').textContent = commaFormat(stats.skip_feeds || 0);
|
||||||
|
document.getElementById('holdFeeds').textContent = commaFormat(stats.hold_feeds || 0);
|
||||||
|
document.getElementById('deadFeeds').textContent = commaFormat(stats.dead_feeds || 0);
|
||||||
|
document.getElementById('emptyFeeds').textContent = commaFormat(stats.empty_feeds || 0);
|
||||||
|
document.getElementById('rssFeeds').textContent = commaFormat(stats.rss_feeds || 0);
|
||||||
|
document.getElementById('atomFeeds').textContent = commaFormat(stats.atom_feeds || 0);
|
||||||
|
document.getElementById('jsonFeeds').textContent = commaFormat(stats.json_feeds || 0);
|
||||||
|
document.getElementById('unknownFeeds').textContent = commaFormat(stats.unknown_feeds || 0);
|
||||||
|
|
||||||
|
document.getElementById('updatedAt').textContent = search ? `Search "${search}" in .${tld}` : `Stats for .${tld}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('TLD stats update failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats for search results
|
||||||
|
async function updateStatsForSearch(query) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/searchStats?search=${encodeURIComponent(query)}`);
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.error('Search stats failed:', resp.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stats = await resp.json();
|
||||||
|
|
||||||
|
document.getElementById('totalDomains').textContent = commaFormat(stats.total_domains || 0);
|
||||||
|
document.getElementById('passDomains').textContent = commaFormat(stats.pass_domains || 0);
|
||||||
|
document.getElementById('skipDomains').textContent = commaFormat(stats.skip_domains || 0);
|
||||||
|
document.getElementById('holdDomains').textContent = commaFormat(stats.hold_domains || 0);
|
||||||
|
document.getElementById('deadDomains').textContent = commaFormat(stats.dead_domains || 0);
|
||||||
|
|
||||||
|
document.getElementById('totalFeeds').textContent = commaFormat(stats.total_feeds || 0);
|
||||||
|
document.getElementById('aliveFeeds').textContent = commaFormat(stats.alive_feeds || 0);
|
||||||
|
document.getElementById('publishFeeds').textContent = commaFormat(stats.publish_feeds || 0);
|
||||||
|
document.getElementById('skipFeeds').textContent = commaFormat(stats.skip_feeds || 0);
|
||||||
|
document.getElementById('holdFeeds').textContent = commaFormat(stats.hold_feeds || 0);
|
||||||
|
document.getElementById('deadFeeds').textContent = commaFormat(stats.dead_feeds || 0);
|
||||||
|
document.getElementById('emptyFeeds').textContent = commaFormat(stats.empty_feeds || 0);
|
||||||
|
document.getElementById('rssFeeds').textContent = commaFormat(stats.rss_feeds || 0);
|
||||||
|
document.getElementById('atomFeeds').textContent = commaFormat(stats.atom_feeds || 0);
|
||||||
|
document.getElementById('jsonFeeds').textContent = commaFormat(stats.json_feeds || 0);
|
||||||
|
document.getElementById('unknownFeeds').textContent = commaFormat(stats.unknown_feeds || 0);
|
||||||
|
|
||||||
|
document.getElementById('updatedAt').textContent = `Search: "${query}"`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Search stats update failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render domain row with feeds
|
||||||
|
function renderDomainRow(d) {
|
||||||
|
const status = d.status || 'hold';
|
||||||
|
|
||||||
|
const fullDomain = d.tld ? d.host + '.' + d.tld : d.host;
|
||||||
|
let html = `<div class="domain-block" data-host="${escapeHtml(fullDomain)}" data-status="${status}">`;
|
||||||
|
html += `<div class="domain-row" style="display: flex; align-items: center; padding: 8px 10px; border-bottom: 1px solid #202020;">`;
|
||||||
|
html += renderStatusBtns(status, 'domain', fullDomain);
|
||||||
|
html += `<a class="domain-name" href="https://${escapeHtml(fullDomain)}" target="_blank" style="color: #0af; text-decoration: none;">${escapeHtml(fullDomain)}</a>`;
|
||||||
|
|
||||||
|
if (d.last_error) {
|
||||||
|
html += `<span class="domain-spacer" style="color: #f66; margin-left: 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;" title="${escapeHtml(d.last_error)}">${escapeHtml(d.last_error)}</span>`;
|
||||||
|
} else {
|
||||||
|
html += '<span class="domain-spacer" style="flex: 1; cursor: pointer;"> </span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Feeds (shown by default in this view)
|
||||||
|
if (d.feeds && d.feeds.length > 0) {
|
||||||
|
html += '<div class="domain-feeds" style="display: block; margin-left: 10px; border-left: 2px solid #333; padding-left: 6px;">';
|
||||||
|
d.feeds.forEach(f => {
|
||||||
|
const feedStatus = f.publish_status || 'hold';
|
||||||
|
const feedType = f.type || 'unknown';
|
||||||
|
html += `<div class="inline-feed-block" data-url="${escapeHtml(f.url)}" data-status="${feedStatus}" data-type="${feedType}">`;
|
||||||
|
html += `<div class="feed-row" style="display: flex; align-items: center; padding: 4px 0;">`;
|
||||||
|
|
||||||
|
html += `<span style="width: 48px; flex-shrink: 0; white-space: nowrap; margin-right: 6px; color: #666; text-align: center;">${escapeHtml(f.language || '')} </span>`;
|
||||||
|
html += renderStatusBtns(feedStatus, 'feed', f.url);
|
||||||
|
|
||||||
|
if (f.item_count > 0) {
|
||||||
|
html += `<span style="color: #888; width: 55px; margin-right: 6px; text-align: center;">${commaFormat(f.item_count)}</span>`;
|
||||||
|
} else {
|
||||||
|
html += `<span style="width: 55px; margin-right: 6px;"></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedPath = f.url;
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(f.url.startsWith('http') ? f.url : 'https://' + f.url);
|
||||||
|
feedPath = urlObj.pathname + urlObj.search;
|
||||||
|
} catch (e) {}
|
||||||
|
html += `<span class="feed-url-toggle" style="color: #0af; margin-right: 8px; white-space: nowrap; cursor: pointer;" title="Click to show feed info">${escapeHtml(feedPath)}</span>`;
|
||||||
|
|
||||||
|
if (f.title) {
|
||||||
|
html += `<span class="feed-title-toggle" style="color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;">${escapeHtml(f.title)}</span>`;
|
||||||
|
}
|
||||||
|
html += '<span class="feed-filler-toggle" style="flex: 1; cursor: pointer;"> </span>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="feed-info" style="display: none; padding: 6px 10px; margin-left: 10px; border-left: 2px solid #444; background: #0a0a0a;"></div>';
|
||||||
|
html += '<div class="feed-items" style="display: block; padding: 4px 10px; margin-left: 10px; border-left: 2px solid #333;"></div>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '<div style="height: 8px;"></div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach status button handlers
|
||||||
|
function attachStatusHandlers(container) {
|
||||||
|
container.querySelectorAll('.status-btn:not(.btn-handled)').forEach(btn => {
|
||||||
|
btn.classList.add('btn-handled');
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = btn.dataset.type;
|
||||||
|
const id = btn.dataset.id;
|
||||||
|
const newStatus = btn.dataset.status;
|
||||||
|
|
||||||
|
const endpoint = type === 'domain' ? '/api/setDomainStatus' : '/api/setPublishStatus';
|
||||||
|
const param = type === 'domain' ? 'host' : 'url';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${endpoint}?${param}=${encodeURIComponent(id)}&status=${newStatus}`);
|
||||||
|
if (resp.ok) {
|
||||||
|
const group = btn.closest('.status-btn-group');
|
||||||
|
group.querySelectorAll('.status-btn').forEach(b => {
|
||||||
|
const s = b.dataset.status;
|
||||||
|
const cfg = statusConfig[s];
|
||||||
|
const isActive = s === newStatus;
|
||||||
|
b.style.background = isActive ? cfg.bg : '#111';
|
||||||
|
b.style.borderColor = isActive ? cfg.border : '#333';
|
||||||
|
b.style.color = isActive ? cfg.color : '#444';
|
||||||
|
});
|
||||||
|
const block = btn.closest(type === 'domain' ? '.domain-block' : '.inline-feed-block');
|
||||||
|
if (block) block.dataset.status = newStatus;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Status update failed:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinite scroll
|
||||||
|
function setupInfiniteScroll(loadMoreFn) {
|
||||||
|
infiniteScrollState = { loadMore: loadMoreFn, ended: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInfiniteScroll() {
|
||||||
|
infiniteScrollState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkInfiniteScroll() {
|
||||||
|
if (!infiniteScrollState || infiniteScrollState.ended || isLoadingMore) return;
|
||||||
|
const scrollY = window.scrollY + window.innerHeight;
|
||||||
|
const docHeight = document.documentElement.scrollHeight;
|
||||||
|
if (scrollY > docHeight - 500) {
|
||||||
|
isLoadingMore = true;
|
||||||
|
await infiniteScrollState.loadMore();
|
||||||
|
isLoadingMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', checkInfiniteScroll);
|
||||||
|
|
||||||
|
// Load and display feeds with lazy-loading TLD sections
|
||||||
|
let tldObserver = null;
|
||||||
|
|
||||||
|
async function loadFeeds(query = '') {
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
output.innerHTML = '<div class="domain-list"></div><div id="expandedTLDContent" style="display: none;"></div><div id="infiniteLoader" style="text-align: center; padding: 10px; color: #666;">Loading TLDs...</div>';
|
||||||
|
|
||||||
|
// Disconnect previous observer if any
|
||||||
|
if (tldObserver) {
|
||||||
|
tldObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch TLDs with optional domain status filter, feed filter, and search
|
||||||
|
let tldsUrl = '/api/tlds';
|
||||||
|
const params = [];
|
||||||
|
if (domainFilter !== 'all') {
|
||||||
|
params.push(`status=${domainFilter}`);
|
||||||
|
}
|
||||||
|
// Add feed filter params if any are selected
|
||||||
|
if (feedFilter.allSelected || feedFilter.statuses.length > 0 || feedFilter.types.length > 0) {
|
||||||
|
if (feedFilter.allSelected) {
|
||||||
|
params.push('feedMode=exclude');
|
||||||
|
} else {
|
||||||
|
params.push('feedMode=include');
|
||||||
|
}
|
||||||
|
if (feedFilter.statuses.length > 0) {
|
||||||
|
params.push(`feedStatuses=${feedFilter.statuses.join(',')}`);
|
||||||
|
}
|
||||||
|
if (feedFilter.types.length > 0) {
|
||||||
|
params.push(`feedTypes=${feedFilter.types.join(',')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query) {
|
||||||
|
params.push(`search=${encodeURIComponent(query)}`);
|
||||||
|
}
|
||||||
|
if (params.length > 0) {
|
||||||
|
tldsUrl += '?' + params.join('&');
|
||||||
|
}
|
||||||
|
const tldsResp = await fetch(tldsUrl);
|
||||||
|
if (!tldsResp.ok) {
|
||||||
|
const errText = await tldsResp.text();
|
||||||
|
throw new Error(`HTTP ${tldsResp.status}: ${errText}`);
|
||||||
|
}
|
||||||
|
const tlds = await tldsResp.json();
|
||||||
|
|
||||||
|
if (!tlds || tlds.length === 0) {
|
||||||
|
// Update stats for empty results
|
||||||
|
if (query) {
|
||||||
|
await updateStatsForSearch(query);
|
||||||
|
} else {
|
||||||
|
await updateStats();
|
||||||
|
}
|
||||||
|
document.getElementById('infiniteLoader').textContent = query ? 'No matches found' : 'No feeds found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = output.querySelector('.domain-list');
|
||||||
|
|
||||||
|
// Render all TLD sections as card placeholders
|
||||||
|
tlds.forEach(t => {
|
||||||
|
const tld = t.tld || 'unknown';
|
||||||
|
container.insertAdjacentHTML('beforeend', `
|
||||||
|
<div class="tld-section" data-tld="${escapeHtml(tld)}" data-loaded="false">
|
||||||
|
<div class="tld-header">
|
||||||
|
<span class="tld-name">.${escapeHtml(tld)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tld-content" style="display: none;">
|
||||||
|
<div class="tld-loading" style="padding: 10px; color: #666;">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('infiniteLoader').textContent = '';
|
||||||
|
|
||||||
|
// Auto-expand if single TLD match, otherwise update stats for search/all
|
||||||
|
if (tlds.length === 1) {
|
||||||
|
const tld = tlds[0].tld;
|
||||||
|
const expandedContainer = document.getElementById('expandedTLDContent');
|
||||||
|
const section = output.querySelector('.tld-section');
|
||||||
|
|
||||||
|
if (section && expandedContainer) {
|
||||||
|
// Mark as expanded
|
||||||
|
section.classList.add('expanded');
|
||||||
|
currentOpenTLD = tld;
|
||||||
|
// Hide TLD list
|
||||||
|
const domainList = document.querySelector('.domain-list');
|
||||||
|
if (domainList) domainList.style.display = 'none';
|
||||||
|
|
||||||
|
// Set up expanded container
|
||||||
|
expandedContainer.innerHTML = `
|
||||||
|
<div class="tld-header">
|
||||||
|
<span class="tld-name">.${escapeHtml(tld)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tld-content">
|
||||||
|
<div class="tld-loading" style="padding: 10px; color: #666;">Loading...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
expandedContainer.style.display = 'block';
|
||||||
|
expandedContainer.dataset.tld = tld;
|
||||||
|
expandedContainer.dataset.loaded = 'false';
|
||||||
|
|
||||||
|
// Load domains
|
||||||
|
loadTLDDomains(expandedContainer, query);
|
||||||
|
|
||||||
|
// Show TLD stats (filtered by search if active)
|
||||||
|
await updateStatsForTLD(tld, query);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Multiple TLDs - show search or global stats
|
||||||
|
if (query) {
|
||||||
|
await updateStatsForSearch(query);
|
||||||
|
} else {
|
||||||
|
await updateStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('infiniteLoader').textContent = 'Error: ' + err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load domains for a specific TLD section
|
||||||
|
async function loadTLDDomains(section, query = '') {
|
||||||
|
const tld = section.dataset.tld;
|
||||||
|
section.dataset.loaded = 'loading';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = `/api/domains?tld=${encodeURIComponent(tld)}&limit=500`;
|
||||||
|
if (domainFilter !== 'all') {
|
||||||
|
url += `&status=${domainFilter}`;
|
||||||
|
}
|
||||||
|
if (query) {
|
||||||
|
url += `&search=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
// Apply feed filter if any feed cards are selected
|
||||||
|
if (feedFilter.allSelected || feedFilter.statuses.length > 0 || feedFilter.types.length > 0) {
|
||||||
|
if (feedFilter.allSelected) {
|
||||||
|
url += '&feedMode=exclude';
|
||||||
|
} else {
|
||||||
|
url += '&feedMode=include';
|
||||||
|
}
|
||||||
|
if (feedFilter.statuses.length > 0) {
|
||||||
|
url += `&feedStatuses=${feedFilter.statuses.join(',')}`;
|
||||||
|
}
|
||||||
|
if (feedFilter.types.length > 0) {
|
||||||
|
url += `&feedTypes=${feedFilter.types.join(',')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const domains = await resp.json();
|
||||||
|
|
||||||
|
const content = section.querySelector('.tld-content');
|
||||||
|
content.innerHTML = '';
|
||||||
|
|
||||||
|
if (!domains || domains.length === 0) {
|
||||||
|
content.innerHTML = '<div style="padding: 10px; color: #666;">No domains with feeds</div>';
|
||||||
|
} else {
|
||||||
|
domains.forEach(d => {
|
||||||
|
content.insertAdjacentHTML('beforeend', renderDomainRow(d));
|
||||||
|
});
|
||||||
|
// Add footer
|
||||||
|
content.insertAdjacentHTML('beforeend', renderTLDFooter(tld));
|
||||||
|
attachStatusHandlers(content);
|
||||||
|
|
||||||
|
// Load items for all feeds
|
||||||
|
content.querySelectorAll('.inline-feed-block').forEach(feedBlock => {
|
||||||
|
const itemsDiv = feedBlock.querySelector('.feed-items');
|
||||||
|
if (itemsDiv && !itemsDiv.dataset.loaded) {
|
||||||
|
itemsDiv.dataset.loaded = 'true';
|
||||||
|
loadFeedItems(feedBlock.dataset.url, itemsDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
section.dataset.loaded = 'true';
|
||||||
|
} catch (err) {
|
||||||
|
const content = section.querySelector('.tld-content');
|
||||||
|
content.innerHTML = `<div style="padding: 10px; color: #f66;">Error: ${escapeHtml(err.message)}</div>`;
|
||||||
|
section.dataset.loaded = 'false';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search handler
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
function doSearch() {
|
||||||
|
searchQuery = searchInput.value.trim();
|
||||||
|
loadFeeds(searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search on button click
|
||||||
|
document.getElementById('searchBtn').addEventListener('click', doSearch);
|
||||||
|
|
||||||
|
// Clear button - clears search and resets all filters
|
||||||
|
document.getElementById('clearBtn').addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchQuery = '';
|
||||||
|
// Reset filters to default
|
||||||
|
domainFilter = 'all';
|
||||||
|
feedFilter = { allSelected: false, statuses: [], types: [] };
|
||||||
|
// Reset active card styling
|
||||||
|
document.querySelectorAll('.card.clickable.active').forEach(c => c.classList.remove('active'));
|
||||||
|
document.querySelector('.card.clickable[data-filter="domain"][data-status="all"]')?.classList.add('active');
|
||||||
|
searchInput.placeholder = 'Search domains...';
|
||||||
|
// Close any expanded TLD
|
||||||
|
currentOpenTLD = null;
|
||||||
|
const expandedContainer = document.getElementById('expandedTLDContent');
|
||||||
|
if (expandedContainer) {
|
||||||
|
expandedContainer.style.display = 'none';
|
||||||
|
expandedContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
// Show TLD list if hidden
|
||||||
|
const domainList = document.querySelector('.domain-list');
|
||||||
|
if (domainList) domainList.style.display = '';
|
||||||
|
// Reload and update stats
|
||||||
|
loadFeeds();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search on Enter key
|
||||||
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
doSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load - set default active cards and load
|
||||||
|
document.querySelector('.card.clickable[data-filter="domain"][data-status="all"]')?.classList.add('active');
|
||||||
|
loadFeeds();
|
||||||
|
|
||||||
|
// Update stats periodically
|
||||||
|
async function updateStats() {
|
||||||
|
// Check actual input value for current search state
|
||||||
|
const currentSearch = document.getElementById('searchInput')?.value.trim() || '';
|
||||||
|
|
||||||
|
// Priority: open TLD > search query > all
|
||||||
|
if (currentOpenTLD) {
|
||||||
|
updateStatsForTLD(currentOpenTLD, currentSearch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSearch) {
|
||||||
|
updateStatsForSearch(currentSearch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/stats');
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const stats = await resp.json();
|
||||||
|
document.getElementById('totalDomains').textContent = commaFormat(stats.total_domains);
|
||||||
|
document.getElementById('holdDomains').textContent = commaFormat(stats.hold_domains);
|
||||||
|
document.getElementById('passDomains').textContent = commaFormat(stats.pass_domains);
|
||||||
|
document.getElementById('skipDomains').textContent = commaFormat(stats.skip_domains);
|
||||||
|
document.getElementById('deadDomains').textContent = commaFormat(stats.dead_domains);
|
||||||
|
document.getElementById('domainCheckRate').textContent = commaFormat(stats.domain_check_rate);
|
||||||
|
document.getElementById('feedCrawlRate').textContent = commaFormat(stats.feed_crawl_rate);
|
||||||
|
document.getElementById('feedCheckRate').textContent = commaFormat(stats.feed_check_rate);
|
||||||
|
document.getElementById('totalFeeds').textContent = commaFormat(stats.total_feeds);
|
||||||
|
document.getElementById('aliveFeeds').textContent = commaFormat(stats.alive_feeds);
|
||||||
|
document.getElementById('publishFeeds').textContent = commaFormat(stats.publish_feeds);
|
||||||
|
document.getElementById('skipFeeds').textContent = commaFormat(stats.skip_feeds);
|
||||||
|
document.getElementById('holdFeeds').textContent = commaFormat(stats.hold_feeds);
|
||||||
|
document.getElementById('deadFeeds').textContent = commaFormat(stats.dead_feeds);
|
||||||
|
document.getElementById('emptyFeeds').textContent = commaFormat(stats.empty_feeds);
|
||||||
|
document.getElementById('rssFeeds').textContent = commaFormat(stats.rss_feeds);
|
||||||
|
document.getElementById('atomFeeds').textContent = commaFormat(stats.atom_feeds);
|
||||||
|
document.getElementById('jsonFeeds').textContent = commaFormat(stats.json_feeds);
|
||||||
|
document.getElementById('unknownFeeds').textContent = commaFormat(stats.unknown_feeds);
|
||||||
|
document.getElementById('updatedAt').textContent = 'All TLDs - ' + new Date().toLocaleTimeString();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Stats update failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateStats, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initDashboard);
|
||||||
Reference in New Issue
Block a user