feat: implement dashboard ban overview (Stage 5)

- Add ban_service reading fail2ban SQLite DB via read-only aiosqlite
- Add geo_service resolving IPs via ip-api.com with 10k in-memory cache
- Add GET /api/dashboard/bans and GET /api/dashboard/accesses endpoints
- Add TimeRange, DashboardBanItem, DashboardBanListResponse, AccessListItem,
  AccessListResponse models in models/ban.py
- Build BanTable component (Fluent UI DataGrid) with bans/accesses modes,
  pagination, loading/error/empty states, and ban-count badges
- Build useBans hook managing time-range and pagination state
- Update DashboardPage: status bar + time-range toolbar + tab switcher
- Add 37 new backend tests (ban service, geo service, dashboard router)
- All 141 tests pass; ruff/mypy --strict/tsc --noEmit clean
This commit is contained in:
2026-03-01 12:57:19 +01:00
parent 94661d7877
commit 9ac7f8d22d
15 changed files with 2346 additions and 29 deletions

View File

@@ -1,12 +1,31 @@
/**
* Dashboard page.
*
* Shows the fail2ban server status bar at the top.
* Full ban-list implementation is delivered in Stage 5.
* Composes the fail2ban server status bar at the top, a shared time-range
* selector, and two tabs: "Ban List" (aggregate bans) and "Access List"
* (individual matched log lines). The time-range selection is shared
* between both tabs so users can compare data for the same period.
*/
import { Text, makeStyles, tokens } from "@fluentui/react-components";
import { useState } from "react";
import {
Tab,
TabList,
Text,
ToggleButton,
Toolbar,
makeStyles,
tokens,
} from "@fluentui/react-components";
import { BanTable } from "../components/BanTable";
import { ServerStatusBar } from "../components/ServerStatusBar";
import type { TimeRange } from "../types/ban";
import { TIME_RANGE_LABELS } from "../types/ban";
import type { BanTableMode } from "../hooks/useBans";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const useStyles = makeStyles({
root: {
@@ -14,22 +33,116 @@ const useStyles = makeStyles({
flexDirection: "column",
gap: tokens.spacingVerticalM,
},
section: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalS,
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,
padding: tokens.spacingVerticalM,
},
sectionHeader: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: tokens.spacingHorizontalM,
paddingBottom: tokens.spacingVerticalS,
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: tokens.colorNeutralStroke2,
},
tabContent: {
paddingTop: tokens.spacingVerticalS,
},
});
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Ordered time-range presets for the toolbar. */
const TIME_RANGES: TimeRange[] = ["24h", "7d", "30d", "365d"];
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* Dashboard page — renders the server status bar and a Stage 5 placeholder.
* Main dashboard landing page.
*
* Displays the fail2ban server status, a time-range selector, and a
* tabbed view toggling between the ban list and the access list.
*/
export function DashboardPage(): JSX.Element {
export function DashboardPage(): React.JSX.Element {
const styles = useStyles();
const [timeRange, setTimeRange] = useState<TimeRange>("24h");
const [activeTab, setActiveTab] = useState<BanTableMode>("bans");
return (
<div className={styles.root}>
{/* ------------------------------------------------------------------ */}
{/* Server status bar */}
{/* ------------------------------------------------------------------ */}
<ServerStatusBar />
<Text as="h1" size={700} weight="semibold">
Dashboard
</Text>
<Text as="p" size={300}>
Ban overview will be implemented in Stage 5.
</Text>
{/* ------------------------------------------------------------------ */}
{/* Ban / access list section */}
{/* ------------------------------------------------------------------ */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
{activeTab === "bans" ? "Ban List" : "Access List"}
</Text>
{/* Shared time-range selector */}
<Toolbar aria-label="Time range" size="small">
{TIME_RANGES.map((r) => (
<ToggleButton
key={r}
size="small"
checked={timeRange === r}
onClick={() => {
setTimeRange(r);
}}
aria-pressed={timeRange === r}
>
{TIME_RANGE_LABELS[r]}
</ToggleButton>
))}
</Toolbar>
</div>
{/* Tab switcher */}
<TabList
selectedValue={activeTab}
onTabSelect={(_, data) => {
setActiveTab(data.value as BanTableMode);
}}
size="small"
>
<Tab value="bans">Ban List</Tab>
<Tab value="accesses">Access List</Tab>
</TabList>
{/* Active tab content */}
<div className={styles.tabContent}>
<BanTable mode={activeTab} timeRange={timeRange} />
</div>
</div>
</div>
);
}