From c5826df806fcb1bcdb72ff339202bc232c6562d2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:44:18 -0500 Subject: [PATCH] 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. --- web/src/pages/Events.tsx | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index bdf4bab4c4..36e72a3feb 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -84,11 +84,6 @@ export default function Events() { .map((conf) => conf.name); }, [allowedCameras, config?.cameras]); - const reviewCamerasParam = useMemo( - () => reviewCameras.join(","), - [reviewCameras], - ); - const selectedMotionSearchCamera = useMemo(() => { if (!motionSearchCamera) { return null; @@ -203,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, @@ -335,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 @@ -344,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, @@ -366,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) { @@ -391,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(); @@ -460,7 +468,7 @@ export default function Events() { "review/summary", { timezone: timezone, - cameras: reviewSearchParams["cameras"] ?? reviewCamerasParam, + cameras: reviewCamerasParam, labels: reviewSearchParams["labels"] ?? null, zones: reviewSearchParams["zones"] ?? null, }, @@ -486,7 +494,7 @@ export default function Events() { "recordings/summary", { timezone: timezone, - cameras: reviewSearchParams["cameras"] ?? reviewCamerasParam, + cameras: reviewCamerasParam, }, ] : null,