Web Run Reports in ForgeGraph Draft
2026-08-14 · ForgeGraph @ feat/consume-runreport-core · consumes @preflight/runreport
Goal: render ForgeGraph web/CI test runs (Playwright/veritas) as first-class run reports — step timeline, screenshots, video, share links — reusing the portable @preflight/runreport core that already powers Preflight's mobile Maestro reports. Most of the substrate already exists in ForgeGraph; this is mainly wiring it into the shared report surface.
Reality check (2026-08-15): the consumer chain is now fully wired and typechecked — provider (incl. P1 artifacts, done), page, styling — and /api/evidence already accepts artifacts:[{kind:screenshot|video,url}] and writes test_artifacts. But the prod DB has 3 test_runs (all gitea_ci "Unit tests") and 0 test_artifacts. So the value-critical path is no longer the wiring — it's a browser E2E producer (veritas/Playwright) that captures screenshots+video, uploads them somewhere servable, and POSTs them to /api/evidence. Without it, reports render status+steps but no media.
~70%
substrate already present
3
surfaces done (provider/page/styling)
What already exists (don't rebuild)
| Piece | Where | Fit |
test_runs (status, source gitea_ci|veritas, counts, timing, changeset/build FKs) | packages/db/schema/test-run.ts | maps to RunDetail head |
test_artifacts (kind screenshot|video|trace|…, url, mimeType, sizeBytes, FK test_run) | packages/db/schema/test-artifact.ts | exactly RunArtifact |
| Evidence ingest (writes test_runs + test_artifacts from veritas) | apps/web/.../api/evidence/route.ts | the producer already exists |
R2 bucket PUBLIC_FEED_MEDIA + a blob-serve route pattern | wrangler.toml, api/public/feed/artifacts/[id] | media storage/serve template |
| changesets → repositories | schema | run naming |
Already built on feat/consume-runreport-core
| Item | File | Status |
Consume @preflight/runreport@0.1.0 from internal registry (npm.forgegraf.com); resolves to compiled dist | .npmrc, apps/web/package.json | done |
createForgeGraphRunReportProvider(db) mapping test_runs → RunDetail (synthetic WEB_RUN_PIPELINE) | apps/web/src/lib/run-report-provider.ts | artifacts: [] |
/runs/[testRunId] page rendering <RunReport> | apps/web/src/app/runs/[testRunId]/page.tsx | done |
Tailwind @source for the package dist (arbitrary classes generate) | apps/web/src/app/globals.css | done |
All typecheck clean; the tailwind-source-globs guard test stays green.
Phases
P0 — Browser E2E producer (the actual value gap)
| Task | Verify | Status |
A veritas/Playwright web-E2E run that captures screenshots + a screen recording (and, ideally, per-step timings), uploads media to a servable place (ForgeGraph R2 PUBLIC_FEED_MEDIA or a dedicated bucket), and POSTs to /api/evidence as a testEvidence with artifacts:[{kind:video|screenshot,url}]. | a real web run lands rows in test_runs + test_artifacts; its report shows video | todo |
| Decide where veritas runs (existing CI runner? a new browser job?) and how media is stored/served — this drives P2. | — | decision |
Everything downstream of the producer is already built (ingest → test_artifacts → provider → <RunReport>). P0 is the one missing link between "reports exist" and "reports show recordings." Prioritize it.
P1 — Wire real artifacts (the keystone, small)
| Task | Verify | Status |
Provider loads test_artifacts for the run and maps → RunArtifact (kind, url→uri, mimeType→contentType, sizeBytes). Replace artifacts: []. | renders whatever media a run has (0 today — see reality check) | done f363759 |
Confirm the servable-media rule: the core hides non-/api/ artifacts on a share view. Decide whether test_artifacts.url is already public (use as-is) or needs a ForgeGraph blob route. | video plays; screenshots load | todo |
P1 alone turns the existing report page from "steps only" into a full media report, because the data is already being ingested. This is the highest value / lowest effort step — do it first.
P2 — Serving + auth for run media
| Task | Verify | Status |
If media isn't already served: add /api/runs/artifacts/[id]/blob mirroring the public-feed R2 route (PUBLIC_FEED_MEDIA), and have the evidence ingest upload bytes + set url to the blob route. | authed fetch 200; correct content-type | todo |
| Access scoping: run reports are scoped to repo collaborators (mirror how other ForgeGraph pages gate). | non-member 403 | todo |
P3 — Fleet board
| Task | Verify | Status |
Provider getFleet: latest test_run per repo/suite. NOTE: the core's RunFleetRow is a mobile ios/android matrix — for web, either bump @preflight/runreport with a generalized fleet row, or render a simple ForgeGraph list. | a /runs board lists repos' latest runs with ▶ video indicator | todo |
P4 — Share links
| Task | Verify | Status |
ForgeGraph share-token model (mirror Preflight's pf_run_share: token, run, expiry, revoke) + resolveShare + public /share/runs/[token] page + token-gated blob serving. Reuses the core's shareToken URL threading. | unauthenticated share URL renders + plays video; invalid/expired → 404/401 | todo |
P5 — Surface + deploy
| Task | Verify | Status |
Link the report from where runs live (changeset view / ci/[runId] / a repo's runs tab). | a run in the UI deep-links to its report | todo |
| Deploy the ForgeGraph web worker (agent-claimed CF Worker deploy) + verify live against a real veritas run. | report renders in prod with real media | todo |
Risks & notes
| Risk | Mitigation |
| Fleet type is mobile-shaped (ios/android) | Generalize RunFleetRow in the core (a lane-keyed map) and republish; keeps Preflight + ForgeGraph on one shape. Or ForgeGraph renders its own list for v1. |
| Core package versioning across two repos | Any core change = rebuild dist + republish a new @preflight/runreport version to npm.forgegraf.com; bump both consumers. Keep the workspace (Preflight) and published versions in lockstep. |
| Whether veritas emits step timelines / video today | P1 renders whatever's ingested; enrich the veritas/Playwright producer (per-step timings, screen recording) as a follow-up if steps are coarse. |
| Tailwind can't scan node_modules | Already handled — @source for the package dist in globals.css, guarded by the source-globs test. |
Verification
- P1: an existing veritas test_run with test_artifacts renders its screenshots + video in
/runs/[id].
- P4: a share link opens the report unauthenticated with playable video; invalid token rejected.
- P5: prod render against a real run, deep-linked from the run's UI.
Open questions
- Is
test_artifacts.url already publicly servable (veritas hosts it), or should ForgeGraph store+serve media from its own R2 (P2)?
- Generalize the core's fleet type now (one shape for mobile+web), or defer with a ForgeGraph-local list?
- Where do run reports surface in the ForgeGraph IA — under a repo, under
/ci, or a top-level /runs?