From 09f04ea60bb7bc8ff64178252bc7146a9abd12b1 Mon Sep 17 00:00:00 2001 From: Nazim Date: Thu, 19 Mar 2026 14:33:10 +0000 Subject: [PATCH] feat: dashboard shows last poll time, relative timestamps, filter reasons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header shows 'last poll Xm ago — N new' like Forgejo dashboard - Notifications table: TIME (relative), ISSUE, REASON (badge), TITLE, FILTER - Own PRs tagged with 🔵, CI status shown - Default view: pending only (non-dismissed) - filter_reason column visible in table --- dashboard/index.html | 54 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/dashboard/index.html b/dashboard/index.html index cd4d7d3..224b68b 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -192,7 +192,7 @@

- Recent Notifications + 🔔 Notifications

@@ -238,6 +238,21 @@ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); } + function timeAgo(iso) { + if (!iso) return '-'; + const d = new Date(iso); + const now = new Date(); + const diffMs = now - d; + const mins = Math.floor(diffMs / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + const remMins = mins % 60; + if (hrs < 24) return remMins > 0 ? `${hrs}h ${remMins}m ago` : `${hrs}h ago`; + const days = Math.floor(hrs / 24); + return `${days}d ago`; + } + function getNotificationStatus(row) { if (row.dismissed) return { class: 'status-dismissed', text: 'Dismissed' }; if (row.acted_at) return { class: 'status-acted', text: 'Acted' }; @@ -263,25 +278,42 @@ "SELECT COUNT(*) as c FROM escalations WHERE status = 'open'"); document.getElementById('escalation-count').textContent = escalations[0]?.c || 0; - // Notifications table + // Last poll info + const polls = await fetchData('notification_polls', + 'SELECT polled_at, new_count FROM notification_polls ORDER BY polled_at DESC LIMIT 1'); + if (polls.length > 0) { + const p = polls[0]; + document.getElementById('poll-info').textContent = + `(last poll ${timeAgo(p.polled_at)} — ${p.new_count} new)`; + } + + // Notifications table — show pending (non-dismissed) by default const notifications = await fetchData('notifications', - 'SELECT * FROM notifications ORDER BY seen_at DESC LIMIT 10'); + 'SELECT * FROM notifications WHERE dismissed = 0 ORDER BY seen_at DESC LIMIT 30'); if (notifications.length === 0) { document.getElementById('notifications-table').innerHTML = - '
No notifications yet.
'; + '
No pending notifications. ✨
'; } else { let html = ` - + `; for (const n of notifications) { - const status = getNotificationStatus(n); - const title = n.url ? `${n.title}` : n.title; + const ownTag = n.is_own_pr ? '🔵 ' : ''; + const ciTag = n.ci_status ? ` [CI:${n.ci_status}]` : ''; + const repo_short = n.repo_id ? n.repo_id.split('/').pop() : '-'; + const issue = n.url ? `${repo_short}` : repo_short; + const reasonClass = n.reason === 'mention' ? 'accent' : + n.reason === 'author' ? 'success' : + n.reason === 'review_requested' ? 'warning' : ''; + const reasonBadge = `${n.reason}`; + const filterShort = (n.filter_reason || '').replace(/^KEEP: /, ''); html += ` - - - - + + + + + `; } html += '
StatusTitleRepoSeen
TIMEISSUEREASONTITLEFILTER
${status.text}${title}${n.repo_id || '-'}${formatDate(n.seen_at)}${timeAgo(n.seen_at)}${issue}${reasonBadge}${ownTag}${n.title || '-'}${ciTag}${filterShort}
';