`;
@@ -192,45 +328,163 @@ function initDashboard() {
}
}
- // Event delegation for TLD header/footer clicks (toggle section)
+ // 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 content = section.querySelector('.tld-content');
- const toggle = section.querySelector('.tld-toggle');
- if (content) {
- const isVisible = content.style.display !== 'none';
- content.style.display = isVisible ? 'none' : 'block';
- if (toggle) toggle.textContent = isVisible ? '▶' : '▼';
+ const tld = section.dataset.tld;
+ const isExpanded = section.classList.contains('expanded');
- if (isVisible) {
- // Closing - scroll to next TLD section
- const nextSection = section.nextElementSibling;
- if (nextSection && nextSection.classList.contains('tld-section')) {
- nextSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- } else {
- // Opening - load domains if not already loaded
- if (section.dataset.loaded === 'false') {
- loadTLDDomains(section, searchQuery);
- }
- }
+ 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';
- let html = `
`;
+ const fullDomain = d.tld ? d.host + '.' + d.tld : d.host;
+ let html = `
`;
html += `
`;
- html += renderStatusBtns(status, 'domain', d.host);
- html += `
${escapeHtml(d.host)}`;
+ html += renderStatusBtns(status, 'domain', fullDomain);
+ html += `
${escapeHtml(fullDomain)}`;
if (d.last_error) {
html += `
${escapeHtml(d.last_error)}`;
@@ -244,7 +498,8 @@ function initDashboard() {
html += '
';
d.feeds.forEach(f => {
const feedStatus = f.publish_status || 'hold';
- html += `
`;
+ const feedType = f.type || 'unknown';
+ html += `
`;
html += `
`;
html += `
${escapeHtml(f.language || '')} `;
@@ -341,7 +596,7 @@ function initDashboard() {
async function loadFeeds(query = '') {
const output = document.getElementById('output');
- output.innerHTML = '
Loading TLDs...
';
+ output.innerHTML = '
Loading TLDs...
';
// Disconnect previous observer if any
if (tldObserver) {
@@ -349,26 +604,59 @@ function initDashboard() {
}
try {
- // Fetch all TLDs first
- const tldsResp = await fetch('/api/tlds?has_feeds=true');
+ // 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) {
- document.getElementById('infiniteLoader').textContent = 'No feeds found';
+ // 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 collapsed placeholders
+ // Render all TLD sections as card placeholders
tlds.forEach(t => {
const tld = t.tld || 'unknown';
container.insertAdjacentHTML('beforeend', `
-