diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py index 9691ac8fd..fadc483c3 100644 --- a/frigate/data_processing/post/review_descriptions.py +++ b/frigate/data_processing/post/review_descriptions.py @@ -132,17 +132,15 @@ class ReviewDescriptionProcessor(PostProcessorApi): if image_source == ImageSourceEnum.recordings: duration = final_data["end_time"] - final_data["start_time"] - buffer_extension = min( - 10, max(2, duration * RECORDING_BUFFER_EXTENSION_PERCENT) - ) + buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT) # Ensure minimum total duration for short review items # This provides better context for brief events total_duration = duration + (2 * buffer_extension) if total_duration < MIN_RECORDING_DURATION: - # Expand buffer to reach minimum duration, still respecting max of 10s per side + # Expand buffer to reach minimum duration, still respecting max of 5s per side additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2 - buffer_extension = min(10, additional_buffer_per_side) + buffer_extension = min(5, additional_buffer_per_side) thumbs = self.get_recording_frames( camera, diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index db3cb22ed..1901a81e1 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -424,7 +424,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): if not res: return { - "message": "No face was recognized.", + "message": "Model is still training, please try again in a few moments.", "success": False, } diff --git a/web/public/notifications-worker.js b/web/public/notifications-worker.js index ab8a6ae44..8a0a8de2f 100644 --- a/web/public/notifications-worker.js +++ b/web/public/notifications-worker.js @@ -44,11 +44,16 @@ self.addEventListener("notificationclick", (event) => { switch (event.action ?? "default") { case "markReviewed": if (event.notification.data) { - fetch("/api/reviews/viewed", { - method: "POST", - headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": 1 }, - body: JSON.stringify({ ids: [event.notification.data.id] }), - }); + event.waitUntil( + fetch("/api/reviews/viewed", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": 1, + }, + body: JSON.stringify({ ids: [event.notification.data.id] }), + }) + ); } break; default: @@ -58,7 +63,7 @@ self.addEventListener("notificationclick", (event) => { // eslint-disable-next-line no-undef if (clients.openWindow) { // eslint-disable-next-line no-undef - return clients.openWindow(url); + event.waitUntil(clients.openWindow(url)); } } } diff --git a/web/src/components/card/ClassificationCard.tsx b/web/src/components/card/ClassificationCard.tsx index 4511c22d5..0e1138feb 100644 --- a/web/src/components/card/ClassificationCard.tsx +++ b/web/src/components/card/ClassificationCard.tsx @@ -398,11 +398,7 @@ export function GroupedClassificationCard({ threshold={threshold} selected={false} i18nLibrary={i18nLibrary} - onClick={(data, meta) => { - if (meta || selectedItems.length > 0) { - onClick(data); - } - }} + onClick={() => {}} > {children?.(data)} diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index ae60ed1c6..8bd90d51f 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -683,6 +683,22 @@ function ObjectDetailsTab({ const mutate = useGlobalMutation(); + // Helper to map over SWR cached search results while preserving + // either paginated format (SearchResult[][]) or flat format (SearchResult[]) + const mapSearchResults = useCallback( + ( + currentData: SearchResult[][] | SearchResult[] | undefined, + fn: (event: SearchResult) => SearchResult, + ) => { + if (!currentData) return currentData; + if (Array.isArray(currentData[0])) { + return (currentData as SearchResult[][]).map((page) => page.map(fn)); + } + return (currentData as SearchResult[]).map(fn); + }, + [], + ); + // users const isAdmin = useIsAdmin(); @@ -810,17 +826,12 @@ function ObjectDetailsTab({ (key.includes("events") || key.includes("events/search") || key.includes("events/explore")), - (currentData: SearchResult[][] | SearchResult[] | undefined) => { - if (!currentData) return currentData; - // optimistic update - return currentData - .flat() - .map((event) => - event.id === search.id - ? { ...event, data: { ...event.data, description: desc } } - : event, - ); - }, + (currentData: SearchResult[][] | SearchResult[] | undefined) => + mapSearchResults(currentData, (event) => + event.id === search.id + ? { ...event, data: { ...event.data, description: desc } } + : event, + ), { optimisticData: true, rollbackOnError: true, @@ -843,7 +854,7 @@ function ObjectDetailsTab({ ); setDesc(search.data.description); }); - }, [desc, search, mutate, t]); + }, [desc, search, mutate, t, mapSearchResults]); const regenerateDescription = useCallback( (source: "snapshot" | "thumbnails") => { @@ -915,9 +926,8 @@ function ObjectDetailsTab({ (key.includes("events") || key.includes("events/search") || key.includes("events/explore")), - (currentData: SearchResult[][] | SearchResult[] | undefined) => { - if (!currentData) return currentData; - return currentData.flat().map((event) => + (currentData: SearchResult[][] | SearchResult[] | undefined) => + mapSearchResults(currentData, (event) => event.id === search.id ? { ...event, @@ -928,8 +938,7 @@ function ObjectDetailsTab({ }, } : event, - ); - }, + ), { optimisticData: true, rollbackOnError: true, @@ -963,7 +972,7 @@ function ObjectDetailsTab({ ); }); }, - [search, apiHost, mutate, setSearch, t], + [search, apiHost, mutate, setSearch, t, mapSearchResults], ); // recognized plate @@ -992,9 +1001,8 @@ function ObjectDetailsTab({ (key.includes("events") || key.includes("events/search") || key.includes("events/explore")), - (currentData: SearchResult[][] | SearchResult[] | undefined) => { - if (!currentData) return currentData; - return currentData.flat().map((event) => + (currentData: SearchResult[][] | SearchResult[] | undefined) => + mapSearchResults(currentData, (event) => event.id === search.id ? { ...event, @@ -1005,8 +1013,7 @@ function ObjectDetailsTab({ }, } : event, - ); - }, + ), { optimisticData: true, rollbackOnError: true, @@ -1040,7 +1047,7 @@ function ObjectDetailsTab({ ); }); }, - [search, apiHost, mutate, setSearch, t], + [search, apiHost, mutate, setSearch, t, mapSearchResults], ); // speech transcription @@ -1102,17 +1109,12 @@ function ObjectDetailsTab({ (key.includes("events") || key.includes("events/search") || key.includes("events/explore")), - (currentData: SearchResult[][] | SearchResult[] | undefined) => { - if (!currentData) return currentData; - // optimistic update - return currentData - .flat() - .map((event) => - event.id === search.id - ? { ...event, plus_id: "new_upload" } - : event, - ); - }, + (currentData: SearchResult[][] | SearchResult[] | undefined) => + mapSearchResults(currentData, (event) => + event.id === search.id + ? { ...event, plus_id: "new_upload" } + : event, + ), { optimisticData: true, rollbackOnError: true, @@ -1120,7 +1122,7 @@ function ObjectDetailsTab({ }, ); }, - [search, mutate], + [search, mutate, mapSearchResults], ); const popoverContainerRef = useRef(null); @@ -1503,7 +1505,7 @@ function ObjectDetailsTab({ ) : (