Show blocklist import error badge in navigation

When the most recent scheduled import completed with errors, surface the
failure in the persistent app shell:
- A warning MessageBar appears at top of main content area
- An amber badge is rendered on the Blocklists sidebar nav item

Backend: add last_run_errors: bool | None to ScheduleInfo model and
populate it in get_schedule_info() from the latest import_log row.

Frontend: extend ScheduleInfo type, add useBlocklistStatus polling hook,
wire both indicators into MainLayout.

Tests: 3 new service tests + 1 new router test (433 total, all pass).
This commit is contained in:
2026-03-07 21:00:00 +01:00
parent 12a859061c
commit 207be94c42
8 changed files with 235 additions and 27 deletions

View File

@@ -235,3 +235,51 @@ export function useRunImport(): UseRunImportReturn {
return { running, lastResult, error, runNow };
}
// ---------------------------------------------------------------------------
// useBlocklistStatus
// ---------------------------------------------------------------------------
/** How often to re-check the schedule endpoint for import errors (ms). */
const BLOCKLIST_POLL_INTERVAL_MS = 60_000;
export interface UseBlocklistStatusReturn {
/** `true` when the most recent import run completed with errors. */
hasErrors: boolean;
}
/**
* Poll `GET /api/blocklists/schedule` every 60 seconds to detect whether
* the most recent blocklist import had errors.
*
* Network failures during polling are silently ignored — the indicator
* simply retains its previous value until the next successful poll.
*/
export function useBlocklistStatus(): UseBlocklistStatusReturn {
const [hasErrors, setHasErrors] = useState(false);
useEffect(() => {
let cancelled = false;
const poll = (): void => {
fetchSchedule()
.then((info) => {
if (!cancelled) {
setHasErrors(info.last_run_errors === true);
}
})
.catch(() => {
// Silently swallow network errors — do not change indicator state.
});
};
poll();
const id = window.setInterval(poll, BLOCKLIST_POLL_INTERVAL_MS);
return (): void => {
cancelled = true;
window.clearInterval(id);
};
}, []);
return { hasErrors };
}