`;
}
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 = `
`;
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', `
`);
});
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 = `
`;
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);