Admin links for non-admins: "That page isn't available"

A customer admin who opens an Admin link lands on the dashboard with a short notice instead of a silent bounce.

2 Oct 2026 · branch fix/admin-deeplink-unavailable-message · captured against propflow-stage · at 8b950826215

Data note. Every name and balance below comes from propflow-stage, which is anonymized by construction (ADR-0097/0110 — phones in the impossible +1000 NPA, emails @example.test). The identical capture against prod would carry real tenants' names and balances and is not safe to attach anywhere, which is why the generator refuses to run against one.

1 · Where an Admin link now lands: the dashboard, with a notice

Signed in as a real customer admin (role org_admin, not platform staff). In production, the Admin gate sends a non-admin who opens /admin, /admin/dev/atlas or /settings/admin to /dashboard?accessDenied=admin. This PR adds the accessDenied part, and middleware.test.ts, page-gate.test.ts and gate-page.surfaces.test.ts pin it. A local dev server skips the page gate on purpose (middleware.ts and gate-page.ts do nothing outside production), so a deep link can't be bounced here. This capture opens the landing address directly, and the browser renders it exactly as it would after the bounce.

The notice is in the top right: a title saying what happened, then one sentence saying where they are now. It shows no page address, role name or internal term. Before this PR, nothing rendered for a normal session, so the link looked broken.

Asserted in the captured DOM — That page isn't available. · We brought you back to your dashboard. · Dismiss. The notice is in the DOM, and the component has already removed accessDenied from the address bar.

Where an Admin link now lands: the dashboard, with a notice
Sections 2–4 and notes

2 · The same notice at phone width

At 390px the notice spans the screen inside the 16px side margins, and Dismiss stays on the same row.

Asserted in the captured DOM — That page isn't available. · We brought you back to your dashboard.

The same notice at phone width

3 · Dismiss closes it

After a click on Dismiss, the notice is gone and the dashboard is unobstructed. A refresh won't bring it back, because the component removes accessDenied from the address on first read.

Asserted in the captured DOM — Dashboard

Dismiss closes it

4 · Control: the dashboard without the param

Without accessDenied, no notice. The notice is keyed on the redirect's reason, not shown on every visit.

Control: the dashboard without the param

Two critique rounds, and what did not change

Round 1 (against docs/runbooks/brand-and-design.md):

Round 2: I added the Dismiss capture above to prove the notice closes, and the no-param control to prove it is keyed on the redirect reason. No code change came out of either round.

Unchanged:

PropFlow Docs