From ec44398b1c3e1c5fbf2d1177d6f1c78c8c8f108a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 24 May 2026 07:48:52 -0500 Subject: [PATCH] Miscellaneous fixes (#23295) * filter motion review by allowed cameras * filter alertCameras by allowed cameras so the recent alerts query for restricted roles doesn't reference cameras they can't access * skip data streams in chapter exports to avoid ffmpeg segfault * formatting * restrict debug replay UI entry points to admin users * Adjust default iGPU name when it can't be found * Fix when model tries to request an invalid camera * Improve prompt * add collapsible main nav items in settings --------- Co-authored-by: Nicolas Mowen --- frigate/api/chat.py | 21 +- frigate/genai/prompts.py | 11 +- frigate/record/export.py | 4 +- frigate/util/services.py | 2 +- .../components/menu/SearchResultActions.tsx | 14 +- .../components/overlay/ActionsDropdown.tsx | 10 +- .../overlay/MobileReviewSettingsDrawer.tsx | 4 +- .../overlay/detail/DetailActionsMenu.tsx | 14 +- web/src/components/timeline/EventMenu.tsx | 14 +- web/src/pages/Settings.tsx | 214 ++++++++++++------ web/src/views/events/EventView.tsx | 29 +-- web/src/views/live/LiveDashboardView.tsx | 10 +- web/src/views/recording/RecordingView.tsx | 20 +- 13 files changed, 253 insertions(+), 114 deletions(-) diff --git a/frigate/api/chat.py b/frigate/api/chat.py index c7d197bf91..4e6bdbd3b4 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -547,9 +547,21 @@ async def _execute_get_live_context( camera: str, allowed_cameras: List[str], ) -> Dict[str, Any]: + # Reject wildcards explicitly so models retry with a real camera name + # instead of silently fanning out across every camera. + if camera in ("*", "all"): + return { + "error": ( + "get_live_context requires a single camera name; wildcards " + "are not supported. Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } + if camera not in allowed_cameras: return { "error": f"Camera '{camera}' not found or access denied", + "available_cameras": allowed_cameras, } if camera not in request.app.frigate_config.cameras: @@ -721,7 +733,14 @@ async def _execute_tool_internal( "Arguments: %s", json.dumps(arguments), ) - return {"error": "Camera parameter is required"} + return { + "error": ( + "get_live_context requires a single camera name; " + "wildcards and empty values are not supported. " + "Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } return await _execute_get_live_context(request, camera, allowed_cameras) elif tool_name == "start_camera_watch": return await _execute_start_camera_watch(request, arguments) diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py index 93e19209bf..af6ddab889 100644 --- a/frigate/genai/prompts.py +++ b/frigate/genai/prompts.py @@ -518,16 +518,21 @@ def get_tool_definitions( "function": { "name": "get_live_context", "description": ( - "Get the current live image and detection information for a camera: objects being tracked, " + "Get the current live image and detection information for a single camera: objects being tracked, " "zones, timestamps. Use this to understand what is visible in the live view. " - "Call this when answering questions about what is happening right now on a specific camera." + "Call this when answering questions about what is happening right now on a specific camera. " + "Operates on one camera at a time; call the tool again for each additional camera. " + "Wildcards and empty values are not accepted." ), "parameters": { "type": "object", "properties": { "camera": { "type": "string", - "description": "Camera name to get live context for.", + "description": ( + "Exact name of a single camera to get live context for. " + "Wildcards (e.g. '*', 'all') and empty strings are not accepted." + ), }, }, "required": ["camera"], diff --git a/frigate/record/export.py b/frigate/record/export.py index 9f571a5a5c..3a943cb3fe 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -579,7 +579,9 @@ class RecordingExporter(threading.Thread): else: chapters_path = self._build_chapter_metadata_file(recordings) chapter_args = ( - f" -i {chapters_path} -map 0 -map_metadata 1" if chapters_path else "" + f" -i {chapters_path} -map 0 -dn -map_metadata 1" + if chapters_path + else "" ) ffmpeg_cmd = ( f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart" diff --git a/frigate/util/services.py b/frigate/util/services.py index 4a715608e2..a5b1af8249 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -478,7 +478,7 @@ def get_intel_gpu_stats( overall_pct = min(100.0, compute_pct + dec_pct) entry: dict[str, Any] = { - "name": names.get(pdev) or f"Intel GPU {pdev}", + "name": names.get(pdev) or "Intel iGPU", "vendor": "intel", "gpu": f"{round(overall_pct, 2)}%", "mem": "-%", diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 90a70ff5d9..c7af518932 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -130,9 +130,15 @@ export default function SearchResultActions({ }, ); } else { - toast.error(t("dialog.toast.error", { error: errorMessage }), { - position: "top-center", - }); + toast.error( + t("dialog.toast.error", { + ns: "views/replay", + error: errorMessage, + }), + { + position: "top-center", + }, + ); } }) .finally(() => { @@ -206,7 +212,7 @@ export default function SearchResultActions({ {t("itemMenu.addTrigger.label")} )} - {searchResult.has_clip && ( + {isAdmin && searchResult.has_clip && ( void; + onDebugReplayClick?: () => void; onExportClick: () => void; onShareTimestampClick: () => void; }; @@ -42,9 +42,11 @@ export default function ActionsDropdown({ {t("recording.shareTimestamp.label", { ns: "components/dialog" })} - - {t("title", { ns: "views/replay" })} - + {onDebugReplayClick && ( + + {t("title", { ns: "views/replay" })} + + )} ); diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 2ad2067462..c2daf6d051 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -29,6 +29,7 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { StartExportResponse } from "@/types/export"; import { ShareTimestampContent } from "./ShareTimestampDialog"; +import { useIsAdmin } from "@/hooks/use-is-admin"; type DrawerMode = | "none" @@ -109,6 +110,7 @@ export default function MobileReviewSettingsDrawer({ "views/replay", "common", ]); + const isAdmin = useIsAdmin(); const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); const [exportTab, setExportTab] = useState("export"); @@ -388,7 +390,7 @@ export default function MobileReviewSettingsDrawer({ {t("filter")} )} - {features.includes("debug-replay") && ( + {isAdmin && features.includes("debug-replay") && (