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

@@ -26,8 +26,8 @@ use uuid::Uuid;
use crate::app_repo::AppRepository;
use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability};
use crate::workflow_repo::{
get_run, get_workflow_by_name, list_run_steps, list_runs_for_workflow, list_workflows_for_app,
start_run, NewWorkflowRun, WorkflowRun, WorkflowRunStep,
get_run, get_workflow_by_id, get_workflow_by_name, list_run_steps, list_runs_for_workflow,
list_workflows_for_app, start_run, NewWorkflowRun, WorkflowRun, WorkflowRunStep,
};
use sqlx::PgPool;
@@ -103,10 +103,12 @@ struct StepDto {
output: Option<Value>,
error: Option<String>,
child_run_id: Option<Uuid>,
/// DAG edges (from the definition) so the dashboard can draw the graph.
depends_on: Vec<String>,
}
impl From<WorkflowRunStep> for StepDto {
fn from(s: WorkflowRunStep) -> Self {
impl StepDto {
fn from_step(s: WorkflowRunStep, depends_on: Vec<String>) -> Self {
Self {
name: s.step_name,
status: s.status.as_str().to_string(),
@@ -115,6 +117,7 @@ impl From<WorkflowRunStep> for StepDto {
output: s.output,
error: s.error,
child_run_id: s.child_run_id.map(WorkflowRunId::into_inner),
depends_on,
}
}
}
@@ -219,8 +222,29 @@ async fn get_run_detail(
let steps = list_run_steps(&s.pool, run.id)
.await
.map_err(|e| WorkflowsApiError::Backend(e.to_string()))?;
// Load the definition for the per-step `depends_on` DAG edges (they live on
// the definition, not the run's step rows).
let deps: std::collections::HashMap<String, Vec<String>> =
match get_workflow_by_id(&s.pool, app_id, run.workflow_id)
.await
.map_err(|e| WorkflowsApiError::Backend(e.to_string()))?
{
Some(w) => w
.definition
.steps
.into_iter()
.map(|st| (st.name, st.depends_on))
.collect(),
None => std::collections::HashMap::new(),
};
let run_dto: RunDto = run.into();
let step_dtos: Vec<StepDto> = steps.into_iter().map(Into::into).collect();
let step_dtos: Vec<StepDto> = steps
.into_iter()
.map(|st| {
let d = deps.get(&st.step_name).cloned().unwrap_or_default();
StepDto::from_step(st, d)
})
.collect();
Ok(Json(json!({ "run": run_dto, "steps": step_dtos })))
}