feat: Stage 4 — fail2ban connection and server status

This commit is contained in:
2026-02-28 21:48:03 +01:00
parent a41a99dad4
commit 60683da3ca
13 changed files with 1085 additions and 18 deletions

View File

@@ -0,0 +1,179 @@
/**
* `ServerStatusBar` component.
*
* Displays a persistent bar at the top of the dashboard showing the
* fail2ban server health snapshot: connectivity status, version, active
* jail count, and aggregated ban/failure totals.
*
* Polls `GET /api/dashboard/status` every 30 seconds and on window focus
* via the {@link useServerStatus} hook.
*/
import {
Badge,
Button,
makeStyles,
Spinner,
Text,
tokens,
Tooltip,
} from "@fluentui/react-components";
import { ArrowClockwiseRegular, ShieldRegular } from "@fluentui/react-icons";
import { useServerStatus } from "../hooks/useServerStatus";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const useStyles = makeStyles({
bar: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalL,
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalL}`,
backgroundColor: tokens.colorNeutralBackground1,
borderRadius: tokens.borderRadiusMedium,
borderTopWidth: "1px",
borderTopStyle: "solid",
borderTopColor: tokens.colorNeutralStroke2,
borderRightWidth: "1px",
borderRightStyle: "solid",
borderRightColor: tokens.colorNeutralStroke2,
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: tokens.colorNeutralStroke2,
borderLeftWidth: "1px",
borderLeftStyle: "solid",
borderLeftColor: tokens.colorNeutralStroke2,
marginBottom: tokens.spacingVerticalL,
flexWrap: "wrap",
},
statusGroup: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
},
statGroup: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalXS,
},
statValue: {
fontVariantNumeric: "tabular-nums",
fontWeight: 600,
},
spacer: {
flexGrow: 1,
},
errorText: {
color: tokens.colorPaletteRedForeground1,
fontSize: "12px",
},
});
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* Persistent bar displaying fail2ban server health.
*
* Render this at the top of the dashboard page (and any page that should
* show live server status).
*/
export function ServerStatusBar(): JSX.Element {
const styles = useStyles();
const { status, loading, error, refresh } = useServerStatus();
return (
<div className={styles.bar} role="status" aria-label="fail2ban server status">
{/* ---------------------------------------------------------------- */}
{/* Online / Offline badge */}
{/* ---------------------------------------------------------------- */}
<div className={styles.statusGroup}>
<ShieldRegular fontSize={16} />
{loading && !status ? (
<Spinner size="extra-tiny" label="Checking…" labelPosition="after" />
) : (
<Badge
appearance="filled"
color={status?.online ? "success" : "danger"}
aria-label={status?.online ? "fail2ban online" : "fail2ban offline"}
>
{status?.online ? "Online" : "Offline"}
</Badge>
)}
</div>
{/* ---------------------------------------------------------------- */}
{/* Version */}
{/* ---------------------------------------------------------------- */}
{status?.version != null && (
<Tooltip content="fail2ban version" relationship="description">
<Text size={200} className={styles.statValue}>
v{status.version}
</Text>
</Tooltip>
)}
{/* ---------------------------------------------------------------- */}
{/* Stats (only when online) */}
{/* ---------------------------------------------------------------- */}
{status?.online === true && (
<>
<Tooltip content="Active jails" relationship="description">
<div className={styles.statGroup}>
<Text size={200}>Jails:</Text>
<Text size={200} className={styles.statValue}>
{status.active_jails}
</Text>
</div>
</Tooltip>
<Tooltip content="Currently banned IPs" relationship="description">
<div className={styles.statGroup}>
<Text size={200}>Bans:</Text>
<Text size={200} className={styles.statValue}>
{status.total_bans}
</Text>
</div>
</Tooltip>
<Tooltip content="Currently failing IPs" relationship="description">
<div className={styles.statGroup}>
<Text size={200}>Failures:</Text>
<Text size={200} className={styles.statValue}>
{status.total_failures}
</Text>
</div>
</Tooltip>
</>
)}
{/* ---------------------------------------------------------------- */}
{/* Error message */}
{/* ---------------------------------------------------------------- */}
{error != null && (
<Text className={styles.errorText} aria-live="polite">
{error}
</Text>
)}
<div className={styles.spacer} />
{/* ---------------------------------------------------------------- */}
{/* Refresh button */}
{/* ---------------------------------------------------------------- */}
<Tooltip content="Refresh server status" relationship="label">
<Button
appearance="subtle"
size="small"
icon={loading ? <Spinner size="extra-tiny" /> : <ArrowClockwiseRegular />}
onClick={refresh}
aria-label="Refresh server status"
disabled={loading}
/>
</Tooltip>
</div>
);
}