2024-03-04 01:19:02 +01:00
|
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
2024-02-25 20:04:44 +01:00
|
|
|
import useApiFilter from "@/hooks/use-api-filter";
|
2024-05-08 16:46:10 +02:00
|
|
|
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
2024-03-04 01:19:02 +01:00
|
|
|
import { useTimezone } from "@/hooks/use-date-utils";
|
2024-04-17 14:02:03 +02:00
|
|
|
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
2024-03-04 01:19:02 +01:00
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
2024-03-19 21:56:38 +01:00
|
|
|
import { RecordingStartingPoint } from "@/types/record";
|
2024-03-05 13:02:34 +01:00
|
|
|
import {
|
2024-06-04 01:10:39 +02:00
|
|
|
REVIEW_PADDING,
|
2024-03-05 13:02:34 +01:00
|
|
|
ReviewFilter,
|
|
|
|
ReviewSegment,
|
|
|
|
ReviewSeverity,
|
|
|
|
ReviewSummary,
|
2024-05-26 23:49:12 +02:00
|
|
|
SegmentedReviewData,
|
2024-03-05 13:02:34 +01:00
|
|
|
} from "@/types/review";
|
2024-02-27 22:39:05 +01:00
|
|
|
import EventView from "@/views/events/EventView";
|
2024-03-17 13:30:19 +01:00
|
|
|
import { RecordingView } from "@/views/events/RecordingView";
|
2024-02-23 01:03:34 +01:00
|
|
|
import axios from "axios";
|
2024-03-15 19:46:17 +01:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2024-02-23 01:03:34 +01:00
|
|
|
import useSWR from "swr";
|
2024-02-21 21:07:32 +01:00
|
|
|
|
|
|
|
export default function Events() {
|
2024-03-26 03:25:06 +01:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
|
|
revalidateOnFocus: false,
|
|
|
|
});
|
2024-03-04 01:19:02 +01:00
|
|
|
const timezone = useTimezone(config);
|
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
// recordings viewer
|
2024-02-27 14:37:39 +01:00
|
|
|
|
2024-03-07 02:17:35 +01:00
|
|
|
const [severity, setSeverity] = useOverlayState<ReviewSeverity>(
|
|
|
|
"severity",
|
|
|
|
"alert",
|
|
|
|
);
|
2024-04-12 14:31:30 +02:00
|
|
|
|
2024-03-19 21:56:38 +01:00
|
|
|
const [recording, setRecording] =
|
|
|
|
useOverlayState<RecordingStartingPoint>("recording");
|
2024-04-12 14:31:30 +02:00
|
|
|
|
2024-04-17 14:02:03 +02:00
|
|
|
useSearchEffect("id", (reviewId: string) => {
|
|
|
|
axios
|
|
|
|
.get(`review/${reviewId}`)
|
|
|
|
.then((resp) => {
|
|
|
|
if (resp.status == 200 && resp.data) {
|
|
|
|
setRecording(
|
|
|
|
{
|
|
|
|
camera: resp.data.camera,
|
2024-06-04 01:10:39 +02:00
|
|
|
startTime: resp.data.start_time - REVIEW_PADDING,
|
2024-04-17 14:02:03 +02:00
|
|
|
severity: resp.data.severity,
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(() => {});
|
|
|
|
});
|
|
|
|
|
2024-03-09 00:24:12 +01:00
|
|
|
const [startTime, setStartTime] = useState<number>();
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-04-12 14:31:30 +02:00
|
|
|
useEffect(() => {
|
|
|
|
if (recording) {
|
|
|
|
document.title = "Recordings - Frigate";
|
|
|
|
} else {
|
|
|
|
document.title = `Review - Frigate`;
|
|
|
|
}
|
|
|
|
}, [recording, severity]);
|
|
|
|
|
2024-02-25 20:04:44 +01:00
|
|
|
// review filter
|
|
|
|
|
|
|
|
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
|
|
|
useApiFilter<ReviewFilter>();
|
|
|
|
|
2024-06-04 17:10:19 +02:00
|
|
|
useSearchEffect("group", (reviewGroup) => {
|
|
|
|
if (config && reviewGroup) {
|
|
|
|
const group = config.camera_groups[reviewGroup];
|
2024-07-15 17:34:41 +02:00
|
|
|
const isBirdseyeOnly =
|
|
|
|
group.cameras.length == 1 && group.cameras[0] == "birdseye";
|
2024-06-04 17:10:19 +02:00
|
|
|
|
2024-07-15 17:34:41 +02:00
|
|
|
if (group && !isBirdseyeOnly) {
|
2024-06-04 17:10:19 +02:00
|
|
|
setReviewFilter({
|
|
|
|
...reviewFilter,
|
|
|
|
cameras: group.cameras,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-03-21 14:43:37 +01: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 20:04:44 +01:00
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
// review paging
|
|
|
|
|
2024-02-28 15:16:32 +01:00
|
|
|
const [beforeTs, setBeforeTs] = useState(Date.now() / 1000);
|
2024-02-26 21:47:44 +01:00
|
|
|
const last24Hours = useMemo(() => {
|
2024-02-28 15:16:32 +01:00
|
|
|
return { before: beforeTs, after: getHoursAgo(24) };
|
|
|
|
}, [beforeTs]);
|
2024-02-26 21:47:44 +01:00
|
|
|
const selectedTimeRange = useMemo(() => {
|
|
|
|
if (reviewSearchParams["after"] == undefined) {
|
|
|
|
return last24Hours;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
before: Math.floor(reviewSearchParams["before"]),
|
|
|
|
after: Math.floor(reviewSearchParams["after"]),
|
|
|
|
};
|
2024-02-28 23:23:56 +01:00
|
|
|
}, [last24Hours, reviewSearchParams]);
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-03-26 03:25:06 +01:00
|
|
|
// we want to update the items whenever the severity changes
|
2024-03-27 01:54:00 +01: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-26 03:25:06 +01:00
|
|
|
|
2024-02-28 23:23:56 +01:00
|
|
|
const reviewSegmentFetcher = useCallback((key: Array<string> | string) => {
|
2024-02-23 01:03:34 +01:00
|
|
|
const [path, params] = Array.isArray(key) ? key : [key, undefined];
|
|
|
|
return axios.get(path, { params }).then((res) => res.data);
|
|
|
|
}, []);
|
|
|
|
|
2024-03-07 15:33:36 +01:00
|
|
|
const getKey = useCallback(() => {
|
|
|
|
const params = {
|
|
|
|
cameras: reviewSearchParams["cameras"],
|
|
|
|
labels: reviewSearchParams["labels"],
|
2024-06-11 16:19:17 +02:00
|
|
|
zones: reviewSearchParams["zones"],
|
2024-03-07 15:33:36 +01:00
|
|
|
reviewed: 1,
|
|
|
|
before: reviewSearchParams["before"] || last24Hours.before,
|
|
|
|
after: reviewSearchParams["after"] || last24Hours.after,
|
|
|
|
};
|
|
|
|
return ["review", params];
|
|
|
|
}, [reviewSearchParams, last24Hours]);
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-03-07 15:33:36 +01:00
|
|
|
const { data: reviews, mutate: updateSegments } = useSWR<ReviewSegment[]>(
|
|
|
|
getKey,
|
|
|
|
reviewSegmentFetcher,
|
|
|
|
{
|
|
|
|
revalidateOnFocus: false,
|
2024-03-20 04:16:22 +01:00
|
|
|
revalidateOnReconnect: false,
|
2024-02-23 01:03:34 +01:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-05-26 23:49:12 +02: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,
|
|
|
|
};
|
|
|
|
}, [reviews]);
|
|
|
|
|
|
|
|
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 (reviewFilter?.showReviewed != 1) {
|
|
|
|
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, reviewItems?.all.length]);
|
|
|
|
|
2024-03-04 01:19:02 +01:00
|
|
|
// review summary
|
|
|
|
|
2024-03-26 03:25:06 +01:00
|
|
|
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
|
|
|
|
[
|
|
|
|
"review/summary",
|
|
|
|
{
|
|
|
|
timezone: timezone,
|
|
|
|
cameras: reviewSearchParams["cameras"] ?? null,
|
|
|
|
labels: reviewSearchParams["labels"] ?? null,
|
2024-06-11 16:19:17 +02:00
|
|
|
zones: reviewSearchParams["zones"] ?? null,
|
2024-03-26 03:25:06 +01:00
|
|
|
},
|
|
|
|
],
|
2024-03-05 13:02:34 +01:00
|
|
|
{
|
2024-03-26 03:25:06 +01:00
|
|
|
revalidateOnFocus: true,
|
|
|
|
refreshInterval: 30000,
|
|
|
|
revalidateOnReconnect: false,
|
2024-03-05 13:02:34 +01:00
|
|
|
},
|
2024-03-26 03:25:06 +01:00
|
|
|
);
|
2024-03-04 01:19:02 +01:00
|
|
|
|
2024-03-05 13:02:34 +01:00
|
|
|
const reloadData = useCallback(() => {
|
|
|
|
setBeforeTs(Date.now() / 1000);
|
|
|
|
updateSummary();
|
|
|
|
}, [updateSummary]);
|
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
// preview videos
|
|
|
|
const previewTimes = useMemo(() => {
|
2024-05-21 15:00:29 +02:00
|
|
|
const startDate = new Date(selectedTimeRange.after * 1000);
|
2024-05-28 16:09:17 +02:00
|
|
|
startDate.setUTCMinutes(0, 0, 0);
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-05-21 15:00:29 +02:00
|
|
|
const endDate = new Date(selectedTimeRange.before * 1000);
|
|
|
|
endDate.setHours(endDate.getHours() + 1, 0, 0, 0);
|
2024-03-15 19:46:17 +01:00
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
return {
|
2024-05-28 16:09:17 +02:00
|
|
|
after: startDate.getTime() / 1000,
|
|
|
|
before: endDate.getTime() / 1000,
|
2024-03-20 04:16:22 +01:00
|
|
|
};
|
2024-05-21 15:00:29 +02:00
|
|
|
}, [selectedTimeRange]);
|
2024-03-15 19:46:17 +01:00
|
|
|
|
2024-05-08 16:46:10 +02:00
|
|
|
const allPreviews = useCameraPreviews(
|
|
|
|
previewTimes ?? { after: 0, before: 0 },
|
|
|
|
{
|
|
|
|
fetchPreviews: previewTimes != undefined,
|
|
|
|
},
|
|
|
|
);
|
2024-03-15 19:46:17 +01:00
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
// review status
|
|
|
|
|
2024-03-14 21:49:03 +01:00
|
|
|
const markAllItemsAsReviewed = useCallback(
|
|
|
|
async (currentItems: ReviewSegment[]) => {
|
|
|
|
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 14:42:16 +02:00
|
|
|
if (seg.end_time && seg.severity == severity) {
|
2024-03-14 21:49:03 +01:00
|
|
|
seg.has_been_reviewed = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return newData;
|
|
|
|
},
|
|
|
|
{ revalidate: false, populateCache: true },
|
|
|
|
);
|
|
|
|
|
2024-04-11 14:42:16 +02:00
|
|
|
const itemsToMarkReviewed = currentItems
|
|
|
|
?.filter((seg) => seg.end_time)
|
|
|
|
?.map((seg) => seg.id);
|
|
|
|
|
|
|
|
if (itemsToMarkReviewed.length > 0) {
|
|
|
|
await axios.post(`reviews/viewed`, {
|
|
|
|
ids: itemsToMarkReviewed,
|
|
|
|
});
|
|
|
|
reloadData();
|
|
|
|
}
|
2024-03-14 21:49:03 +01:00
|
|
|
},
|
|
|
|
[reloadData, updateSegments],
|
|
|
|
);
|
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
const markItemAsReviewed = useCallback(
|
2024-03-05 13:02:34 +01:00
|
|
|
async (review: ReviewSegment) => {
|
2024-03-06 01:39:37 +01:00
|
|
|
const resp = await axios.post(`reviews/viewed`, { ids: [review.id] });
|
2024-02-23 01:03:34 +01:00
|
|
|
|
|
|
|
if (resp.status == 200) {
|
|
|
|
updateSegments(
|
2024-03-07 15:33:36 +01:00
|
|
|
(data: ReviewSegment[] | undefined) => {
|
2024-02-23 01:03:34 +01:00
|
|
|
if (!data) {
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2024-03-07 15:33:36 +01:00
|
|
|
const reviewIndex = data.findIndex((item) => item.id == review.id);
|
|
|
|
if (reviewIndex == -1) {
|
|
|
|
return data;
|
|
|
|
}
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-03-07 15:33:36 +01:00
|
|
|
const newData = [
|
|
|
|
...data.slice(0, reviewIndex),
|
|
|
|
{ ...data[reviewIndex], has_been_reviewed: true },
|
|
|
|
...data.slice(reviewIndex + 1),
|
|
|
|
];
|
2024-02-23 01:03:34 +01:00
|
|
|
|
|
|
|
return newData;
|
|
|
|
},
|
2024-02-28 23:23:56 +01:00
|
|
|
{ revalidate: false, populateCache: true },
|
2024-02-23 01:03:34 +01:00
|
|
|
);
|
2024-03-05 13:02:34 +01:00
|
|
|
|
|
|
|
updateSummary(
|
2024-03-06 01:39:37 +01:00
|
|
|
(data: ReviewSummary | undefined) => {
|
2024-03-05 13:02:34 +01:00
|
|
|
if (!data) {
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
const day = new Date(review.start_time * 1000);
|
2024-03-06 01:39:37 +01:00
|
|
|
const today = new Date();
|
|
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
let key;
|
|
|
|
if (day.getTime() > today.getTime()) {
|
|
|
|
key = "last24Hours";
|
|
|
|
} else {
|
|
|
|
key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
|
|
|
|
}
|
2024-03-05 13:02:34 +01:00
|
|
|
|
2024-03-06 01:39:37 +01:00
|
|
|
if (!Object.keys(data).includes(key)) {
|
2024-03-05 13:02:34 +01:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2024-03-06 01:39:37 +01:00
|
|
|
const item = data[key];
|
|
|
|
return {
|
|
|
|
...data,
|
|
|
|
[key]: {
|
2024-03-05 13:02:34 +01: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,
|
|
|
|
reviewed_motion:
|
|
|
|
review.severity == "significant_motion"
|
|
|
|
? item.reviewed_motion + 1
|
|
|
|
: item.reviewed_motion,
|
|
|
|
},
|
2024-03-06 01:39:37 +01:00
|
|
|
};
|
2024-03-05 13:02:34 +01:00
|
|
|
},
|
|
|
|
{ revalidate: false, populateCache: true },
|
|
|
|
);
|
2024-02-23 01:03:34 +01:00
|
|
|
}
|
|
|
|
},
|
2024-03-05 13:02:34 +01:00
|
|
|
[updateSegments, updateSummary],
|
2024-02-23 01:03:34 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
// selected items
|
|
|
|
|
2024-03-05 20:55:44 +01:00
|
|
|
const selectedReviewData = useMemo(() => {
|
2024-03-19 21:56:38 +01:00
|
|
|
if (!recording) {
|
2024-03-05 13:03:10 +01:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2024-03-19 21:56:38 +01:00
|
|
|
if (!config) {
|
2024-02-23 01:03:34 +01:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2024-03-19 21:56:38 +01:00
|
|
|
if (!reviews) {
|
2024-02-23 01:03:34 +01:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2024-03-19 21:56:38 +01:00
|
|
|
setStartTime(recording.startTime);
|
2024-03-05 13:03:10 +01:00
|
|
|
const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras);
|
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
return {
|
2024-03-19 21:56:38 +01:00
|
|
|
camera: recording.camera,
|
|
|
|
start_time: recording.startTime,
|
2024-03-05 13:03:10 +01:00
|
|
|
allCameras: allCameras,
|
2024-02-23 01:03:34 +01:00
|
|
|
};
|
2024-02-28 23:23:56 +01:00
|
|
|
|
|
|
|
// previews will not update after item is selected
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2024-03-19 21:56:38 +01:00
|
|
|
}, [recording, reviews]);
|
2024-02-23 01:03:34 +01:00
|
|
|
|
2024-03-04 01:19:02 +01:00
|
|
|
if (!timezone) {
|
|
|
|
return <ActivityIndicator />;
|
|
|
|
}
|
|
|
|
|
2024-04-02 21:25:02 +02:00
|
|
|
if (recording) {
|
|
|
|
if (selectedReviewData) {
|
|
|
|
return (
|
|
|
|
<RecordingView
|
|
|
|
startCamera={selectedReviewData.camera}
|
|
|
|
startTime={selectedReviewData.start_time}
|
|
|
|
allCameras={selectedReviewData.allCameras}
|
|
|
|
reviewItems={reviews}
|
|
|
|
reviewSummary={reviewSummary}
|
|
|
|
allPreviews={allPreviews}
|
|
|
|
timeRange={selectedTimeRange}
|
|
|
|
filter={reviewFilter}
|
|
|
|
updateFilter={onUpdateFilter}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2024-02-23 01:03:34 +01:00
|
|
|
} else {
|
|
|
|
return (
|
2024-02-27 22:39:05 +01:00
|
|
|
<EventView
|
2024-05-26 23:49:12 +02:00
|
|
|
reviewItems={reviewItems}
|
|
|
|
currentReviewItems={currentItems}
|
2024-03-04 01:19:02 +01:00
|
|
|
reviewSummary={reviewSummary}
|
2024-02-23 01:03:34 +01:00
|
|
|
relevantPreviews={allPreviews}
|
2024-02-26 21:47:44 +01:00
|
|
|
timeRange={selectedTimeRange}
|
2024-02-25 20:04:44 +01:00
|
|
|
filter={reviewFilter}
|
2024-03-07 02:17:35 +01:00
|
|
|
severity={severity ?? "alert"}
|
2024-03-09 00:24:12 +01:00
|
|
|
startTime={startTime}
|
2024-02-27 14:37:39 +01:00
|
|
|
setSeverity={setSeverity}
|
2024-02-23 01:03:34 +01:00
|
|
|
markItemAsReviewed={markItemAsReviewed}
|
2024-03-14 21:49:03 +01:00
|
|
|
markAllItemsAsReviewed={markAllItemsAsReviewed}
|
2024-03-19 21:56:38 +01:00
|
|
|
onOpenRecording={setRecording}
|
2024-02-27 14:37:39 +01:00
|
|
|
pullLatestData={reloadData}
|
2024-02-25 20:04:44 +01:00
|
|
|
updateFilter={onUpdateFilter}
|
2024-02-23 01:03:34 +01:00
|
|
|
/>
|
|
|
|
);
|
2024-02-21 21:07:32 +01:00
|
|
|
}
|
2024-02-23 01:03:34 +01:00
|
|
|
}
|
2024-02-21 21:07:32 +01:00
|
|
|
|
2024-02-23 01:03:34 +01:00
|
|
|
function getHoursAgo(hours: number): number {
|
|
|
|
const now = new Date();
|
|
|
|
now.setHours(now.getHours() - hours);
|
|
|
|
return now.getTime() / 1000;
|
2024-02-21 21:07:32 +01:00
|
|
|
}
|