2026-05-14 Gate 1 local. Fourth slice of the POS Section Rework arc (Narong spec item 9.2): the Sessions history page. Replaces the Slice 3 placeholder at /cafe/pos/sessions with a real paginated, register-filterable table — Session · Point of Sale · Opened By · Opened/Closed · Starting/Ending balance · Theoretical Closing · Status. No migration.
listSessions DAO in cafe_sessions.ts — generalizes listSessionsForDay: no date bound, limit/offset pagination with a total count, an optional configIds register filter, the register display name, and a derived theoreticalClosing per row. Newest sessions first, db.transaction-wrapped for Hyperdrive freshness.aggregateSessionMetricsBySessionIds — the per-session money math (paid-order gross + count, cash/non-cash payment splits, cash in/out) was inline in fetchLatestSessionSummaries; it's now a shared helper so the landing cards (Slice 1) and the Sessions page compute theoreticalClosing from one formula path. fetchLatestSessionSummaries rewired to use it — behaviour unchanged (regression-checked)./pos/sessions gated on nix_cafe.reports.view (manager-level, matching the reports/daily precedent), scoped to the user's accessible shops, with a register filter dropdown built from every register across those shops.sessions-client.tsx: desktop 9-column table + mobile cards (the established StarterOrdersClient convention), a register <select> filter, prev/next pagination ("Showing X–Y of N"), and a Status badge (Open green / Closed grey / Reconciling amber).| Session | Point of Sale | Opened By | Opened | Closed | Starting | Ending | Theoretical Closing | Status |
|---|---|---|---|---|---|---|---|---|
| #00063 | Register 1 | Narong | May 13 · 6:19 PM | May 14 · 9:02 AM | $142.25 | $142.25 | $142.25 | Closed |
| 4f2e05fe | Register 2 | Sok | May 14 · 7:10 AM | — | $200.00 | — | $665.38 | Open |
Newest sessions first. "Point of Sale" = register name. The register <select> filter scopes to one register; prev/next paginate (50/page). Theoretical Closing = beginning_cash + cash sales + cash in − cash out, the same formula the landing cards use (now a shared helper).
✓ listSessions probe ran clean against local nix-db ✓ listSessions returns a page of rows + an accurate total ✓ SessionListRow carries every column the table needs ✓ Pagination — page 1 and page 2 are disjoint, newest-first ✓ Register filter — every returned session belongs to the filtered register ✓ theoreticalClosing matches an independent recompute from the tables ✓ Regression — fetchLatestSessionSummaries still works after the helper extraction ✓ sessions-client.tsx — desktop table + mobile cards, all 9 columns, filter + pagination ✓ sessions page — reports.view gated, calls listSessions, gathers the register filter Probe ran against local lumiere-coffee (5 registers, 364 sessions): • listSessions total = 364, exactly matching a raw COUNT • page 1 = 50 rows, page 2 = 50 rows, 0 overlap, newest-first • register filter → 91 sessions for that register, 0 belonging elsewhere • theoreticalClosing recompute: $665.38 actual == $665.38 expected • fetchLatestSessionSummaries still returns 4 summaries post-refactor SessionsClient uses useRouter (the filter + pagination navigate), so it can't be renderToString'd standalone — its structure is asserted against the source. Gate 2 does the real visual + behavioural pass on prod.
nix-cafe:
lib/db/cafe_sessions.ts + aggregateSessionMetricsBySessionIds
+ listSessions / SessionListRow / SessionListResult
fetchLatestSessionSummaries rewired to the helper
app/(authed)/pos/sessions/page.tsx placeholder → real page (listSessions + filter)
app/(authed)/pos/sessions/sessions-client.tsx new — table + cards + filter + pagination
Gate 2 (prod, get-coffee + lumiere): • /cafe/pos/sessions renders the table inside the POS shell • register filter narrows the list; pagination prev/next works • theoretical-closing column matches a prod recompute • Playwright screenshots both tenants • 59/59 regression sweep Arc: Slice 1 ✅ · Slice 2 ✅ · Slice 3 ✅ · Slice 4 (this) · Slice 5 — Registers/Orders relocation cleanup + the section's permission gating (still: /pos/sessions needs reports.view on top of the layout's pos.view).