Compare commits

...
2 Commits
Author SHA1 Message Date
Josh Hawkins c5826df806 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.
2026-07-30 12:44:18 -05:00
Josh Hawkins c160a0dfeb 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.
2026-07-30 07:56:45 -05:00
2 changed files with 52 additions and 25 deletions
+42 -23
View File
@@ -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<ReviewSegment[]>(
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<ReviewSummary>(
[
"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<RecordingsSummary>([
"recordings/summary",
{
timezone: timezone,
cameras: reviewSearchParams["cameras"] ?? null,
},
]);
const { data: recordingsSummary } = useSWR<RecordingsSummary>(
timezone
? [
"recordings/summary",
{
timezone: timezone,
cameras: reviewCamerasParam,
},
]
: null,
);
// preview videos
const previewTimes = useMemo(() => {
+10 -2
View File
@@ -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,
},
]);