Add a notice registry and System Health tab (#24178)

* add a notice registry and System Health tab

Problems Frigate detects on its own (ffmpeg crash loops, stuck detectors, failed model downloads, recordings deleted before their retention period) only ever existed as log lines. This adds a `NoticeRegistry` in the main process backed by two tables, an `update_notice` IPC topic so producers in other processes can reach it through the dispatcher, an admin-only API and websocket topic, and a Health tab that lists them. Kinds declare their own mode, severity, and category in one place: state notices are resolved by their producer, event notices are dismissed by the user.

* treat a prerelease as behind its final release

* fix notices clearing early

* rename menu items and update docs

* don't resolve the update notice on a failed version lookup
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent af60d2db48
commit f3a31e2fb4
55 changed files with 2346 additions and 117 deletions
+3
View File
@@ -1301,6 +1301,9 @@ async def delete_camera(
if request.app.dispatcher is not None:
request.app.dispatcher.clear_runtime_state_for_camera(camera_name)
if request.app.notice_registry is not None:
request.app.notice_registry.resolve_camera(camera_name)
# Publish removal to stop ffmpeg processes and clean up runtime state
request.app.config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, camera_name),
+1
View File
@@ -13,6 +13,7 @@ class Tags(Enum):
logs = "Logs"
media = "Media"
motion_search = "Motion Search"
notices = "Notices"
notifications = "Notifications"
preview = "Preview"
recordings = "Recordings"
+5
View File
@@ -24,6 +24,7 @@ from frigate.api import (
hardware,
media,
motion_search,
notices,
notification,
preview,
record,
@@ -41,6 +42,7 @@ from frigate.config.profile_manager import ProfileManager
from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog
from frigate.embeddings import EmbeddingsContext
from frigate.genai import GenAIClientManager
from frigate.notices.registry import NoticeRegistry
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer
@@ -77,6 +79,7 @@ def create_fastapi_app(
profile_manager: ProfileManager | None = None,
enforce_default_admin: bool = True,
config_holder: ConfigHolder | None = None,
notice_registry: NoticeRegistry | None = None,
):
logger.info("Starting FastAPI app")
app = FastAPI(
@@ -147,6 +150,7 @@ def create_fastapi_app(
app.include_router(notification.router)
app.include_router(export.router)
app.include_router(hardware.router)
app.include_router(notices.router)
app.include_router(event.router)
app.include_router(media.router)
app.include_router(motion_search.router)
@@ -163,6 +167,7 @@ def create_fastapi_app(
app.camera_error_image = None
app.onvif = onvif
app.stats_emitter = stats_emitter
app.notice_registry = notice_registry
app.event_metadata_updater = event_metadata_updater
app.config_publisher = config_publisher
app.replay_manager = replay_manager
+60
View File
@@ -0,0 +1,60 @@
"""Notice APIs."""
import logging
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from frigate.api.auth import require_role
from frigate.api.defs.tags import Tags
from frigate.notices.registry import DismissResult
logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.notices])
@router.get("/notices", dependencies=[Depends(require_role(["admin"]))])
def get_notices(request: Request, include_dismissed: bool = False) -> JSONResponse:
"""Get the active notices, most severe first.
Args:
include_dismissed: Also return event notices the user has dismissed
Returns:
The active notices
"""
return JSONResponse(
content=request.app.notice_registry.active(include_dismissed=include_dismissed)
)
@router.get("/notices/stats", dependencies=[Depends(require_role(["admin"]))])
def get_notice_stats(request: Request) -> JSONResponse:
"""Get lifetime occurrence counts per notice kind."""
return JSONResponse(content=request.app.notice_registry.stats())
@router.post(
"/notices/{notice_id}/dismiss", dependencies=[Depends(require_role(["admin"]))]
)
def dismiss_notice(request: Request, notice_id: str) -> JSONResponse:
"""Hide an event notice until it is raised again."""
result = request.app.notice_registry.dismiss(notice_id)
if result == DismissResult.not_found:
return JSONResponse(
content={"success": False, "message": "Notice not found"},
status_code=404,
)
if result == DismissResult.not_dismissable:
return JSONResponse(
content={
"success": False,
"message": "This notice clears itself when the problem is fixed",
},
status_code=400,
)
return JSONResponse(content={"success": True, "message": "Notice dismissed"})