* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest
Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.
- D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
Tables are namespaced (`compat_runs`, `compat_file_results`) so
future metric kinds can live alongside.
- POST /api/compatibility ingest endpoint. Authenticates with a
`COMPAT_INGEST_SECRET` worker secret and upserts by
(kind, runKey).
- GitHub-style contribution grid (one dot per test file) and a
pass-rate-over-time line chart. Both are SVG, no chart libs.
Pass rate excludes Next.js-skipped tests from the denominator.
- Shared header/footer extracted into app/_components and rendered
by the root layout. Nav uses next/link for soft client nav.
- Workflow change: on top of the existing per-test pass/fail/skip
aggregation, the report job now POSTs results to the ingest
endpoint. Guarded to only run when (a) filter=all and
(b) targeting main, so partial runs and branch spot-checks don't
pollute the historical record.
The first nightly run after merge will seed the production D1.
* address review + add ISR with KV cache
Review fixes (apps/web/app/api/compatibility/route.ts):
- constant-time secret comparison via SHA-256 + bytewise XOR
- single-statement atomic upsert with onConflictDoUpdate (no
SELECT->INSERT race for concurrent retried runs)
- top-level try/catch with structured error body so workflow logs
show why ingest failed instead of a generic 500
- MAX_FILES=2000 cap in validation as defensive bound
Page perf (apps/web/app/compatibility/page.tsx):
- latest-run and trend D1 queries run in parallel via Promise.all
ISR + KV:
- new vinext-web-cache KV namespace bound as VINEXT_CACHE
- worker installs KVCacheHandler once per isolate
- both pages opt into ISR with revalidate = 300; compat page drops
force-dynamic
* address bonk re-review
- Drop the ?kind= query param. Only one kind exists today; gating it
behind an unused param invited ISR cache pollution. Hardcoded to
"deploy" with a comment pointing future kinds at a dedicated route.
- Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
crash mid-write can no longer leave a run with zero file results.
D1 executes batch statements inside one transaction.
- Resolve workflow expressions once via step-level `env:` and read
them as `process.env.*` from the github-script body. Removes the
fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
string literals (the nested single quotes were the worst offender).