diff --git a/apps/web/src/global-routines.ts b/apps/web/src/global-routines.ts index 402cc1f30..5965ef3aa 100644 --- a/apps/web/src/global-routines.ts +++ b/apps/web/src/global-routines.ts @@ -12,11 +12,7 @@ import { useBench } from "./bench-context"; import { useFromBench } from "./shell/page-crumbs"; import { WORKFLOWS_PATH_PREFIX } from "./path-ids"; import { tenantKeys } from "./query-client"; -import { - listScheduledWorkflows, - runScheduledWorkflowNow, - setScheduledWorkflowStatus, -} from "./routines-api"; +import { listScheduledWorkflows, runScheduledWorkflowNow } from "./routines-api"; import type { ScheduledWorkflowDefinition } from "./routines-api"; const ROUTINES_QUERY_SCOPE = "global-page"; @@ -98,7 +94,6 @@ export function useInvalidateRoutines(): (tenantId: string) => void { export type RoutineActions = { readonly runNow: (row: GlobalRoutineRow) => Promise; - readonly setEnabled: (row: GlobalRoutineRow, enabled: boolean) => Promise; }; export function useRoutineActions(): RoutineActions { @@ -106,37 +101,20 @@ export function useRoutineActions(): RoutineActions { return { runNow: async (row) => { try { - await runScheduledWorkflowNow(row.tenantId, row.definition.definitionId); - invalidate(row.tenantId); - toast(`${row.definition.name} started`); - } catch (cause) { - reportError(cause, { - operation: "scheduled_workflow_run_now", - tenantId: row.tenantId, - }); - toast( - `Couldn't start ${row.definition.name}: ${describeApiError(cause, "starting this routine")}`, - ); - } - }, - setEnabled: async (row, enabled) => { - try { - await setScheduledWorkflowStatus( + await runScheduledWorkflowNow( row.tenantId, row.definition.definitionId, - enabled ? "deployed" : "stopped", + row.definition.name, ); invalidate(row.tenantId); + toast(`${row.definition.name} started`); } catch (cause) { reportError(cause, { - operation: "scheduled_workflow_set_status", + operation: "scheduled_workflow_run_now", tenantId: row.tenantId, }); toast( - `Couldn't ${enabled ? "resume" : "pause"} ${row.definition.name}: ${describeApiError( - cause, - enabled ? "resuming this routine" : "pausing this routine", - )}`, + `Couldn't start ${row.definition.name}: ${describeApiError(cause, "starting this routine")}`, ); } }, diff --git a/apps/web/src/pages/routine-detail-page.tsx b/apps/web/src/pages/routine-detail-page.tsx index e37b9f7ef..361d688f7 100644 --- a/apps/web/src/pages/routine-detail-page.tsx +++ b/apps/web/src/pages/routine-detail-page.tsx @@ -1,4 +1,4 @@ -import { Button, PageShell, RichEmptyState, RunNowButton, Skeleton } from "@corbits/react-ui"; +import { PageShell, RichEmptyState, RunNowButton, Skeleton } from "@corbits/react-ui"; import { type } from "arktype"; import { useState } from "react"; import type { ReactNode } from "react"; @@ -165,14 +165,11 @@ function RoutineNotice({ export function RoutineDetailPage({ row, - onToggleEnabled, onRunNow, }: { readonly row: GlobalRoutineRow; - readonly onToggleEnabled: (enabled: boolean) => void; readonly onRunNow: () => Promise; }) { - const enabled = row.definition.status === "deployed"; const sentence = scheduleSentence(row.definition.schedule); const runs = useRoutineRuns(row.tenantId, row.definition.definitionId, 1); const lastRun = runs.kind === "ready" ? runs.data.data[0] : undefined; @@ -186,14 +183,6 @@ export function RoutineDetailPage({ ]} actions={
-
} @@ -249,13 +238,5 @@ export function RoutineDetailRoute({ segment }: { readonly segment: string }) { ); } - return ( - { - void actions.setEnabled(resolved, enabled); - }} - onRunNow={() => actions.runNow(resolved)} - /> - ); + return actions.runNow(resolved)} />; } diff --git a/apps/web/src/pages/routines-page.tsx b/apps/web/src/pages/routines-page.tsx index 88528024a..4e2179942 100644 --- a/apps/web/src/pages/routines-page.tsx +++ b/apps/web/src/pages/routines-page.tsx @@ -1,5 +1,5 @@ // Workflows: an ops table of deployed workflow definitions, including -// paused (`stopped`) ones. Pause/resume and run-now are the only writes. +// paused (`stopped`) ones. Run-now is the only write. import { EmptyState, RichEmptyState, RunNowButton } from "@corbits/react-ui"; import { cronSentence } from "@corbits/workflows/client"; import { useState } from "react"; diff --git a/apps/web/src/routines-api.ts b/apps/web/src/routines-api.ts index ac1af1146..1a5f1fade 100644 --- a/apps/web/src/routines-api.ts +++ b/apps/web/src/routines-api.ts @@ -1,6 +1,6 @@ // The Routines page's seam to stock workflow deployments. See // docs/routines-scheduling.md for the schedule join and why run-now/ -// pause/resume stay rejected promises. +// run-now goes through the stock trigger route. import { type } from "arktype"; import { useQuery } from "@tanstack/react-query"; @@ -183,35 +183,39 @@ export async function listScheduledWorkflows( }); } -/** No stock route reruns a deployment on demand yet. */ -export function runScheduledWorkflowNow( - tenantId: string, - definitionId: string, -): Promise<{ runId: string }> { - return Promise.reject( - new ApiQueryError( - "Running a workflow now has no stock route yet.", - undefined, - `/api/tenants/${tenantId}/workflows/deployments/${encodeURIComponent(definitionId)}/run`, - ), - ); -} +const TriggerResponse = type({ runId: "string", address: "string", messageId: "string" }); -/** No stock route pauses or resumes a deployment yet. */ -export function setScheduledWorkflowStatus( +/** Fires the deployment through the stock trigger route. The deployment id + * is its anchor run id; the message mirrors the workflow's cron row when one + * exists so a manual run carries what the schedule would. */ +export async function runScheduledWorkflowNow( tenantId: string, definitionId: string, - status: "deployed" | "stopped", -): Promise<{ readonly status: string }> { - return Promise.reject( - new ApiQueryError( - status === "deployed" - ? "Resuming a deployment has no stock route yet." - : "Pausing a deployment has no stock route yet.", - undefined, - `/api/tenants/${tenantId}/workflows/deployments/${encodeURIComponent(definitionId)}`, - ), + definitionName?: string, +): Promise<{ runId: string }> { + const path = `/api/tenants/${tenantId}/workflows/${encodeURIComponent(definitionId)}/mail`; + const schedules = await listCronSchedules(tenantId); + const row = schedules.find( + (schedule) => schedule.stoppedAt === null && schedule.definitionName === definitionName, ); + const content = row === undefined ? "Run now" : `${row.subject}\n\n${row.body}`; + const response = await fetch(path, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify({ content }), + }); + if (response.status === 401) throw new UnauthenticatedError(); + if (response.status === 409) { + throw new ApiQueryError("This workflow has ended; redeploy to run it again.", 409, path); + } + if (!response.ok) { + throw new ApiQueryError(`The server answered ${response.status}.`, response.status, path); + } + const parsed = TriggerResponse(await response.json()); + if (parsed instanceof type.errors) { + throw new ApiQueryError(`Unexpected response shape: ${parsed.summary}`, undefined, path); + } + return { runId: parsed.runId }; } // Keys must be stable arrays under `["tenant", tenantId, ...]` so a bench diff --git a/apps/web/src/shell/context-menu/items.tsx b/apps/web/src/shell/context-menu/items.tsx index d45816c20..79d5ab4a6 100644 --- a/apps/web/src/shell/context-menu/items.tsx +++ b/apps/web/src/shell/context-menu/items.tsx @@ -121,7 +121,7 @@ function routineMenu( onSelect: () => { void (async () => { try { - await runScheduledWorkflowNow(tenantId, target.id); + await runScheduledWorkflowNow(tenantId, target.id, target.name); toast(`${target.name} started`); actions.onRoutineRan(tenantId); } catch (cause) { diff --git a/docs/routines-scheduling.md b/docs/routines-scheduling.md index b1ef1370d..2f60a3fbd 100644 --- a/docs/routines-scheduling.md +++ b/docs/routines-scheduling.md @@ -30,6 +30,8 @@ for a name no definition carries is rejected the same way ## Run-now and pause/resume -Neither has a backing stock route (`/deployments` is list/create only; no -per-deployment PATCH or trigger route exists), so both stay rejected -promises naming the missing route. +Run now posts to the stock trigger route +(`POST /workflows/:runId/mail`, `:runId` = the deployment id), carrying the +cron row's subject and body when one exists. A 409 means the run has ended. +Pause/resume has no stock or `@corbits/cron` primitive, so no control is +shown.