Miscellaneous fixes (#23238)
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run

* start audio transcription post processor when enabled on any camera

* Fetch embed key whenever an error occurs in case the llama server was restarted

* mypy

* add tooltips for colored dots in settings menu

* add ability to reorder cameras from management pane

* add ability to reorder birdseye

* add reordering save text to camera management view

* Include NPU in latency performance hint

* Implement turbo for NPU on object detection

* hide order fields

* drop auto-derived field paths from camera value when unset globally

* use correct field type for export hwaccel args

* add debug replay to detail actions menu

* clarify debug replay in docs

* guard get_current_frame_time against missing camera state

* Implement debug reply from export

* Refactor debug replay to use sources for dynamic playback

* Mypy

* fix debug export replay source timestamp handling

* skip replay cameras in stats immediately

* broadcast debug replay state over ws and buffer pre-OPEN sends

- push debug replay session state over the job_state ws topic so the status bar reacts instantly to start/stop without polling
- fix child-effect-before-parent-effect race in WsProvider that silently dropped initial snapshot requests on cold load

* fix debug replay test hang

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2026-05-18 22:52:40 -05:00
committed by GitHub
co-authored by Nicolas Mowen
parent d968f00500
commit 43d97acd21
28 changed files with 1176 additions and 170 deletions
@@ -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: (
<a
href={`${baseUrl}replay`}
target="_blank"
rel="noopener noreferrer"
>
<Button>
{t("dialog.toast.goToReplay", { ns: "views/replay" })}
</Button>
</a>
),
});
} 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({
</div>
</DropdownMenuItem>
)}
{search.has_clip && (
<DropdownMenuItem
className="cursor-pointer"
aria-label={t("itemMenu.debugReplay.aria")}
disabled={isStarting}
onSelect={() => {
setIsOpen(false);
handleDebugReplay();
}}
>
<span>
{isStarting
? t("dialog.starting", { ns: "views/replay" })
: t("itemMenu.debugReplay.label")}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>