1. Bottom line
Every fix has a test that fails on the old code. Three reviewers argued against each fix before it landed.
Full API contract suite: 1,303 tests, 0 failures. Admin 195, mobile 1,037.
Merged to main by fast-forward (f15bfab5) and pushed to origin. Deployed to staging at 04:20 GST via just deploy-staging: build passed, all four services restarted, health check 200 on the first attempt. Click through the portal fixes at admin.staging.ela.now; the caption contrast change is visible everywhere.
Deliberate. Prod is missing the payment-slice tables. See section 4.
2. Decisions that need you
The code is built either way. Each choice picks a contract. My recommendation is in grey.
| # | Decision | Status |
|---|---|---|
| 1 | Force-assign the same firm twice. Today: 409, no audit row, no push. Alternative: silent 200. Recommend: keep 409. A double-tap must never read as a second reassignment in the ledger. | Open |
| 2 | Move a case from a firm that already accepted to a different firm. Today: refused. Ops must escalate first, then assign. Recommend: keep two steps. It forces a recorded reason for pulling a live engagement. | Open |
| 3 | Send the brief link to a force-assigned firm. Built: yes, the firm gets the same brief as a normal offer and can confirm by OTP. Recommend: keep. Without it the firm has no portal access and ops relays by phone. | Open |
| 4 | Member push on force-assign. Today it reuses the accept push: “A lawyer has your case.” The firm has not confirmed contact yet. Recommend: a softer title, “Your case has been assigned to a law firm.” Small change. | Open |
| 5 | Show force-assign on cases where no firm was ever eligible. The console hides it. The API now handles it. The console has no firm picker, so showing it would post a guaranteed 400. Recommend: leave hidden until the console gets a firm picker. | Open |
| 6 | SLA when a case is stood down before any pickup. Surfaces now say “No pickup recorded · SLA not assessed.” That is a fact, not a verdict. Recommend: keep “not assessed.” A stand-down is not a response. | Open |
| 7 | General Help channel. The portal promises in-app replies and the server sends a push. The app has no handler and no inbox. You retired the message form. Recommend: reword the portal to call and email now. Build the inbox only if members ask. | Open |
| 8 | Legacy renewal (F12). Active legacy Shield members see “Renew any time” and land on “You're already covered.” Recommend: hide the renew action while a legacy plan is active. Show the expiry date instead. | Open |
| 9 | Retired portal routes (team, reports, analytics, map, DSAR) redirect into the console. Recommend: retire them explicitly in the operators' guide. | Open |
| 10 | Policy number for a Family Shield roster where every adult recorded a prior. Today the server records a number that covers nobody. Recommend: return 409 “no insurable member.” | Open |
| 11 | Help chip wording for Apple grace period. Built: “Shield · active · grace period · renewal failed · insured.” Recommend: accept. | Open |
| 12 | Member 360 verb for a manual-review hold on passed documents. Built: “Finish verification.” Alternative: “Resolve the pending review.” Recommend: “Resolve the pending review.” It names the real task. | Open |
3. What changed
Before pictures come from the review. After evidence is the test that fails on the old code and passes on the new one.
| Finding | What was wrong | What is true now | Status |
|---|---|---|---|
| F2 dialogs | Confirmation dialogs disabled their own buttons. Manual takeover and KYC resubmission could not be completed. | Dialog controls stay live. Only the page behind goes inert. | Done |
| F3 force-assign | Ops saw success. The old firm kept the case. The phone said “Your duty lawyer has your case” with no lawyer. | One atomic transition. Target validated. Old offer stood down. Target gets the brief. Every read model names the same firm. Phone claims a lawyer only when one is known. | Done |
| F4 Help coverage | Unpaid members showed “COVERED · active.” | Chip comes from the entitlement and the same gate that allows a dispatch. NO PLAN / PENDING / COVERED / EXPIRED / LAPSED / REFUNDED. | Done |
| F5 Help console | After one reply, nothing could be opened until a reload. | Busy state always released. A sent reply is never reported as unsent. | Done |
| F6 microphone | First “Don't Allow” lost the recovery sheet until the app was restarted. | Sheets hand off through the primitive's close callback. Recovery sheet appears every time. | Done · sim-verified |
| F7 origin gate | Valid console writes rejected as cross-origin on 127.0.0.1. Probably on prod too. | Gate checks the browser's Sec-Fetch-Site, else Origin against the Host header. Refusals are logged. | Done |
| F8 /cases | Law-firm and consulate roles got a 500 “API unreachable.” | Roles are routed to their landing before any fetch. A late 403 shows access denied. | Done |
| F9 pause count | Pause preview always said 0 live cases. | Counts the open cases the firm actually holds. | Done |
| F10 SLA | Closing with no pickup produced “Ops took over in 2m · SLA met.” | Pickup comes only from a firm acceptance or a recorded takeover. Otherwise “No pickup recorded · SLA not assessed” in archive, ledger and Member 360. | Done |
| F11 Member 360 | “Identity verified” and “documents in review” on one page. Refund open here, forfeited there. | One set of facts drives every panel. Guarantee sentence comes from the guarantee status. | Done |
| Copy | “notification-only” still lived in five admin places and the mobile roster chip. | Gone. A test now blocks the phrase across the admin tree. | Done |
| F1 guard | Production dispatch returns 503 for ordinary members. | Unchanged on purpose. It is the last step of the go-live sequence in the master backlog. | By design |
Before, from the review






