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 = 'Loading...'; 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 = '
'; 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 += `${escapeHtml(label)}:${escapeHtml(String(value))}`; } }); html += '
'; infoDiv.innerHTML = html; } catch (err) { infoDiv.innerHTML = `Error: ${escapeHtml(err.message)}`; } } // Load feed items async function loadFeedItems(feedUrl, itemsDiv) { itemsDiv.innerHTML = 'Loading...'; 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 += `
`; html += `
${escapeHtml(date)} 
`; if (item.link) { html += `${escapeHtml(item.title || item.link)}`; } else { html += `${escapeHtml(item.title || '(no title)')}`; } html += '
'; }); itemsDiv.innerHTML = html; } catch (err) { itemsDiv.innerHTML = `Error: ${escapeHtml(err.message)}`; } } // 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 = '
'; 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 += ``; }); html += '
'; return html; } // Render TLD section header function renderTLDHeader(tld) { return `
.${escapeHtml(tld)}
`; } function renderTLDFooter(tld) { return ``; } 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 = `
.${escapeHtml(tld)}
Loading...
`; 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 = `
`; html += `
`; html += renderStatusBtns(status, 'domain', fullDomain); html += `${escapeHtml(fullDomain)}`; if (d.last_error) { html += `${escapeHtml(d.last_error)}`; } else { html += ' '; } html += '
'; // Feeds (shown by default in this view) if (d.feeds && d.feeds.length > 0) { html += '
'; d.feeds.forEach(f => { const feedStatus = f.publish_status || 'hold'; const feedType = f.type || 'unknown'; html += `
`; html += `
`; html += `${escapeHtml(f.language || '')} `; html += renderStatusBtns(feedStatus, 'feed', f.url); if (f.item_count > 0) { html += `${commaFormat(f.item_count)}`; } else { html += ``; } 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 += `${escapeHtml(feedPath)}`; if (f.title) { html += `${escapeHtml(f.title)}`; } html += ' '; html += '
'; html += ''; html += '
'; html += '
'; }); html += '
'; html += '
'; } html += '
'; 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 = '
Loading TLDs...
'; // 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', `
.${escapeHtml(tld)}
`); }); 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 = `
.${escapeHtml(tld)}
Loading...
`; 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 = '
No domains with feeds
'; } 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 = `
Error: ${escapeHtml(err.message)}
`; 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);