From d1b46706c9cc06af2216cc5ad34ef372ed9a1189 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 May 2026 14:48:28 -0500 Subject: [PATCH] add debug replay to detail actions menu --- web/public/locales/en/views/explore.json | 2 +- .../overlay/detail/DetailActionsMenu.tsx | 78 ++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 43db9bda48..d1087b3c96 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -222,7 +222,7 @@ "label": "Hide object path" }, "debugReplay": { - "label": "Debug replay", + "label": "Debug Replay", "aria": "View this tracked object in the debug replay view" }, "more": { diff --git a/web/src/components/overlay/detail/DetailActionsMenu.tsx b/web/src/components/overlay/detail/DetailActionsMenu.tsx index dc4ea5b2cf..789f396772 100644 --- a/web/src/components/overlay/detail/DetailActionsMenu.tsx +++ b/web/src/components/overlay/detail/DetailActionsMenu.tsx @@ -1,4 +1,6 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; import { Event } from "@/types/event"; import { baseUrl } from "@/api/baseUrl"; import { ReviewSegment, REVIEW_PADDING } from "@/types/review"; @@ -12,6 +14,7 @@ import { DropdownMenuTrigger, DropdownMenuPortal, } from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; import { HiDotsHorizontal } from "react-icons/hi"; import { SearchResult } from "@/types/search"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -33,9 +36,14 @@ export default function DetailActionsMenu({ setSearch, setSimilarity, }: Props) { - const { t } = useTranslation(["views/explore", "views/faceLibrary"]); + const { t } = useTranslation([ + "views/explore", + "views/faceLibrary", + "views/replay", + ]); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); + const [isStarting, setIsStarting] = useState(false); const isAdmin = useIsAdmin(); const clipTimeRange = useMemo(() => { @@ -49,6 +57,54 @@ export default function DetailActionsMenu({ search.data?.type === "audio" ? null : [`review/event/${search.id}`], ); + const handleDebugReplay = useCallback(() => { + setIsStarting(true); + + axios + .post("debug_replay/start", { + camera: search.camera, + start_time: search.start_time, + end_time: search.end_time, + }) + .then((response) => { + if (response.status === 202 || response.status === 200) { + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, [navigate, search.camera, search.start_time, search.end_time, t]); + // don't render menu at all if no options are available const hasSemanticSearchOption = config?.semantic_search.enabled && @@ -172,6 +228,24 @@ export default function DetailActionsMenu({ )} + + {search.has_clip && ( + { + setIsOpen(false); + handleDebugReplay(); + }} + > + + {isStarting + ? t("dialog.starting", { ns: "views/replay" }) + : t("itemMenu.debugReplay.label")} + + + )}