2026-05-14 Gate 1 local. Follow-up after the arc: per user feedback on the shipped Slice 3, the POS submenu should live in the dark sidebar, not as an in-page rail. This replaces the flat "Register" + "Orders" OPERATIONS rows with one expandable "Point of Sale" group, deletes the in-page rail, and re-adds the page header the old shell owned. No migration.
Register (/pos) + Orders (/orders) OPERATIONS rows with one group: Registers / Sessions / Orders / Settings. Auto-expands on any /pos route; the most-specific child wins the active highlight; a chevron toggles it when you're elsewhere. Same component drives the desktop rail and the mobile drawer.nix_cafe.pos.view; each child is filtered by its own gate (Sessions → reports.view, Orders → orders.view, Settings → settings.view). A pure cashier sees just the "Registers" child; a user without pos.view doesn't see the group at all.pos-nav.tsx + pos-shell.tsx deleted; pos/layout.tsx simplified to a plain requirePermission passthrough. With no rail, the ?config=N register terminal needs no special-casing — its -m-8 escapes the layout padding as it did pre-Slice-3.buildNav + filterNavForPermissions + the nav types moved to a new components/layout/nav-model.ts with no React-component / server-action import chain — so the permission filtering is unit-testable directly (and it's cleaner: data model separate from view).✓ filterNavForPermissions probe ran clean ✓ Owner sees the full 'Point of Sale' group (Registers/Sessions/Orders/Settings) ✓ Permission filtering — cashier sees only Registers; no pos.view → no group at all ✓ Old flat 'Register' + 'Orders' OPERATIONS rows are gone ✓ nav-model.ts — NavGroup model + the Point of Sale group, no server import chain ✓ sidebar.tsx — render components + imports the model from nav-model.ts ✓ In-page rail removed — pos-shell + pos-nav deleted, pos/layout.tsx is a passthrough ✓ Landing re-owns its 'Point of Sale' header (the shell that held it is gone) filterNavForPermissions is a pure exported function (nav-model.ts) — unit-tested directly: owner → all 4 children; cashier (pos.view only) → just Registers; no pos.view → no group; pos.view+reports.view → Registers + Sessions. The expandable group's render (auto-expand, active highlight, mobile drawer) is verified structurally + by typecheck; Gate 2 does the visual + behavioural pass on prod.
new:
components/layout/nav-model.ts buildNav + filterNavForPermissions + nav types
deleted:
app/(authed)/pos/_components/pos-nav.tsx the in-page rail
app/(authed)/pos/_components/pos-shell.tsx the in-page master-detail wrapper
edited:
components/layout/sidebar.tsx + expandable NavGroup support (NavGroupItem),
imports the model from nav-model.ts
app/(authed)/pos/layout.tsx PosShell/Suspense → plain requirePermission passthrough
app/(authed)/pos/pos-landing.tsx re-added the "Point of Sale" header
app/(authed)/pos/page.tsx re-added the header to the empty state
app/(authed)/pos/loading.tsx re-added the header skeleton
1 commit to nix-cafe (8 files, no migration) → push → CF deploy.
Prod test on get-coffee + lumiere:
• the sidebar shows the "Point of Sale" group; it expands; the 4
children navigate to /pos, /pos/registers, /pos/sessions, /pos/orders
• the old flat "Register" / "Orders" rows are gone
• /cafe/pos renders the landing with its own header, no in-page rail
• /cafe/pos?config=N register terminal still renders full-bleed
• Playwright screenshots both tenants
• 59/59 regression sweep (r1-2-landing + phase1 route sweep exercise
the sidebar on every page)
Note: this reverses Slice 3's in-page-rail decision — that was the
user-approved choice at scoping time, changed after seeing it live.