feat(workflows): M6 — dashboard run-history + DAG view

A per-app "Workflows" tab: list definitions, browse run history, and inspect a
run's per-step progress with a static layered DAG.

- API: the run-detail endpoint now returns each step's `depends_on` (loaded
  from the definition via a new `get_workflow_by_id` reader) so the dashboard
  can draw the graph edges — the run's step rows don't carry them.
- dashboard `$lib/api`: a `workflows` namespace (list / runs / start / run) +
  the matching types.
- `apps/[slug]/workflows/+page.svelte`: workflow table → drill into runs →
  drill into a run. The run view renders a step table plus an SVG DAG (nodes =
  steps colored by status, laid out by longest-path level; edges = depends_on,
  arrowed) and a "Start run" button. Nested/parked steps show their child run.
- AppTabBar + the per-app layout gain the Workflows tab (admin-only).

Tests: `npm run check` clean (0 errors); two Playwright smoke tests
(navigation tabs — workflows loads cleanly + renders its empty state) pass
against the full stack.

With M6 the v1.2 Workflows track (M1 schema/validation → M2 durable
orchestrator → M3 when/templating → M4 nested → M5 SDK/API/CLI → M6 dashboard)
is COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-12 17:57:56 +02:00
parent 5a630e1d9f
commit bd9c3fa4f0
7 changed files with 576 additions and 5 deletions

View File

@@ -989,6 +989,23 @@ pub async fn get_workflow_by_name(
row.map(TryInto::try_into).transpose()
}
/// Resolve an app-owned workflow by id (app-scoped). Backs the run-detail
/// API's DAG-edge lookup (`depends_on` lives on the definition, not the run).
pub async fn get_workflow_by_id(
pool: &PgPool,
app_id: AppId,
id: WorkflowId,
) -> Result<Option<Workflow>, WorkflowRepoError> {
let row: Option<WorkflowRow> = sqlx::query_as(&format!(
"SELECT {SELECT_COLS} FROM workflows WHERE app_id = $1 AND id = $2"
))
.bind(app_id.into_inner())
.bind(id.into_inner())
.fetch_optional(pool)
.await?;
row.map(TryInto::try_into).transpose()
}
/// Start a nested sub-workflow run and **park** the parent step on it. In one
/// token-gated transaction: the parent step (still claimed by the caller) is
/// set `running` with `claim_token` cleared and `child_run_id` pointing at a