diff --git a/server/src/routes/routines.ts b/server/src/routes/routines.ts index 7e144d60..dbd860b4 100644 --- a/server/src/routes/routines.ts +++ b/server/src/routes/routines.ts @@ -172,6 +172,33 @@ export function routineRoutes(db: Db) { res.json(updated); }); + router.delete("/routine-triggers/:id", async (req, res) => { + const trigger = await svc.getTrigger(req.params.id as string); + if (!trigger) { + res.status(404).json({ error: "Routine trigger not found" }); + return; + } + const routine = await assertCanManageExistingRoutine(req, trigger.routineId); + if (!routine) { + res.status(404).json({ error: "Routine not found" }); + return; + } + await svc.deleteTrigger(trigger.id); + const actor = getActorInfo(req); + await logActivity(db, { + companyId: routine.companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "routine.trigger_deleted", + entityType: "routine_trigger", + entityId: trigger.id, + details: { routineId: routine.id, kind: trigger.kind }, + }); + res.status(204).end(); + }); + router.post( "/routine-triggers/:id/rotate-secret", validate(rotateRoutineTriggerSecretSchema), diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index d5a8b603..b4730c0b 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -843,6 +843,13 @@ export function routineService(db: Db) { return (updated as RoutineTrigger | undefined) ?? null; }, + deleteTrigger: async (id: string): Promise => { + const existing = await getTriggerById(id); + if (!existing) return false; + await db.delete(routineTriggers).where(eq(routineTriggers.id, id)); + return true; + }, + rotateTriggerSecret: async ( id: string, actor: Actor, diff --git a/ui/src/api/routines.ts b/ui/src/api/routines.ts index c40a9398..f6e5099b 100644 --- a/ui/src/api/routines.ts +++ b/ui/src/api/routines.ts @@ -32,6 +32,7 @@ export const routinesApi = { api.post(`/routines/${id}/triggers`, data), updateTrigger: (id: string, data: Record) => api.patch(`/routine-triggers/${id}`, data), + deleteTrigger: (id: string) => api.delete(`/routine-triggers/${id}`), rotateTriggerSecret: (id: string) => api.post(`/routine-triggers/${id}/rotate-secret`, {}), run: (id: string, data?: Record) => diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 529436e4..00b2ab36 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -422,6 +422,11 @@ function invalidateActivityQueries( return; } + if (entityType === "routine" || entityType === "routine_trigger" || entityType === "routine_run") { + queryClient.invalidateQueries({ queryKey: ["routines"] }); + return; + } + if (entityType === "company") { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } diff --git a/ui/src/pages/RoutineDetail.tsx b/ui/src/pages/RoutineDetail.tsx index a3209319..16e45100 100644 --- a/ui/src/pages/RoutineDetail.tsx +++ b/ui/src/pages/RoutineDetail.tsx @@ -12,6 +12,7 @@ import { RefreshCw, Repeat, Save, + Trash2, Webhook, Zap, } from "lucide-react"; @@ -115,10 +116,12 @@ function TriggerEditor({ trigger, onSave, onRotate, + onDelete, }: { trigger: RoutineTrigger; onSave: (id: string, patch: Record) => void; onRotate: (id: string) => void; + onDelete: (id: string) => void; }) { const [draft, setDraft] = useState({ label: trigger.label ?? "", @@ -241,6 +244,16 @@ function TriggerEditor({ Rotate secret )} +
+ +
{trigger.lastResult && Last: {trigger.lastResult}} @@ -405,11 +418,13 @@ export function RoutineDetail() { }; const saveRoutine = useMutation({ - mutationFn: () => - routinesApi.update(routineId!, { - ...editDraft, - description: editDraft.description.trim() || null, - }), + mutationFn: () => { + const { status: _status, ...payload } = editDraft; + return routinesApi.update(routineId!, { + ...payload, + description: payload.description.trim() || null, + }); + }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), @@ -429,6 +444,8 @@ export function RoutineDetail() { const runRoutine = useMutation({ mutationFn: () => routinesApi.run(routineId!), onSuccess: async () => { + pushToast({ title: "Routine run started", tone: "success" }); + setActiveTab("runs"); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.runs(routineId!) }), @@ -447,7 +464,11 @@ export function RoutineDetail() { const updateRoutineStatus = useMutation({ mutationFn: (status: string) => routinesApi.update(routineId!, { status }), - onSuccess: async () => { + onSuccess: async (_data, status) => { + pushToast({ + title: status === "paused" ? "Routine paused" : "Routine resumed", + tone: "success", + }); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), @@ -521,6 +542,24 @@ export function RoutineDetail() { }, }); + const deleteTrigger = useMutation({ + mutationFn: (id: string) => routinesApi.deleteTrigger(id), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }), + queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }), + queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }), + ]); + }, + onError: (error) => { + pushToast({ + title: "Failed to delete trigger", + body: error instanceof Error ? error.message : "Paperclip could not delete the trigger.", + tone: "error", + }); + }, + }); + const rotateTrigger = useMutation({ mutationFn: (id: string): Promise => routinesApi.rotateTriggerSecret(id), onSuccess: async (result) => { @@ -933,6 +972,7 @@ export function RoutineDetail() { trigger={trigger} onSave={(id, patch) => updateTrigger.mutate({ id, patch })} onRotate={(id) => rotateTrigger.mutate(id)} + onDelete={(id) => deleteTrigger.mutate(id)} /> ))}