From a3d8f4ea8eb3927b8042b7d771171725ad26d266 Mon Sep 17 00:00:00 2001 From: primal Date: Fri, 30 Jan 2026 16:59:38 -0500 Subject: [PATCH] v53: add feed info and items panels with click toggles --- api_feeds.go | 18 ++++-- static/dashboard.js | 138 ++++++++++++++++++++++++++++++++++++++++++-- templates.go | 2 +- 3 files changed, 145 insertions(+), 13 deletions(-) diff --git a/api_feeds.go b/api_feeds.go index b97dfb6..770cd64 100644 --- a/api_feeds.go +++ b/api_feeds.go @@ -20,12 +20,14 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) { type FeedDetails struct { URL string `json:"url"` Type string `json:"type,omitempty"` + Category string `json:"category,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Language string `json:"language,omitempty"` SiteURL string `json:"siteUrl,omitempty"` DiscoveredAt string `json:"discoveredAt,omitempty"` LastCrawledAt string `json:"lastCrawledAt,omitempty"` + NextCrawlAt string `json:"nextCrawlAt,omitempty"` LastBuildDate string `json:"lastBuildDate,omitempty"` TTLMinutes int `json:"ttlMinutes,omitempty"` UpdatePeriod string `json:"updatePeriod,omitempty"` @@ -42,8 +44,8 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) { } var f FeedDetails - var title, description, language, siteUrl *string - var lastCrawledAt, lastBuildDate *time.Time + var category, title, description, language, siteUrl *string + var lastCrawledAt, nextCrawlAt, lastBuildDate *time.Time var updatePeriod, status, lastError *string var oldestItemDate, newestItemDate *time.Time var ttlMinutes, updateFreq, errorCount, itemCount *int @@ -52,16 +54,16 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) { var publishStatus, publishAccount *string err := c.db.QueryRow(` - SELECT url, type, title, description, language, site_url, - discovered_at, last_crawled_at, last_build_date, + SELECT url, type, category, title, description, language, site_url, + discovered_at, last_crawled_at, next_crawl_at, last_build_date, ttl_minutes, update_period, update_freq, status, error_count, last_error, item_count, avg_post_freq_hrs, oldest_item_date, newest_item_date, publish_status, publish_account FROM feeds WHERE url = $1 `, feedURL).Scan( - &f.URL, &f.Type, &title, &description, &language, &siteUrl, - &discoveredAt, &lastCrawledAt, &lastBuildDate, + &f.URL, &f.Type, &category, &title, &description, &language, &siteUrl, + &discoveredAt, &lastCrawledAt, &nextCrawlAt, &lastBuildDate, &ttlMinutes, &updatePeriod, &updateFreq, &status, &errorCount, &lastError, &itemCount, &avgPostFreqHrs, &oldestItemDate, &newestItemDate, @@ -77,6 +79,7 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) { return } + f.Category = StringValue(category) f.Title = StringValue(title) f.Description = StringValue(description) f.Language = StringValue(language) @@ -85,6 +88,9 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) { if lastCrawledAt != nil { f.LastCrawledAt = lastCrawledAt.Format(time.RFC3339) } + if nextCrawlAt != nil { + f.NextCrawlAt = nextCrawlAt.Format(time.RFC3339) + } if lastBuildDate != nil { f.LastBuildDate = lastBuildDate.Format(time.RFC3339) } diff --git a/static/dashboard.js b/static/dashboard.js index 9fce5d2..40fcd6c 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -28,11 +28,130 @@ function initDashboard() { if (feedsDiv) { const isVisible = feedsDiv.style.display !== 'none'; feedsDiv.style.display = isVisible ? 'none' : 'block'; + // Load items for all feeds when opening + 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 into info div + async function loadFeedInfo(feedUrl, infoDiv) { + infoDiv.innerHTML = 'Loading feed info...'; + 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], + ['Category', f.category], + ['Site URL', f.siteUrl], + ['Status', f.status], + ['Error Count', f.errorCount], + ['Last Error', f.lastError], + ['Item Count', f.itemCount], + ['Avg Post Freq', f.avgPostFreqHrs ? f.avgPostFreqHrs.toFixed(1) + ' hrs' : null], + ['Oldest Item', f.oldestItemDate], + ['Newest Item', f.newestItemDate], + ['Discovered', f.discoveredAt], + ['Last Crawled', f.lastCrawledAt], + ['Next Crawl', f.nextCrawlAt], + ['TTL', f.ttlMinutes ? f.ttlMinutes + ' min' : null], + ['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 into items div + async function loadFeedItems(feedUrl, itemsDiv) { + itemsDiv.innerHTML = 'Loading items...'; + 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) { + itemsDiv.innerHTML = 'No items'; + 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 and labels const statusConfig = { hold: { color: '#f90', bg: '#330', border: '#550', dimColor: '#664', dimBg: '#1a1a00', label: 'hold' }, @@ -159,22 +278,29 @@ function initDashboard() { html += ``; } - // Feed path (URL without domain) + // Feed path (URL without domain) - click to toggle feed info 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)}`; + html += `${escapeHtml(feedPath)}`; - // Feed title + // Feed title - click to toggle items if (f.title) { - html += `${escapeHtml(f.title)}`; - } else { - html += ''; + html += `${escapeHtml(f.title)}`; } + // Filler span - also toggles items + html += ' '; html += ''; + + // Feed info section (hidden by default) + html += ''; + + // Feed items section (shown by default) + html += '
'; + html += ''; }); html += ''; diff --git a/templates.go b/templates.go index 8d54587..8e65dfa 100644 --- a/templates.go +++ b/templates.go @@ -534,7 +534,7 @@ const dashboardHTML = `
-
v52
+
v53
Last updated: {{.UpdatedAt.Format "2006-01-02 15:04:05"}}