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=`), URL-encoding the X-Plex-Token / X-Emby-Token / Subsonic auth straight into the log line. also one site in `core/discovery/sync.py` logged the playlist poster URL during sync. fixes: dropped the three artist-endpoint dev-time debug log lines entirely (before-fix, after-fix, "Final artist data being sent"). playlist-image log now logs `has_image=True/False`, not the URL. strengthened `_redact_url_secrets` with a second regex pattern that matches the URL-encoded form (`%3FX-Plex-Token%3D...`) so any future log-through-redactor catches both plain and encoded shapes. wipe your existing app.log if it captured tokens in either form, and rotate Plex / Jellyfin / Navidrome credentials.', page: 'settings' }, { title: 'Stop Leaking Plex / Jellyfin / Navidrome Tokens Into app.log', desc: 'security: artwork URL fixer was logging full media-server URLs (including the X-Plex-Token / X-Emby-Token / Subsonic auth params) at INFO level on every cover-art lookup. tokens piled up in app.log on disk — anyone with read access to the log file gained full read access to the user\'s media server. fix: log lines moved to DEBUG (so they don\'t persist by default) and routed through a new `_redact_url_secrets` helper that masks the values of `X-Plex-Token` / `X-Emby-Token` / `api_key` / `apikey` / Subsonic `t` / `s` / `p` / generic `token` / `password` query params. anchor regex on `?` or `&` boundary so short keys like `t` don\'t false-match inside `format=Jpg`. also dropped the noisy per-call "Plex/Jellyfin/Navidrome config - base_url: ..., token: ..." INFO lines that fired on every thumbnail. wipe your existing app.log if your config has been logged.', page: 'settings' }, { title: 'AcoustID + Quarantine Modal: Three Bug Fixes', desc: 'github issues #607 + #608. (1) live recordings no longer false-quarantine when AcoustID returns the live recording with a bare title — common case where venue/live annotation lives on the release entity, not the recording entity itself ("Clarity (Live at ...)" expected, AcoustID returns bare "Clarity"). new pure helper `core/matching/version_mismatch.py:is_acceptable_version_mismatch` accepts the mismatch only when one-sided live + bare AND fingerprint score >= 0.85 AND bare titles agree (>=0.7) AND artist matches (>=0.6). other version mismatches (instrumental, remix, acoustic, demo) stay strict — those have distinct fingerprints + MB always annotates them in the recording title. 23 boundary tests pin every shape; existing test_acoustid_version_mismatch suite passes unchanged. (2) audio-mismatch failure message no longer reports "identified as \'\' by \'\' (artist=100%)" when AcoustID returns multiple recordings — prior code mixed recordings[0]\'s strings (which can be empty) with best_rec\'s scores. now uses matched_title/matched_artist consistently in both the high-confidence-skip path and the final fail message. (3) quarantine modal Approve/Delete buttons no longer silently no-op when filename contains an apostrophe — id is now wrapped via `escapeHtml(JSON.stringify(id))` so quotes / backslashes / unicode all round-trip safely through the HTML attribute → JS string boundary. (4) bonus UX: quarantine entry expanded view now shows source uploader (username) + original soulseek filename when the sidecar carries that context, helping trace which uploader the bad file came from.', page: 'downloads' },