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

@@ -105,6 +105,7 @@ const TABS: Array<{ subpath: string; label: string }> = [
{ subpath: '', label: 'main' },
{ subpath: '/queues', label: 'queues' },
{ subpath: '/queues/never-enqueued', label: 'queue drilldown' },
{ subpath: '/workflows', label: 'workflows' },
{ subpath: '/files', label: 'files' },
{ subpath: '/dead-letters', label: 'dead-letters' },
{ subpath: '/users', label: 'app users' },
@@ -154,4 +155,21 @@ test.describe('per-app tab navigation accepts slug URLs', () => {
// DOM element.
await expect(page.getByTestId('queues-empty-state')).toBeVisible();
});
test('workflows tab renders its empty state (v1.2)', async ({ page, uniqueSlug }) => {
const slug = uniqueSlug('wf');
cleanup.app(slug);
const api = await adminApi();
try {
const res = await api.post('/api/v1/admin/apps', {
data: { slug, name: slug }
});
expect(res.ok()).toBe(true);
} finally {
await api.dispose();
}
await expectTabLoadsCleanly(page, `/admin/apps/${slug}/workflows`);
// A fresh app has no workflows → the empty-state anchors the smoke test.
await expect(page.getByTestId('workflows-empty-state')).toBeVisible();
});
});