Skip to content

fix(ops): generated migration manifest, E2E route sweep, and the Add person role fix - #6

Merged
Asapteejo merged 4 commits into
mainfrom
chore/migration-drift-guard-and-e2e
Sep 16, 2026
Merged

Asapteejo merged 4 commits into
mainfrom
chore/migration-drift-guard-and-e2e

Conversation

@Asapteejo

Copy link
Copy Markdown
Owner

Two independent fixes that were sitting uncommitted in the working tree.

1. Migration drift could report a false green

0042_site_content_cms was never applied to production, so pages reading tenant site content threw P2022 "column does not exist". The real bug was the safety net: /api/readyz compared applied migrations against a hand-maintained list that stopped at 0035 while the repo had grown to 0049. Readiness reported "missing": [] while 14 migrations were absent.

  • EXPECTED_PRODUCTION_MIGRATIONS is now generated from prisma/migrations (scripts/generate-migration-manifest.mjs). A generated list cannot go stale; the hand-maintained one did.
  • run-build.mjs regenerates it before next build, so every deploy ships a current list. The build still requires no database access.
  • CI guard: npm run migrations:check fails the build when a migration is added without regenerating the manifest.
  • The site-content editor degrades to an explanatory banner instead of a 500 when the columns are missing.
  • Playwright suite (e2e/): loads every public/admin/portal route in a real browser and fails on any 5xx, error boundary, or console error — exactly the class of failure unit tests and typecheck cannot see. health.spec.ts asserts migrations.missing === [] against a deployment.

Docs included: the incident writeup, the deploy pipeline (migrations run in a release step before app code), the provisioning spec, and UI/Paystack fix notes.

2. "Add person" always submitted Front Desk

/admin/users → Add person posted STAFF no matter which role was picked.

The Role <select> is controlled, and its onChange deferred setSelectedRole into a setTimeout that read e.target.value lazily, then returned a cleanup function React ignores. React restores a controlled select to its current state right after the event, so by the time the timeout ran the DOM value had already snapped back to STAFF — and that is what got submitted.

Fixed by updating state synchronously; same pattern fixed in add-buyer-form's profile toggle. The setTimeout calls inside useEffect are a deliberate pattern in this codebase and are left alone, as are genuine delays (copy feedback, progress animation).

The server action was already correct: it reads role from the FormData with no fallback, and provisionCompanyUser rejects a role the actor may not create rather than defaulting.

Regression test (e2e/add-person-role.spec.ts): picks Finance, waits out any deferred revert, and asserts the submitted server-action payload carries role=FINANCE. It intercepts that request, so no Clerk account is created and no invite is sent. Verified it fails on the old handler (expected "FINANCE", received "STAFF") and passes on the fix.

Verified: npm run check green (535 tests, typecheck, lint, build) and npm run migrations:check clean (51 migrations).

🤖 Generated with Claude Code

Asapteejo and others added 2 commits September 16, 2026 00:17
…sweep

Migration 0042_site_content_cms was never applied to production, so pages
reading tenant site content threw P2022 "column does not exist". The real
bug was the safety net: /api/readyz compared applied migrations against a
HAND-MAINTAINED list that stopped at 0035 while the repo had grown to
0049, so readiness reported a false green with 14 migrations missing.

- Generate EXPECTED_PRODUCTION_MIGRATIONS from prisma/migrations
  (scripts/generate-migration-manifest.mjs). A generated list cannot go
  stale; a hand-maintained one did.
- Regenerate the manifest in run-build.mjs before next build, so every
  deploy ships a current list. The build still needs no database access.
- CI guard: `npm run migrations:check` fails when a migration was added
  without regenerating the manifest.
- Degrade gracefully: the site-content editor renders an explanatory
  banner instead of a 500 when the columns are missing.
- Playwright suite: load every public/admin/portal route in a real
  browser and fail on any 5xx, error boundary, or console error — the
  class of failure unit tests and typecheck cannot see. health.spec.ts
  asserts migrations.missing === [] against a deployment.

Docs: incident writeup, deploy pipeline (migrations run in a release
step, before app code), provisioning spec, UI/Paystack fix notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…picked

The Role <select> is controlled, and its onChange deferred setSelectedRole
into a setTimeout that read e.target.value lazily, returning a cleanup
function React ignores. React restores a controlled select to its current
state right after the event, so by the time the timeout ran the DOM value
had already snapped back to STAFF — and that is what the form posted.

Update state synchronously instead. Same deferred-handler pattern fixed in
add-buyer-form's profile toggle. The setTimeout calls inside useEffect are
a deliberate pattern here (avoiding set-state-during-render) and are left
alone, as are the genuine delays (copy feedback, progress animation).

The server action was already correct: it reads `role` from the FormData
with no fallback, and provisionCompanyUser rejects a role the actor may
not create rather than defaulting.

Regression test: e2e/add-person-role.spec.ts picks Finance, waits out any
deferred revert, and asserts the submitted server-action payload carries
role=FINANCE. It intercepts that request, so the test creates no Clerk
account and sends no invite. Verified it fails on the old handler
(expected "FINANCE", received "STAFF") and passes on the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
estate-os Ready Ready Preview Sep 16, 2026 11:30pm UTC

…rver noise

The suite could never pass in CI. Three harness faults, no app changes:

- Public pages resolve their tenant from the HOST, and marketing routes
  deliberately ignore the DEFAULT_COMPANY_SLUG fallback — only a tenant
  host counts. Against 127.0.0.1 every tenant page rendered the platform
  site or 404'd, so the sweep failed on nearly every public route. Drive
  the seeded demo tenant through <slug>.localhost, which is the supported
  dev tenant host (resolveTenantSubdomainFromHost).
- Windows, and some Linux setups, do not resolve *.localhost, so map it to
  loopback in the browser itself rather than trusting the OS resolver. The
  webServer readiness probe keeps using 127.0.0.1, which Node can resolve.
- `next dev` logs a hot-reload WebSocket console error in headless CI, and
  the sweep counts any console error as a defect. It is dev-server noise,
  not an application fault, so it joins the ignore list.

Also raise the per-test timeout to 120s: a route's first request compiles
it (30s+ for admin routes on a cold machine), and the old 60s budget made
the first visit time out and abort the rest of the file's navigations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… dev

The suite ran against `next dev`, which compiles each route on its first
request. On a 2-core runner that is 30s+ per admin route, so tests timed
out one after another and the job hit its 30-minute limit — 49 failures,
none of them real.

Build once, then serve that build with NODE_ENV=test. The test value keeps
the dev-session bypass available (allowDevBypass in src/lib/config.ts
treats "test" as non-production), so the authenticated sweep still runs
rather than skipping itself on a production build. Locally the default
stays `npm run dev` so the suite still picks up edits; CI overrides it
with E2E_SERVER_COMMAND.

Measured on the same machine, same suite:
  next dev          49 failed, 14 passed, 33 min
  production build   1 failed, 61 passed, 3.8 min
(the one failure is this machine's database being behind — exactly what
health.spec.ts exists to report; CI migrates a throwaway database first.)

Also: accept either submit transport in the Add person regression test. A
hydrated page sends a server action (Next-Action header, multipart); before
hydration the same form posts natively (url-encoded). The test asserts
which role was submitted, so it should not care which transport carried
it — it only failed on a production build because hydration timing differs.

The *.localhost resolver mapping now applies to external targets too; it
only affects that suffix, so it is inert against a deployed environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Asapteejo
Asapteejo merged commit 7148452 into main Sep 16, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — fc6eac4d Deployed Sep 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant