Files
frigate/web/src/pages/Events.tsx
T

755 lines
20 KiB
TypeScript
Raw Normal View History

2024-03-03 17:19:02 -07:00
import ActivityIndicator from "@/components/indicators/activity-indicator";
2024-09-13 07:25:29 -06:00
import useApiFilter from "@/hooks/use-api-filter";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
2024-03-03 17:19:02 -07:00
import { useTimezone } from "@/hooks/use-date-utils";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { useUserPersistence } from "@/hooks/use-user-persistence";
2024-03-03 17:19:02 -07:00
import { FrigateConfig } from "@/types/frigateConfig";
import { RecordingStartingPoint } from "@/types/record";
2024-03-05 05:02:34 -07:00
import {
2025-02-09 17:02:36 -06:00
RecordingsSummary,
REVIEW_PADDING,
2024-03-05 05:02:34 -07:00
ReviewFilter,
ReviewSegment,
ReviewSeverity,
ReviewSummary,
2024-05-26 15:49:12 -06:00
SegmentedReviewData,
2024-03-05 05:02:34 -07:00
} from "@/types/review";
2025-11-17 08:12:05 -06:00
import { TimelineType } from "@/types/timeline";
2024-09-19 10:01:57 -06:00
import {
getBeginningOfDayTimestamp,
getEndOfDayTimestamp,
} from "@/utils/dateUtil";
import {
parseRecordingReviewLink,
RECORDING_REVIEW_LINK_PARAM,
} from "@/utils/recordingReviewUrl";
2024-02-27 14:39:05 -07:00
import EventView from "@/views/events/EventView";
import MotionSearchView from "@/views/motion-search/MotionSearchView";
2024-06-23 14:58:00 -06:00
import { RecordingView } from "@/views/recording/RecordingView";
import { useFrigateReviews } from "@/api/ws";
2024-02-22 17:03:34 -07:00
import axios from "axios";
2024-03-15 12:46:17 -06:00
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
2024-02-22 17:03:34 -07:00
import useSWR from "swr";
2024-02-21 13:07:32 -07:00
export default function Events() {
const { t } = useTranslation(["views/events"]);
2024-03-25 20:25:06 -06:00
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
2024-03-03 17:19:02 -07:00
const timezone = useTimezone(config);
const allowedCameras = useAllowedCameras();
2024-03-03 17:19:02 -07:00
2024-02-22 17:03:34 -07:00
// recordings viewer
const [severity, setSeverity] = useOverlayState<ReviewSeverity>(
"severity",
"alert",
);
2024-04-12 06:31:30 -06:00
const [showReviewed, setShowReviewed] = useUserPersistence(
"showReviewed",
false,
);
2026-05-30 22:35:03 -05:00
const [recording, setRecording] = useOverlayState<
RecordingStartingPoint | undefined
>("recording", undefined, false);
const [motionPreviewsCamera, setMotionPreviewsCamera] = useOverlayState<
string | undefined
>("motionPreviewsCamera", undefined);
const [motionSearchCamera, setMotionSearchCamera] = useState<string | null>(
null,
);
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
undefined,
);
const reviewCameras = useMemo(() => {
if (!config?.cameras) {
return [] as string[];
}
2026-07-07 11:56:43 -05:00
return Object.values(config.cameras)
.filter(
(conf) =>
allowedCameras.includes(conf.name) && conf.ui?.review !== false,
)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order)
.map((conf) => conf.name);
}, [allowedCameras, config?.cameras]);
const selectedMotionSearchCamera = useMemo(() => {
if (!motionSearchCamera) {
return null;
}
if (reviewCameras.includes(motionSearchCamera)) {
return motionSearchCamera;
}
return reviewCameras[0] ?? null;
}, [motionSearchCamera, reviewCameras]);
const motionSearchTimeRange = useMemo(() => {
if (motionSearchDay) {
return {
after: getBeginningOfDayTimestamp(new Date(motionSearchDay)),
before: getEndOfDayTimestamp(new Date(motionSearchDay)),
};
}
const now = Date.now() / 1000;
return {
after: now - 86400,
before: now,
};
}, [motionSearchDay]);
const closeMotionSearch = useCallback(() => {
setMotionSearchCamera(null);
setMotionSearchDay(undefined);
setBeforeTs(Date.now() / 1000);
}, []);
const handleMotionSearchCameraSelect = useCallback((camera: string) => {
setMotionSearchCamera(camera);
}, []);
const handleMotionSearchDaySelect = useCallback((day: Date | undefined) => {
if (day == undefined) {
setMotionSearchDay(undefined);
return;
}
const normalizedDay = new Date(day);
normalizedDay.setHours(0, 0, 0, 0);
setMotionSearchDay(normalizedDay);
}, []);
2024-04-12 06:31:30 -06:00
2025-11-17 08:12:05 -06:00
const [notificationTab, setNotificationTab] =
useState<TimelineType>("timeline");
const getReviewDayBounds = useCallback((date: Date) => {
const now = Date.now() / 1000;
return {
after: getBeginningOfDayTimestamp(date),
before: Math.min(getEndOfDayTimestamp(date), now),
};
}, []);
2025-11-17 08:12:05 -06:00
useSearchEffect("tab", (tab: string) => {
if (tab === "timeline" || tab === "events" || tab === "detail") {
setNotificationTab(tab as TimelineType);
}
return true;
});
useSearchEffect("id", (reviewId: string) => {
axios
.get(`review/${reviewId}`)
.then((resp) => {
if (resp.status == 200 && resp.data) {
2024-09-19 10:01:57 -06:00
const startTime = resp.data.start_time - REVIEW_PADDING;
const date = new Date(startTime * 1000);
setReviewFilter(getReviewDayBounds(date));
setRecording(
{
camera: resp.data.camera,
2024-09-19 10:01:57 -06:00
startTime,
severity: resp.data.severity,
2025-11-17 08:12:05 -06:00
timelineType: notificationTab,
},
true,
);
}
})
.catch(() => {});
2024-09-12 08:46:29 -06:00
return true;
});
const [startTime, setStartTime] = useState<number>();
2024-02-21 13:07:32 -07:00
2024-04-12 06:31:30 -06:00
useEffect(() => {
if (recording) {
document.title = t("recordings.documentTitle");
2024-04-12 06:31:30 -06:00
} else {
document.title = t("documentTitle");
2024-04-12 06:31:30 -06:00
}
}, [recording, severity, t]);
2024-04-12 06:31:30 -06:00
2024-02-25 12:04:44 -07:00
// review filter
const [reviewFilter, setReviewFilter, reviewSearchParams] =
2024-09-13 07:25:29 -06:00
useApiFilter<ReviewFilter>();
useSearchEffect("cameras", (cameras: string) => {
setReviewFilter({
...reviewFilter,
cameras: cameras.includes(",") ? cameras.split(",") : [cameras],
});
return true;
});
2026-07-30 17:20:41 -05:00
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]);
2024-09-13 07:25:29 -06:00
useSearchEffect("labels", (labels: string) => {
setReviewFilter({
...reviewFilter,
labels: labels.includes(",") ? labels.split(",") : [labels],
});
return true;
});
useSearchEffect("zones", (zones: string) => {
setReviewFilter({
...reviewFilter,
zones: zones.includes(",") ? zones.split(",") : [zones],
});
return true;
});
2024-02-25 12:04:44 -07:00
2024-06-04 09:10:19 -06:00
useSearchEffect("group", (reviewGroup) => {
2024-07-15 10:18:01 -06:00
if (config && reviewGroup && reviewGroup != "default") {
2024-06-04 09:10:19 -06:00
const group = config.camera_groups[reviewGroup];
2024-07-15 09:34:41 -06:00
const isBirdseyeOnly =
group.cameras.length == 1 && group.cameras[0] == "birdseye";
2024-06-04 09:10:19 -06:00
2024-07-15 09:34:41 -06:00
if (group && !isBirdseyeOnly) {
2024-06-04 09:10:19 -06:00
setReviewFilter({
...reviewFilter,
cameras: group.cameras,
});
}
2024-09-12 08:46:29 -06:00
return true;
2024-06-04 09:10:19 -06:00
}
2024-09-12 08:46:29 -06:00
return false;
2024-06-04 09:10:19 -06:00
});
2024-03-21 07:43:37 -06:00
const onUpdateFilter = useCallback(
(newFilter: ReviewFilter) => {
setReviewFilter(newFilter);
// update recording start time if filter
// was changed on recording page
if (recording != undefined && newFilter.after != undefined) {
setRecording({ ...recording, startTime: newFilter.after }, true);
}
},
[recording, setRecording, setReviewFilter],
);
2024-02-25 12:04:44 -07:00
useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => {
if (!config) {
return false;
}
const reviewLink = parseRecordingReviewLink(reviewLinkValue);
if (!reviewLink) {
toast.error(t("recordings.invalidSharedLink"), {
position: "top-center",
});
return true;
}
const validCamera =
config.cameras[reviewLink.camera] &&
allowedCameras.includes(reviewLink.camera);
if (!validCamera) {
toast.error(t("recordings.invalidSharedCamera"), {
position: "top-center",
});
return true;
}
setReviewFilter({
...reviewFilter,
...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
});
setRecording(
{
camera: reviewLink.camera,
startTime: reviewLink.timestamp,
// severity not actually applicable here, but the type requires it
// this pattern is also used LiveCameraView to enter recording view
severity: "alert",
timelineType: notificationTab,
navigationSource: "shared-link",
},
true,
);
return true;
});
2024-02-22 17:03:34 -07:00
// review paging
2024-08-12 14:30:16 -06:00
const [beforeTs, setBeforeTs] = useState(Math.ceil(Date.now() / 1000));
const last24Hours = useMemo(() => {
2024-02-28 07:16:32 -07:00
return { before: beforeTs, after: getHoursAgo(24) };
}, [beforeTs]);
const selectedTimeRange = useMemo(() => {
if (reviewSearchParams["after"] == undefined) {
return last24Hours;
}
return {
2024-08-13 08:23:46 -06:00
before: Math.ceil(reviewSearchParams["before"]),
after: Math.floor(reviewSearchParams["after"]),
};
2024-02-28 15:23:56 -07:00
}, [last24Hours, reviewSearchParams]);
2024-02-22 17:03:34 -07:00
2024-03-25 20:25:06 -06:00
// we want to update the items whenever the severity changes
2024-03-26 18:54:00 -06:00
useEffect(() => {
if (recording) {
return;
}
const now = Date.now() / 1000;
if (now - beforeTs > 60) {
setBeforeTs(now);
}
// only refresh when severity changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity]);
2024-03-25 20:25:06 -06:00
2024-02-28 15:23:56 -07:00
const reviewSegmentFetcher = useCallback((key: Array<string> | string) => {
2024-02-22 17:03:34 -07:00
const [path, params] = Array.isArray(key) ? key : [key, undefined];
return axios.get(path, { params }).then((res) => res.data);
}, []);
2024-03-07 07:33:36 -07:00
const getKey = useCallback(() => {
2026-07-30 17:20:41 -05:00
if (!timezone) {
return null;
}
2024-03-07 07:33:36 -07:00
const params = {
2026-07-30 17:20:41 -05:00
cameras: reviewCamerasParam,
2024-03-07 07:33:36 -07:00
labels: reviewSearchParams["labels"],
2024-06-11 09:19:17 -05:00
zones: reviewSearchParams["zones"],
2026-01-12 20:36:38 -07:00
reviewed: null, // We want both reviewed and unreviewed items as we filter in the UI
2024-03-07 07:33:36 -07:00
before: reviewSearchParams["before"] || last24Hours.before,
after: reviewSearchParams["after"] || last24Hours.after,
};
return ["review", params];
2026-07-30 17:20:41 -05:00
}, [reviewSearchParams, reviewCamerasParam, last24Hours, timezone]);
2024-02-22 17:03:34 -07:00
2024-03-07 07:33:36 -07:00
const { data: reviews, mutate: updateSegments } = useSWR<ReviewSegment[]>(
getKey,
reviewSegmentFetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
2024-02-22 17:03:34 -07:00
},
);
2024-05-26 15:49:12 -06:00
const reviewItems = useMemo<SegmentedReviewData>(() => {
if (!reviews) {
return undefined;
}
const all: ReviewSegment[] = [];
const alerts: ReviewSegment[] = [];
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
case "alert":
alerts.push(segment);
break;
case "detection":
detections.push(segment);
break;
default:
motion.push(segment);
break;
}
});
return {
all: all,
alert: alerts,
detection: detections,
significant_motion: motion,
};
2026-07-30 17:20:41 -05:00
}, [reviews]);
2024-05-26 15:49:12 -06:00
// update review items in place when a review segment ends
const reviewUpdate = useFrigateReviews();
const [endedReviews, setEndedReviews] = useState(
new Map<string, ReviewSegment>(),
);
useEffect(() => {
2026-04-16 10:10:03 -05:00
if (reviewUpdate?.type === "end" && reviews) {
updateSegments(
(data) => {
if (!data) return data;
return data.map((seg) =>
seg.id === reviewUpdate.after.id ? reviewUpdate.after : seg,
);
},
{ revalidate: false, populateCache: true },
);
setEndedReviews((prev) =>
new Map(prev).set(reviewUpdate.after.id, reviewUpdate.after),
);
}
2026-04-16 10:10:03 -05:00
// reviews is intentionally excluded - only used to guard against
// updating the SWR cache before data has loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reviewUpdate, updateSegments]);
2024-05-26 15:49:12 -06:00
const currentItems = useMemo(() => {
if (!reviewItems || !severity) {
return null;
}
let current;
if (reviewFilter?.showAll) {
current = reviewItems.all;
} else {
current = reviewItems[severity];
}
if (!current || current.length == 0) {
return [];
}
if (!showReviewed) {
2024-05-26 15:49:12 -06:00
return current.filter((seg) => !seg.has_been_reviewed);
} else {
return current;
}
// only refresh when severity or filter changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity, reviewFilter, showReviewed, reviewItems?.all.length]);
2024-05-26 15:49:12 -06:00
// overlay end_time updates onto currentItems without re-running
// the has_been_reviewed filter, so hover-reviewed items stay visible
const displayItems = useMemo(() => {
if (!currentItems || endedReviews.size === 0) return currentItems;
return currentItems.map((seg) => endedReviews.get(seg.id) ?? seg);
}, [currentItems, endedReviews]);
2024-03-03 17:19:02 -07:00
// review summary
2024-03-25 20:25:06 -06:00
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
2026-07-30 17:20:41 -05:00
timezone
? [
"review/summary",
{
timezone: timezone,
cameras: reviewCamerasParam,
labels: reviewSearchParams["labels"] ?? null,
zones: reviewSearchParams["zones"] ?? null,
},
]
: null,
2024-03-05 05:02:34 -07:00
{
2024-03-25 20:25:06 -06:00
revalidateOnFocus: true,
refreshInterval: 30000,
revalidateOnReconnect: false,
2024-03-05 05:02:34 -07:00
},
2024-03-25 20:25:06 -06:00
);
2024-03-03 17:19:02 -07:00
2024-03-05 05:02:34 -07:00
const reloadData = useCallback(() => {
setBeforeTs(Date.now() / 1000);
updateSummary();
}, [updateSummary]);
2025-02-09 17:02:36 -06:00
// recordings summary
2026-07-30 17:20:41 -05:00
const { data: recordingsSummary } = useSWR<RecordingsSummary>(
timezone
? [
"recordings/summary",
{
timezone: timezone,
cameras: reviewCamerasParam,
},
]
: null,
);
2025-02-09 17:02:36 -06:00
2024-02-22 17:03:34 -07:00
// preview videos
const previewTimes = useMemo(() => {
2024-05-21 18:30:29 +05:30
const startDate = new Date(selectedTimeRange.after * 1000);
2024-05-28 08:09:17 -06:00
startDate.setUTCMinutes(0, 0, 0);
2024-02-22 17:03:34 -07:00
2024-05-21 18:30:29 +05:30
const endDate = new Date(selectedTimeRange.before * 1000);
endDate.setHours(endDate.getHours() + 1, 0, 0, 0);
2024-03-15 12:46:17 -06:00
2024-02-22 17:03:34 -07:00
return {
2024-05-28 08:09:17 -06:00
after: startDate.getTime() / 1000,
before: endDate.getTime() / 1000,
2024-02-22 17:03:34 -07:00
};
2024-05-21 18:30:29 +05:30
}, [selectedTimeRange]);
const allPreviews = useCameraPreviews(
previewTimes ?? { after: 0, before: 0 },
{
fetchPreviews: previewTimes != undefined,
},
2024-02-22 17:03:34 -07:00
);
// review status
2026-03-24 13:53:39 -06:00
const markItemsAsReviewed = useCallback(
async (
currentItems: ReviewSegment[],
itemsToMarkReviewed: ReviewSegment[] | undefined = undefined,
) => {
2024-03-14 14:49:03 -06:00
if (currentItems.length == 0) {
return;
}
const severity = currentItems[0].severity;
updateSegments(
(data: ReviewSegment[] | undefined) => {
if (!data) {
return data;
}
const newData = [...data];
newData.forEach((seg) => {
2024-04-11 06:42:16 -06:00
if (seg.end_time && seg.severity == severity) {
2024-03-14 14:49:03 -06:00
seg.has_been_reviewed = true;
}
});
return newData;
},
{ revalidate: false, populateCache: true },
);
2026-03-24 13:53:39 -06:00
const reviewList =
itemsToMarkReviewed?.map((seg) => seg.id) ||
currentItems?.filter((seg) => seg.end_time)?.map((seg) => seg.id) ||
[];
2024-04-11 06:42:16 -06:00
2026-03-24 13:53:39 -06:00
if (reviewList.length > 0) {
2024-04-11 06:42:16 -06:00
await axios.post(`reviews/viewed`, {
2026-03-24 13:53:39 -06:00
ids: reviewList,
reviewed: true,
2024-04-11 06:42:16 -06:00
});
reloadData();
2026-01-21 18:46:24 -06:00
if (reviewSearchParams["after"] != undefined) {
updateSegments();
}
2024-04-11 06:42:16 -06:00
}
2024-03-14 14:49:03 -06:00
},
2026-01-21 18:46:24 -06:00
[reloadData, updateSegments, reviewSearchParams],
2024-03-14 14:49:03 -06:00
);
2024-02-22 17:03:34 -07:00
const markItemAsReviewed = useCallback(
2024-03-05 05:02:34 -07:00
async (review: ReviewSegment) => {
const resp = await axios.post(`reviews/viewed`, {
ids: [review.id],
reviewed: true,
});
2024-02-22 17:03:34 -07:00
if (resp.status == 200) {
updateSegments(
2024-03-07 07:33:36 -07:00
(data: ReviewSegment[] | undefined) => {
2024-02-22 17:03:34 -07:00
if (!data) {
return data;
}
2024-03-07 07:33:36 -07:00
const reviewIndex = data.findIndex((item) => item.id == review.id);
if (reviewIndex == -1) {
return data;
}
2024-02-22 17:03:34 -07:00
2024-03-07 07:33:36 -07:00
const newData = [
...data.slice(0, reviewIndex),
{ ...data[reviewIndex], has_been_reviewed: true },
...data.slice(reviewIndex + 1),
];
2024-02-22 17:03:34 -07:00
return newData;
},
2024-02-28 15:23:56 -07:00
{ revalidate: false, populateCache: true },
2024-02-22 17:03:34 -07:00
);
2024-03-05 05:02:34 -07:00
updateSummary(
2024-03-05 17:39:37 -07:00
(data: ReviewSummary | undefined) => {
2024-03-05 05:02:34 -07:00
if (!data) {
return data;
}
const day = new Date(review.start_time * 1000);
2024-03-05 17:39:37 -07:00
const today = new Date();
today.setHours(0, 0, 0, 0);
2024-03-05 05:02:34 -07:00
2024-03-05 17:39:37 -07:00
let key;
if (day.getTime() > today.getTime()) {
key = "last24Hours";
} else {
key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
}
if (!Object.keys(data).includes(key)) {
2024-03-05 05:02:34 -07:00
return data;
}
2024-03-05 17:39:37 -07:00
const item = data[key];
return {
...data,
[key]: {
2024-03-05 05:02:34 -07:00
...item,
reviewed_alert:
review.severity == "alert"
? item.reviewed_alert + 1
: item.reviewed_alert,
reviewed_detection:
review.severity == "detection"
? item.reviewed_detection + 1
: item.reviewed_detection,
},
2024-03-05 17:39:37 -07:00
};
2024-03-05 05:02:34 -07:00
},
{ revalidate: false, populateCache: true },
);
2024-02-22 17:03:34 -07:00
}
},
2024-03-05 05:02:34 -07:00
[updateSegments, updateSummary],
2024-02-22 17:03:34 -07:00
);
// selected items
2024-03-05 12:55:44 -07:00
const selectedReviewData = useMemo(() => {
if (!recording) {
return undefined;
}
2024-03-05 05:03:10 -07:00
if (!config) {
return undefined;
}
2024-03-07 07:33:36 -07:00
if (!reviews) {
2024-02-22 17:03:34 -07:00
return undefined;
}
setStartTime(recording.startTime);
const allCameras = reviewFilter?.cameras ?? reviewCameras;
2024-03-05 05:03:10 -07:00
2024-02-22 17:03:34 -07:00
return {
camera: recording.camera,
start_time: recording.startTime,
2024-03-05 05:03:10 -07:00
allCameras: allCameras,
2024-02-22 17:03:34 -07:00
};
2024-02-28 15:23:56 -07:00
// previews will not update after item is selected
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recording, reviews]);
2024-02-22 17:03:34 -07:00
2024-03-03 17:19:02 -07:00
if (!timezone) {
return <ActivityIndicator />;
}
2024-04-02 13:25:02 -06:00
if (recording) {
if (selectedReviewData) {
return (
<RecordingView
key={selectedTimeRange.before}
2024-04-02 13:25:02 -06:00
startCamera={selectedReviewData.camera}
startTime={selectedReviewData.start_time}
allCameras={selectedReviewData.allCameras}
reviewItems={reviews}
reviewSummary={reviewSummary}
allPreviews={allPreviews}
timeRange={selectedTimeRange}
filter={reviewFilter}
updateFilter={onUpdateFilter}
refreshData={reloadData}
2026-05-30 22:35:03 -05:00
onMotionSearch={(camera) => {
setMotionSearchCamera(camera);
setRecording(undefined);
}}
2024-04-02 13:25:02 -06:00
/>
);
}
2024-02-22 17:03:34 -07:00
} else {
return motionSearchCamera ? (
!config || !selectedMotionSearchCamera ? (
<ActivityIndicator />
) : (
<MotionSearchView
config={config}
cameras={reviewCameras}
selectedCamera={selectedMotionSearchCamera}
onCameraSelect={handleMotionSearchCameraSelect}
cameraLocked={true}
selectedDay={motionSearchDay}
onDaySelect={handleMotionSearchDaySelect}
timeRange={motionSearchTimeRange}
timezone={timezone}
onBack={closeMotionSearch}
/>
)
) : (
2024-02-27 14:39:05 -07:00
<EventView
2024-05-26 15:49:12 -06:00
reviewItems={reviewItems}
currentReviewItems={displayItems}
2024-03-03 17:19:02 -07:00
reviewSummary={reviewSummary}
2025-02-09 17:02:36 -06:00
recordingsSummary={recordingsSummary}
2024-02-22 17:03:34 -07:00
relevantPreviews={allPreviews}
timeRange={selectedTimeRange}
2024-02-25 12:04:44 -07:00
filter={reviewFilter}
severity={severity ?? "alert"}
startTime={startTime}
showReviewed={showReviewed ?? false}
setShowReviewed={setShowReviewed}
setSeverity={setSeverity}
2024-02-22 17:03:34 -07:00
markItemAsReviewed={markItemAsReviewed}
2026-03-24 13:53:39 -06:00
markItemsAsReviewed={markItemsAsReviewed}
onOpenRecording={setRecording}
motionPreviewsCamera={motionPreviewsCamera ?? null}
setMotionPreviewsCamera={(camera) =>
setMotionPreviewsCamera(camera ?? undefined)
}
setMotionSearchCamera={setMotionSearchCamera}
pullLatestData={reloadData}
2024-02-25 12:04:44 -07:00
updateFilter={onUpdateFilter}
2024-02-22 17:03:34 -07:00
/>
);
}
}
function getHoursAgo(hours: number): number {
const now = new Date();
now.setHours(now.getHours() - hours);
2024-08-12 14:30:16 -06:00
return Math.ceil(now.getTime() / 1000);
2024-02-21 13:07:32 -07:00
}