UI updates and add last triggering event time and link

This commit is contained in:
Josh Hawkins 2025-07-03 10:50:18 -05:00
parent 4cfc72dfb5
commit f553c59517
4 changed files with 192 additions and 119 deletions

View File

@ -1755,3 +1755,45 @@ def delete_trigger_embedding(
}, },
status_code=500, status_code=500,
) )
@router.get(
"/triggers/status/{camera_name}",
response_model=dict,
dependencies=[Depends(require_role(["admin"]))],
)
def get_triggers_status(
camera_name: str,
):
try:
# Fetch all triggers for the specified camera
triggers = Trigger.select().where(Trigger.camera == camera_name)
# Prepare the response with trigger status
status = {
trigger.name: {
"last_triggered": trigger.last_triggered.timestamp()
if trigger.last_triggered
else None,
"triggering_event_id": trigger.triggering_event_id
if trigger.triggering_event_id
else None,
}
for trigger in triggers
}
if not status:
return JSONResponse(
content={
"success": False,
"message": f"No triggers found for camera {camera_name}",
},
status_code=404,
)
return {"success": True, "triggers": status}
except Exception as e:
return JSONResponse(
content={"success": False, "message": str(e)},
status_code=500,
)

View File

@ -163,6 +163,13 @@ class SemanticTriggerProcessor(PostProcessorApi):
f"Trigger {trigger['name']} activated with similarity {similarity:.4f}" f"Trigger {trigger['name']} activated with similarity {similarity:.4f}"
) )
# Update the trigger's last_triggered and triggering_event_id
Trigger.update(
last_triggered=datetime.datetime.now(), triggering_event_id=event_id
).where(
Trigger.camera == camera, Trigger.name == trigger["name"]
).execute()
# Always publish MQTT message # Always publish MQTT message
self.requestor.send_data( self.requestor.send_data(
"triggers", "triggers",

View File

@ -649,7 +649,7 @@
"documentTitle": "Triggers", "documentTitle": "Triggers",
"management": { "management": {
"title": "Trigger Management", "title": "Trigger Management",
"desc": "Manage triggers for camera '{{camera}}' to detect specific events." "desc": "Manage triggers for {{camera}}. Use the thumbnail type to trigger on similar thumbnails to your selected tracked object, and the description type to trigger on similar descriptions to text you specify."
}, },
"addTrigger": "Add Trigger", "addTrigger": "Add Trigger",
"table": { "table": {

View File

@ -5,14 +5,6 @@ import useSWR from "swr";
import axios from "axios"; import axios from "axios";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Tooltip, Tooltip,
@ -20,13 +12,16 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { LuPlus, LuTrash, LuPencil } from "react-icons/lu"; import { LuPlus, LuTrash, LuPencil, LuSearch } from "react-icons/lu";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog"; import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog";
import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog"; import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger"; import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { Link } from "react-router-dom";
type ConfigSetBody = { type ConfigSetBody = {
requires_restart: number; requires_restart: number;
@ -70,6 +65,7 @@ export default function TriggerView({
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
const { data: trigger_status } = useSWR(`/triggers/status/${selectedCamera}`);
const [showCreate, setShowCreate] = useState(false); const [showCreate, setShowCreate] = useState(false);
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null); const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
@ -374,118 +370,146 @@ export default function TriggerView({
</div> </div>
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt"> <div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
<div className="h-full overflow-auto"> <div className="h-full overflow-auto p-0">
<Table> {triggers.length === 0 ? (
<TableHeader className="sticky top-0 bg-muted/50"> <div className="flex h-24 items-center justify-center">
<TableRow> <p className="text-center text-muted-foreground">
<TableHead className="w-[200px]"> {t("triggers.table.noTriggers")}
{t("triggers.table.name")} </p>
</TableHead> </div>
<TableHead>{t("triggers.table.type")}</TableHead> ) : (
<TableHead>{t("triggers.table.content")}</TableHead> <div className="space-y-2">
<TableHead>{t("triggers.table.threshold")}</TableHead> {triggers.map((trigger) => (
<TableHead>{t("triggers.table.actions")}</TableHead> <div
</TableRow> key={trigger.name}
</TableHeader> className="flex items-center justify-between rounded-lg border border-border bg-background p-4 transition-opacity"
<TableBody> >
{triggers.length === 0 ? ( <div className="min-w-0 flex-1">
<TableRow> <h3
<TableCell colSpan={4} className="h-24 text-center"> className={cn(
{t("triggers.table.noTriggers")} "truncate text-lg font-medium",
</TableCell> !trigger.enabled && "opacity-60",
</TableRow> )}
) : ( >
triggers.map((trigger) => (
<TableRow key={trigger.name} className="group">
<TableCell className="font-medium">
{trigger.name} {trigger.name}
</TableCell> </h3>
<TableCell> <div
<Badge className={cn(
variant={ "mt-1 flex flex-col gap-0.5 text-sm text-muted-foreground md:flex-row md:items-center md:gap-3",
trigger.type === "thumbnail" !trigger.enabled && "opacity-60",
? "default" )}
: "outline" >
} <div>
className={ <Badge
trigger.type === "thumbnail" variant={
? "bg-primary/20 text-primary hover:bg-primary/30" trigger.type === "thumbnail"
: "" ? "default"
} : "outline"
}
className={
trigger.type === "thumbnail"
? "bg-primary/20 text-primary hover:bg-primary/30"
: ""
}
>
{t(`triggers.type.${trigger.type}`)}
</Badge>
</div>
<span>{trigger.threshold.toFixed(2)} threshold</span>
<span>{trigger.actions.length} actions</span>
<Link
to={`/explore?event_id=${trigger_status?.triggers[trigger.name]?.triggering_event_id || ""}`}
className={cn(
"text-sm",
!trigger_status?.triggers[trigger.name]
?.triggering_event_id &&
"pointer-events-none opacity-60",
)}
> >
{t(`triggers.type.${trigger.type}`)} <div className="flex flex-row items-center justify-center">
</Badge> Last:{" "}
</TableCell> {trigger_status &&
<TableCell> trigger_status.triggers[trigger.name]
{trigger.type === "thumbnail" ?.last_triggered
? trigger.data ? formatUnixTimestampToDateTime(
: trigger.data.length > 30 trigger_status.triggers[trigger.name]
? `${trigger.data.substring(0, 30)}...` ?.last_triggered,
: trigger.data} {
</TableCell> timezone: config.ui.timezone,
<TableCell className="font-medium"> date_format:
{trigger.threshold.toFixed(2)} config.ui.time_format == "24hour"
</TableCell> ? t(
<TableCell> "time.formattedTimestamp2.24hour",
{trigger.actions {
.map((action) => t(`triggers.actions.${action}`)) ns: "common",
.join(", ")} },
</TableCell> )
<TableCell className="text-right"> : t(
<TooltipProvider> "time.formattedTimestamp2.12hour",
<div className="flex items-center justify-end gap-2"> {
<Tooltip> ns: "common",
<TooltipTrigger asChild> },
<Button ),
size="sm" time_style: "medium",
variant="outline" date_style: "medium",
className="h-8 px-2" },
onClick={() => { )
setSelectedTrigger(trigger); : "Never"}
setShowCreate(true); <LuSearch className="ml-2 size-3.5" />
}}
disabled={isLoading}
>
<LuPencil className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block">
{t("triggers.table.edit")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("triggers.table.edit")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="destructive"
className="h-8 px-2 text-white"
onClick={() => {
setSelectedTrigger(trigger);
setShowDelete(true);
}}
disabled={isLoading}
>
<LuTrash className="size-3.5" />
<span className="ml-1.5 hidden sm:inline-block">
{t("button.delete", { ns: "common" })}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("triggers.table.deleteTrigger")}</p>
</TooltipContent>
</Tooltip>
</div> </div>
</TooltipProvider> </Link>
</TableCell> </div>
</TableRow> </div>
))
)} <div className="flex items-center gap-2">
</TableBody> <TooltipProvider>
</Table> <Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
onClick={() => {
setSelectedTrigger(trigger);
setShowCreate(true);
}}
disabled={isLoading}
>
<LuPencil className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("triggers.table.edit")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="destructive"
className="h-8 w-8 p-0 text-white"
onClick={() => {
setSelectedTrigger(trigger);
setShowDelete(true);
}}
disabled={isLoading}
>
<LuTrash className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("triggers.table.deleteTrigger")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>