2026-05-14 Gate 1 local. Final slice of the POS Section Rework arc (Narong spec items 8 & 9): the Registers relocation + section permission gating. The registers admin moves fully under the Point of Sale section, and the nav rail becomes permission-aware. No migration.
registers-client.tsx git mv'd from settings/registers/ → pos/registers/ (history preserved; all its imports are @/-absolute so it moved clean). /cafe/pos/registers/page.tsx is now the real page (was a Slice 3 re-export); /cafe/settings/registers is now a redirect stub → /cafe/pos/registers (preserves ?shop=). The Registers row is dropped from the Settings nav, and registers.ts's 4 revalidatePath calls retarget /pos/registers.PosNav item declares the permission its sub-page enforces (Registers → settings.view, Sessions → reports.view, Orders → orders.view; Dashboard has none — the section layout already gates pos.view). The rail hides what the user can't reach, so there's no 403-on-click. requirePermission on each page is still the real guard — this is the UX layer.PosShell now takes the user's permissions; when only Dashboard is reachable (a cashier with just pos.view) it skips the master-detail grid entirely and the landing gets the full width. pos/layout.tsx resolves permissions via auth() (React-cache()-wrapped, so it's a cache hit after requirePermission) and threads them through.settings.view / reports.view / orders.view are the correct gates for those surfaces), and the rail becomes permission-aware rather than widening everything to pos.view. No access widened.visiblePosNav(["nix_cafe.pos.view"]) → ["Dashboard"]
visiblePosNav([]) → ["Dashboard"] (no 'requires')
visiblePosNav(["…pos.view", "…reports.view"]) → ["Dashboard", "Sessions"]
visiblePosNav(["…pos.view", "…orders.view"]) → ["Dashboard", "Orders"]
visiblePosNav([all 4 perms]) → ["Dashboard", "Registers", "Sessions", "Orders"]
PosShell: visiblePosNav(perms).length > 1 ? master-detail grid + rail
: header + content, no rail
✓ visiblePosNav pure-function probe ran clean ✓ visiblePosNav — a pure cashier (pos.view only) sees just Dashboard ✓ visiblePosNav — each manager permission unlocks exactly its rail item ✓ Registers admin relocated — /pos/registers is the real page ✓ /settings/registers is now a redirect stub; old client file removed ✓ Settings nav drops the Registers row; revalidatePath retargeted ✓ PosNav is permission-aware — requires codes + visiblePosNav export + permissions prop ✓ PosShell takes permissions + drops the rail when only Dashboard is visible ✓ pos/layout.tsx resolves + passes the user's permissions to PosShell visiblePosNav is a pure exported function — unit-tested directly. The relocation is verified structurally against the source tree (the moved client exists at its new home, the old one is gone, /settings/registers is a redirect, the Settings nav row is dropped, revalidatePath retargeted). Gate 2 does the behavioural pass on prod — rail filtering per role, the redirect stub, registers admin working at its new home.
moved (git mv, history preserved):
app/(authed)/settings/registers/registers-client.tsx
→ app/(authed)/pos/registers/registers-client.tsx
new content:
app/(authed)/pos/registers/page.tsx re-export → the real registers page
app/(authed)/settings/registers/page.tsx real page → redirect stub
edited:
app/(authed)/settings/_components/settings-nav.tsx dropped the Registers row
lib/actions/registers.ts revalidatePath ×4 → /pos/registers
app/(authed)/pos/_components/pos-nav.tsx + requires gates + visiblePosNav export
app/(authed)/pos/_components/pos-shell.tsx + permissions prop, rail-hide logic
app/(authed)/pos/layout.tsx resolves + passes permissions
Gate 2 (prod, get-coffee + lumiere): • /cafe/pos rail renders the right items for the owner (all 4) • /cafe/settings/registers redirects to /cafe/pos/registers • the registers admin works at /cafe/pos/registers (shop picker, etc.) • the Settings nav no longer lists Registers • 59/59 regression sweep This closes the POS Section Rework arc — Slices 1–5 all shipped: 1 ✅ session-scoped data layer 2 ✅ landing card UI rework 3 ✅ master-detail submenu shell 4 ✅ Sessions history page 5 ✅ Registers relocation + permission gating ← this Out of scope (noted, not done): the top-level "Orders" sidebar item stays in both places (memo Open Q3 default = "keep both for now").