v53: add feed info and items panels with click toggles

This commit is contained in:
primal
2026-01-30 16:59:38 -05:00
parent 442e010672
commit a3d8f4ea8e
3 changed files with 145 additions and 13 deletions
+12 -6
View File
@@ -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)
}
+132 -6
View File
@@ -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 = '<span style="color: #666;">Loading feed info...</span>';
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 = '<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; color: #888;">';
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 += `<span style="color: #666;">${escapeHtml(label)}:</span><span style="color: #fff;">${escapeHtml(String(value))}</span>`;
}
});
html += '</div>';
infoDiv.innerHTML = html;
} catch (err) {
infoDiv.innerHTML = `<span style="color: #f66;">Error: ${escapeHtml(err.message)}</span>`;
}
}
// Load feed items into items div
async function loadFeedItems(feedUrl, itemsDiv) {
itemsDiv.innerHTML = '<span style="color: #666;">Loading items...</span>';
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 = '<span style="color: #666;">No items</span>';
return;
}
let html = '';
items.forEach(item => {
const date = item.pub_date ? new Date(item.pub_date).toLocaleDateString() : '';
html += `<div style="padding: 2px 0; font-size: 11px; border-bottom: 1px solid #1a1a1a;">`;
html += `<span style="color: #666; margin-right: 8px;">${escapeHtml(date)}</span>`;
if (item.link) {
html += `<a href="${escapeHtml(item.link)}" target="_blank" style="color: #0af; text-decoration: none;">${escapeHtml(item.title || item.link)}</a>`;
} else {
html += `<span style="color: #fff;">${escapeHtml(item.title || '(no title)')}</span>`;
}
html += '</div>';
});
itemsDiv.innerHTML = html;
} catch (err) {
itemsDiv.innerHTML = `<span style="color: #f66;">Error: ${escapeHtml(err.message)}</span>`;
}
}
// 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 += `<span style="width: 50px; margin-right: 6px;"></span>`;
}
// 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 += `<span style="color: #0af; font-size: 11px; margin-right: 8px; white-space: nowrap;" title="${escapeHtml(f.url)}">${escapeHtml(feedPath)}</span>`;
html += `<span class="feed-url-toggle" style="color: #0af; font-size: 11px; margin-right: 8px; white-space: nowrap; cursor: pointer;" title="Click to show feed info">${escapeHtml(feedPath)}</span>`;
// Feed title
// Feed title - click to toggle items
if (f.title) {
html += `<span style="color: #fff; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(f.title)}</span>`;
} else {
html += '<span style="flex: 1;"></span>';
html += `<span class="feed-title-toggle" style="color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;" title="Click to toggle items">${escapeHtml(f.title)}</span>`;
}
// Filler span - also toggles items
html += '<span class="feed-filler-toggle" style="flex: 1; cursor: pointer;">&nbsp;</span>';
html += '</div>';
// Feed info section (hidden by default)
html += '<div class="feed-info" style="display: none; padding: 6px 10px; margin-left: 10px; border-left: 2px solid #444; background: #0a0a0a; font-size: 11px;"></div>';
// Feed items section (shown by default)
html += '<div class="feed-items" style="display: block; padding: 4px 10px; margin-left: 10px; border-left: 2px solid #333;"></div>';
html += '</div>';
});
html += '</div>';
+1 -1
View File
@@ -534,7 +534,7 @@ const dashboardHTML = `<!DOCTYPE html>
<div id="output"></div>
</div>
<div style="color: #333; font-size: 11px; margin-top: 10px;">v52</div>
<div style="color: #333; font-size: 11px; margin-top: 10px;">v53</div>
<div class="updated" id="updatedAt">Last updated: {{.UpdatedAt.Format "2006-01-02 15:04:05"}}</div>
</body>