← All tasks

Slice I — Unlock rework LOCAL · GATE 1

Tracked followup from Slice C (PIN unlock latency was measured at 4403ms on prod, dominated by router.refresh() re-running ~20 DAOs in register/[configId]/page.tsx). Slice I returns the OPEN-phase data inline from the unlock action; the shell transitions state locally instead of refetching the page.

7/7 local checks · tsc clean · DAO probe Pro=110ms / Starter=13ms on local Docker. The shared loader fans out all 9-12 fetches in a single Promise.allSettled, so total wall time = max of the round-trips instead of the sum. Per-fetch try/catch fallback preserved via an unwrap() helper — a single slow/failing DAO never blocks the others.

Architecture

BEFORE AFTER ────── ───── PIN entered PIN entered │ │ ▼ ▼ unlockRegisterAction unlockRegisterAction │ verify PIN │ verify PIN │ write cookie │ write cookie │ return { ok } │ IN PARALLEL with audit log: │ │ load all OPEN-phase data ▼ │ return { ok, openData } client: router.refresh() │ │ ▼ │ server re-renders page.tsx client: setOverride({ │ ~20 DAOs serial phase: 'open', │ ~3-4 s wall time cashier, │ bundle: openData ▼ }) RENDER OPEN │ ▼ RENDER OPEN (no page render; no refetch) Measured 4403 ms prod baseline Target < 1500 ms prod

What changed

New lib/server/open_phase_loader.ts — two loaders (loadProOpenPhaseBundle, loadStarterOpenPhaseBundle) that fan out the OPEN-phase fetches in a single Promise.allSettled. Per-fetch unwrap() + warn-on-reject preserves the "nice-to-have" fallbacks the old sequential try/catch blocks had.

lib/actions/register.ts — three actions (unlockRegisterAction, unlockRegisterAsUserAction, openShiftAction) now return an optional openData field, tier- discriminated ({ tier: "pro", ... } | { tier: "starter", ... }). A shared internal loadBundleForSession() helper detects tier via getCurrentTenant() and dispatches to the right loader. The bundle load runs in parallel with the audit-log INSERT so the existing critical-path cost stays roughly the same.

lock-screens.tsxUnlockResult + OpenShiftResult types exported. LockScreen.onUnlocked and PreShiftScreen.onOpened receive the full action result so the parent shell can transition state locally.

Both lockable shells ((pos-fullscreen)/.../lockable-shell.tsx Pro + starter-lockable-shell.tsx) became client state machines keyed on an override ({phase, cashier, bundle}). On unlock action: setOverride and render the new phase directly. On lock / close-shift: setOverride(null) + router.refresh() so the server-driven LOCKED render takes over cleanly. First page load (cookie already present, mid-shift reload) — override is null, falls back to the server-rendered props.phase path. No regression.

What stays unchanged

Risks flagged for Gate 2

Critical-path change. Lockable shell is used by every cashier login. Mitigation: kept the old action behavior intact (extending vs replacing); defensive router.refresh() fallback in the shell if the action returns an unexpected shape.

Phase state machine. Moving from server-derived phase to client state introduces edge cases (e.g. session closed externally mid-shift). Today's flow has the same issue (router.refresh only on user action). Won't regress.

Lock action override-clear race. Both onLock and close-shift onDone explicitly call onAfterCloseShift?.() (the setOverride(null) hook from the shell) BEFORE router.refresh() so the next render lands on server-driven locked state without a stale override flicker.

Checks — 7/7

Raw: 01-loader-probe.json · result.json

Files (1 new + 4 modified, no migration, no new dep)

New: lib/server/open_phase_loader.ts
Modified: lib/actions/register.ts · app/(authed)/pos/_components/lock-screens.tsx · app/(pos-fullscreen)/pos/register/[configId]/lockable-shell.tsx · app/(authed)/pos/_components/starter-lockable-shell.tsx

Gate 2 plan

Push → CF deploy → time the unlock → workspace-visible latency on get-coffee Pro. Pre-Slice-I baseline = 4403ms. Target < 1500ms. Plus regression sweep (51 checks).