After, on the simulator tonight




4. Verification and release
| Check | Result |
|---|---|
| Typecheck, lint, format | Clean across the monorepo. |
| API contract suite | 1,303 tests · 0 failed · 16 skipped (integration-gated). |
| Admin and mobile suites | 195 and 1,037 pass. Admin Next build passes. |
| Full CI gate | Pass. npm run ci exit 0: format, lint, env and taxonomy gates, typecheck, full build, admin smoke, local API smoke, provider-readiness check, and every workspace test suite. |
| Admin browser smoke | Pass, rewritten. The old gate died on its first assertion against a retired dashboard. The new one drives headless Chrome through login, triage, a live case, the takeover dialog (F2), a same-origin Help reply and the next thread without reload (F5, F7), records, coverage, role routing for four restricted roles (F8), and axe accessibility on four pages. It found two real bugs on the way, both now fixed: answering a Help thread from a member with no KYC name broke the whole inbox with a 400 until midnight, and console captions failed WCAG AA contrast on every page. |
| Microphone fix on a simulator | Pass, all four scenarios on a real iOS debug build in a throw-away simulator: first denial shows the recovery sheet by itself; dismiss and re-tap shows it again; grant starts recording; cold start while denied shows it on the first tap. Zero “presentation in progress” warnings in the streamed app log. Evidence in docs/qa/2026-09-05-end-to-end-review/verification-2026-09-06/. |
| Commits | 13 commits, one per slice, each through the pre-commit lint and format hook:
|
Why prod is not deployed
Prod runs the 24 August commit. Main is 68 commits ahead. That includes the whole pay-005 payment slice with eight migrations. Prod has none of those tables and no migration ledger. Staging has all eight. The deploy script does not run migrations. Deploying main to prod today would start an API that expects tables that do not exist. That is a money-path release, not a hot-fix.
Release sequence when you are ready
- Staging first.
just deploy-stagingpushes main and deploys it. Staging already has the migrations. Click through the portal fixes there. - Apply the eight pay-005 migrations on prod in order, and record them in a new
ela_schema_migrationstable. Use the exact commands in the migration headers. One deployer only. - Deploy prod with
~/bin/ela-deploy. - Optional belt and braces: set
ELA_ADMIN_PUBLIC_ORIGIN=https://admin.ela.nowin the admin service env. - Check the origin gate. Send a Help reply from the console. It must succeed.
journalctl -u ela-adminmust show noadmin_cross_origin_rejectedline for the console origin. - Mobile.
just shipafter the microphone scenarios pass on a simulator. The phone-headline fix rides the same build.
5. Follow-up slices, already specified
- Lock screen and Dynamic Island still say “Your law firm has your case” from the state flag alone. The phone screen no longer does. Seven mechanical edits across Swift, the module bridge, App.tsx and the server push producer are written out in the plan doc, section 3.1. A one-line fallback exists if no Swift owner is free.
- Logout and MFA-refresh routes still compare Origin with the Next bind address. Same fix as F7, spec in section 3.2.
- Small items: archive filter chip for “no pickup recorded”; a server component that imports a client-only export in the delivery record page; member-facing grace status; the operators' guide entries for the new refusal codes.
Plan doc in the repo: plans/2026-09-06-e2e-review-remediation-followups.md. Master backlog updated to point at it. Review evidence committed under docs/qa/2026-09-05-end-to-end-review/ (7.5 MB; oversized build logs and contaminated captures stay on disk only).