d.value), 1);
+ const barW = Math.max(8, Math.floor(320 / Math.max(data.length, 1)) - 4);
+ const svgW = data.length * (barW + 4) + 4;
+
+ return (
+
+
+
+ );
+}
+
+// ── Stacked bar chart (risk severity trend) ───────────────────────────────────
+
+type SevKey = "critical" | "high" | "medium" | "low";
+const SEV_KEYS: SevKey[] = ["critical", "high", "medium", "low"];
+
+interface StackedBarChartProps {
+ data: {
+ label: string;
+ critical: number;
+ high: number;
+ medium: number;
+ low: number;
+ }[];
+ height?: number;
+}
+
+function StackedBarChart({ data, height = 120 }: StackedBarChartProps) {
+ const totals = data.map((d) => d.critical + d.high + d.medium + d.low);
+ const max = Math.max(...totals, 1);
+ const barW = Math.max(8, Math.floor(320 / Math.max(data.length, 1)) - 4);
+ const svgW = data.length * (barW + 4) + 4;
+
+ return (
+
+
+
+ );
+}
+
+// ── Legend ────────────────────────────────────────────────────────────────────
+
+function SevLegend() {
+ return (
+
+ {SEV_KEYS.map((sev) => (
+
+
+ {sev.charAt(0).toUpperCase() + sev.slice(1)}
+
+ ))}
+
+ );
+}
+
+// ── Page ──────────────────────────────────────────────────────────────────────
+
+export function HistoryPage() {
+ const { scans, loading, error } = useScans();
+
+ // Oldest → newest for charts (API returns newest first)
+ const completed = useMemo(
+ () =>
+ [...scans]
+ .filter((s: Scan) => s.status === "completed")
+ .sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? "")),
+ [scans],
+ );
+
+ const deviceData = useMemo(
+ () =>
+ completed.map((s) => ({
+ label: shortDate(s.started_at),
+ value: s.devices_found ?? 0,
+ })),
+ [completed],
+ );
+
+ const riskData = useMemo(
+ () =>
+ completed.map((s) => ({
+ label: shortDate(s.started_at),
+ critical: s.risks_critical ?? 0,
+ high: s.risks_high ?? 0,
+ medium: s.risks_medium ?? 0,
+ low: s.risks_low ?? 0,
+ })),
+ [completed],
+ );
+
+ return (
+
+
0
+ ? `${scans.length} scan${scans.length !== 1 ? "s" : ""} recorded`
+ : undefined
+ }
+ />
+
+ {loading && (
+
+
+
+
+ )}
+ {error && (
+ Error: {error}
+ )}
+
+ {!loading && !error && completed.length === 0 && (
+
+
+ No completed scans yet. Run a scan from the Dashboard.
+
+
+ )}
+
+ {!loading && !error && completed.length > 0 && (
+
+ {/* Device count trend */}
+
+
+ Devices discovered per scan
+
+
+
+
+ {/* Risk trend */}
+
+
+ Risk counts per scan
+
+
+
+
+
+ {/* Scan table */}
+
+
+
+
+
+ | Started |
+ By |
+ Duration |
+ Devices |
+ Critical |
+ High |
+ Medium |
+ Low |
+
+
+
+ {[...scans].map((s) => (
+
+ |
+ {shortDate(s.started_at)}
+ |
+ {s.triggered_by} |
+
+ {s.duration_seconds != null
+ ? `${s.duration_seconds}s`
+ : "—"}
+ |
+
+ {s.devices_found ?? "—"}
+ |
+
+ {s.risks_critical ?? "—"}
+ |
+
+ {s.risks_high ?? "—"}
+ |
+
+ {s.risks_medium ?? "—"}
+ |
+
+ {s.risks_low ?? "—"}
+ |
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts
index 3366a57..842e610 100644
--- a/frontend/src/pages/index.ts
+++ b/frontend/src/pages/index.ts
@@ -1,3 +1,4 @@
+export { HistoryPage } from "./HistoryPage";
/* Page exports */
export { DashboardPage } from "./DashboardPage";
export { DevicesPage } from "./DevicesPage";
diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts
index 275b60d..42a183b 100644
--- a/frontend/src/types/api.ts
+++ b/frontend/src/types/api.ts
@@ -32,6 +32,10 @@ export interface Scan {
duration_seconds: number | null;
devices_found: number | null;
error_message: string | null;
+ risks_critical: number | null;
+ risks_high: number | null;
+ risks_medium: number | null;
+ risks_low: number | null;
}
export interface TriggerResponse {