A focused bug-fix release tightening up five rough edges visible in the v1.3.5 production rollout: nested SPA routes serving a blank page, three different numbers shown for the same scan-progress quantity, scans being abandoned on abrupt container restarts even when they had hours of saved progress, the per-instance webhook URL in the UI pointing at the wrong credential (causing Sonarr/Radarr connection tests to fail with 401), and noisy DB lines in the log viewer that looked like errors but were not.
Fixed
- Nested routes like
/scans/<id>no longer render as a blank page (#291). The Vite build was configured withbase: './'(relative asset paths) on the theory that this would support a sub-path mount like/healarr/. In practice it broke every nested route: when the browser is at/scans/1it resolves./assets/index.jsas/scans/assets/index.js, the Go SPA fallback returnsindex.htmlfor that path, and the browser refuses the import on MIME-type grounds. The "sub-path mount" promise was already broken by SPA routing semantics, so the build now uses an absolutebase: '/'. Nested routes load assets identically to the root route. Real sub-path mount support would need a build-time env var plus a server-side index.html base-href rewrite and is now explicitly out of scope. - The scan-detail page shows one consistent file-scanned count instead of three slightly-different numbers (#289). The header progress bar (
Running (X/Y)), the "Files scanned" stat card, and the "Healthy files" stat card were each computed from a different source at a different cadence: in-memory WebSocketFilesDone(per-file), thescans.files_scannedcolumn (persisted only every 10 files), and a liveCOUNT(scan_files)(per-file). On a long-running scan you would routinely see e.g.Running (1855/31107),Files scanned: 1791,Healthy files: 1792. The handler now derivesfiles_scannedandcorruptions_foundfrom the sameGROUP BY statusoverscan_filesthat produces the per-status breakdown, sofiles_scanned == healthy + corrupt + skipped + inaccessibleby construction. The cached column is kept as a fallback for the unlikely case where the count query errors. - Scans that were running when the container was killed abruptly now auto-resume on the next start instead of being marked cancelled (#292). When
docker kill/ SIGKILL / OOM-kill / host crash takes the container down, the graceful-shutdown handler that callsMarkInterruptednever runs. The startup reconcile (ReconcileOrphanScans, originally #259) caught these orphanrunningrows but marked them all'cancelled'(terminal), so a multi-hour scan with thousands of files done was wasted on every hard restart. The reconcile now splits by progress: rows with a savedfile_listandcurrent_file_index > 0are demoted to'interrupted'and picked up by the existingResumeInterruptedScanssweep on the same startup. Rows with no resumable state (mid-enumerate, or zero progress) still get cancelled. Both updates run in a single transaction so a partial reconcile cannot leave a row stuck between states. - The webhook URL shown in each Arr-instance row now uses the per-instance webhook secret, not the master API key (#293). The UI was hardcoded to splice
${apiKeyData.api_key}into the copy field for every instance. When the instance had a per-instancewebhook_secret(the default for any instance created after the per-instance-secret migration), the backend correctly rejected the master-key URL with401 Invalid webhook secret, but the user had no way to discover the right URL from the UI. Sonarr/Radarr connection tests therefore failed for every freshly-created Healarr instance. The per-row field now prefersarr.webhook_secretand falls back to the master api_key only for legacy rows that have no per-instance secret. The Config-page "Webhook API Key" card is renamed to "Master API Key" and reframed as the fallback-for-legacy path it actually is. Closes #286. - The in-app log viewer no longer fills with misleading
database is locked (SQLITE_BUSY)lines (#288). Authenticated requests bumpsessions.last_used_atas a best-effort diagnostic ("when did this session last act?"). Under page-load fanout the frontend fires several parallel API calls that all race on the same UPDATE; SQLite's WAL serializes writers, so all but the winner getSQLITE_BUSY. The bump error was already discarded, but the log line at DEBUG level was passed through to the log file and surfaced in the in-app viewer where it read like a real database error. The line is now silenced. No behavior change — the bump was already fire-and-forget.
Documentation
- Hardware acceleration setup guide (#284). New "Hardware Acceleration" section in the README with ready-to-paste compose snippets for NVIDIA (NVIDIA Container Toolkit +
runtime: nvidia), Intel QSV (/dev/dridevice map), and VAAPI (/dev/dri+LIBVA_DRIVER_NAME). A matching accordion in the in-app Help page covers vendor selection, theHEALARR_HEALTH_CHECK_HWACCELenv var, and how to verify hwaccel is engaging by spotting-c:v <codec>_cuvidin the livepsoutput. The header sells the actual win: AV1 thorough scans drop from CPU-bound to GPU-bound, taking the per-file decode from tens of seconds to under one.
Full Changelog: v1.3.5...v1.3.6