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")}
+
+
+ )}