fix(web): scan progress — correct percentages, no layout shift, refresh-survivable

Bug: the admin 'Scanning Libraries' widget fed the server's 0-1
progress straight into percent strings and bar widths, so the overall
and per-library percentages capped at '1%' and the bars never filled.
Both now scale by 100 (matching the setup page, which was already
correct).

Bug: scan state only reached clients via websocket events, so a page
opened (or refreshed) mid-scan showed nothing until the next tick —
and the admin widget never re-attached at all. The worker now exposes
active jobs (with their library_id so rows keep their names), served
as GET /api/scanner/active (admin, re-attaches the widget + polling)
and GET /api/scanner/active-summary (any user, restores the sidebar
indicator on load). Idle hides both, exactly as before.

Annoyance: the sidebar's 'Scanning… N%' was a block element between
the logo and the nav, shoving every menu entry down while a scan ran.
The percent now lives inside the fixed-height logo row next to the
spinner (spinner last, so growing digits extend leftward); nothing
below it moves.
This commit is contained in:
John O'Keefe committed 2026-10-15 08:29:04 -04:00
1 parent 9bc149f3d9
commit fd6f0e3353
11 files changed
+245 -38

No files matched your search

+32
View File
@@ -177,6 +177,38 @@ func (h *Handler) GetScanStatus(c *echo.Context) error {
})
}
// GetActiveScans handles GET /api/scanner/active (admin): every
// pending/running job. Lets the admin page re-attach to in-flight
// scans after a page refresh instead of only tracking scans it
// started itself in this page load.
func (h *Handler) GetActiveScans(c *echo.Context) error {
jobs := h.worker.GetActiveScanJobs()
return c.JSON(http.StatusOK, map[string]interface{}{
"scanning": len(jobs) > 0,
"jobs": jobs,
})
}
// GetScanSummary handles GET /api/scanner/active-summary (any
// authenticated user): whether a scan is running and its average
// progress (0-1). Powers the sitewide sidebar indicator, which must
// survive page refreshes — websocket events alone only reach pages
// that were already open when the scan started.
func (h *Handler) GetScanSummary(c *echo.Context) error {
jobs := h.worker.GetActiveScanJobs()
progress := 0.0
if len(jobs) > 0 {
for _, job := range jobs {
progress += job.Progress
}
progress /= float64(len(jobs))
}
return c.JSON(http.StatusOK, map[string]interface{}{
"scanning": len(jobs) > 0,
"progress": progress,
})
}
// StartWatchModeForLibrary starts watching a specific library's folders
func (h *Handler) StartWatchModeForLibrary(ctx context.Context, libraryID pgtype.UUID, adminID pgtype.UUID) error {
h.mu.Lock()