v53: add feed info and items panels with click toggles
This commit is contained in:
+12
-6
@@ -20,12 +20,14 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
type FeedDetails struct {
|
type FeedDetails struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
SiteURL string `json:"siteUrl,omitempty"`
|
SiteURL string `json:"siteUrl,omitempty"`
|
||||||
DiscoveredAt string `json:"discoveredAt,omitempty"`
|
DiscoveredAt string `json:"discoveredAt,omitempty"`
|
||||||
LastCrawledAt string `json:"lastCrawledAt,omitempty"`
|
LastCrawledAt string `json:"lastCrawledAt,omitempty"`
|
||||||
|
NextCrawlAt string `json:"nextCrawlAt,omitempty"`
|
||||||
LastBuildDate string `json:"lastBuildDate,omitempty"`
|
LastBuildDate string `json:"lastBuildDate,omitempty"`
|
||||||
TTLMinutes int `json:"ttlMinutes,omitempty"`
|
TTLMinutes int `json:"ttlMinutes,omitempty"`
|
||||||
UpdatePeriod string `json:"updatePeriod,omitempty"`
|
UpdatePeriod string `json:"updatePeriod,omitempty"`
|
||||||
@@ -42,8 +44,8 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var f FeedDetails
|
var f FeedDetails
|
||||||
var title, description, language, siteUrl *string
|
var category, title, description, language, siteUrl *string
|
||||||
var lastCrawledAt, lastBuildDate *time.Time
|
var lastCrawledAt, nextCrawlAt, lastBuildDate *time.Time
|
||||||
var updatePeriod, status, lastError *string
|
var updatePeriod, status, lastError *string
|
||||||
var oldestItemDate, newestItemDate *time.Time
|
var oldestItemDate, newestItemDate *time.Time
|
||||||
var ttlMinutes, updateFreq, errorCount, itemCount *int
|
var ttlMinutes, updateFreq, errorCount, itemCount *int
|
||||||
@@ -52,16 +54,16 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
var publishStatus, publishAccount *string
|
var publishStatus, publishAccount *string
|
||||||
|
|
||||||
err := c.db.QueryRow(`
|
err := c.db.QueryRow(`
|
||||||
SELECT url, type, title, description, language, site_url,
|
SELECT url, type, category, title, description, language, site_url,
|
||||||
discovered_at, last_crawled_at, last_build_date,
|
discovered_at, last_crawled_at, next_crawl_at, last_build_date,
|
||||||
ttl_minutes, update_period, update_freq,
|
ttl_minutes, update_period, update_freq,
|
||||||
status, error_count, last_error,
|
status, error_count, last_error,
|
||||||
item_count, avg_post_freq_hrs, oldest_item_date, newest_item_date,
|
item_count, avg_post_freq_hrs, oldest_item_date, newest_item_date,
|
||||||
publish_status, publish_account
|
publish_status, publish_account
|
||||||
FROM feeds WHERE url = $1
|
FROM feeds WHERE url = $1
|
||||||
`, feedURL).Scan(
|
`, feedURL).Scan(
|
||||||
&f.URL, &f.Type, &title, &description, &language, &siteUrl,
|
&f.URL, &f.Type, &category, &title, &description, &language, &siteUrl,
|
||||||
&discoveredAt, &lastCrawledAt, &lastBuildDate,
|
&discoveredAt, &lastCrawledAt, &nextCrawlAt, &lastBuildDate,
|
||||||
&ttlMinutes, &updatePeriod, &updateFreq,
|
&ttlMinutes, &updatePeriod, &updateFreq,
|
||||||
&status, &errorCount, &lastError,
|
&status, &errorCount, &lastError,
|
||||||
&itemCount, &avgPostFreqHrs, &oldestItemDate, &newestItemDate,
|
&itemCount, &avgPostFreqHrs, &oldestItemDate, &newestItemDate,
|
||||||
@@ -77,6 +79,7 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f.Category = StringValue(category)
|
||||||
f.Title = StringValue(title)
|
f.Title = StringValue(title)
|
||||||
f.Description = StringValue(description)
|
f.Description = StringValue(description)
|
||||||
f.Language = StringValue(language)
|
f.Language = StringValue(language)
|
||||||
@@ -85,6 +88,9 @@ func (c *Crawler) handleAPIFeedInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
if lastCrawledAt != nil {
|
if lastCrawledAt != nil {
|
||||||
f.LastCrawledAt = lastCrawledAt.Format(time.RFC3339)
|
f.LastCrawledAt = lastCrawledAt.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
if nextCrawlAt != nil {
|
||||||
|
f.NextCrawlAt = nextCrawlAt.Format(time.RFC3339)
|
||||||
|
}
|
||||||
if lastBuildDate != nil {
|
if lastBuildDate != nil {
|
||||||
f.LastBuildDate = lastBuildDate.Format(time.RFC3339)
|
f.LastBuildDate = lastBuildDate.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-6
@@ -28,11 +28,130 @@ function initDashboard() {
|
|||||||
if (feedsDiv) {
|
if (feedsDiv) {
|
||||||
const isVisible = feedsDiv.style.display !== 'none';
|
const isVisible = feedsDiv.style.display !== 'none';
|
||||||
feedsDiv.style.display = isVisible ? 'none' : 'block';
|
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
|
// Status colors and labels
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
hold: { color: '#f90', bg: '#330', border: '#550', dimColor: '#664', dimBg: '#1a1a00', label: 'hold' },
|
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>`;
|
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;
|
let feedPath = f.url;
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(f.url.startsWith('http') ? f.url : 'https://' + f.url);
|
const urlObj = new URL(f.url.startsWith('http') ? f.url : 'https://' + f.url);
|
||||||
feedPath = urlObj.pathname + urlObj.search;
|
feedPath = urlObj.pathname + urlObj.search;
|
||||||
} catch (e) {}
|
} 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) {
|
if (f.title) {
|
||||||
html += `<span style="color: #fff; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(f.title)}</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>`;
|
||||||
} else {
|
|
||||||
html += '<span style="flex: 1;"></span>';
|
|
||||||
}
|
}
|
||||||
|
// Filler span - also toggles items
|
||||||
|
html += '<span class="feed-filler-toggle" style="flex: 1; cursor: pointer;"> </span>';
|
||||||
|
|
||||||
html += '</div>';
|
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>';
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|||||||
+1
-1
@@ -534,7 +534,7 @@ const dashboardHTML = `<!DOCTYPE html>
|
|||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
</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>
|
<div class="updated" id="updatedAt">Last updated: {{.UpdatedAt.Format "2006-01-02 15:04:05"}}</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user