mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 16:08:59 +03:00
* Notice and status bar improvements Status bar problems added in the same pass got the same `Date.now()` id and overwrote each other, so usually only one showed. Messages now fall back to their text as the id. The desktop status bar shows the most severe message with a count of the rest that opens a popover listing all of them, and the mobile drawer stacks them vertically instead of placing them side by side. Dismissing a notice hid it for good, so a detector that restarted again after a dismissal was never shown. Dismiss is replaced by acknowledge, which hides a notice until it happens again, and mute, which hides it permanently. Kinds that never repeat (config and stream checks, the update notice) can only be muted. `reopen_at_count` is removed since acknowledge covers the failed login case. * move camera CPU warnings to notices High ffmpeg and detect CPU warnings sat in the status bar with no way to dismiss them. They're now `ffmpeg_high_cpu` and `detect_high_cpu` notices, raised per episode by the same tracker as skipped detections. Also stop failed login attempts held from before an acknowledgement from reopening the notice. * fix mypy and handle missing cpu stats in notices
161 lines
6.2 KiB
Python
161 lines
6.2 KiB
Python
"""Tests for the notices API."""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from frigate.models import Notice, NoticeStats
|
|
from frigate.notices.registry import NoticeRegistry
|
|
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
|
|
|
CONFIG_CHECK = "config:detect:fps-greater-than-five:camera.garage"
|
|
|
|
|
|
class TestHttpNotices(BaseTestHttp):
|
|
def setUp(self):
|
|
super().setUp([Notice, NoticeStats])
|
|
self.registry = NoticeRegistry()
|
|
self.app = self.create_app(notice_registry=self.registry)
|
|
|
|
def client(self) -> TestClient:
|
|
return AuthTestClient(self.app)
|
|
|
|
def test_get_notices_orders_by_severity(self):
|
|
self.registry.raise_notice(
|
|
"detector_stuck", scope="ov", params={"detector": "ov"}
|
|
)
|
|
self.registry.raise_notice(
|
|
"model_download_failed",
|
|
scope="yolo/model.onnx",
|
|
params={"file": "model.onnx", "error": "timeout"},
|
|
)
|
|
|
|
with self.client() as client:
|
|
response = client.get("/notices")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(
|
|
[n["id"] for n in response.json()],
|
|
["model_download_failed:yolo/model.onnx", "detector_stuck:ov"],
|
|
)
|
|
self.assertEqual(response.json()[0]["severity"], "error")
|
|
self.assertEqual(
|
|
[n["link"] for n in response.json()], [None, "/system#general"]
|
|
)
|
|
|
|
def test_get_stats(self):
|
|
self.registry.raise_notice(
|
|
"detector_stuck", scope="ov", params={"detector": "ov"}
|
|
)
|
|
|
|
with self.client() as client:
|
|
response = client.get("/notices/stats")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json()[0]["kind"], "detector_stuck")
|
|
self.assertEqual(response.json()[0]["occurrences"], 1)
|
|
|
|
def test_acknowledge_hides_and_the_hidden_list_keeps_it(self):
|
|
self.registry.raise_notice(
|
|
"detector_stuck", scope="ov", params={"detector": "ov"}
|
|
)
|
|
|
|
with self.client() as client:
|
|
response = client.post("/notices/detector_stuck:ov/acknowledge")
|
|
listed = client.get("/notices").json()
|
|
hidden = client.get("/notices", params={"include_hidden": True}).json()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(listed, [])
|
|
self.assertEqual([n["id"] for n in hidden], ["detector_stuck:ov"])
|
|
self.assertIsNotNone(hidden[0]["acknowledged_at"])
|
|
|
|
def test_acknowledging_a_check_is_404(self):
|
|
with self.client() as client:
|
|
response = client.post(f"/notices/{CONFIG_CHECK}/acknowledge")
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_mute_an_id_with_a_slash(self):
|
|
self.registry.raise_notice(
|
|
"model_download_failed", scope="yolo/model.onnx", params={}
|
|
)
|
|
|
|
with self.client() as client:
|
|
response = client.post(
|
|
"/notices/model_download_failed:yolo/model.onnx/mute"
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(self.registry.active(), [])
|
|
|
|
def test_unknown_ids_are_404(self):
|
|
with self.client() as client:
|
|
responses = [
|
|
client.post("/notices/nope/acknowledge"),
|
|
client.post("/notices/nope/mute"),
|
|
client.delete("/notices/nope/hidden"),
|
|
]
|
|
|
|
self.assertEqual([r.status_code for r in responses], [404, 404, 404])
|
|
|
|
def test_requires_admin(self):
|
|
viewer = {"remote-user": "viewer", "remote-role": "viewer"}
|
|
|
|
with TestClient(self.app) as client:
|
|
responses = [
|
|
client.get("/notices", headers=viewer),
|
|
client.get("/notices/muted_checks", headers=viewer),
|
|
client.post("/notices/detector_stuck:ov/acknowledge", headers=viewer),
|
|
client.post("/notices/detector_stuck:ov/mute", headers=viewer),
|
|
client.delete("/notices/detector_stuck:ov/hidden", headers=viewer),
|
|
client.delete("/notices/hidden", headers=viewer),
|
|
]
|
|
|
|
self.assertEqual([r.status_code for r in responses], [403] * 6)
|
|
|
|
def test_muted_checks_are_listed_once(self):
|
|
with self.client() as client:
|
|
first = client.post(f"/notices/{CONFIG_CHECK}/mute")
|
|
again = client.post(f"/notices/{CONFIG_CHECK}/mute")
|
|
listed = client.get("/notices/muted_checks").json()
|
|
hidden = client.get("/notices", params={"include_hidden": True}).json()
|
|
|
|
self.assertEqual(first.status_code, 200)
|
|
self.assertEqual(again.status_code, 200)
|
|
self.assertEqual([check["id"] for check in listed], [CONFIG_CHECK])
|
|
self.assertIsNotNone(listed[0]["muted_at"])
|
|
# the Health tab builds the row itself, so it never comes back as a notice
|
|
self.assertEqual(hidden, [])
|
|
|
|
def test_unhide_one_row(self):
|
|
self.registry.raise_notice(
|
|
"detector_stuck", scope="ov", params={"detector": "ov"}
|
|
)
|
|
self.registry.mute("detector_stuck:ov")
|
|
self.registry.mute(CONFIG_CHECK)
|
|
|
|
with self.client() as client:
|
|
notice = client.delete("/notices/detector_stuck:ov/hidden")
|
|
check = client.delete(f"/notices/{CONFIG_CHECK}/hidden")
|
|
listed = client.get("/notices").json()
|
|
checks = client.get("/notices/muted_checks").json()
|
|
|
|
self.assertEqual([notice.status_code, check.status_code], [200, 200])
|
|
self.assertEqual([n["id"] for n in listed], ["detector_stuck:ov"])
|
|
self.assertEqual(checks, [])
|
|
|
|
def test_unhide_all_shows_notices_and_drops_check_mutes(self):
|
|
self.registry.raise_notice(
|
|
"detector_stuck", scope="ov", params={"detector": "ov"}
|
|
)
|
|
self.registry.acknowledge("detector_stuck:ov")
|
|
self.registry.mute(CONFIG_CHECK)
|
|
|
|
with self.client() as client:
|
|
response = client.delete("/notices/hidden")
|
|
listed = client.get("/notices").json()
|
|
checks = client.get("/notices/muted_checks").json()
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual([n["id"] for n in listed], ["detector_stuck:ov"])
|
|
self.assertEqual(checks, [])
|