mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 09:28:59 +03:00
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:
committed by
Nicolas Mowen
parent
6d33b31bc6
commit
70ce193e09
@@ -22,6 +22,7 @@ import {
|
||||
type ApiMockOverrides,
|
||||
} from "../helpers/api-mocker";
|
||||
import { WsMocker } from "../helpers/ws-mocker";
|
||||
import { statsFactory } from "./mock-data/stats";
|
||||
import { installErrorCollector, type ErrorCollector } from "./error-collector";
|
||||
import { GLOBAL_ALLOWLIST } from "./error-allowlist";
|
||||
|
||||
@@ -53,7 +54,7 @@ export class FrigateApp {
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await this.ws.install(this.page);
|
||||
await this.ws.install(this.page, statsFactory(overrides?.stats));
|
||||
await this.api.install(overrides);
|
||||
// media goes last so its per-event routes win over the broader
|
||||
// `**/api/events**` list route, which otherwise answers thumbnail and
|
||||
|
||||
@@ -41,6 +41,13 @@ export const BASE_STATS = {
|
||||
},
|
||||
gpu_usages: {},
|
||||
npu_usages: {},
|
||||
embeddings: {
|
||||
image_embedding_speed: 0,
|
||||
face_embedding_speed: 0,
|
||||
plate_recognition_speed: 0,
|
||||
text_embedding_speed: 0,
|
||||
devices: {} as Record<string, string>,
|
||||
},
|
||||
processes: {},
|
||||
service: {
|
||||
last_updated: Date.now() / 1000,
|
||||
@@ -57,6 +64,13 @@ export const BASE_STATS = {
|
||||
used: 500000000,
|
||||
mount_type: "tmpfs",
|
||||
},
|
||||
"/dev/shm": {
|
||||
free: 98,
|
||||
total: 128,
|
||||
used: 30,
|
||||
mount_type: "tmpfs",
|
||||
min_shm: 64,
|
||||
},
|
||||
},
|
||||
uptime: 86400,
|
||||
latest_version: "0.15.0",
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import { test, expect } from "../fixtures/frigate-test";
|
||||
|
||||
const NOW = Math.floor(Date.now() / 1000);
|
||||
|
||||
// the fixture detector runs at 75.5 ms, above the live warning threshold
|
||||
const QUIET_STATS = { detectors: { cpu: { inference_speed: 10 } } };
|
||||
|
||||
const STATE_NOTICE = {
|
||||
id: "ffmpeg_crash_loop:front_door",
|
||||
kind: "ffmpeg_crash_loop",
|
||||
@@ -41,6 +44,7 @@ test.describe("System — Health tab @medium", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [STATE_NOTICE, EVENT_NOTICE],
|
||||
noticeStats: [
|
||||
{
|
||||
@@ -65,8 +69,6 @@ test.describe("System — Health tab @medium", () => {
|
||||
await expect(rows.nth(0)).toHaveAttribute("data-severity", "error");
|
||||
await expect(rows.nth(0)).toContainText("ffmpeg has crashed 6 times");
|
||||
await expect(rows.nth(0)).toContainText("14 times since");
|
||||
// the default fixture's cpu detector is slow (75.5 ms); PR 2 merges live
|
||||
// problems into this list, PR 1 does not, so no extra row here
|
||||
await expect(
|
||||
rows.nth(0).getByRole("button", { name: "Dismiss" }),
|
||||
).toHaveCount(0);
|
||||
@@ -78,7 +80,10 @@ test.describe("System — Health tab @medium", () => {
|
||||
});
|
||||
|
||||
test("dismiss posts and removes the row", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({ notices: [EVENT_NOTICE] });
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [EVENT_NOTICE],
|
||||
});
|
||||
|
||||
// the list shrinks after the dismiss so the refetch shows the row gone
|
||||
let dismissed = false;
|
||||
@@ -105,14 +110,18 @@ test.describe("System — Health tab @medium", () => {
|
||||
await expect(
|
||||
frigateApp.page.locator("[data-testid^='health-problem-']"),
|
||||
).toHaveCount(0, { timeout: 5_000 });
|
||||
await expect(frigateApp.page.getByText("No notices")).toBeVisible();
|
||||
await expect(
|
||||
frigateApp.page.getByText("Your Frigate installation is healthy"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("empty state with no notices", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults();
|
||||
await frigateApp.installDefaults({ stats: QUIET_STATS });
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(frigateApp.page.getByText("No notices")).toBeVisible({
|
||||
await expect(
|
||||
frigateApp.page.getByText("Your Frigate installation is healthy"),
|
||||
).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
@@ -121,6 +130,7 @@ test.describe("System — Health tab @medium", () => {
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [
|
||||
{
|
||||
id: "update_available",
|
||||
@@ -157,7 +167,10 @@ test.describe("System — Health tab mobile @medium @mobile", () => {
|
||||
test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
test("notices render at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({ notices: [STATE_NOTICE] });
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [STATE_NOTICE],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
@@ -167,3 +180,648 @@ test.describe("System — Health tab mobile @medium @mobile", () => {
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("System — Health hardware pane @medium", () => {
|
||||
test("detection row is ok with matching probe and fast inference", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:GPU"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:GPU": { inference_speed: 12.3 } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId("hardware-row-detection:0");
|
||||
await expect(row).toHaveAttribute("data-state", "ok", { timeout: 15_000 });
|
||||
await expect(row).toContainText("12.3 ms");
|
||||
});
|
||||
|
||||
test("detection row errors when the device is not probed", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["hailo8l"] }] },
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId("hardware-row-detection:0");
|
||||
await expect(row).toHaveAttribute("data-state", "error", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(row).toContainText("hailo8l was not found on this system");
|
||||
});
|
||||
|
||||
test("a generic device the probe cannot enumerate is judged by its runtime", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:AUTO"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:AUTO": { inference_speed: 12 } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId("hardware-row-detection:0");
|
||||
await expect(row).toHaveAttribute("data-state", "ok", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(row).toContainText("12 ms");
|
||||
});
|
||||
|
||||
test("a bare onnx detector with no probed accelerator is not an error", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// the default image runs onnx on the CPU and the probe reports nothing
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["onnx"] }] },
|
||||
stats: { ...QUIET_STATS, detectors: { onnx: { inference_speed: 40 } } },
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId("hardware-row-detection:0");
|
||||
await expect(row).toHaveAttribute("data-state", "ok", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("detection row warns on slow inference", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { models: [{ scene: "all", devices: ["openvino:GPU"] }] },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
detectors: { "openvino:GPU": { inference_speed: 60 } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId("hardware-row-detection:0");
|
||||
await expect(row).toHaveAttribute("data-state", "warning", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(row).toContainText("Inference is slow (60 ms)");
|
||||
});
|
||||
|
||||
test("hwaccel row states", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
front_door: { ffmpeg: { hwaccel_args: "preset-vaapi" } },
|
||||
backyard: { ffmpeg: { hwaccel_args: "preset-nvidia" } },
|
||||
garage: { ffmpeg: { hwaccel_args: "" } },
|
||||
},
|
||||
},
|
||||
hwaccel: {
|
||||
recommended: "vaapi",
|
||||
available: [{ key: "vaapi", presets: { any: "preset-vaapi" } }],
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("hardware-row-hwaccel:preset-vaapi"),
|
||||
).toHaveAttribute("data-state", "ok", { timeout: 15_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("hardware-row-hwaccel:preset-nvidia"),
|
||||
).toHaveAttribute("data-state", "warning");
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("hardware-row-hwaccel:preset-nvidia"),
|
||||
).toContainText("the hardware probe did not report it");
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("hardware-row-hwaccel:"),
|
||||
).toHaveAttribute("data-state", "warning");
|
||||
});
|
||||
|
||||
test("face recognition row reflects the runtime device", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true } },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
embeddings: { devices: { face_recognition: "CPU" } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"hardware-row-enrichment:face_recognition",
|
||||
);
|
||||
await expect(row).toHaveAttribute("data-state", "warning", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(row).toContainText(
|
||||
"Running on the CPU although an accelerator is available",
|
||||
);
|
||||
});
|
||||
|
||||
test("explicit GPU that loaded on CUDA is ok despite the probe", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// the fixture probes an Intel GPU only; ONNX Runtime still puts a GPU
|
||||
// request on CUDA when that image has it
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true, device: "GPU" } },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
embeddings: { devices: { face_recognition: "CUDA" } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"hardware-row-enrichment:face_recognition",
|
||||
);
|
||||
await expect(row).toHaveAttribute("data-state", "ok", { timeout: 15_000 });
|
||||
await expect(row).toContainText("CUDA");
|
||||
});
|
||||
|
||||
test("explicit GPU falling back to CPU is an error", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true, device: "GPU" } },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
embeddings: { devices: { face_recognition: "CPU" } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"hardware-row-enrichment:face_recognition",
|
||||
);
|
||||
await expect(row).toHaveAttribute("data-state", "error", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("enrichment without a runtime device is unknown", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true } },
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"hardware-row-enrichment:face_recognition",
|
||||
);
|
||||
await expect(row).toHaveAttribute("data-state", "unknown", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("all-excellent camera connections show a green check", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({ stats: QUIET_STATS });
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const line = frigateApp.page.getByText(
|
||||
"All cameras have an excellent connection.",
|
||||
);
|
||||
await expect(line).toBeVisible({ timeout: 15_000 });
|
||||
await expect(line.locator("svg")).toHaveClass(/text-success/);
|
||||
});
|
||||
|
||||
test("camera connections lists only non-excellent cameras", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
cameras: {
|
||||
backyard: { connection_quality: "poor", camera_fps: 2.1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("camera-connection-backyard"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByTestId("camera-connection-front_door"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("System — Health notices sources @medium", () => {
|
||||
test("live offline camera and config checks render with links", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
garage: { detect: { width: 2560, height: 1440, fps: 10 } },
|
||||
},
|
||||
},
|
||||
stats: { ...QUIET_STATS, cameras: { front_door: { camera_fps: 0 } } },
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const rows = frigateApp.page.locator("[data-testid^='health-problem-']");
|
||||
await expect(rows.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(rows.filter({ hasText: "Front Door is offline" })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByText(
|
||||
"This detect resolution is higher than recommended",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
rows
|
||||
.filter({ hasText: "Front Door is offline" })
|
||||
.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/logs");
|
||||
const fpsRow = frigateApp.page.getByTestId(
|
||||
"health-problem-config:detect:fps-greater-than-five:garage",
|
||||
);
|
||||
await expect(fpsRow).toHaveAttribute("data-severity", "info");
|
||||
await expect(
|
||||
fpsRow.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/settings?page=cameraDetect&camera=garage");
|
||||
});
|
||||
|
||||
// the fixture's cameras all have a record role with recording off, so
|
||||
// dropping the role isolates the gate on record.enabled
|
||||
const NO_RECORD_ROLE = {
|
||||
ffmpeg: { inputs: [{ path: "rtsp://x", roles: ["detect"] }] },
|
||||
};
|
||||
|
||||
test("record role warning is hidden while recording is off", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
front_door: { ...NO_RECORD_ROLE, record: { enabled: false } },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
await expect(frigateApp.page.getByLabel("Select health")).toHaveAttribute(
|
||||
"data-state",
|
||||
"on",
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
await expect(
|
||||
frigateApp.page.getByText("No streams have the record role defined"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("record role warning shows once recording is on", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
front_door: { ...NO_RECORD_ROLE, record: { enabled: true } },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
await expect(
|
||||
frigateApp.page.getByText("No streams have the record role defined"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("a global config problem is not repeated per camera", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// every fixture camera inherits the global size, with resolved defaults
|
||||
// the global block leaves null, so only text equality can dedupe them
|
||||
const size = { width: 2560, height: 1440 };
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
detect: size,
|
||||
cameras: {
|
||||
front_door: { detect: size },
|
||||
backyard: { detect: size },
|
||||
garage: { detect: size },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const rows = frigateApp.page.locator(
|
||||
"[data-testid^='health-problem-config:detect:detect-resolution-high']",
|
||||
);
|
||||
await expect(rows.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first()).toHaveAttribute(
|
||||
"data-testid",
|
||||
"health-problem-config:detect:detect-resolution-high:global",
|
||||
);
|
||||
});
|
||||
|
||||
test("a camera that overrides the global value keeps its own row", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// two cameras inherit the global size and are folded into the global
|
||||
// row; the one that overrides it keeps a row with its own link
|
||||
const size = { width: 2560, height: 1440 };
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
detect: size,
|
||||
cameras: {
|
||||
front_door: { detect: size },
|
||||
backyard: { detect: size },
|
||||
garage: { detect: { width: 3840, height: 2160 } },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const rows = frigateApp.page.locator(
|
||||
"[data-testid^='health-problem-config:detect:detect-resolution-high']",
|
||||
);
|
||||
await expect(rows.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(
|
||||
frigateApp.page.getByTestId(
|
||||
"health-problem-config:detect:detect-resolution-high:garage",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("registry rows sort ahead of live rows and keep Dismiss", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
notices: [
|
||||
{
|
||||
id: "detector_stuck:cpu",
|
||||
kind: "detector_stuck",
|
||||
mode: "event",
|
||||
severity: "warning",
|
||||
category: "detector",
|
||||
scope: "cpu",
|
||||
params: { detector: "cpu" },
|
||||
first_seen: NOW - 60,
|
||||
last_seen: NOW - 60,
|
||||
count: 1,
|
||||
dismissed_at: null,
|
||||
},
|
||||
],
|
||||
// the default fixture's slow cpu detector is the live warning here
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const rows = frigateApp.page.locator("[data-severity='warning']");
|
||||
await expect(rows.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(rows.nth(0)).toContainText("Detector cpu was restarted");
|
||||
await expect(
|
||||
rows.nth(0).getByRole("button", { name: "Dismiss" }),
|
||||
).toBeVisible();
|
||||
await expect(rows.nth(1)).toContainText("Cpu is slow");
|
||||
});
|
||||
|
||||
test("empty state when stats, config, and registry are clean", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({ stats: QUIET_STATS });
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText("Your Frigate installation is healthy"),
|
||||
).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.locator("[data-testid^='health-problem-']"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("stream checks probe every camera and flag non-AAC audio", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
// the default record preset transcodes to AAC, so the codec only
|
||||
// matters for a camera that copies audio through
|
||||
config: {
|
||||
cameras: {
|
||||
backyard: {
|
||||
ffmpeg: {
|
||||
output_args: { record: "preset-record-generic-audio-copy" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ffprobe: {
|
||||
backyard: [
|
||||
{
|
||||
return_code: 0,
|
||||
stderr: "",
|
||||
stdout: {
|
||||
streams: [
|
||||
{
|
||||
codec_type: "video",
|
||||
codec_name: "h264",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
{ codec_type: "audio", codec_name: "pcm_mulaw" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
garage: [
|
||||
{
|
||||
return_code: 1,
|
||||
// the backend sends every line; the tab shows the last one
|
||||
stderr: [
|
||||
"[tcp @ 0x1] Connection to tcp://10.0.0.3:554 failed",
|
||||
"Connection refused",
|
||||
],
|
||||
stdout: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const requests: string[] = [];
|
||||
frigateApp.page.on("request", (req) => {
|
||||
if (req.url().includes("/api/ffprobe")) {
|
||||
requests.push(req.url());
|
||||
}
|
||||
});
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Run stream checks" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText("Stream 1: The AAC audio codec is required"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
frigateApp.page.getByText(
|
||||
"Stream 1 could not be probed: Connection refused",
|
||||
),
|
||||
).toBeVisible();
|
||||
const streams = frigateApp.page.getByTestId("camera-streams");
|
||||
await expect(streams).toContainText("3 cameras checked");
|
||||
await expect(streams).toContainText("2 with problems");
|
||||
await expect(frigateApp.page.getByText(/^Checked/)).toBeVisible();
|
||||
await expect(
|
||||
frigateApp.page.getByRole("button", { name: "Run again" }),
|
||||
).toBeVisible();
|
||||
// axios leaves ":" unescaped in query strings
|
||||
expect(requests.filter((u) => u.includes("paths=camera:")).length).toBe(3);
|
||||
});
|
||||
|
||||
test("non-AAC audio is fine when recordings transcode to AAC", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
ffprobe: {
|
||||
backyard: [
|
||||
{
|
||||
return_code: 0,
|
||||
stderr: "",
|
||||
stdout: {
|
||||
streams: [
|
||||
{
|
||||
codec_type: "video",
|
||||
codec_name: "h264",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
{ codec_type: "audio", codec_name: "pcm_mulaw" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Run stream checks" })
|
||||
.click();
|
||||
|
||||
await expect(frigateApp.page.getByText(/^Checked/)).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.getByText("The AAC audio codec is required"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("wizard-only Reolink advice stays off the tab", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
front_door: {
|
||||
ffmpeg: {
|
||||
inputs: [
|
||||
{
|
||||
path: "rtsp://10.0.0.1:554/h264Preview_01_main",
|
||||
roles: ["detect", "record"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Run stream checks" })
|
||||
.click();
|
||||
|
||||
await expect(frigateApp.page.getByText(/^Checked/)).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.getByText("Reolink RTSP is not recommended"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("re-check re-probes the hardware and dates the probe", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({ stats: QUIET_STATS });
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const probes: string[] = [];
|
||||
frigateApp.page.on("request", (req) => {
|
||||
if (req.url().includes("refresh=true")) {
|
||||
probes.push(req.url());
|
||||
}
|
||||
});
|
||||
|
||||
const recheck = frigateApp.page.getByRole("button", {
|
||||
name: "Re-check hardware",
|
||||
});
|
||||
await expect(recheck).toBeVisible({ timeout: 15_000 });
|
||||
await expect(frigateApp.page.getByText(/^Probed/)).toHaveCount(0);
|
||||
await recheck.click();
|
||||
await expect(frigateApp.page.getByText(/^Probed/)).toBeVisible();
|
||||
await expect(recheck).toBeEnabled();
|
||||
expect(probes.length).toBe(1);
|
||||
});
|
||||
|
||||
test("a camera notice link opens that camera's settings page", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// garage is not the camera Settings would pick on its own, so a wrong
|
||||
// selection here is visible rather than accidentally right
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
lpr: { enabled: false },
|
||||
cameras: { garage: { lpr: { enabled: true } } },
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-config:lpr:global-disabled:garage",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
row.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/settings?page=cameraLpr&camera=garage");
|
||||
|
||||
await row.getByRole("link", { name: "Open settings" }).click();
|
||||
|
||||
await expect(frigateApp.page.getByLabel("Select a camera")).toContainText(
|
||||
"Garage",
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
});
|
||||
|
||||
test("status bar healthy text links to the Health tab", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test.skip(frigateApp.isMobile, "Status bar is desktop-only");
|
||||
await frigateApp.installDefaults({ stats: QUIET_STATS });
|
||||
await frigateApp.goto("/");
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("link", { name: "System is healthy" })
|
||||
.click();
|
||||
await expect(frigateApp.page).toHaveURL(/\/system#health/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user