diff --git a/frigate/api/review.py b/frigate/api/review.py index 34155ec5e..23c7b971d 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -76,11 +76,112 @@ def review(): def review_summary(): tz_name = request.args.get("timezone", default="utc", type=str) hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name) + day_ago = (datetime.now() - timedelta(hours=24)).timestamp() month_ago = (datetime.now() - timedelta(days=30)).timestamp() cameras = request.args.get("cameras", "all") labels = request.args.get("labels", "all") + clauses = [(ReviewSegment.start_time > day_ago)] + + if cameras != "all": + camera_list = cameras.split(",") + clauses.append((ReviewSegment.camera << camera_list)) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + ) + + label_clause = reduce(operator.or_, label_clauses) + clauses.append((label_clause)) + + last_24 = ( + ReviewSegment.select( + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "alert"), + ReviewSegment.has_been_reviewed, + ) + ], + 0, + ) + ).alias("reviewed_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "detection"), + ReviewSegment.has_been_reviewed, + ) + ], + 0, + ) + ).alias("reviewed_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "significant_motion"), + ReviewSegment.has_been_reviewed, + ) + ], + 0, + ) + ).alias("reviewed_motion"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "alert"), + 1, + ) + ], + 0, + ) + ).alias("total_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "detection"), + 1, + ) + ], + 0, + ) + ).alias("total_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == "significant_motion"), + 1, + ) + ], + 0, + ) + ).alias("total_motion"), + ) + .where(reduce(operator.and_, clauses)) + .dicts() + .get() + ) + clauses = [(ReviewSegment.start_time > month_ago)] if cameras != "all": @@ -101,7 +202,7 @@ def review_summary(): label_clause = reduce(operator.or_, label_clauses) clauses.append((label_clause)) - groups = ( + last_month = ( ReviewSegment.select( fn.strftime( "%Y-%m-%d", @@ -192,7 +293,15 @@ def review_summary(): .order_by(ReviewSegment.start_time.desc()) ) - return jsonify([e for e in groups.dicts().iterator()]) + data = { + "last24Hours": last_24, + } + + for e in last_month.dicts().iterator(): + data[e["day"]] = e + + + return jsonify(data) @ReviewBp.route("/reviews/viewed", methods=("POST",)) diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index c7564573c..18d5781e7 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -115,9 +115,7 @@ export default function Events() { // review summary - const { data: reviewSummary, mutate: updateSummary } = useSWR< - ReviewSummary[] - >([ + const { data: reviewSummary, mutate: updateSummary } = useSWR([ "review/summary", { timezone: timezone, @@ -197,23 +195,30 @@ export default function Events() { ); updateSummary( - (data: ReviewSummary[] | undefined) => { + (data: ReviewSummary | undefined) => { if (!data) { return data; } const day = new Date(review.start_time * 1000); - const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`; - const index = data.findIndex((summary) => summary.day == key); + const today = new Date(); + today.setHours(0, 0, 0, 0); - if (index == -1) { + 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)) { return data; } - const item = data[index]; - return [ - ...data.slice(0, index), - { + const item = data[key]; + return { + ...data, + [key]: { ...item, reviewed_alert: review.severity == "alert" @@ -228,8 +233,7 @@ export default function Events() { ? item.reviewed_motion + 1 : item.reviewed_motion, }, - ...data.slice(index + 1), - ]; + }; }, { revalidate: false, populateCache: true }, ); diff --git a/web/src/types/review.ts b/web/src/types/review.ts index 8e52dc4af..7366738ab 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -28,7 +28,7 @@ export type ReviewFilter = { showReviewed?: 0 | 1; }; -export type ReviewSummary = { +type ReviewSummaryDay = { day: string; reviewed_alert: number; reviewed_detection: number; @@ -38,6 +38,10 @@ export type ReviewSummary = { total_motion: number; }; +export type ReviewSummary = { + [day: string]: ReviewSummaryDay; +}; + export type MotionData = { start_time: number; motion: number; diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 495a10f30..ec70cef62 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -39,7 +39,7 @@ import { Button } from "@/components/ui/button"; type EventViewProps = { reviewPages?: ReviewSegment[][]; - reviewSummary?: ReviewSummary[]; + reviewSummary?: ReviewSummary; relevantPreviews?: Preview[]; timeRange: { before: number; after: number }; reachedEnd: boolean; @@ -75,17 +75,17 @@ export default function EventView({ // review counts const reviewCounts = useMemo(() => { - if (!reviewSummary || reviewSummary.length == 0) { + if (!reviewSummary) { return { alert: 0, detection: 0, significant_motion: 0 }; } let summary; if (filter?.before == undefined) { - summary = reviewSummary[0]; + summary = reviewSummary["last24Hours"]; } else { const day = new Date(filter.before * 1000); const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`; - summary = reviewSummary.find((check) => check.day == key); + summary = reviewSummary[key]; } if (!summary) {