diff --git a/frigate/api/event.py b/frigate/api/event.py
index 2766f0814..3b4b413be 100644
--- a/frigate/api/event.py
+++ b/frigate/api/event.py
@@ -1647,24 +1647,14 @@ def update_trigger_embedding(
status_code=400,
)
- old = list(
- Trigger.select(Trigger.camera, Trigger.name, Trigger.data)
- .where(Trigger.camera == camera, Trigger.name == name)
- .dicts()
- .iterator()
- )
- if not old:
- return JSONResponse(
- content={
- "success": False,
- "message": f"Trigger {camera}:{name} not found",
- },
- status_code=404,
- )
+ # Check if trigger exists for upsert
+ trigger = Trigger.get_or_none(Trigger.camera == camera, Trigger.name == name)
- context.delete_trigger_thumbnail(camera, old[0]["data"])
+ if trigger:
+ # Update existing trigger
+ if trigger.data != body.data: # Delete old thumbnail only if data changes
+ context.delete_trigger_thumbnail(camera, trigger.data)
- updated = (
Trigger.update(
data=body.data,
model=request.app.frigate_config.semantic_search.model,
@@ -1672,18 +1662,19 @@ def update_trigger_embedding(
threshold=body.threshold,
triggering_event_id="",
last_triggered=None,
- )
- .where(Trigger.camera == camera, Trigger.name == name)
- .execute()
- )
-
- if updated == 0:
- return JSONResponse(
- content={
- "success": False,
- "message": f"Trigger {camera}:{name} not found",
- },
- status_code=404,
+ ).where(Trigger.camera == camera, Trigger.name == name).execute()
+ else:
+ # Create new trigger (for rename case)
+ Trigger.create(
+ camera=camera,
+ name=name,
+ type=body.type,
+ data=body.data,
+ threshold=body.threshold,
+ model=request.app.frigate_config.semantic_search.model,
+ embedding=np.array(embedding, dtype=np.float32).tobytes(),
+ triggering_event_id="",
+ last_triggered=None,
)
if body.type == "thumbnail":
diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py
index 99f2ff970..302140c10 100644
--- a/frigate/comms/webpush.py
+++ b/frigate/comms/webpush.py
@@ -196,6 +196,7 @@ class WebPushClient(Communicator): # type: ignore[misc]
# notification action enabled
if (
not self.config.cameras[camera].notifications.enabled
+ or name not in self.config.cameras[camera].semantic_search.triggers
or "notification"
not in self.config.cameras[camera]
.semantic_search.triggers[name]
diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py
index 14fc479be..b56ca4a56 100644
--- a/frigate/data_processing/post/semantic_trigger.py
+++ b/frigate/data_processing/post/semantic_trigger.py
@@ -81,7 +81,9 @@ class SemanticTriggerProcessor(PostProcessorApi):
for trigger in triggers:
if (
- not self.config.cameras[camera]
+ trigger["name"]
+ not in self.config.cameras[camera].semantic_search.triggers
+ or not self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]]
.enabled
):
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 6713186df..91eefd5c3 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -681,7 +681,7 @@
},
"deleteTrigger": {
"title": "Delete Trigger",
- "desc": "Are you sure you want to delete the trigger {{triggerName}} from camera {{camera}}? This action cannot be undone."
+ "desc": "Are you sure you want to delete the trigger {{triggerName}}? This action cannot be undone."
},
"form": {
"name": {
diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx
index b74da75f6..243dd8502 100644
--- a/web/src/components/overlay/CreateTriggerDialog.tsx
+++ b/web/src/components/overlay/CreateTriggerDialog.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useMemo } from "react";
+import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -40,6 +40,7 @@ type CreateTriggerDialogProps = {
show: boolean;
trigger: Trigger | null;
selectedCamera: string;
+ isLoading: boolean;
onCreate: (
enabled: boolean,
name: string,
@@ -56,12 +57,12 @@ export default function CreateTriggerDialog({
show,
trigger,
selectedCamera,
+ isLoading,
onCreate,
onEdit,
onCancel,
}: CreateTriggerDialogProps) {
const { t } = useTranslation("views/settings");
- const [isLoading, setIsLoading] = useState(false);
const { data: config } = useSWR("config");
const existingTriggerNames = useMemo(() => {
@@ -112,7 +113,6 @@ export default function CreateTriggerDialog({
});
const onSubmit = async (values: z.infer) => {
- setIsLoading(true);
if (trigger) {
onEdit({ ...values });
} else {
@@ -125,7 +125,6 @@ export default function CreateTriggerDialog({
values.actions,
);
}
- setIsLoading(false);
};
useEffect(() => {
diff --git a/web/src/components/overlay/DeleteTriggerDialog.tsx b/web/src/components/overlay/DeleteTriggerDialog.tsx
index cf249e282..79752817d 100644
--- a/web/src/components/overlay/DeleteTriggerDialog.tsx
+++ b/web/src/components/overlay/DeleteTriggerDialog.tsx
@@ -9,10 +9,12 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Trans } from "react-i18next";
+import ActivityIndicator from "@/components/indicators/activity-indicator";
type DeleteTriggerDialogProps = {
show: boolean;
triggerName: string;
+ isLoading: boolean;
onCancel: () => void;
onDelete: () => void;
};
@@ -20,6 +22,7 @@ type DeleteTriggerDialogProps = {
export default function DeleteTriggerDialog({
show,
triggerName,
+ isLoading,
onCancel,
onDelete,
}: DeleteTriggerDialogProps) {
@@ -48,16 +51,25 @@ export default function DeleteTriggerDialog({
aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel}
type="button"
+ disabled={isLoading}
>
{t("button.cancel", { ns: "common" })}
diff --git a/web/src/components/overlay/DeleteUserDialog.tsx b/web/src/components/overlay/DeleteUserDialog.tsx
index 677047a11..162ef2241 100644
--- a/web/src/components/overlay/DeleteUserDialog.tsx
+++ b/web/src/components/overlay/DeleteUserDialog.tsx
@@ -60,7 +60,7 @@ export default function DeleteUserDialog({