Test-Run #3 doc sub-item 3l (yellow-flagged "deal with it later"). PIN-unlocked
cashier could previously escape the POS by typing /cafe/dashboard in the URL bar
or clicking sidebar links — the middleware only signature-checked the JWT, never
the active-cashier cookie. Now: the cashier is hard-locked to the POS surfaces
(Register, Picker, Orders). Owner-operators step into the backend via a 30-min
manager-PIN override (reuses tenant_users.pos_pin_hash via
verifyPosPinForUser) — designed for Cambodian SME cafes where the owner
routinely works the register and needs quick stock checks without closing shift.
Decisions locked without Narong per his original "deal with it later" tag.
/cafe/pos/register/<configId> when nix_active_cashier cookie is present and no valid nix_manager_override cookie. /api/* routes are excluded (they self-gate via readActiveCashier() where needed)./pos/register/[configId] + sub-routes, /pos exact (landing/picker), /pos/orders + sub-routes (cashier needs order list for refunds/lookups during shift).requestManagerOverrideAction gates on nix_cafe.pos.session_open permission + verifies POS PIN via existing verifyPosPinForUser. New nix_manager_override JWT cookie (HttpOnly, SameSite=Lax, path=/cafe, 30 min TTL, signed with same JWT_SECRET as nix_session / nix_active_cashier). Audit log pos.manager_override.granted / .cleared via Next after().filterNavForPermissions(perms, { lockdownActive }) narrows the nav to POS group (Registers + Orders only) when lockdown is active; admin sections stripped. Mirrors the middleware allow-list patterns exactly (/^\/pos$/ exact + /^\/pos\/orders(\/|$)/) to avoid /pos/sessions matching the /pos prefix.PosMoreMenu (⋮) — visible only when caller has POS PIN set. Threaded through 3 PosMoreMenu callsites: Pro fullscreen lockable-shell.tsx, Starter in-app starter-lockable-shell.tsx, Starter starter-top-bar.tsx. Starter starter-register-page.tsx gains the userHasPosPin probe + permission check (mirrors the Pro page.tsx pattern)./dashboard on success.expiresAt from JWT payload, client-side countdown ticker (15s interval), "Return to POS" button calls clearManagerOverrideAction + redirects to register cookie's fromPosConfigId.| ✓ | Pre-flight: lumiere owner has POS PIN (seeded "1234" since absent) |
| ✓ | SSO login on lumiere |
| ✓ | Baseline: /cafe/dashboard renders normally before unlock (no active-cashier cookie → no lockdown) |
| ✓ | Resolve active register's pos_config int id (1000000024 Test 1 / Lumière BKK1) |
| ✓ | PIN-unlock as Manager via lock screen → active-cashier cookie set (actorKind="user") |
| ✓ | Lockdown URL-bar test: page.goto(/cafe/dashboard) → middleware 302s back to /cafe/pos/register/1000000024 ← THE load-bearing test |
| ✓ | Lockdown: /cafe/settings URL bar → same bounce |
| ✓ | Sidebar on /cafe/pos shows POS-only items (admin labels Dashboard / Settings / Products / Customers / Inventory / Team / Reports / Finance / Configurations / Telegram / Subscription all absent) |
| ✓ | Open ⋮ menu → Manager Mode dialog opens (testid manager-override-dialog + PIN input visible) |
| ✓ | Wrong PIN ("0000") → error surface displays "Invalid PIN" + cookie NOT set |
| ✓ | Correct PIN ("1234") → redirects to /cafe/dashboard → page renders → amber banner present with countdown (29-30 min left) |
| ✓ | Override active: /cafe/settings loads normally + banner still visible |
| ✓ | Return to POS → cookie cleared → redirected to register → banner gone |
| ✓ | After Return to POS, lockdown is back: /cafe/dashboard bounces to register again |
| ✓ | Close shift via SQL (cleanup) — test session marked closed |
| ✓ | No 5xx during the suite |
/cafe/cafe/dashboard?_rsc=.... Root cause: router.push("/cafe/dashboard") in the dialog and banner. Next 15 with basePath: "/cafe" automatically prepends; passing the full path produces /cafe/cafe/.... Fixed in commit 2da5394 — paths reduced to /dashboard and /pos/register/X.CashierPickerScreen → PinKeypadScreen via local setTarget) was racing Playwright on a freshly cold Worker. Added page.waitForTimeout(800) before click (hydration settle) + 500ms after click before the next selector wait, per feedback_playwright_useState_dom_swap. Same pattern as the Slice O cold-Worker fix.commerce.pos_configs but the table lives at cafe.pos_configs. Migration shift was earlier in the multi-register arc (R10/Bundle 3); the test query was stale. One-line fix.prodProbe (temp .mjs with knex parametrized) for all cleanup queries per project_prodsql_escape_limits.| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
| test-phase2-cafe-multishop-prod.mjs (solo) | 6/6 |
phase2-cafe-multishop ran solo (the bash parallel sub-shell dropped the
backgrounded job on this run) — first-attempt green. 19th validation of
feedback_phase2_cafe_multishop_solo_retry.