mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 10:56:52 +03:00
Refactor Notices and System Health pane (#24243)
* refactor notices * show startup message for enrichments in health pane * tweaks
This commit is contained in:
committed by
Nicolas Mowen
parent
5cef6823a6
commit
7bc32fd4c9
@@ -16,6 +16,7 @@ function cameraStats(_name: string) {
|
||||
pid: 102,
|
||||
process_fps: 5.0,
|
||||
skipped_fps: 0,
|
||||
skipped_pct: 0,
|
||||
connection_quality: "excellent" as const,
|
||||
expected_fps: 5,
|
||||
reconnects_last_hour: 0,
|
||||
@@ -75,6 +76,7 @@ export const BASE_STATS = {
|
||||
uptime: 86400,
|
||||
latest_version: "0.15.0",
|
||||
version: "0.15.0-test",
|
||||
retention_unmet: false,
|
||||
},
|
||||
camera_fps: 15.0,
|
||||
process_fps: 15.0,
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface ApiMockOverrides {
|
||||
};
|
||||
users?: { username: string; role: string }[];
|
||||
notices?: unknown[];
|
||||
noticeStats?: unknown[];
|
||||
dismissedChecks?: unknown[];
|
||||
/** camera name to the ffprobe entries returned for `paths=camera:<name>` */
|
||||
ffprobe?: Record<string, unknown[]>;
|
||||
}
|
||||
@@ -234,13 +234,12 @@ export class ApiMocker {
|
||||
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.
|
||||
// Notices
|
||||
await this.page.route("**/api/notices", (route) =>
|
||||
route.fulfill({ json: overrides?.notices ?? [] }),
|
||||
);
|
||||
await this.page.route("**/api/notices/stats", (route) =>
|
||||
route.fulfill({ json: overrides?.noticeStats ?? [] }),
|
||||
await this.page.route("**/api/notices/dismissed_checks", (route) =>
|
||||
route.fulfill({ json: overrides?.dismissedChecks ?? [] }),
|
||||
);
|
||||
|
||||
// Users. GET lists them; POST/PUT (create, password) just succeed, so
|
||||
|
||||
@@ -5,34 +5,35 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import { viewerProfile } from "../fixtures/mock-data/profile";
|
||||
|
||||
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",
|
||||
mode: "state",
|
||||
const ERROR_NOTICE = {
|
||||
id: "model_download_failed:yolo/model.onnx",
|
||||
kind: "model_download_failed",
|
||||
severity: "error",
|
||||
category: "camera",
|
||||
scope: "front_door",
|
||||
params: { restarts: 6 },
|
||||
category: "model",
|
||||
scope: "yolo/model.onnx",
|
||||
params: { file: "model.onnx", model: "yolo", error: "timeout" },
|
||||
link: null,
|
||||
first_seen: NOW - 600,
|
||||
last_seen: NOW,
|
||||
count: 1,
|
||||
count: 2,
|
||||
dismissed_at: null,
|
||||
};
|
||||
|
||||
const EVENT_NOTICE = {
|
||||
id: "detector_stuck",
|
||||
kind: "detector_stuck",
|
||||
mode: "event",
|
||||
severity: "warning",
|
||||
category: "detector",
|
||||
scope: null,
|
||||
params: { detector: "ov" },
|
||||
link: "/system#general",
|
||||
first_seen: NOW - 7200,
|
||||
last_seen: NOW - 60,
|
||||
count: 3,
|
||||
@@ -45,16 +46,7 @@ test.describe("System — Health tab @medium", () => {
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [STATE_NOTICE, EVENT_NOTICE],
|
||||
noticeStats: [
|
||||
{
|
||||
kind: "ffmpeg_crash_loop",
|
||||
occurrences: 14,
|
||||
dismissals: 0,
|
||||
first_seen: NOW - 86400 * 20,
|
||||
last_seen: NOW,
|
||||
},
|
||||
],
|
||||
notices: [ERROR_NOTICE, EVENT_NOTICE],
|
||||
});
|
||||
await frigateApp.goto("/system");
|
||||
|
||||
@@ -67,11 +59,13 @@ test.describe("System — Health tab @medium", () => {
|
||||
const rows = frigateApp.page.locator("[data-testid^='health-problem-']");
|
||||
await expect(rows).toHaveCount(2);
|
||||
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");
|
||||
await expect(rows.nth(0)).toContainText(
|
||||
"Downloading model.onnx for yolo failed: timeout",
|
||||
);
|
||||
await expect(rows.nth(0)).toContainText("2 times");
|
||||
await expect(
|
||||
rows.nth(0).getByRole("button", { name: "Dismiss" }),
|
||||
).toHaveCount(0);
|
||||
).toBeVisible();
|
||||
await expect(rows.nth(1)).toContainText("Detector ov was restarted");
|
||||
await expect(rows.nth(1)).toContainText("3 times");
|
||||
await expect(
|
||||
@@ -104,7 +98,10 @@ test.describe("System — Health tab @medium", () => {
|
||||
req.url().includes("/api/notices/detector_stuck/dismiss") &&
|
||||
req.method() === "POST",
|
||||
);
|
||||
await frigateApp.page.getByRole("button", { name: "Dismiss" }).click();
|
||||
await frigateApp.page
|
||||
.getByTestId("health-problem-notice:detector_stuck")
|
||||
.getByRole("button", { name: "Dismiss" })
|
||||
.click();
|
||||
await request;
|
||||
|
||||
await expect(
|
||||
@@ -133,13 +130,13 @@ test.describe("System — Health tab @medium", () => {
|
||||
stats: QUIET_STATS,
|
||||
notices: [
|
||||
{
|
||||
id: "update_available",
|
||||
id: "update_available:0.19.0",
|
||||
kind: "update_available",
|
||||
mode: "state",
|
||||
severity: "info",
|
||||
category: "system",
|
||||
scope: null,
|
||||
scope: "0.19.0",
|
||||
params: { version: "0.19.0" },
|
||||
link: "https://github.com/blakeblackshear/frigate/releases/tag/v0.19.0",
|
||||
first_seen: NOW - 3600,
|
||||
last_seen: NOW,
|
||||
count: 1,
|
||||
@@ -150,7 +147,7 @@ test.describe("System — Health tab @medium", () => {
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-notice:update_available",
|
||||
"health-problem-notice:update_available:0.19.0",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(row).toHaveAttribute("data-severity", "info");
|
||||
@@ -159,7 +156,229 @@ test.describe("System — Health tab @medium", () => {
|
||||
"href",
|
||||
"https://github.com/blakeblackshear/frigate/releases/tag/v0.19.0",
|
||||
);
|
||||
await expect(row.getByRole("button", { name: "Dismiss" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("the filter shows dismissed notices without a Dismiss button", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [EVENT_NOTICE],
|
||||
});
|
||||
await frigateApp.page.route(
|
||||
(url) =>
|
||||
url.pathname.endsWith("/api/notices") &&
|
||||
url.searchParams.get("include_dismissed") === "true",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: [EVENT_NOTICE, { ...ERROR_NOTICE, dismissed_at: NOW - 120 }],
|
||||
}),
|
||||
);
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await frigateApp.page.getByRole("button", { name: "Filter" }).click();
|
||||
const showDismissed = frigateApp.page.getByRole("switch", {
|
||||
name: "Show dismissed",
|
||||
});
|
||||
await expect(showDismissed).toHaveAttribute("aria-checked", "false");
|
||||
await showDismissed.click();
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-notice:model_download_failed:yolo/model.onnx",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(row).toContainText("Dismissed");
|
||||
await expect(row.getByRole("button", { name: "Dismiss" })).toHaveCount(0);
|
||||
|
||||
await showDismissed.click();
|
||||
await expect(row).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("clear dismissed deletes the dismissed rows after confirming", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [EVENT_NOTICE],
|
||||
});
|
||||
|
||||
// the history loses its dismissed row once the DELETE lands
|
||||
let cleared = false;
|
||||
await frigateApp.page.route(
|
||||
(url) =>
|
||||
url.pathname.endsWith("/api/notices") &&
|
||||
url.searchParams.get("include_dismissed") === "true",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: cleared
|
||||
? [EVENT_NOTICE]
|
||||
: [EVENT_NOTICE, { ...ERROR_NOTICE, dismissed_at: NOW - 120 }],
|
||||
}),
|
||||
);
|
||||
await frigateApp.page.route("**/api/notices/dismissed", (route) => {
|
||||
cleared = true;
|
||||
return route.fulfill({ json: { success: true } });
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await frigateApp.page.getByRole("button", { name: "Filter" }).click();
|
||||
await frigateApp.page
|
||||
.getByRole("switch", { name: "Show dismissed" })
|
||||
.click();
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-notice:model_download_failed:yolo/model.onnx",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await frigateApp.page.keyboard.press("Escape");
|
||||
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Clear dismissed" })
|
||||
.click();
|
||||
const request = frigateApp.page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().endsWith("/api/notices/dismissed") &&
|
||||
req.method() === "DELETE",
|
||||
);
|
||||
await frigateApp.page
|
||||
.getByRole("alertdialog")
|
||||
.getByRole("button", { name: "Clear dismissed" })
|
||||
.click();
|
||||
await request;
|
||||
|
||||
await expect(row).toHaveCount(0);
|
||||
await expect(
|
||||
frigateApp.page.getByText("No dismissed notices"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("severity switches hide notices of that severity", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [ERROR_NOTICE, EVENT_NOTICE],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const rows = frigateApp.page.locator("[data-testid^='health-problem-']");
|
||||
await expect(rows).toHaveCount(2, { timeout: 15_000 });
|
||||
|
||||
await frigateApp.page.getByRole("button", { name: "Filter" }).click();
|
||||
await frigateApp.page.getByRole("switch", { name: "Warning" }).click();
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first()).toHaveAttribute("data-severity", "error");
|
||||
|
||||
await frigateApp.page.getByRole("switch", { name: "Error" }).click();
|
||||
await expect(rows).toHaveCount(0);
|
||||
await expect(
|
||||
frigateApp.page.getByText("No notices match the filter"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("failed login bursts are per user and show their attempt count", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
const burst = (user: string, start: number, count: number) => ({
|
||||
id: `failed_login:${user}:${start}`,
|
||||
kind: "failed_login",
|
||||
severity: "warning",
|
||||
category: "system",
|
||||
scope: `${user}:${start}`,
|
||||
params: { user },
|
||||
link: "/logs",
|
||||
first_seen: start,
|
||||
last_seen: start + 60,
|
||||
count,
|
||||
dismissed_at: null,
|
||||
});
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [burst("admin", NOW - 60, 7), burst("ghost", NOW - 3600, 1)],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const attack = frigateApp.page.getByTestId(
|
||||
`health-problem-notice:failed_login:admin:${NOW - 60}`,
|
||||
);
|
||||
await expect(attack).toBeVisible({ timeout: 15_000 });
|
||||
await expect(attack).toContainText("Failed login attempts for admin");
|
||||
await expect(attack).toContainText("7 times");
|
||||
await expect(attack).not.toContainText(String(NOW - 60));
|
||||
await expect(
|
||||
attack.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/logs");
|
||||
await expect(
|
||||
frigateApp.page.getByTestId(
|
||||
`health-problem-notice:failed_login:ghost:${NOW - 3600}`,
|
||||
),
|
||||
).toContainText("Failed login attempt for ghost");
|
||||
});
|
||||
|
||||
test("skipped detections notice links to camera stats", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [
|
||||
{
|
||||
id: "skipped_detections:front_door",
|
||||
kind: "skipped_detections",
|
||||
severity: "warning",
|
||||
category: "camera",
|
||||
scope: "front_door",
|
||||
params: { pct: 12.5 },
|
||||
link: "/system#cameras",
|
||||
first_seen: NOW - 600,
|
||||
last_seen: NOW - 600,
|
||||
count: 1,
|
||||
dismissed_at: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-notice:skipped_detections:front_door",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(row).toContainText("skipped 12.5% of frames");
|
||||
await expect(
|
||||
row.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/system#cameras");
|
||||
});
|
||||
|
||||
test("shm notice links to storage metrics", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [
|
||||
{
|
||||
id: "shm_too_low",
|
||||
kind: "shm_too_low",
|
||||
severity: "warning",
|
||||
category: "system",
|
||||
scope: null,
|
||||
params: { total: 64, min: 180 },
|
||||
link: "/system#storage",
|
||||
first_seen: NOW - 600,
|
||||
last_seen: NOW - 600,
|
||||
count: 1,
|
||||
dismissed_at: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-notice:shm_too_low",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(row).toContainText(
|
||||
"/dev/shm allocation (64 MB) should be increased to at least 180 MB",
|
||||
);
|
||||
await expect(
|
||||
row.getByRole("link", { name: "Open settings" }),
|
||||
).toHaveAttribute("href", "/system#storage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,13 +388,13 @@ test.describe("System — Health tab mobile @medium @mobile", () => {
|
||||
test("notices render at mobile viewport", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [STATE_NOTICE],
|
||||
notices: [ERROR_NOTICE],
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByTestId(
|
||||
"health-problem-notice:ffmpeg_crash_loop:front_door",
|
||||
"health-problem-notice:model_download_failed:yolo/model.onnx",
|
||||
),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
@@ -298,14 +517,17 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
).toHaveAttribute("data-state", "warning");
|
||||
});
|
||||
|
||||
test("face recognition row reflects the runtime device", async ({
|
||||
test("slow face recognition on the CPU warns when an accelerator exists", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true } },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
embeddings: { devices: { face_recognition: "CPU" } },
|
||||
embeddings: {
|
||||
face_recognition_speed: 800,
|
||||
devices: { face_recognition: "CPU" },
|
||||
},
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
@@ -321,6 +543,27 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("fast face recognition on the CPU is ok", async ({ frigateApp }) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: { face_recognition: { enabled: true } },
|
||||
stats: {
|
||||
...QUIET_STATS,
|
||||
embeddings: {
|
||||
face_recognition_speed: 40,
|
||||
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", "ok", { timeout: 15_000 });
|
||||
await expect(row).toContainText("CPU");
|
||||
await expect(row).not.toContainText("although an accelerator");
|
||||
});
|
||||
|
||||
test("explicit GPU that loaded on CUDA is ok despite the probe", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
@@ -415,36 +658,24 @@ test.describe("System — Health hardware pane @medium", () => {
|
||||
});
|
||||
|
||||
test.describe("System — Health notices sources @medium", () => {
|
||||
test("live offline camera and config checks render with links", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test("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 } } },
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
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");
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
const fpsRow = frigateApp.page.getByTestId(
|
||||
"health-problem-config:detect:fps-greater-than-five:garage",
|
||||
"health-problem-config:detect:fps-greater-than-five:camera.garage",
|
||||
);
|
||||
await expect(fpsRow).toHaveAttribute("data-severity", "info");
|
||||
await expect(
|
||||
@@ -452,6 +683,28 @@ test.describe("System — Health notices sources @medium", () => {
|
||||
).toHaveAttribute("href", "/settings?page=cameraDetect&camera=garage");
|
||||
});
|
||||
|
||||
test("status bar problems stay out of the list", async ({ frigateApp }) => {
|
||||
test.skip(frigateApp.isMobile, "Status bar is desktop-only");
|
||||
await frigateApp.installDefaults({
|
||||
stats: {
|
||||
service: { retention_unmet: true },
|
||||
cameras: { front_door: { camera_fps: 0 } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
// the status bar shows a problem, so stats have loaded
|
||||
await expect(
|
||||
frigateApp.page.getByText("Front Door is offline"),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
frigateApp.page.locator("[data-testid^='health-problem-']"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
frigateApp.page.getByText("Your Frigate installation is healthy"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// 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 = {
|
||||
@@ -553,43 +806,11 @@ test.describe("System — Health notices sources @medium", () => {
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(
|
||||
frigateApp.page.getByTestId(
|
||||
"health-problem-config:detect:detect-resolution-high:garage",
|
||||
"health-problem-config:detect:detect-resolution-high:camera.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,
|
||||
}) => {
|
||||
@@ -797,7 +1018,7 @@ test.describe("System — Health notices sources @medium", () => {
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(
|
||||
"health-problem-config:lpr:global-disabled:garage",
|
||||
"health-problem-config:lpr:global-disabled:camera.garage",
|
||||
);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
@@ -824,4 +1045,120 @@ test.describe("System — Health notices sources @medium", () => {
|
||||
.click();
|
||||
await expect(frigateApp.page).toHaveURL(/\/system#health/);
|
||||
});
|
||||
|
||||
test("status bar counts undismissed notices next to the health text", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test.skip(frigateApp.isMobile, "Status bar is desktop-only");
|
||||
await frigateApp.installDefaults({
|
||||
stats: QUIET_STATS,
|
||||
notices: [EVENT_NOTICE],
|
||||
});
|
||||
await frigateApp.goto("/");
|
||||
|
||||
const notices = frigateApp.page.getByRole("link", {
|
||||
name: "1 system notice",
|
||||
});
|
||||
await expect(notices).toBeVisible({ timeout: 15_000 });
|
||||
await expect(notices).toHaveAttribute("href", "/system#health");
|
||||
await expect(
|
||||
frigateApp.page.getByRole("link", { name: "System is healthy" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("status bar shows viewers no problems or health text", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test.skip(frigateApp.isMobile, "Status bar is desktop-only");
|
||||
await frigateApp.installDefaults({
|
||||
profile: viewerProfile(),
|
||||
stats: {
|
||||
cpu_usages: { "frigate.full_system": { cpu: "12.0" } },
|
||||
cameras: { front_door: { camera_fps: 0 } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto("/");
|
||||
|
||||
// the CPU reading comes from the same stats as the offline problem
|
||||
await expect(frigateApp.page.getByText("CPU 12%")).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.getByText("Front Door is offline"),
|
||||
).toHaveCount(0);
|
||||
await expect(frigateApp.page.getByText("System is healthy")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a config row can be dismissed", async ({ frigateApp }) => {
|
||||
const id = "config:detect:fps-greater-than-five:camera.garage";
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
garage: { detect: { width: 2560, height: 1440, fps: 10 } },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
});
|
||||
|
||||
// the list gains the dismissal after the POST so the refetch hides the row
|
||||
let dismissed = false;
|
||||
await frigateApp.page.route(`**/api/notices/${id}/dismiss`, (route) => {
|
||||
dismissed = true;
|
||||
return route.fulfill({ json: { success: true } });
|
||||
});
|
||||
await frigateApp.page.route("**/api/notices/dismissed_checks", (route) =>
|
||||
route.fulfill({ json: dismissed ? [{ id, dismissed_at: NOW }] : [] }),
|
||||
);
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
const row = frigateApp.page.getByTestId(`health-problem-${id}`);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
const request = frigateApp.page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().includes(`/api/notices/${id}/dismiss`) &&
|
||||
req.method() === "POST",
|
||||
);
|
||||
await row.getByRole("button", { name: "Dismiss" }).click();
|
||||
await request;
|
||||
await expect(row).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dismissed config rows move to the dismissed list", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
const id = "config:detect:fps-greater-than-five:camera.garage";
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: {
|
||||
garage: { detect: { width: 2560, height: 1440, fps: 10 } },
|
||||
},
|
||||
},
|
||||
stats: QUIET_STATS,
|
||||
dismissedChecks: [{ id, dismissed_at: NOW - 120 }],
|
||||
});
|
||||
await frigateApp.page.route(
|
||||
(url) =>
|
||||
url.pathname.endsWith("/api/notices") &&
|
||||
url.searchParams.get("include_dismissed") === "true",
|
||||
(route) => route.fulfill({ json: [] }),
|
||||
);
|
||||
await frigateApp.goto("/system#health");
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText(
|
||||
"This detect resolution is higher than recommended",
|
||||
),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
const row = frigateApp.page.getByTestId(`health-problem-${id}`);
|
||||
await expect(row).toHaveCount(0);
|
||||
|
||||
await frigateApp.page.getByRole("button", { name: "Filter" }).click();
|
||||
await frigateApp.page
|
||||
.getByRole("switch", { name: "Show dismissed" })
|
||||
.click();
|
||||
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row).toContainText("Dismissed");
|
||||
await expect(row.getByRole("button", { name: "Dismiss" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user