feat(dashboard): MCP server, insights & monitors, Workers Cache, schema rework + OSS/repo hardening#13
Draft
bumper-joe-fairburn wants to merge 74 commits into
Draft
feat(dashboard): MCP server, insights & monitors, Workers Cache, schema rework + OSS/repo hardening#13bumper-joe-fairburn wants to merge 74 commits into
bumper-joe-fairburn wants to merge 74 commits into
Conversation
Runs with more tests than the loader's 200-row seed showed truncated filter counts (chips counted only loaded rows) and made the overflow tests unbrowsable. Wire the existing GET /results back-paginator into useRunRoom: page the tail in after mount and merge existing-wins under the live accumulator, so the Tests list + per-status counts cover the whole run.
A single retryable infra error (sandbox unavailable, container boot timeout) emailed "down" then a spurious "recovered" on the retry. Gate safeAlert on !result.infraError in runMonitorJob, and skip the monitor lastStatus/lastRunAt bump in recordExecutionResult for infra errors so the retry classifies against the true prior health baseline (mirrors the stale-execution reaper's policy). The execution row still records the error for the timeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A same-run idempotent re-registration whose bytes changed reused the existing row verbatim, so the upload guard (contentLength === sizeBytes) rejected the new bytes with lengthMismatch. planArtifactRegistration now emits row refreshes for reused rows whose stored size/type differ, and registerArtifacts applies them in-batch and meters the signed byte delta (shrinks never blocked; artifactCount counts only inserts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ta (P1-1, P2-createdAt, P2-delta) buildResultInsertStatements now folds the per-existing-result UPDATE + child DELETEs into one multi-row INSERT ... ON CONFLICT (runId,testId) DO UPDATE plus three IN-list child DELETEs, so a flush is a handful of statements instead of ~4 per prefilled result (~20k serial round-trips for a 5000-result flush). createdAt is now insert-only (omitted from DO UPDATE); a new nullable updatedAt column carries the write time — fixes the old UPDATE path that rewrote createdAt to flush time and skewed usage/analytics/retention. appendRunResults takes a run-row FOR UPDATE lock and reads prev-status under it (mirrors completeShardedRun), so concurrent duplicate flushes can't double-apply the additive counter delta and the upsert id-mapping stays race-safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…drain budget (P1-4) sweepRetention selected projects with no ORDER BY, so the budget-bounded drain always started at the same head and the tail beyond the chunk budget was swept late or never every 6h pass. Add ORDER BY random() and charge the chunk budget only for PRODUCTIVE chunks (idle projects cost only their probe SELECTs, bounded by the wall-clock deadline), so the productive budget is reserved for projects with eligible rows and every project is eventually reached. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(P1-5) The ⌘K test search matches title/file with a leading-wildcard ILIKE that no b-tree can accelerate, so it full-scanned the project's testResults partition. Add two gin_trgm_ops GIN indexes (planner BitmapOrs them) so the ILIKE becomes a Bitmap Index Scan; the migration hand-adds CREATE EXTENSION IF NOT EXISTS pg_trgm (drizzle-kit omits it). No query change — the client already debounces. Verified on real Postgres 16: EXPLAIN shows BitmapOr over both trgm indexes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three loaders (run-detail, test-detail, runs-list) returned the full runs row — including idempotencyKey, the write-reopen credential — into browser SSR props (runs-list leaked a whole page). Project the new RUN_PUBLIC_COLUMNS allowlist (every runs column except idempotencyKey) instead of a bare .select(); a regression test forbids re-adding the key and forces a conscious decision for any future runs column. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mment sweep (P3) Correct ARCHITECTURE.md (direct-R2 path, 7 crons incl reconcile-billing, billing env) and SELF-HOSTING.md (retention default 200->1000 + new knobs, Polar billing section); qualify root CLAUDE.md dashboard-only commands. Add SECURITY.md (private vuln reporting), CONTRIBUTING.md, CODE_OF_CONDUCT.md, PR + issue templates. Fix misdirecting comments: schema.ts nonexistent src/live.ts pointer, artifacts.ts stale D1/SQLite refs + false 'no presign', keys.ts backwards SQLite-LIKE note. Delete stale apps/dashboard/todo.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rter) Clamp title (hard-rejected identity field → 400s the run) to MAX_TITLE and error message/stack/annotation text to MAX_MESSAGE/MAX_STACK so an oversized value can't 400/413 a whole run non-retryably; caps mirror the dashboard MAX (pinned in contract.test). Re-raise SIGTERM after the best-effort /complete so the reporter doesn't linger until SIGKILL (Playwright <=1.61 ignores SIGTERM and our listener suppressed the default); SIGINT still defers to Playwright. Fix the batcher's phantom 'fallback file' docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the test title
TestRow only stripped the `${file} > ` title prefix when the title started
with the file, which fails once a project name leads Playwright's titlePath
(`project > file > describe… > test`). Multi-project runs therefore showed the
project + file inside the `>` chain AND repeated the project in a fixed 60px
column with no truncate — long names ("Google Chrome for Android") wrapped and
grew the row past min-h-8.
Parse the title with the existing parseTitleSegments() (strips project + file)
so the row shows a clean `describe > test`, and render the complementary axis
(project when grouped by file, file basename when grouped by project) as a
compact truncating pill. Pill is omitted for single-project runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(runs): back-paginate run-detail tests beyond the SSR seed window
Move the alert-recipients picker off the monitor detail page and into the monitor edit form, and render the edit surface as a Base UI Dialog driven by the existing ?edit=1 flag. Config + recipients now persist atomically in a single updateMonitor UPDATE; the standalone setAlertRecipients action and setMonitorAlertTargets repo helper were removed. alertTargets flows as the domain AlertTargets type through UpdateMonitorInput with serialization owned by the repo. E2E page objects + the recipients round-trip spec drive the modal flow. See docs/worklog/2026-07-02-monitor-edit-modal-recipients.md.
Threads a new per-test `shardIndex` end-to-end (reporter wire type ->
Zod ingest schema -> testResults column -> SSR/paginate -> realtime row
-> client grouping engine) so the run-detail Tests tab can group rows by
shard alongside file / Playwright project. 1-based, nullable ("Unsharded"
for non-sharded runs). Additive + optional, so no protocol bump and
version skew parses clean.
Also adds a `seed:sharded` local seeder that injects one genuinely
sharded run into the dev dashboard (opens/appends/completes once per
shard under a shared idempotencyKey), plus the additive migration
(ALTER TABLE testResults ADD COLUMN shardIndex integer).
* perf(db): drop redundant testOwners_project_testId_idx index Implements PlanetScale schema recommendation #1 (duplicate_index, left_prefix). The standalone (projectId, testId) index is a strict left-prefix of the unique index testOwners_project_testId_owner_idx on (projectId, testId, owner), which already serves the same access pattern (the resolveTestOwners per-test lookup). Dropping it removes write amplification and storage for zero read cost. * style(worklog): oxfmt markdown table alignment
…#38) Rolls out the defer()/skeleton streaming pattern across the dashboard and upgrades Void 0.9.2 -> 0.10.4. Each converted page keeps its cheap shell/404-gate/realtime-seed eager and streams heavy reads behind a CLS-matched skeleton (Cache-Control: private, no-store). Pages: insights/index, insights/run-duration, insights/suite-size, insights/slowest-tests, flaky, runs/[runId] (+ history chart), runs/[runId]/tests/[testResultId], runs/[runId]/diff, tests catalog, settings audit log. tests/[testId], project index, and monitors/[monitorId] stay eager by design. - run-diff.ts split additively (resolveRunDiffTargets + computeRunDiff); JSON API unchanged. - run-history-chart.tsx: shared RunHistoryChartFrame co-locates the chart skeleton (no drift-prone measured height). - Shared TablePaginationFooterSkeleton + BottlenecksTableHead; removed unused ListRowsSkeleton. - Fixed: defer() incompatible with defineHandler.withValidator (collapses the Deferred prop at runtime) -> tests + audit parse searchParams manually. - Void 0.10.4 upgrade + re-authored pnpm patches. wrangler held at 4.94.0 (4.107.0 removes unstable_getWorkerNameFromProject, which @cloudflare/vite-plugin@1.38.0 needs for the dev server). - Docs-only worklog recording the declined fate data-layer spike. Verified: pnpm check (0 errors), unit tests (dashboard 233 + 1140, reporter 281), build clean, full CI green (incl. Postgres Integration + both E2E suites).
Deploy failed at db:migrate:remote because 20260703211031_massive_justice.sql does a DROP INDEX (redundant testOwners_project_testId_idx from #37), which Void refuses to run without an explicit opt-in. Add the -- void:allow-destructive pragma so the migration applies on deploy.
#39) * feat(schema): tests catalog, jsonb columns, integrity FKs, NOT NULL tightening First-principles schema pass (docs/schema-rework-plan.md) — stacked forward-only migrations with in-migration backfills: - New per-project `tests` identity catalog; the ⌘K palette search and the tests/slowest-tests `q` filter now resolve against it, so the trigram GIN indexes are bounded by live suite size instead of retained-result history. - text→jsonb for monitors.config/alertTargets/retryConfig, monitorExecutions.resultDetail, and auditLog.metadata — objects are stored directly (no JSON.stringify) and read back already-parsed. - Real FKs with onDelete:"set null" for runs.monitorId and monitorExecutions.runId (the cycle rationale was disproven); openRun recovers a monitor deleted mid-open via FK-violation retry. - NOT NULL tightening on runs.lastActivityAt and testResults.updatedAt (backfilled), dropping the coalesce fallbacks in staleRunFilter et al. - Drop the half-alive usageCounters.testResultsCount — derived-on-read (countTeamTestResults) is now the single source. - User-teardown for the Better Auth deleteUser gap: sole-owner guard (beforeDelete) + logical-FK row sweep (afterDelete). - Retention-window CHECK on teams (artifact window ≤ testResults window). Verified: pnpm check (0 errors), dashboard vitest (node 244 pass/4 skip, workers 1138 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(e2e): de-flake login specs — wait out Void's hydration re-navigation `auth.spec.ts:23 (invalid sign-in credentials)` flaked in the E2E UI job (on main too): `getByRole('alert')` never appeared, call log showing "3× waiting for /login navigation to finish". Root cause: Void's client performs one client-side History re-navigation to the current route on hydration, which remounts the login island and resets its local React state (email/password/error). Sign-in is fully client-side, so a fill or submit that races ahead of that re-nav is silently wiped and the error alert never renders. The existing enabled-button gate mostly-but-not-reliably dodged it; under CI's 3-worker shared-dev-server load the timing slips and it flakes. Fix: `gotoSignIn`/`gotoSignUp` now wait for `networkidle` after `goto`, so the re-navigation settles before any spec touches the form. Measured against a booted dashboard: interacting before the settle fails 0/8; after it, 8/8. CI-faithful run (workers:3, retries:2, repeat-each=3): 24 passed, 0 flaky. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a StatusGlyph to each collapsible group header (file / project / shard) on the run-detail Tests tab, rolling up the group's worst status (failed → flaky → passed → skipped; skipped only when the group is entirely skipped) so a collapsed group reads at a glance. Also shrink the run header summary tiles to text-[11px] so they no longer read oversized next to the bar.
…t-feel nav Close the gap to CSR-instant navigation across the dashboard: - Add a <Link> wrapper (src/components/ui/link.tsx) defaulting prefetch="hover"; migrate all internal nav to it. Prefetch resolves the full page (incl defer() bodies) server-side into the client cache, so a warmed click paints instantly. Tier cacheFor: PREFETCH_STABLE (SWR) for heavy non-realtime pages, PREFETCH_REALTIME (short) for realtime-seeded pages. - Re-enable Better Auth session.cookieCache (maxAge 60) — lost in the rwsdk->void/auth migration; drops a per-nav Postgres session read. - Parallelize the run-detail, runs-list, and monitor-detail loaders with better-all (dependency-aware all()). - Lazy-load CodeMirror off the monitor pages and the CommandMenu off the layout. - defer() the test-detail, settings usage/billing, and monitor-detail loaders behind skeletons. The monitors LIST stays eager (verified realtime seed). Worklogs: docs/worklog/2026-07-04-link-prefetch.md, docs/worklog/2026-07-04-nav-speed-followups.md
Follow-up to b2f9697 (nav-perf) from a code-quality review of that commit. - Apply PREFETCH_REALTIME ("5s") to every inbound link to a realtime-seeded page — sidebar Runs/Monitors, the "View full report" popover, and the runs-list/run-detail breadcrumbs — so a hover-prefetch can't commit a stale room seed. Rooms have no replay and useFeedRoom only refreshes on WS re-open (no initial-mount reconcile), so the 30s default could show a completed run as still-running or hide a brand-new run. The commit had applied 5s only to run-list rows. - Wrap the new React.lazy CodeMirror + CommandMenu mounts in an error boundary (reusing DeferErrorBoundary) so a post-deploy chunk-load 404 degrades to the textarea / no-op instead of throwing past Suspense and blanking the form/app. - Fix a <div>-in-<p> skeleton that triggered a hydration warning on test detail. - Correct two stale worklog claims and add a follow-up worklog. Verified: pnpm check (0 errors), tsgo typecheck clean, dashboard tests 244 + 1150 passed.
The remote deploy failed at db:migrate:remote — Void's migrator refuses destructive statements without an explicit opt-in. These two migrations from the #39 schema rework are intentionally destructive: - unique_vapor: drops the testResults trigram indexes (recreated on the new tests catalog table) - mixed_the_fury: drops runs_project_monitor_created_at_idx and the usageCounters.testResultsCount column Add `-- void:allow-destructive` to each so the deploy migrate step passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(runs): paginate run tests by group
* style(worklog): fix markdown formatting for CI
* docs(worklog): add group-header status-icons entry
* refactor(run-detail): paginate-by-group follow-ups + code-quality pass
Lands the outstanding Tests-tab paginate-by-group refinements (deferred
client-loaded group list, Recommended filter, keyset-paginated group headers)
together with a strict maintainability pass over the diff.
Quality pass (behavior-preserving except the recommended ordering fix):
- Split run-progress.tsx (721 -> 353) into run-progress{,-group,-row,-skeletons}.tsx
- Extract mergeGroupRows/dedupeGroups to the tested pure layer (group-tests-by-file)
- Fix recommended-view page ordering: server orders failed-before-flaky via a
CASE bucket rank + rank-carrying keyset cursor, so a large group no longer
reorders rows across the scroll boundary
- Server emits hasFailingGroup; client drops the re-derived hasBadGroup
- Extract useInfiniteScrollSentinel; reuse useDebouncedValue; single-source the
failed*4+flaky*2 severity weight
- De-footgun skipOwnershipCheck (fix stale docs + wire the CSV export loop);
drop the redundant file-axis key ternary in the /results route
See docs/worklog/2026-07-04-run-tests-paginate-by-group.md.
Verification: pnpm check 0 errors; dashboard tests 253 (node) + 1153 (workers) pass.
* style(worklog): fix markdown formatting for CI
* fix(e2e): adapt Tests-tab e2e to paginate-by-group UX
The Tests tab now loads its group list client-side and renders test rows only
inside expanded groups (only failing groups auto-expand), so the flat-list
assumptions in the e2e suites broke:
- run-progress-group: add data-testid + aria-expanded to the group disclosure
button (a11y improvement + stable test hook).
- run-detail POM: add `expandTestGroups()` (waits for the client-loaded list,
opens each collapsed group); `clickFirstTest()` expands first.
- run-detail / test-detail / realtime specs: expand groups before locating
`a[href*="/tests/"]` rows.
- vitest e2e: the run-detail SSR HTML no longer contains file names (client-
loaded), so assert the `.spec` files via the grouped-read API the island calls.
runRows/runRow gain an optional `tags` arg that appends a SQLCommenter key='value' comment (application/service/environment/source/feature/ route/release_sha) so PlanetScale Query Insights can attribute raw queries to a code path and deploy. No tags → the query is unchanged; builder queries stay untagged (cheap indexed lookups, no clean hook). Adopted at the test-owners and monitor-uptime raw reads. release_sha reads a build-time VITE_RELEASE_SHA (omitted when unset).
Under run_worker_first:["/**"] every /assets/*.js chunk fetch runs the full middleware stack. 01.context resolved the tenant bundle (a Postgres read) for those requests too, and a DB/Hyperdrive blip made it throw — which 00.errors' guard-less catch arm rewrote to the HTML /oops page, so the .js chunk answered 500 and client-side navigation (Settings was first to hit it) broke with "Failed to fetch dynamically imported module". - 01.context: short-circuit static assets + error pages to a stub bundle before the DB read, like the /api/* branch. - 00.errors: catch arm mirrors the post-next() static-asset guard (pass a thrown Response through, else log + 503, never HTML). Reproduced + validated against a production build (vp preview on workerd) with Postgres stopped: existing chunks 500 -> 200, missing 500 -> 404. Adds a 6-case middleware regression test + worklog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cuts per-nav Postgres session reads for active users to ~one per 5 min (from one per 60s window), trading a slightly longer cross-device revocation lag — fine for a CI dashboard. Complements the asset-serving fix in the prior commit (static chunks no longer run a tenant DB read at all). `refreshCache` was considered and deliberately NOT set: Better Auth force-disables it when a database adapter is configured (it re-signs cached data without a DB read, so it's for stateless setups) and otherwise only logs a warning on every auth-context creation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Port two improvements from gitasf/bumper PR #12, adapted to this repo's tokens: - Invite-link modal now truncates to one line with a Copy button (reuses useCopiedFlag); replaces the raw scrolling <pre>. Uses text-13 (this repo's token) rather than the fork's diverged --text-fs-13. - gen-wrangler.mjs emits localConnectionString (from DATABASE_URL) on the Hyperdrive binding so own-account `vp dev` on workerd can reach local Postgres. Gated behind CF_HYPERDRIVE_ID; ignored on deploy (id wins); fallback output unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ree auto-expand Run-history duration strip now hangs off the same shared, gliding ChartColumnTooltip as the analytics charts (deleting the bespoke per-bar Popover), with rich async run/test summary payloads, kind-specific pixel-matched loading skeletons, and ±1-neighbour prefetch on hover. The current run/test bar keeps its hovercard (drops only the self-link). A single summaryQuery() factory keeps the prefetch and the read in lockstep. Tests-tab accordion no longer flashes collapsed-then-open on first load: the one-shot auto-expand of server-flagged default groups moved from a post-paint effect to a render-phase seed, extracted into useAutoExpandedGroups so the required render-time state write lives behind a named boundary. Latch is state (StrictMode-safe), frozen once per group-by axis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd (#48) Self-hosted Playwright Trace Viewer embed — deep-linkable Replay modal (test-detail rail) + per-row button in the run's test list — replacing the trace.playwright.dev link-out. Plus a guarded test:dashboard e2e wrapper + CI Postgres, a reconnect-mid-navigation guard, and own-account header parity. See docs/worklog/2026-07-08-embedded-trace-replay.md.
The self-hosted trace viewer (an https page) fetches the trace from an absolute `?trace=<download URL>`. Behind Cloudflare `new URL(c.req.url).origin` can surface `http://` even on an https deploy (TLS terminated upstream), so the embedded URL was http — blocked as mixed content / a `connect-src 'self'` CSP violation, hence "Could not load trace". Prefer the canonical WRIGHTFUL_PUBLIC_URL via a new resolvePublicOrigin() helper at both trace-URL entry points: the test-detail loader and the on-demand replay.ts route (per-row Replay button + ?replay= deep-link). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A four-theme presentational pass plus a strict code-quality review of the result. No data-flow or behaviour changes beyond loading/animation timing. - SSR-first avatars: AvatarImage ships a native <img> in server HTML so the fetch starts on first paint (fixes late-loading GitHub avatars); adds https://github.com to CSP img-src; avatar drops the now-vestigial Base UI primitive for plain spans and keys the load-failure state to src. - Motion: origin-aware enter/exit on popover/menu/combobox/autocomplete, copy-to-clipboard pop, progressive-enhancement pending submit buttons, animated monitor exec-row disclosures, and a visual-diff drag slider. - Accessibility triad: one global layer for prefers-reduced-motion / prefers-reduced-transparency / prefers-contrast. - De-drift: single-sourced ease-out-strong token, a spin theme override (all spinners 0.6s), type-ramp tracking, scoped width-bar transitions, and a root TooltipProvider arming the shared skip-delay. See docs/worklog/2026-07-09-*.md for the full rationale and verification.
#50) * feat(dashboard): architecture deepening + security & DB perf hardening Consolidated `review` branch work (see docs/worklog/2026-07-12-review-branch-consolidated.md): - 9 shallow->deep module refactors (canonical run read-model, artifact-serve seam, typed form-flash slots, keyset-cursor codec, monitor-badge projection bug fix, offset-pagination helper, capability gates). - H1 security: verify GitHub App installation ownership in the setup callback (closes confused-deputy / installation-takeover hole). - DB review: convention-parity WHERE-scoping on runs writes, set-based reconcileUsage, new indexes (runs_team_createdAt, trigram GINs), runs-list OFFSET->keyset pagination, deferred heavy KPI aggregates. - Split pg-integration.test.ts into a domain directory (69/69 preserved), Playwright best-practices pass, dropped unnecessary use-effects. Migrations 20260711160102 (githubCheckClaimedAt) + 20260711220029 (indexes), both additive. * docs(worklog): fold perf/e2e/useEffect worklogs into consolidated entry
Rename the 7-step type ramp from px-literal token names (text-11…text-26) to role names (text-micro…text-display), and fix typography gaps found by a whole-dashboard review: off-ramp heading sizes snapped to the ramp, iOS input zoom, underline offsets, deliberate text wrapping, and font-smoothing/synthesis parity. Registers the new names in cn.ts (tailwind-merge classGroup) and adds a token-conventions guard banning the legacy numeric names. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: refresh agent guidance * docs: clarify artifact upload paths
…t SW engine (#52) * feat(dashboard): native trace viewer — own Replay workbench on the vendored Playwright SW engine Replace the Replay modal's iframe of the official Playwright trace viewer with Wrightful's own React workbench (src/trace-viewer/), built from the dashboard's component library. The hard parts stay upstream: DOM-snapshot rendering, trace.zip range-reading, and the format modernizer remain in Playwright's compiled service worker, which our UI drives over its HTTP contract (contexts / snapshot/<pageId> / sha1). A hidden inline bridge iframe under the SW scope loads the model and relays it via postMessage; snapshot documents render as SW-served navigation iframes directly from the dashboard tree. - Vendor the trace-model SOURCE (Apache-2.0, tag v1.61.1) into src/trace-viewer/vendor/ with provenance headers; nothing is published on npm (upstream is private) — copies are the maintainer-sanctioned path. - Drift guards: vendor/version.ts + a unit test that fails when playwright-core moves past the synced tag; the replay e2e drives a real trace through the real SW as the runtime contract test. - Workbench: action tree (status, duration, console badges, keyboard nav), Before/Action/After snapshot scrubber with click-pointer overlay, Log/Errors/Console/Network/Attachments/Metadata tabs, bespoke split panes. - Dialog keeps ?replay= deep-linking; official viewer stays vendored as a new-tab fallback ("Official viewer"), public-viewer link unchanged. Verified: pnpm check 0 errors (baseline warnings); node lane 290 passed (10 new model tests + vendor guard), workers lane 1222 passed; isolated dashboard e2e test-replay.spec.ts 3/3; manual Chromium run against a real ingested trace (action list, snapshot render, tabs, Escape) screenshotted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): bridge fetch-proxy + filmstrip, Source tab, previews, attempts, scoping, warm, vendor sync The one architectural addition is the bridge fetch-proxy: bridge.html gains a postMessage RPC that fetches snapshotInfo/* and sha1/* from inside the SW-controlled client (the only place the SW answers) and returns JSON/Blobs to the dashboard. use-trace-model exposes it as a stable TraceBridge; use-object-url turns proxied blobs into managed object URLs. That unlocks, in one pass: - Filmstrip/timeline above the workbench: proxied screencast thumbnails, selected-action window overlay, hover time cursor, click/drag-to-seek (impossible without the proxy — sha1 <img> URLs 404 from the dashboard). - Source tab: sha1/src@<SHA-1(raw stack-frame path)>.txt (mechanism verified against upstream v1.61.1 AND empirically against a real trace), target line highlighted + scrolled, error lines annotated, per-file tabs. - Inline image attachment previews (proxied blob / data-URL thumbnails). - Snapshot URL bar + exact per-snapshot viewport via snapshotInfo (fixes mid-run setViewportSize; falls back to the context viewport). Plus the review follow-up list: - No-flash snapshot tabs: Before/Action/After iframes stay mounted, stacked, visibility-toggled (e2e locator updated for the new per-tab titles). - Action-group chips: route/getter/configuration hidden by default (official filteredActions semantics), toggleable with counts, persisted. - Attempt switcher: /replay returns per-attempt signed URLs; the modal shows a SegmentedControl for retried tests (default: last attempt). - Console/Network crosshair toggle: filter to the selected action's window instead of highlight-only. - TRACE_TOKEN_TTL_SECONDS (8h) for trace tokens everywhere they're minted — the SW range-reads lazily, so 1h tokens failed quietly mid-scrub. - warmTraceViewer(): SW pre-warm on Replay-button hover (register-only on rows, full model prefetch on the rail where the URL is known at SSR). - scripts/sync-trace-vendor.mjs (+ sync:trace-vendor script): automated vendor re-sync from the installed playwright-core's tag with import rewrites, header preservation, --dry-run and --pr modes; dry-run at the current pin round-trips every verbatim file byte-identically. Server-side step extraction is scoped (docs/step-extraction-plan.md) and deliberately deferred to its own ingest-touching change. Verified: pnpm check 0 errors (baseline warnings); node 291 / workers 1222 passed; isolated dashboard e2e replay spec 3/3; real-browser drive against seeded traces exercised every feature (screenshots) including a 3-attempt flaky test for the switcher. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): official-viewer parity pass — Call tab, network drawer, search, timeline bars/preview, popout, previews, copy-prompt - Call tab (default when no errors): params, return value, timing, page, error - Network detail drawer: General / stacked HAR timing bar / headers / body previews via the bridge proxy - Action-list search (SearchFilterInput; ancestor-preserving tree filter) - Timeline: per-action bars lane + hover screencast preview card - Snapshot popout (vendored snapshot.html shell) + persisted canvas-from-screenshot toggle - Attachment text/JSON inline previews; Copy-prompt button on errors (1.51 parity, useCopiedFlag) - Source tab: stack-frames pane (click to switch file + highlight line); CodeMirror highlighting attempted and REVERTED (@codemirror/state dual-instance breakage under Vite dev pre-bundling — documented) - bridge.html: on load failure, probe the trace URL and surface the real HTTP status + hint (fixes the misleading "Local Network Access" copy when local artifact bytes are missing — diagnosed live from a 404) Verified: pnpm check 0 errors; node 291 passed; real-browser screenshots of every feature; isolated dashboard e2e replay spec 3/3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(trace-viewer): extensive coverage — fixture, protocol, and component suites Nine new node-lane suites + a shared synthetic-trace fixture (runs through the real vendored MultiTraceModel; TraceBridge fake keyed by path prefix). Node lane 291 -> 375 tests. - Pure: format helpers (bytes/offsets) - Protocol: the full bridge postMessage contract — origin/source checks, model/error/progress, fetchJson round-trips, 30s timeout, unmount rejection — happy-dom satisfies event.source === iframe.contentWindow, so the checks are tested as-is; warm-iframe dedupe - Components: action-list (chips/persistence, search incl. ancestors, collapse, keyboard, selection), detail-tabs (default-tab, counts, hasSource gating, scope toggle), call/errors/metadata (params, result, copy-prompt clipboard contract), console/network/attachments (ANSI strip, scope filtering, drawer sections + body preview, visibility/expansion), snapshot-pane (tab derivation, point params, URL bar, canvas toggle, popout, empty state), timeline (bars, thumbs via bridge, seek math, null render) - e2e spec additionally asserts the Call tab + action searchbox Worklog documents the coverage map, the happy-dom shims, and the honest gaps (bridge.html inline script e2e-only, split-pane drag, no live network-bearing fixture trace yet). Verified: pnpm check 0 errors; node 375 passed; workers 1222 passed; dashboard e2e replay spec 3/3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): playback controls, collapse defaults, popover clamp, source highlighting - Timeline gains the official viewer's playback cluster (prev/play-pause/stop/ next/speed 0.5-2x): rAF clock, nearest-action selection, stop-at-end, restart-at-end semantics matched to the vendored v1.61.1 bundle. Playback, stepping, and click-seek walk filteredActions([]) so hidden noise-group actions are never selected into a row-less void. - Action-list groups start collapsed unless their subtree holds a failing action; manual toggles are XOR overrides that survive chip toggles; external selection (seek/playback/deep link) auto-expands its ancestor chain. - Timeline hover preview flips below the strip when the viewport lacks headroom (it always clipped inside the overflow-hidden replay dialog). - Source tab: pure-lezer syntax highlighting (@lezer/javascript + classHighlighter), no @codemirror imports so the reverted dual-instance trap cannot recur; scoped .trace-source tok-* palette in styles.css, light+dark. Tests: timeline 4->14, action-list 13->19, new source-tab suite (6). Verified: pnpm check 0 errors; node 397 passed; workers 1222 passed; e2e replay 3/3; 15/15 live-browser checks against a seeded failed run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(trace-viewer): bigger timeline hover preview, above the panes PREVIEW_HEIGHT 140 -> 220 (clearance derived), and z-50 on the card: when flipped below the strip it overlays the workbench panes, which come later in DOM order and painted over the un-z-indexed card (tabs + snapshot iframe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): native Replay workbench + inline attachments, quality pass Replaces the official Playwright trace-viewer iframe with Wrightful's own React workbench over the vendored Playwright service-worker engine, adds inline attachment previews (image/video lightbox), network filters, timeline hover action captions, and an in-place attempt switch. Includes a structural quality pass (timeline/playback split, shared detail-tab primitives), an expanded seed suite with console/network traces, and broadened test coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(trace-viewer): use semantic type-ramp tokens instead of legacy numeric names The type-ramp rename (#51) that landed on main replaced numeric font-size tokens (text-11…text-26) with semantic names; the trace-viewer components still used the old ones, tripping the token-conventions guardrail test in CI. Remap to text-micro/caption/body/body-lg/heading/title/display. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(trace-viewer): move playback controls into snapshot nav, drop paint-canvas toggle Move the prev/play/stop/next/speed cluster out of the timeline strip into the snapshot pane's Before/Action/After nav (right side, before the popout link). The strip's moving Playhead and the control cluster are now siblings, so the usePlayback controller is lifted up to the Workbench and shared with both; the timeline strip spans full width. Keep the nav row height constant when an action captured no snapshot via an invisible tab-height placeholder. Remove the paint-<canvas>-from-screenshot toggle button as unhelpful, along with its now-dead plumbing: the usePersistedFlag hook (deleted, no other consumer), the CANVAS_FROM_SCREENSHOT_KEY localStorage key, and the populateCanvasFromScreenshot option on snapshotIframeUrl. Also consolidates the trace-viewer worklogs into a single entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(trace-viewer): span snapshot nav divider full width The rule under the Before/Action/After row was the TabBar's own border-b, which spans only the flex-1 TabBar and stopped where the playback/popout controls begin. Move the divider onto the whole header row and drop the TabBar's rule so a single 1px line spans full width; the active-tab underline still overhangs it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(trace-viewer): align action-list and snapshot-pane header dividers The action-list filter header (h-8 input + py-1.5) was taller than the snapshot pane's Before/Action/After nav (tabs), so the two panes' bottom dividers didn't line up across the split. Give both header rows a shared h-9 (36px): the nav keeps its bottom-aligned tabs, the filter header centers its input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(trace-viewer): borderless filter input as the action-list header Rework the "Filter actions" field into the borderless, full-width command-menu style (matching ComboboxFilterPopup's search row): the input itself is the pane's top edge (h-9 w-full bg-transparent, no box) and the wrapper carries the hairline divider. The h-9 matches the snapshot pane's Before/Action/After nav so both dividers align across the split, and the boxed input border is gone. Drops SearchFilterInput from this pane; keeps type="search" for the searchbox role. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(trace-viewer): icon actions in Replay dialog header, drop official viewer Replace the Replay dialog header's three text buttons with icon-only buttons (tooltip on hover): a Share2 icon opening the public Playwright viewer (trace.playwright.dev) and a Download icon. Remove the "Official viewer" link — the public viewer serves the same purpose for the user. traceViewerUrl stays on the wire contract as TraceViewerDialog's availability gate but is no longer rendered as a link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(trace-viewer): use styled Tooltip for Replay dialog icon actions Wrap the share (public viewer) and download icon buttons in the app's Base UI Tooltip component (ui/tooltip) instead of the native title attribute, matching the rest of the app's icon-button tooltips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(trace-viewer): hover preview, row toggles, source/errors/timeline polish - Action rows with children toggle their accordion on click (chevron still toggles without selecting) - Hovering an action row previews it in the snapshot canvas and Source tab (highlightedAction || selectedAction, upstream parity), reverting on unhover; other tabs/timeline/playback stay on the real selection - Filter Playwright's synthetic project#<id> fixture-pool location out of the Source tab (tabs, default file pick, frame availability) - Errors tab count rendered as the COSS destructive Badge - Source target line highlighted with bg-running-soft + inset accent bar - Timeline filmstrip no longer flashes on attempt swaps: slots keyed by index + useObjectUrl keepPrevious mode (revoke-after-replace) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(trace-viewer): activeAction in the tab contract; Call/Log follow hover Promote the hover-coalesced action (hoveredAction ?? selectedAction) from an ad-hoc SourceTab prop override into TraceTabProps itself, so each detail tab declares which action it keys on. Call, Log and Source now follow hover (upstream viewer parity); Errors/Console/Network/Attachments/Metadata stay on the real selection so a hover sweep can't yank scoping filters or scroll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): timeline range selection — play a section, scope the action list Drag on the timeline strip to select a time window (a plain click still seeks; a press becomes a drag after 4px of travel). While a selection is active, Play starts inside the window and pauses when the playhead reaches its end; the action list shows only intersecting actions with a "Timeline selection / Show all" bar to clear it; and stepping, seeks, and hover captions all walk the selection-scoped playable set. The selection is model-keyed workbench state, reset on attempt swap like selection/hover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): timeline click dismisses the range selection A dragless click on the strip now seeks to that exact point and clears any active selection window on release — matching the native viewer. Click-seeks and hover captions resolve against a new seekActions prop (the full default-visible set) rather than the selection-scoped playable set, so a click outside the window lands on the true action at that time instead of clamping to the window's nearest; the scoped set stays in place for the playhead and stepping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(trace-viewer): timeline selection filters Console, Network, and Log The drag-selected time window now scopes the time-windowed detail tabs, not just the action list: Console rows (by event time) and Network requests (by request start time) outside the window are hidden, the Console/Network tab-label counts narrow to match, and the Log tab shows only the active action's in-window entries. The selection travels on TraceTabProps (with a shared timeInRange predicate in model.ts) and composes with the existing crosshair scope-to-action toggle, which further narrows within the window. Each tab gets a selection-specific empty message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(trace-viewer): thermo-nuclear review fixes — policy blocker, code-judo dedup, convention BLOCKER: re-consolidate artifact-serving into serveArtifactBytes and move the presign TTL cap into it — the download route had re-inlined the fork and the two copies diverged on the cap (an 8h trace token could mint an 8h anonymous-read R2 URL via the orphaned helper the policy test still exercised). Code-judo: extract useModelScopedState (4 dup sites), a shared BridgeBodyPreview + mime.ts classifiers (network/attachments), rebuild useSnapshotInfo on useBridgeFetch + un-fork useObjectUrl into useObjectUrl/useBufferedObjectUrl, a timeScale value object for the timeline geometry, shared console/network window selectors, a basename helper, isWithinSelectedAction->timeInRange, and actionsCount->atStart/atEnd on the playback controller. Convention/boundary: Field/Section now cn()-merge (delete MetaField + the className="" reset; add a Field variant + formatWallClock); restore react-query in ReplayModalHost; drop the never-read traceViewerUrl from the replay attempts contract; add warm.ts releaseWarmedTrace(); fix snapshot-pane location.replace doc-rot; add aria-labels the tooltip pass dropped. Verify: pnpm check 0 errors; full node (512) + workers (1314) lanes green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mcp): hand out our self-hosted trace viewer, not trace.playwright.dev MCP's get_artifact was returning a trace.playwright.dev viewer link, shipping trace bytes to a third party — the exact leak the vendored same-origin viewer exists to avoid. Unify the two divergent helpers (signedTraceViewerUrl, rail; traceViewerUrlFor, MCP/public) into one selfHostedTraceViewerUrl(absoluteUrl) and point MCP at it, so an agent's viewer link keeps the trace on this dashboard. The native React viewer can't back an MCP link (tenant-scoped, auth-gated dialog); the self-hosted SPA works cold from a signed URL. Verify: pnpm check 0 errors; artifact-tokens/test-artifact-actions/mcp-server suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(trace-viewer): decompose network-tab + snapshot-pane Pure, behavior-preserving splits. network-tab.tsx 737 -> 339: pure column/sort/ classification helpers to network-columns.ts (now unit-testable, no React) and the request detail panel subtree to network-detail-panel.tsx. snapshot-pane.tsx 456 -> 249: the scale + double-buffer iframe stage to snapshot-stage.tsx. Verify: pnpm check 0 errors; network/detail-tabs/snapshot suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(trace-viewer): extract useActionTreeCollapse + split playback-controls Pure, behavior-preserving. action-list.tsx 489 -> 351: the collapse/override/ auto-reveal state machine + the visible-row walk lift into use-action-tree- collapse.ts (one scope, one dependency set — the walk and the collapse check can no longer drift). playback-controls.tsx 426 -> 119: the engine + time-search primitives move to use-playback.ts and the rAF line to playhead.tsx, so the "controls" file holds only the button cluster. Verify: pnpm check 0 errors; action-list/timeline/snapshot/shell suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(trace-viewer): consolidate empty states + action-list micro-cleanups ScopedEmpty now owns the scoped-vs-range message choice the Console/Network call sites duplicated, and a shared TabEmpty replaces the hand-rolled <Empty> blocks in errors/attachments. In action-list, the ActionRow chevron renders once (wrapped interactively or not) instead of duplicating the icon across a 3-way branch, and localStorage group persistence folds into one usePersistentGroupSet hook so its read and write can't drift. Verify: pnpm check 0 errors; console/network/errors/attachments/action-list green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(trace-viewer): lift timeline pointer machine into useTimelineSeek The strip's click-seek + drag-to-select-range state machine + hover cursor (pointer handlers, the drag threshold/latch in a ref, pointer-capture bookkeeping) move into a useTimelineSeek hook, so the Timeline render body reads measure -> scale -> seek-machine -> derive -> compose instead of interleaving imperative pointer math with the overlay JSX. The draggingRef/hover ref-vs-state split is preserved (a drag must not re-render per move). Worklog updated to record the full decomposition + MCP follow-ups. Verify: pnpm check 0 errors; trace-viewer node (196) + workers (1314) lanes green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(e2e): assert the shipped replay contract, not the deleted traceViewerUrl The replay spec still asserted attempts[].traceViewerUrl — removed from the route's contract in 81eff2f — so the suite was guaranteed red (typeof null). Assert downloadHref against signedDownloadHref's real shape instead, keep the third-party-leak guard, and rewrite the stale comments describing the deleted field. Statically verified against the route (no Postgres in this sandbox); run the dashboard e2e suite before merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(dashboard): drop the write-only traceViewerUrl channel; scope dialog attempt state The UI artifact contract minted a signed self-hosted viewer URL per trace row that was only ever consumed as a truthiness gate equivalent to type === "trace" — gate on the type, keep selfHostedTraceViewerUrl for its one real consumer (MCP). That orphaned the origin argument end-to-end: signArtifactRows and loadAttemptArtifactGroups lose the param, the test-detail loader loses its resolvePublicOrigin computation, and resolvePublicOrigin itself (zero production callers, kept alive only by its own tests) is deleted. Also: key TestReplayContent by the replay id — with staleTime:Infinity a cached deep-link swap carried one test's attempt selection into another test (regression test pre-seeds the query cache for two ids); and repoint artifact-tokens' two TTL docstrings at serveArtifactBytes, the single presign- cap home since the round-1 blocker fix (they still described the pre-fix world — the exact stale-pointer failure mode behind the original regression). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(trace-viewer): thermo-nuclear round 2 — semantic fixes, contract judo, ui-wrapper adoption Semantic fixes (approved behavior changes): - defaultSelectedActionId delegates to vendor failedAction() — the INNERMOST failed action for nested test.step failures, restoring official-viewer parity (was find-first, selecting the outer step wrapper) - the 30s bridge load timeout is a true silence watchdog — re-arms on every message from the loading iframe instead of killing actively-progressing loads with a misleading "SW may be blocked" error - source-tab's fetch key encodes the trace URL (the useBridgeFetch key invariant — key must encode every loader input — is now documented), fixing stale text served across attempt swaps with recurring file paths - the register-only warm iframe removes itself on the bridge's warm ack (10s fallback) instead of leaking one hidden iframe per session Contract simplifications (behavior-preserving): - TraceBridge carries traceUrl; the redundant {bridge, traceUrl} pair is gone from TraceTabProps and ~10 leaf signatures; TraceTabProps moved from model.ts to its owner detail-tabs.tsx; pure modules name vendor TraceModel - detail-tabs: one TabConfig registry replaces the triple tab enumeration; LogTab extracted to log-tab.tsx sharing the offset grid with Console; props are Omit<TraceTabProps,"scopeToSelected">; honest ConsoleRow type; dead exports and always-passed-optional props dropped - playback: PlaybackControls takes the controller (was 9 unpacked props); the playable set lives on the controller; usePlayback takes selectedAction and model — playing is model-scoped state, deleting the workbench's effect-based pause-on-swap and its one-frame stale-playing gap; TimelineAction is a real Pick, not an identity alias - timeline: HoverOverlay receives a narrowed non-null hover (the ??-fallback papering is gone); TimeScale gains percentAt({clamp})/spanPercent and all hand-rolled fraction*100 sites route through it - attachments: shared PreviewPre/useSha1PreviewText re-unify the re-drifted text preview with body-preview; attachmentDataUrl/useAttachmentMediaUrl collapse the triplicated base64 fork; isVideoMime closes the mime hole - shortUrl rebuilt on lib/basename (with a trailing-slash parity fix over the naive swap); BRIDGE_PATH derives from TRACE_VIEWER_SCOPE; AnsiPre gains an inline variant making its single-owner claim true; the sync script's half-automated --pr mode is deleted; use-trace-model's duplicated reject-all-pending loop hoisted UI conventions: PlaybackButton + snapshot popout on ui/button (render-prop anchor arm), both progressbars on one TraceProgress over ui/progress, network sort headers mapped from a column table, filter input consumes the new FILTER_INPUT_CLASSES export instead of a byte-copied string. Tests: cast-free makeResource/makeConsoleEvent/makePageErrorEvent fixture builders restore the compile-time link to vendor/har.ts; renderDetailTabs replaces 13 hand-written prop blocks; new direct characterization suites for network-columns (31), mime (22), and the extracted source-highlight.ts (24, pinning the tokenizer's line-count invariant); brittle class-string assertions retargeted at data attributes. See docs/worklog/2026-07-15-trace-viewer-thermo-nuclear-round-2.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(worklog): trace viewer thermo-nuclear review round 2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(trace-viewer): address maintainability review * test(e2e): expect self-hosted trace viewer * refactor(trace-viewer): streamline replay artifacts * fix: enforce trace replay artifact policy --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
void 0.10.4 does not apply void.json routing.rewrites in the deployed Cloudflare worker, so MCP OAuth discovery 404'd in production: the root /.well-known/oauth-* paths reached the Hono router unrewritten and fell through to the not-found page, while their /api/auth/.well-known/* targets returned 200. vp dev simulates the edge rewrite so the e2e OAuth dance passed while prod discovery was dead. Add middleware/00.oauth-discovery.ts to perform the four-path rewrite in-worker via c.rewrite() (the mechanism 00.errors.ts already uses), covered by a workers-lane test that exercises the actual worker. Keep the void.json rewrites as a redundant edge path.
* Harden dashboard Playwright suite against flakes * Address Playwright review feedback * Run E2E against the production preview * Allow direct R2 artifact sources in CSP * Isolate global monitor scheduler E2E * Address CodeRabbit review feedback - Guard the stale-lease reclaim against deleting a lock another worker just rewrote: re-read and only rm when the owner token is unchanged. - Wait for hydration after the monitor-create redirect so chained detail-page clicks can't be swallowed mid-hydration. - Retry + time-bound the production migration request in the boot fixture, mirroring the signup bounded-retry pattern. - Give both groups form-submit assertions the cohort's explicit 15s timeout (the flagged duplicate-name alert plus its create() sibling). * Address post-push review feedback (Codex + CodeRabbit) - Reclaim stale scheduler locks atomically: compare-then-rm still let two waiters both pass the compare before either deleted, so the loser could remove a lease written in between. rename() gives exactly one contender custody of the lock file; an accidentally captured fresh lease is restored via non-clobbering link(). - Scope monitor URL assertions to the active tenant: submitCreate() now matches a detail URL built from this.listPath, and openEdit() pins its ?edit=1 check to the current detail pathname.
* feat(mcp): add flaky-test diagnosis and test-history tools diagnose_flaky_tests returns explicit counters, normalized error-signature groups, representative result ids, same-run co-failures, and latest-run health on top of the shared rankFlakyTests ordering. get_test_history resolves exactly one selector (test_id / file / query) through the tests catalog into a commit-to-attempt timeline. Reads are bounded on every axis (rows per test, co-failure runs and rows, 2 KB error heads); error fingerprinting lives in the pure src/lib/error-signature.ts leaf. * fix(mcp): cap signature groups per test; update e2e tool list Codex review: a test whose 500 window failures normalize to hundreds of distinct signatures could blow the diagnosis payload to multi-MB; return the top 10 by count with distinctSignatures reporting the uncapped total. Also add the two new tools to the e2e MCP tool-list assertion (CI fail). * fix(mcp): trim history selector value; expose analyzedRows sampling bound CodeRabbit review: a padded test_id/file passed validation but was forwarded untrimmed, exact-matching nothing; and signature/co-failure breakdowns are computed from at most 500 window rows per test while the counters cover the full window — analyzedRows makes that visible. * fix(mcp): full-window representatives, per-test co-failure budget, truncation flags Codex P2s: representatives (latest flaky/hard-fail/passed ids + lastFlakyAt) now come from a dedicated latest-per-(test,status) distinct-on read so a high-volume test's newest flaky occurrence cannot fall out of the 500-row sample while retryPasses stays positive; the 200-run co-failure budget is allocated per test (newest first) with coFailureRunsAnalyzed disclosed; and get_test_history reports matchedTestsTruncated when a file/query selector matches more than 50 catalog tests. The file selector deliberately matches the current cataloged path (documented in the arg description) — resolving historical paths would need unindexed fact-table scans; query covers fuzzy. * fix(mcp): aggregate window-row budget, per-run co-failure budget, blank-message stack fallback Codex round 2: a max-size diagnosis (50 tests x 500 rows x 2KB heads) could hold ~50M chars of error text in Worker memory — the 5,000-row window budget is now split across the selected tests (full 500/test at the default limit); the co-failure row budget is split per run so one mass-failure run cannot starve the other budgeted runs out of failuresByRun; and errorHead treats a blank/whitespace errorMessage as absent (nullif(trim(...), '')) so a useful stack is not shadowed. * fix(mcp): exclude queued prefills from the diagnosis window sample Codex: queued placeholder rows matched the ne(skipped) predicate, inflating analyzedRows past samples and spending the per-test row budget on rows that carry no error; the window now selects exactly the counter statuses (passed/flaky/failed/timedout), matching the representatives read.
* feat(dashboard): sticky GitHub PR comment summarizing runs Upsert a single App-posted PR comment per (project, repo, prNumber) on run completion, alongside the existing check run: summary table, failures split new-vs-known against the branch's previous terminal run, flaky detections, and deep links into the dashboard. New githubPrComments table carries the comment id plus the check-run-style claim CAS (no duplicate comments under concurrent completions) and a ULID stale-run guard (a late watchdog finalize can't overwrite a newer run's summary). Wired into completeRun, completeShardedRun, and finalizeStaleRun as a best-effort step; requires the App's Pull requests: Read & write permission (docs updated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(dashboard): dedupe GitHub check-run and PR-comment surfaces Extract shared run-context resolution (github-run-context.ts), shared rendering (github-run-render.ts), claim-before-POST mechanics (github-surface-post.ts), and a single postGithubRunSurfaces entry point (github-run-surfaces.ts) so ingest finalize paths resolve context and mint the installation token once for both surfaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): serialize sticky PR-comment writes across runs Review pass on PR #60: - Codex P1: the sticky comment is one GitHub resource shared by every run on a PR, but only the first POST was claim-protected. Concurrent completions of two runs could PATCH concurrently (older body landing last at GitHub despite the DB runId CAS), and a newer run losing the first-comment POST claim returned without retrying. All comment writes now serialize through postWithWriteMutex: claim -> re-read -> write -> persist {commentId, runId} -> release, with a bounded wait-and-retry for claim losers and the persisted runId doubling as the monotonic skip guard. The check-run surface keeps postWithClaimedSlot (per-run resource; concurrent posters render identical content). - CodeRabbit: release a held claim when GitHub's 2xx response carries no id, instead of leaking it for the 120s TTL (both flows). - CodeRabbit: formatDuration rounds to the displayed tenth before the sub-minute comparison, so 59.96s renders "1m 0s" instead of "60.0s". - CodeRabbit nitpicks: sub-minute rounding-boundary test; @Schema alias imports in the claim tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): scope PR-comment diff baseline to the PR Codex P2 on PR #60: branch names are not unique across PRs (two fork PRs can both report head ref "fix" while run.repo stays the target repository), so the PR comment's branch-only resolveBaseRun could adopt an unrelated PR's terminal run as its baseline and mislabel failures as known-vs-new. resolveBaseRun grows an optional pr: { repo, prNumber } predicate; the PR-comment path passes it, the diff page keeps the branch-wide default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): recover the sticky comment after a lost initial POST Codex P2 on PR #60: if GitHub accepts the initial comment POST but the 2xx response is lost in transit, the comment exists with no persisted id, and the next completed run's fresh POST duplicates the marker comment. Before any fresh POST the surface now scans one page of the PR's issue comments for the project-scoped marker and PATCHes a hit instead of creating a new comment (mirroring the reporter's findExistingComment, but failing closed on a broken listing — the claim releases and the next run retries). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dashboard): sanitize untrusted text in PR-comment code spans Codex P2 on PR #60: test filenames (and commit shas) were interpolated into the sticky comment's Markdown code spans raw. Git permits backticks and newlines in path components, so a hostile filename could close the span and inject arbitrary Markdown - including @-mentions - into the App-posted comment. mdCodeSpanText strips backticks (backslash escapes don't work inside code spans) and collapses whitespace runs; shas sanitize before the 7-char slice. Titles were already escaped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tallations (#64) The setup callback proved a user administers a GitHub App installation solely via `GET /user/installations` with the user's sign-in OAuth token. A minimal-scope (`user:email`) sign-in token can be refused by that endpoint, so linking any *personal* installation failed with "Could not verify your access to this GitHub installation" — the confused-deputy (H1) check erred out for every user, not just one. Add a personal-account fast path in the setup callback: resolve the installation's owner via the App JWT (trusted, server-side) and, for a `User`-type install, authorize when it matches the signed-in user's own login from `GET /user` (which any valid user token can call regardless of scope). Org installs still defer to `/user/installations`. An attacker can only ever fast-path an install on THEIR OWN account, so the H1 boundary holds. Also surface the previously-swallowed GitHub HTTP status on the verification failure paths via `logger.warn`, so org-install failures can be diagnosed (401 token vs 403 scope). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure move: git mv the fourteen run-*/runs-* components in apps/dashboard/src/components/ into src/components/run/ (dropping the redundant filename prefix, following the existing analytics/ / monitors/ / settings/ / ui/ convention) and rewrite the @/components/run-<name> and @/components/runs-filter-bar specifiers to @/components/run/<name>. No logic changes. running-spinner.tsx is deliberately excluded: it only shares the "run" prefix and is a generic primitive unrelated to this component family. No index.ts barrel was added, matching the sibling subfolders. Doc-comment references to the old filenames (in meta-pills.tsx, use-run-summary.ts, status-registry.workers.test.ts, the runs page, and a Playwright spec) were updated to match; docs/worklog/ and the historical HTML review under docs/reviews/ were left untouched as point-in-time records. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Harden platform boundaries and background workflows * docs: clarify bootstrap and record verification * Address PR review findings * Address second-round PR review threads - ingest: reset stale shard metadata on a terminal run's duplicate open — an unsharded re-run clears expectedShards/shardExpectedTests and stale runShards rows; a changed-total re-run restarts the expected-tests map and replaces the total (mid-flight opens keep coalesce semantics) - trace viewer: hold the snapshot iframe off the first origin-less render so separate-origin snapshots load with the right sandbox from the start - docs: clarify pnpm deploy:void wraps void deploy; distinguish the PR's index-only migration from "no logical schema changes" in the 07-18 worklog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address third-round PR review threads A terminal sharded re-run that kept the SAME shard total retained the previous run's runShards completion rows, so its first new shard /complete saw a full count and finalized against dead sibling results. applyShardExpectedTests now latches the reset on the terminal status alone: under a FOR UPDATE lock on the run row it drops every previous completion row, restarts the expected-tests map, and re-arms the run (status='running', completedAt=null) — the status flip makes the reset exactly-once under racing sibling opens. Also documents the deliberate ORDER BY random() trade-off in billing reconcile (bounded by paying-customer cardinality; keyset cursor is the evolution path) and qualifies the round-2 worklog's no-migration note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address fourth-round PR review threads - artifacts: allow the configured separate trace-viewer origin in the download route's CORS allow-list so bridge.html on the cookieless host can fetch signed trace URLs; document the matching R2 bucket CORS step - ingest: enforce WRIGHTFUL_MAX_TEST_RESULTS_PER_RUN at open — a fresh run whose planned-test set exceeds the ceiling is refused (413) before the prefill persists any rows - docs: record the Vite+ commands behind the pnpm validation scripts in the round-3 worklog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: consolidate the architecture-review worklogs into one file Replaces the six per-workstream 2026-07-16 worklogs, the integration verification pass, and the four PR-review round logs with a single subsystem-organized record of the final state, kept decisions, and deferred items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep scripts disabled on dashboard-hosted trace snapshots The strict script-src 'none' snapshot CSP was gated on !isSeparateTraceViewerOrigin(pageOrigin), which is true exactly on the dashboard origin once a separate viewer origin is configured — so the session origin served snapshot HTML under the relaxed scripting CSP. New isTraceViewerHost asks the server-side question directly: the strict policy now applies on every origin except the configured cookieless viewer host itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(dashboard): sort test catalog and simplify pagination
* refactor(dashboard): co-locate catalog sort SQL contract
The tests-catalog sort spread each column's SQL knowledge across three
ternaries in tests.server.ts plus a separate switch in the sort lib, coupled
by an unenforced cross-file alias contract (a projected alias like "n" produced
in one file, consumed by ORDER BY in the other). The lib also imported `sql`
from void/db, which the client page bundles transitively -> browser build
failed with MISSING_EXPORT.
Collapse all of it into testsCatalogSortSql(), returning each key's
{ projection, join, group, orderBy } as pure strings from the closed
TestsSortKey/Direction unions. Alias and ORDER BY now sit adjacent and can't
drift; the lib no longer imports void/db, fixing the client build. Server
splices the constant fragments via sql.raw (URL value is parsed, never
interpolated). Also pin the testId pagination tiebreaker to asc uniformly (the
test column used to flip it with direction) and document why the new
SortableHead stays separate from the trace-viewer's.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#70) Switching filters quickly on list pages (tests catalog and the other analytics pages) could start a second router.visit() before the first committed, aborting the pending navigation and rejecting its still-loading deferred props — surfacing as an uncaught "Navigation disposed before deferred props resolved" error. Add a `useIsNavigating()` hook (backed by @void/react's `useNavigation`) and a `NavBusyGuard` wrapper, then make the header filters inert/disabled while a visit is in flight: the range/group segmented groups, the branch combobox, the sort column headers, and the tag chips. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(security): enable security-guidance plugin with repo rules Enable the security-guidance@claude-plugins-official plugin in checked-in settings and add .claude/claude-security-guidance.md describing this repo's tenancy, auth, artifact, and secret-handling threat model for the plugin's model-backed reviews. * docs(security): exempt unique-ID lookups from two-column run scoping Clarify that projectId-alone scoping is correct for globally-unique ULID lookups (runByIdWhere, childByIdWhere); only list/aggregate runs queries AND teamId. Prevents the reviewer plugin from flagging the blessed predicates. * docs(security): carve out trusted crons and middleware-authed routes Address two P2 false-positive risks: (1) fleet-wide maintenance scans (staleRunFilter sweep, dueMonitorsWhere scheduler) legitimately SELECT across projects then scope per-row, and (2) Bearer ingest/query handlers rely on 02.api-auth.ts centrally, so they carry no handler-local auth check. Tell the reviewer to verify the cron/queue context and the middleware route matcher rather than flag these as leaks.
…ers (#63) Pure move: git mv the run-*, db-run, db-batch, artifact-tokens, trace-artifacts, artifacts, use-search-param, and use-copied-flag modules in apps/dashboard/src/lib/ into runs/, db/, artifacts/, and hooks/ folders, and rewrite every @/lib/<old> specifier (plus the handful of relative imports and doc-comment references) to match. No logic changes. - run-columns.ts, run-diff.ts, run-outcome.ts, run-read-model.ts, run-groups-page.ts, run-results-page.ts, runs-filters.ts, runs-filters-where.ts, db-run.ts -> runs/{columns,diff,outcome, read-model,groups-page,results-page,filters,filters-where,db}.ts - db-batch.ts -> db/batch.ts - artifact-tokens.ts, trace-artifacts.ts, artifacts.ts -> artifacts/{tokens,trace,store}.ts - use-search-param.ts, use-copied-flag.ts -> hooks/use-{search-param, copied-flag}.ts Deliberately no index.ts barrel files in runs/, db/, artifacts/, or hooks/, matching the repo's existing no-barrel convention. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add GitHub installation repository controls * Address GitHub settings review feedback
…sweep (#71) * chore(deps): pnpm 11 + Vite+/void toolchain bump and dependency sweep Upgrade the package manager (pnpm 10.33 -> 11.16) and core toolchain (vite-plus/void/vitest), reconcile the local patch set against fixes that shipped in void 0.10.10, and sweep every remaining outdated direct dependency (including majors) to latest. Notable knock-on changes: better-auth aligned to 1.6.24 to dedupe with void; lucide-react v1 icon renames + a shared GithubIcon component (v1 dropped brand icons), also adopted in login.tsx; react-email v6 consolidation onto the top-level package; and GitHub Actions major bumps. Also send an Origin header on the MCP OAuth consent POSTs in the e2e suite: better-auth 1.6.24 now rejects consent POSTs missing an Origin (MISSING_OR_NULL_ORIGIN), which a real browser button already sends. Verified: pnpm check (0 errors), pnpm test (dashboard/workers/reporter), and the full pnpm test:e2e harness (29/29) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: disable checkout credential persistence in CI jobs These jobs only install/test/build and never push, so the checkout token doesn't need to persist. Addresses the CodeRabbit/zizmor nitpick. release.yml is left untouched because its Changesets step needs write credentials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Large batch bringing the dashboard up to date: a built-in MCP server, a new
Insights surface, monitor/alerting improvements, Cloudflare Workers Cache
adoption, a tests-catalog schema rework, plus perf/security hardening and the
open-source repo scaffolding (CONTRIBUTING, SECURITY, issue/PR templates).
Changes
Features
pages/oauth/consent.tsx), stdout/stderr capture, and a diagnostics tab; auth wired throughauth.ts+02.api-auth.ts.middleware/00.cache.ts), hardened post-review.Schema / DB
pg_trgmGIN indexes for the command-palette ILIKE scans; drop redundanttestOwnersindex; opt-in SQLCommenter query tags.Perf
/resultsflush on ingest, insert-onlycreatedAt, locked deletes.Security
idempotencyKeyinto page props.state_mismatchvia secure cookies; preserve?nextthrough login/signup.UI / design
Verification
pnpm check(format + lint + type-check)pnpm test(dashboard + reporter unit tests)pnpm test:e2e/pnpm --filter @wrightful/e2e test:dashboardNotes & follow-ups
The bulk of the +75k diff is auto-generated Drizzle migration snapshots under
db/migrations/meta/; the substantive review surface is the pages/, middleware/,auth.ts, anddb/schema.tschanges.