Improve System Health pane (#24188)

* build out system health pane

* tweaks

* fixes

* fix notice link so it opens the correct camera

* tweak language
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent 6d33b31bc6
commit 70ce193e09
57 changed files with 3965 additions and 464 deletions
+31
View File
@@ -50,8 +50,29 @@ export interface ApiMockOverrides {
users?: { username: string; role: string }[];
notices?: unknown[];
noticeStats?: unknown[];
/** camera name to the ffprobe entries returned for `paths=camera:<name>` */
ffprobe?: Record<string, unknown[]>;
}
export const FFPROBE_OK = [
{
return_code: 0,
stderr: "",
stdout: {
streams: [
{
codec_type: "video",
codec_name: "h264",
width: 1920,
height: 1080,
avg_frame_rate: "15/1",
},
{ codec_type: "audio", codec_name: "aac" },
],
},
},
];
export class ApiMocker {
private page: Page;
@@ -203,6 +224,16 @@ export class ApiMocker {
}),
);
// ffprobe. The Health tab's stream checks probe `camera:<name>`; the
// wizard probes raw URLs. Both get a healthy h264 + aac answer by default.
await this.page.route("**/api/ffprobe**", (route) => {
const url = new URL(route.request().url());
const paths = url.searchParams.get("paths") ?? "";
const camera = paths.startsWith("camera:") ? paths.slice(7) : undefined;
const entries = (camera && overrides?.ffprobe?.[camera]) || FFPROBE_OK;
return route.fulfill({ json: entries });
});
// Notices. The stats route is registered after the list route so it wins
// for /api/notices/stats; the list glob does not match a sub-path anyway.
await this.page.route("**/api/notices", (route) =>
+35 -29
View File
@@ -12,12 +12,16 @@ import { cameraActivityPayload } from "../fixtures/mock-data/camera-activity";
export class WsMocker {
private mockWs: WebSocketRoute | null = null;
private cameras: string[];
// the live stats payload wins over the REST one in useAutoFrigateStats, so
// both come from the same factory or a test's `stats` override is ignored
private stats: unknown;
constructor(cameras: string[] = ["front_door", "backyard", "garage"]) {
this.cameras = cameras;
}
async install(page: Page) {
async install(page: Page, stats?: unknown) {
this.stats = stats;
await page.routeWebSocket("**/ws", (ws) => {
this.mockWs = ws;
@@ -42,35 +46,37 @@ export class WsMocker {
// Send initial stats
this.send(
"stats",
JSON.stringify({
cameras: Object.fromEntries(
this.cameras.map((c) => [
c,
{
camera_fps: 5,
detection_fps: 5,
process_fps: 5,
skipped_fps: 0,
detection_enabled: 1,
connection_quality: "excellent",
},
]),
),
service: {
last_updated: Date.now() / 1000,
uptime: 86400,
version: "0.15.0-test",
latest_version: "0.15.0",
storage: {},
JSON.stringify(
this.stats ?? {
cameras: Object.fromEntries(
this.cameras.map((c) => [
c,
{
camera_fps: 5,
detection_fps: 5,
process_fps: 5,
skipped_fps: 0,
detection_enabled: 1,
connection_quality: "excellent",
},
]),
),
service: {
last_updated: Date.now() / 1000,
uptime: 86400,
version: "0.15.0-test",
latest_version: "0.15.0",
storage: {},
},
detectors: {},
cpu_usages: {},
gpu_usages: {},
camera_fps: 15,
process_fps: 15,
skipped_fps: 0,
detection_fps: 15,
},
detectors: {},
cpu_usages: {},
gpu_usages: {},
camera_fps: 15,
process_fps: 15,
skipped_fps: 0,
detection_fps: 15,
}),
),
);
}