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

@@ -8,6 +8,7 @@
import { useCallback, useEffect, useState } from "react";
import {
Badge,
Button,
makeStyles,
mergeClasses,
@@ -31,6 +32,7 @@ import {
import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { useAuth } from "../providers/AuthProvider";
import { useServerStatus } from "../hooks/useServerStatus";
import { useBlocklistStatus } from "../hooks/useBlocklist";
// ---------------------------------------------------------------------------
// Styles
@@ -100,6 +102,12 @@ const useStyles = makeStyles({
flexGrow: 1,
},
navLinkContent: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
flexGrow: 1,
},
navLink: {
display: "flex",
alignItems: "center",
@@ -199,6 +207,7 @@ export function MainLayout(): React.JSX.Element {
// with the icon-only sidebar rather than the full-width one.
const [collapsed, setCollapsed] = useState(() => window.innerWidth < 640);
const { status } = useServerStatus();
const { hasErrors: blocklistHasErrors } = useBlocklistStatus();
/** True only after the first successful poll and fail2ban is unreachable. */
const serverOffline = status !== null && !status.online;
@@ -249,32 +258,45 @@ export function MainLayout(): React.JSX.Element {
{/* Nav links */}
<ul className={styles.navList} role="list" aria-label="Pages">
{NAV_ITEMS.map((item) => (
<li key={item.to} role="listitem">
<Tooltip
content={collapsed ? item.label : ""}
relationship="label"
positioning="after"
>
<NavLink
to={item.to}
end={item.end}
className={({ isActive }) =>
mergeClasses(
styles.navLink,
isActive && styles.navLinkActive,
)
}
aria-label={collapsed ? item.label : undefined}
{NAV_ITEMS.map((item) => {
const showBadge = item.to === "/blocklists" && blocklistHasErrors;
return (
<li key={item.to} role="listitem">
<Tooltip
content={collapsed ? item.label : ""}
relationship="label"
positioning="after"
>
{item.icon}
{!collapsed && (
<Text className={styles.navLabel}>{item.label}</Text>
)}
</NavLink>
</Tooltip>
</li>
))}
<NavLink
to={item.to}
end={item.end}
className={({ isActive }) =>
mergeClasses(
styles.navLink,
isActive && styles.navLinkActive,
)
}
aria-label={collapsed ? item.label : undefined}
>
<span className={styles.navLinkContent}>
{item.icon}
{!collapsed && (
<Text className={styles.navLabel}>{item.label}</Text>
)}
</span>
{showBadge && (
<Badge
appearance="filled"
color="warning"
size="extra-small"
aria-label="Import errors"
/>
)}
</NavLink>
</Tooltip>
</li>
);
})}
</ul>
{/* Footer — Logout */}
@@ -313,6 +335,18 @@ export function MainLayout(): React.JSX.Element {
</MessageBar>
</div>
)}
{/* Blocklist import error warning — shown when the last scheduled import had errors */}
{blocklistHasErrors && (
<div className={styles.warningBar} role="alert">
<MessageBar intent="warning">
<MessageBarBody>
<MessageBarTitle>Blocklist Import Errors</MessageBarTitle>
The most recent blocklist import encountered errors. Check the
Blocklists page for details.
</MessageBarBody>
</MessageBar>
</div>
)}
<div className={styles.content}>
<Outlet />
</div>