`;
+ }
+
+ 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 header/footer clicks (toggle section)
+ document.addEventListener('click', (e) => {
+ const tldHeader = e.target.closest('.tld-header');
+ const tldFooter = e.target.closest('.tld-footer');
+ 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 ? '▶' : '▼';
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+ });
+
// Render domain row with feeds
function renderDomainRow(d) {
const status = d.status || 'hold';
- const hasError = !!d.last_error;
let html = `
`;
html += `
`;
- html += renderStatusBtns(status, 'domain', d.host, hasError ? 'error' : null);
+ html += renderStatusBtns(status, 'domain', d.host);
html += `
${escapeHtml(d.host)}`;
if (d.last_error) {
@@ -206,15 +247,11 @@ function initDashboard() {
html += `
`;
html += `
`;
- const lang = f.language || '';
- html += `
${escapeHtml(lang)}`;
- html += renderStatusBtns(feedStatus, 'feed', f.url, f.status);
-
- const statusColor = f.status === 'active' ? '#484' : f.status === 'error' ? '#a66' : '#666';
- html += `
${escapeHtml(f.status || 'active')}`;
+ html += `
${escapeHtml(f.language || '')} `;
+ html += renderStatusBtns(feedStatus, 'feed', f.url);
if (f.item_count > 0) {
- html += `
${commaFormat(f.item_count)}`;
+ html += `
${commaFormat(f.item_count)}`;
} else {
html += `
`;
}
@@ -235,6 +272,7 @@ function initDashboard() {
html += '
';
html += '
';
});
+ html += '
';
html += '
';
}
html += '
';
@@ -285,7 +323,7 @@ function initDashboard() {
infiniteScrollState = null;
}
- window.addEventListener('scroll', async () => {
+ async function checkInfiniteScroll() {
if (!infiniteScrollState || infiniteScrollState.ended || isLoadingMore) return;
const scrollY = window.scrollY + window.innerHeight;
const docHeight = document.documentElement.scrollHeight;
@@ -294,60 +332,119 @@ function initDashboard() {
await infiniteScrollState.loadMore();
isLoadingMore = false;
}
- });
+ }
+
+ window.addEventListener('scroll', checkInfiniteScroll);
+
+ // Load and display feeds with lazy-loading TLD sections
+ let tldObserver = null;
- // Load and display feeds
async function loadFeeds(query = '') {
const output = document.getElementById('output');
- output.innerHTML = '
Loading...
';
+ output.innerHTML = '
Loading TLDs...
';
- let offset = 0;
- const limit = 100;
+ // Disconnect previous observer if any
+ if (tldObserver) {
+ tldObserver.disconnect();
+ }
- async function loadMore() {
- try {
- let url = `/api/domains?limit=${limit}&offset=${offset}&sort=alpha&has_feeds=true`;
- if (query) {
- url += `&search=${encodeURIComponent(query)}`;
- }
+ try {
+ // Fetch all TLDs first
+ const tldsResp = await fetch('/api/tlds?has_feeds=true');
+ const tlds = await tldsResp.json();
- const resp = await fetch(url);
- const domains = await resp.json();
+ if (!tlds || tlds.length === 0) {
+ document.getElementById('infiniteLoader').textContent = 'No feeds found';
+ return;
+ }
- if (!domains || domains.length === 0) {
- if (infiniteScrollState) infiniteScrollState.ended = true;
- document.getElementById('infiniteLoader').textContent = offset === 0 ? 'No feeds found' : 'End of list';
- return;
- }
+ const container = output.querySelector('.domain-list');
- const container = output.querySelector('.domain-list');
- domains.forEach(d => {
- container.insertAdjacentHTML('beforeend', renderDomainRow(d));
+ // Render all TLD sections as collapsed placeholders
+ tlds.forEach(t => {
+ const tld = t.tld || 'unknown';
+ container.insertAdjacentHTML('beforeend', `
+
+ `);
+ });
+
+ document.getElementById('infiniteLoader').textContent = `${tlds.length} TLDs loaded`;
+
+ // Set up IntersectionObserver for lazy loading (loads even when collapsed)
+ tldObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const section = entry.target;
+ if (section.dataset.loaded === 'false') {
+ loadTLDDomains(section, query);
+ tldObserver.unobserve(section);
+ }
+ }
});
- attachStatusHandlers(container);
+ }, { rootMargin: '500px' });
+
+ // Observe all TLD sections
+ container.querySelectorAll('.tld-section').forEach(section => {
+ tldObserver.observe(section);
+ });
+
+ } 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?has_feeds=true&tld=${encodeURIComponent(tld)}&limit=500`;
+ if (query) {
+ url += `&search=${encodeURIComponent(query)}`;
+ }
+
+ const resp = await fetch(url);
+ 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
- container.querySelectorAll('.inline-feed-block').forEach(feedBlock => {
+ 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);
}
});
-
- offset += domains.length;
-
- if (domains.length < limit) {
- if (infiniteScrollState) infiniteScrollState.ended = true;
- document.getElementById('infiniteLoader').textContent = 'End of list';
- }
- } catch (err) {
- document.getElementById('infiniteLoader').textContent = 'Error: ' + err.message;
}
- }
- await loadMore();
- setupInfiniteScroll(loadMore);
+ section.dataset.loaded = 'true';
+ } catch (err) {
+ const content = section.querySelector('.tld-content');
+ content.innerHTML = `
Error: ${escapeHtml(err.message)}
`;
+ section.dataset.loaded = 'false';
+ }
}
// Search handler
@@ -357,7 +454,6 @@ function initDashboard() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchQuery = searchInput.value.trim();
- clearInfiniteScroll();
loadFeeds(searchQuery);
}, 300);
});
@@ -374,7 +470,6 @@ function initDashboard() {
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('failDomains').textContent = commaFormat(stats.fail_domains);
document.getElementById('crawlRate').textContent = commaFormat(stats.crawl_rate);
document.getElementById('checkRate').textContent = commaFormat(stats.check_rate);
document.getElementById('totalFeeds').textContent = commaFormat(stats.total_feeds);
diff --git a/templates.go b/templates.go
index b5023b4..87190cb 100644
--- a/templates.go
+++ b/templates.go
@@ -444,8 +444,9 @@ const dashboardHTML = `
1440.news Feed Crawler
+
-
+
1440.news Feed Crawler
@@ -468,10 +469,6 @@ const dashboardHTML = `
{{comma .SkipDomains}}
Skip