From daccbd9c2b0a163204cbcc371ff03c26977e4d37 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:17:03 -0500 Subject: [PATCH] highlight entry in UI when triggered --- web/public/locales/en/views/settings.json | 7 +-- web/src/api/ws.tsx | 11 ++++ web/src/types/ws.ts | 8 +++ web/src/views/settings/TriggerView.tsx | 62 +++++++++++++++++++---- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index c4ae23115..6fe5adde3 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -660,7 +660,8 @@ "actions": "Actions", "noTriggers": "No triggers configured for this camera.", "edit": "Edit", - "deleteTrigger": "Delete Trigger" + "deleteTrigger": "Delete Trigger", + "lastTriggered": "Last triggered" }, "type": { "thumbnail": "Thumbnail", @@ -685,7 +686,7 @@ }, "form": { "name": { - "title": "Trigger Name", + "title": "Name", "placeholder": "Enter trigger name", "error": { "minLength": "Name must be at least 2 characters long.", @@ -697,7 +698,7 @@ "description": "Enable or disable this trigger" }, "type": { - "title": "Trigger Type", + "title": "Type", "placeholder": "Select trigger type" }, "content": { diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 78c596e13..cc3ea05bf 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -9,6 +9,7 @@ import { ModelState, ToggleableSetting, TrackedObjectUpdateReturnType, + TriggerStatus, } from "@/types/ws"; import { FrigateStats } from "@/types/stats"; import { createContainer } from "react-tracked"; @@ -572,3 +573,13 @@ export function useNotificationTest(): { } = useWs("notification_test", "notification_test"); return { payload: payload as string, send }; } + +export function useTriggers(): { payload: TriggerStatus } { + const { + value: { payload }, + } = useWs("triggers", ""); + const parsed = payload + ? JSON.parse(payload as string) + : { name: "", camera: "", event_id: "", type: "", score: 0 }; + return { payload: useDeepMemo(parsed) }; +} diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts index 06ec9ae1d..7fad6e953 100644 --- a/web/src/types/ws.ts +++ b/web/src/types/ws.ts @@ -105,3 +105,11 @@ export type TrackedObjectUpdateReturnType = { timestamp?: number; text?: string; } | null; + +export type TriggerStatus = { + name: string; + camera: string; + event_id: string; + type: string; + score: number; +}; diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index d96e26244..ad471120a 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -22,6 +22,7 @@ import { useSearchEffect } from "@/hooks/use-overlay-state"; import { cn } from "@/lib/utils"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { Link } from "react-router-dom"; +import { useTriggers } from "@/api/ws"; type ConfigSetBody = { requires_restart: number; @@ -65,10 +66,16 @@ export default function TriggerView({ const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); - const { data: trigger_status } = useSWR(`/triggers/status/${selectedCamera}`); + const { data: trigger_status, mutate } = useSWR( + `/triggers/status/${selectedCamera}`, + { + revalidateOnFocus: false, + }, + ); const [showCreate, setShowCreate] = useState(false); const [showDelete, setShowDelete] = useState(false); const [selectedTrigger, setSelectedTrigger] = useState(null); + const [triggeredTrigger, setTriggeredTrigger] = useState(); const [isLoading, setIsLoading] = useState(false); const triggers = useMemo(() => { @@ -91,6 +98,38 @@ export default function TriggerView({ })); }, [config, selectedCamera]); + // watch websocket for updates + const { payload: triggers_status_ws } = useTriggers(); + + useEffect(() => { + if (!triggers_status_ws) return; + + mutate(); + + setTriggeredTrigger(triggers_status_ws.name); + const target = document.querySelector( + `#trigger-${triggers_status_ws.name}`, + ); + if (target) { + target.scrollIntoView({ + block: "center", + behavior: "smooth", + inline: "nearest", + }); + const ring = target.querySelector(".trigger-ring"); + if (ring) { + ring.classList.add(`outline-selected`); + ring.classList.remove("outline-transparent"); + + const timeout = setTimeout(() => { + ring.classList.remove(`outline-selected`); + ring.classList.add("outline-transparent"); + }, 3000); + return () => clearTimeout(timeout); + } + } + }, [triggers_status_ws, selectedCamera, mutate]); + useEffect(() => { document.title = t("triggers.documentTitle"); }, [t]); @@ -382,8 +421,17 @@ export default function TriggerView({ {triggers.map((trigger) => (
+

@@ -416,10 +464,6 @@ export default function TriggerView({
- {trigger.threshold.toFixed(2)} threshold - - {trigger.actions.length} actions - -
- Last:{" "} +
+ {t("triggers.table.lastTriggered")}{" "} {trigger_status && trigger_status.triggers[trigger.name] ?.last_triggered