From f1cc0e49d41c0aece88251a458c61481b007e42c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:20:41 -0500 Subject: [PATCH] Miscellaneous fixes (0.18 beta) (#23873) * improve display of gpu graphs in system metrics * docs tweaks * Only hide cameras with ui.dashboard disabled from the All Cameras dashboard The settings camera selector and zone editor also filtered on ui.dashboard, so hiding a camera from the dashboard made its zones and masks uneditable in the UI (GH 23870). Drop those filters and correct the field title, help text, and reference docs to describe what the option actually does * hide cameras with ui.review disabled from the Motion tab and the review summaries The Motion tab built its own camera list that never checked ui.review, so a hidden camera still got a preview tile, and its motion and overlap queries fell back to every allowed camera. The review and recordings summaries had the same gap: they are aggregate day counts that can't be filtered client side, so a hidden camera kept contributing to the severity tab counts and calendar indicators while its items were absent from the list. Filter the motion camera list on ui.review and query all four endpoints with the visible camera list instead of letting the backend default to all, and skip the summary queries until the config resolves so the counts don't briefly render as zero. * Scope every review page query to the cameras visible in review The segments and the summary counts were derived from different camera sets: the list was fetched for all cameras and filtered client side, while the summaries were fetched for the visible cameras only when no explicit camera filter was set. A ?cameras= link can name a camera hidden from review, which left the count above zero with an empty list, pinning the new items to review popover open and making the auto refresh effect loop. Intersect an explicit camera selection with the visible list rather than trusting it, pass that to the segment and summary queries alike, and drop the now redundant client side filter, which the raw segments handed to the history view were bypassing anyway. --- docs/docs/configuration/advanced/reference.md | 4 +- docs/docs/configuration/live.md | 2 +- docs/docs/troubleshooting/dummy-camera.md | 2 +- frigate/config/camera/ui.py | 4 +- web/public/locales/en/config/cameras.json | 4 +- web/public/locales/en/config/global.json | 4 +- web/public/locales/en/views/settings.json | 2 +- web/src/components/settings/ZoneEditPane.tsx | 2 +- web/src/pages/Events.tsx | 65 ++++++++++++------- web/src/pages/Settings.tsx | 7 +- web/src/views/events/EventView.tsx | 12 +++- web/src/views/system/GeneralMetrics.tsx | 39 +++++++++-- 12 files changed, 99 insertions(+), 48 deletions(-) diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md index e70eeeca83..802f3d7856 100644 --- a/docs/docs/configuration/advanced/reference.md +++ b/docs/docs/configuration/advanced/reference.md @@ -981,7 +981,9 @@ cameras: # Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below) # By default the cameras are sorted alphabetically. order: 0 - # Optional: Whether or not to show the camera in the Frigate UI (default: shown below) + # Optional: Whether or not to show the camera on the default All Cameras live dashboard. + # The camera is still available everywhere else, including camera groups and settings + # (default: shown below) dashboard: True # Optional: Whether this camera is visible in review (the review page and its camera # filter, motion review, and the history view) (default: shown below) diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 3183ba2c33..6f32c9ec3e 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -334,7 +334,7 @@ When your browser runs into problems playing back your camera streams, it will l - **stalled** - What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving). - - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings. + - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in . - Possible console messages from the player code: - `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.` diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md index 630a7921d5..3f13b1e190 100644 --- a/docs/docs/troubleshooting/dummy-camera.md +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -39,7 +39,7 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time. -The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame. +The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame. The Debug Replay camera does not save recordings or snapshots or surface anything in Explore, but it otherwise behaves like a regular camera, including running enrichments such as Face Recognition, LPR, and custom classification. Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature. diff --git a/frigate/config/camera/ui.py b/frigate/config/camera/ui.py index 7db1c537fa..869a28b903 100644 --- a/frigate/config/camera/ui.py +++ b/frigate/config/camera/ui.py @@ -13,8 +13,8 @@ class CameraUiConfig(FrigateBaseModel): ) dashboard: bool = Field( default=True, - title="Show in UI", - description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", + title="Show on Live dashboard", + description="Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings.", ) review: bool = Field( default=True, diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index 2ddc30b6b8..248de30739 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -859,8 +859,8 @@ "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later." }, "dashboard": { - "label": "Show in UI", - "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + "label": "Show on Live dashboard", + "description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings." }, "review": { "label": "Show in review", diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 9e3a71b7a8..bf7ef337cc 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1543,8 +1543,8 @@ "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later." }, "dashboard": { - "label": "Show in UI", - "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + "label": "Show on Live dashboard", + "description": "Toggle whether this camera is visible on the default All Cameras live dashboard. The camera remains available everywhere else in the UI, including camera groups and settings." }, "review": { "label": "Show in review", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 165569638d..817c9f66c4 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -499,7 +499,7 @@ "webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.", "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).", "dashboardLabel": "Show on Live dashboard", - "dashboardHelp": "Show this camera on the Live dashboard.", + "dashboardHelp": "Show this camera on the default All Cameras live dashboard. It remains available everywhere else, including camera groups.", "reviewLabel": "Show in Review", "reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view." } diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 6132fb9072..6e49307542 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -78,7 +78,7 @@ export default function ZoneEditPane({ } return Object.values(config.cameras) - .filter((conf) => conf.ui.dashboard && conf.enabled_in_config) + .filter((conf) => conf.enabled_in_config) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config]); diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 88a210400e..36e72a3feb 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -198,6 +198,19 @@ export default function Events() { return true; }); + const reviewCamerasParam = useMemo(() => { + const selected: string | undefined = reviewSearchParams["cameras"]; + + if (!selected) { + return reviewCameras.join(","); + } + + const selectedCameras = new Set(selected.split(",")); + return reviewCameras + .filter((camera) => selectedCameras.has(camera)) + .join(","); + }, [reviewCameras, reviewSearchParams]); + useSearchEffect("labels", (labels: string) => { setReviewFilter({ ...reviewFilter, @@ -330,8 +343,12 @@ export default function Events() { }, []); const getKey = useCallback(() => { + if (!timezone) { + return null; + } + const params = { - cameras: reviewSearchParams["cameras"], + cameras: reviewCamerasParam, labels: reviewSearchParams["labels"], zones: reviewSearchParams["zones"], reviewed: null, // We want both reviewed and unreviewed items as we filter in the UI @@ -339,7 +356,7 @@ export default function Events() { after: reviewSearchParams["after"] || last24Hours.after, }; return ["review", params]; - }, [reviewSearchParams, last24Hours]); + }, [reviewSearchParams, reviewCamerasParam, last24Hours, timezone]); const { data: reviews, mutate: updateSegments } = useSWR( getKey, @@ -361,10 +378,6 @@ export default function Events() { const motion: ReviewSegment[] = []; reviews?.forEach((segment) => { - if (config?.cameras[segment.camera]?.ui?.review === false) { - return; - } - all.push(segment); switch (segment.severity) { @@ -386,7 +399,7 @@ export default function Events() { detection: detections, significant_motion: motion, }; - }, [reviews, config?.cameras]); + }, [reviews]); // update review items in place when a review segment ends const reviewUpdate = useFrigateReviews(); @@ -450,15 +463,17 @@ export default function Events() { // review summary const { data: reviewSummary, mutate: updateSummary } = useSWR( - [ - "review/summary", - { - timezone: timezone, - cameras: reviewSearchParams["cameras"] ?? null, - labels: reviewSearchParams["labels"] ?? null, - zones: reviewSearchParams["zones"] ?? null, - }, - ], + timezone + ? [ + "review/summary", + { + timezone: timezone, + cameras: reviewCamerasParam, + labels: reviewSearchParams["labels"] ?? null, + zones: reviewSearchParams["zones"] ?? null, + }, + ] + : null, { revalidateOnFocus: true, refreshInterval: 30000, @@ -473,13 +488,17 @@ export default function Events() { // recordings summary - const { data: recordingsSummary } = useSWR([ - "recordings/summary", - { - timezone: timezone, - cameras: reviewSearchParams["cameras"] ?? null, - }, - ]); + const { data: recordingsSummary } = useSWR( + timezone + ? [ + "recordings/summary", + { + timezone: timezone, + cameras: reviewCamerasParam, + }, + ] + : null, + ); // preview videos const previewTimes = useMemo(() => { diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6f445f0c47..e2666f57e3 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -706,12 +706,7 @@ export default function Settings() { } return Object.values(config.cameras) - .filter( - (conf) => - conf.ui.dashboard && - conf.enabled_in_config && - !isReplayCamera(conf.name), - ) + .filter((conf) => conf.enabled_in_config && !isReplayCamera(conf.name)) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config]); diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index dd89e78780..50236b284a 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -1024,6 +1024,9 @@ function MotionReview({ if (!allowedCameras.includes(cam.name)) { return false; } + if (cam.ui?.review === false) { + return false; + } if (selectedCams && !selectedCams.includes(cam.name)) { return false; } @@ -1033,6 +1036,11 @@ function MotionReview({ return cameras.sort((a, b) => a.ui.order - b.ui.order); }, [config, filter, allowedCameras]); + const reviewCamerasParam = useMemo( + () => reviewCameras.map((cam) => cam.name).join(","), + [reviewCameras], + ); + const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({}); // motion data @@ -1052,7 +1060,7 @@ function MotionReview({ before: alignedBefore, after: alignedAfter, scale: segmentDuration / 2, - cameras: filter?.cameras?.join(",") ?? null, + cameras: reviewCamerasParam, }, ]); @@ -1061,7 +1069,7 @@ function MotionReview({ { before: alignedBefore, after: alignedAfter, - cameras: filter?.cameras?.join(",") ?? null, + cameras: reviewCamerasParam, }, ]); diff --git a/web/src/views/system/GeneralMetrics.tsx b/web/src/views/system/GeneralMetrics.tsx index 4cba965298..1a7767ffb5 100644 --- a/web/src/views/system/GeneralMetrics.tsx +++ b/web/src/views/system/GeneralMetrics.tsx @@ -540,6 +540,36 @@ export default function GeneralMetrics({ return Object.keys(series).length > 0 ? Object.values(series) : undefined; }, [statsHistory]); + // Number of cards the hardware grid renders. Which ones appear depends on + // the vendor, so the column count follows the count rather than assuming a + // fixed set is present. + const hardwareCardCount = useMemo(() => { + if (!statsHistory[0]?.gpu_usages) { + return 0; + } + + const hasNpu = statsHistory[0].npu_usages != undefined; + + return ( + 1 + // gpu usage always renders alongside gpu_usages + (gpuMemSeries ? 1 : 0) + + (gpuEncSeries?.length ? 1 : 0) + + (gpuComputeSeries?.length ? 1 : 0) + + (gpuDecSeries?.length ? 1 : 0) + + (gpuTempSeries?.length ? 1 : 0) + + (hasNpu ? 1 : 0) + + (hasNpu && npuTempSeries?.length ? 1 : 0) + ); + }, [ + statsHistory, + gpuMemSeries, + gpuEncSeries, + gpuComputeSeries, + gpuDecSeries, + gpuTempSeries, + npuTempSeries, + ]); + // other processes stats const hardwareType = useMemo(() => { @@ -763,12 +793,9 @@ export default function GeneralMetrics({
= 3 && "lg:grid-cols-3", + hardwareCardCount >= 4 && "xl:grid-cols-4", + hardwareCardCount >= 5 && "3xl:grid-cols-5", )} > {statsHistory[0]?.gpu_usages && (