feat(ui): upgrade dashboard UX with filters and scan trend insights#24
feat(ui): upgrade dashboard UX with filters and scan trend insights#24root-Manas merged 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the embedded dashboard UI to make scan discovery and at-a-glance status insights faster, adding mode-based filtering and a lightweight “trend” view alongside scan overview details.
Changes:
- Added mode “chip” filters + updated search handling to persist filter state across refreshes.
- Added scan status badges (last finished, duration, health) and a “Recent URL Yield Trend” panel.
- Added small UI animations (fade-in list items, grow animation for trend bars) and improved empty state messaging.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function trendRows(currentId) { | ||
| const recent = scans.slice(0, 12); | ||
| const max = Math.max(...recent.map(s => s.stats.urls || 0), 1); | ||
| return recent.map(s => { | ||
| const width = Math.max(4, Math.round(((s.stats.urls || 0) / max) * 100)); | ||
| const marker = s.id === currentId ? ' <b>(selected)</b>' : ''; | ||
| return `<div class="trend-item"><span>${s.target}</span><span class="spark"><span class="spark-fill" style="width:${width}%"></span></span><span>${s.stats.urls || 0}${marker}</span></div>`; | ||
| }).join(''); |
There was a problem hiding this comment.
trendRows() injects s.target directly into innerHTML without escaping. Since target originates from stored scan data, this allows HTML/script injection into the dashboard if a scan target contains markup. Escape s.target (and any other string fields rendered here) before interpolating, or build DOM nodes with textContent instead of concatenating HTML strings.
| if (!items.length) { | ||
| root.innerHTML = `<div class="muted mono">No scans match current filters.</div>`; | ||
| return; | ||
| } |
There was a problem hiding this comment.
When filters reduce the list to zero (or exclude the currently selected activeId), renderScanList() returns early without reconciling activeId/detail view. This can leave the detail panel showing a scan that isn’t present in the filtered list and no item highlighted. Consider clearing activeId (and resetting the detail panel) when items is empty, and when activeId is not found in items, auto-select the first filtered item (or keep selection only if it’s still visible).
| if (!items.length) { | |
| root.innerHTML = `<div class="muted mono">No scans match current filters.</div>`; | |
| return; | |
| } | |
| if (!items.length) { | |
| // No items match the current filters: clear selection and detail view. | |
| activeId = null; | |
| root.innerHTML = `<div class="muted mono">No scans match current filters.</div>`; | |
| const detailRoot = document.getElementById('scanDetail'); | |
| if (detailRoot) { | |
| detailRoot.innerHTML = ''; | |
| } | |
| return; | |
| } | |
| // If there is an activeId but it is no longer in the filtered items, select the first item. | |
| if (activeId) { | |
| const stillVisible = items.some(s => String(s.id) === String(activeId)); | |
| if (!stillVisible && items[0]) { | |
| activeId = items[0].id; | |
| // Ensure detail view points to a visible item. | |
| loadResult(activeId); | |
| } | |
| } |
Summary
Validation