From 19a18ba9925d98acec84ec896afbcbfcf83b1c98 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 15 May 2026 14:02:00 -0700 Subject: [PATCH] Dashboard activity feed: stop showing 'NaNmo ago' Recent activity items on the dashboard all rendered 'NaNmo ago' because the formatter parsed `activity.time` (a human label like 'Now' / 'Just now') with `new Date(...)` -> Invalid Date -> NaN arithmetic -> 'NaNmo ago'. Backend (`core/runtime_state.add_activity_item`) has always emitted `activity.timestamp` (Unix epoch seconds) alongside the label. Frontend now uses the epoch for relative-time formatting via a new local `_activityTimeAgo` helper: - typeof timestamp === 'number' -> diff against Date.now() in ms - < 60s -> 'Just now' - < 60m -> 'Nm ago' - < 24h -> 'Nh ago' - < 30d -> 'Nd ago' - otherwise 'Nmo ago' - falls back to the literal `activity.time` label only when no timestamp is present (legacy items / future shapes) Both call sites in api-monitor.js (initial render + timestamp-only refresh path) updated to the new helper. --- webui/static/api-monitor.js | 24 ++++++++++++++++++++++-- webui/static/helper.js | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index 01e27f0a..7aa846fc 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -614,6 +614,26 @@ async function fetchAndUpdateActivityFeed() { // Cache last feed signature to avoid unnecessary DOM rebuilds (prevents blink) let _lastActivityFeedSig = ''; +// Activity items carry `timestamp` (Unix epoch seconds) — `activity.time` +// is a human label like "Now" that doesn't parse as a date. Use the +// epoch for relative-time formatting; fall back to the label only +// when no timestamp is present (legacy items, future shapes). +function _activityTimeAgo(activity) { + const ts = activity && activity.timestamp; + if (typeof ts !== 'number' || !isFinite(ts)) { + return (activity && activity.time) || ''; + } + const diffMs = Date.now() - ts * 1000; + if (diffMs < 60000) return 'Just now'; + const mins = Math.floor(diffMs / 60000); + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return `${Math.floor(days / 30)}mo ago`; +} + function updateActivityFeed(activities) { const feedContainer = document.getElementById('dashboard-activity-feed'); if (!feedContainer) return; @@ -644,7 +664,7 @@ function updateActivityFeed(activities) { // Just update timestamps without rebuilding DOM const timeEls = feedContainer.querySelectorAll('.activity-time'); items.forEach((activity, i) => { - if (timeEls[i]) timeEls[i].textContent = timeAgo(activity.time); + if (timeEls[i]) timeEls[i].textContent = _activityTimeAgo(activity); }); return; } @@ -660,7 +680,7 @@ function updateActivityFeed(activities) {
${escapeHtml(activity.title)}
${escapeHtml(activity.subtitle)}
-${timeAgo(activity.time)}
+${_activityTimeAgo(activity)}
`; feedContainer.appendChild(activityElement); diff --git a/webui/static/helper.js b/webui/static/helper.js index 19226949..1dd2e445 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3416,6 +3416,7 @@ const WHATS_NEW = { '2.5.2': [ // --- May 13, 2026 — 2.5.2 release --- { date: 'May 13, 2026 — 2.5.2 release' }, + { title: 'Dashboard Activity Feed: Stop Showing "NaNmo ago"', desc: 'recent activity items on the dashboard all rendered "NaNmo ago" because the formatter was parsing `activity.time` (a human label like "Now") as a date. backend has always emitted `activity.timestamp` (Unix epoch seconds) alongside the label — frontend now uses that for relative-time formatting. falls back to the literal label only when no timestamp present (legacy items / future shapes).', page: 'home' }, { title: 'Token Leak Round 2: URL-Encoded Form In Artist Endpoint + Playlist Sync', desc: 'security follow-up to the prior token-leak fix. found three sites in `web_server.py` (artist endpoint) that logged the full `image_url` and the entire artist_info dict at INFO on every artist-page render — the dict contained the `image_url` field routed through the image proxy (`/api/image-proxy?url=