Visual QA screenshots for completed tasks.
2026-05-08 on prod. Open-shift form's Beginning cash input now pre-fills with the previous close's ending_cash on the same register (still editable). Eliminates "type yesterday's number" friction on shift handover. New DAO getLastClosedSessionEndingCash in lib/db/cafe_sessions.ts (Hyperdrive-cache wrapped, filters ending_cash IS NOT NULL so a force-close doesn't shadow a real close), threaded through both Pro lockable + Starter lockable + Starter in-app shift forms. PreShift helper text flips between "Carried over from the last shift's close ($X.XX). Edit if today's drawer count differs." and the existing "Suggested if unsure: $0.00…" copy depending on whether a prior close exists. Hotfix in this Gate 2 cycle: initial DAO ordered by closed_at DESC regardless of NULL — config 7 on get-coffee had a force-close 18min after the $117 close, leaving NULL ending_cash and shadowing the real value. Local DB happened to have only non-null closes for the test register, so the bug only surfaced in prod. Fix: isNotNull(ending_cash) in WHERE. 2 cafe commits (bb0fb8c + 94067a5, +107/-7 net across 7 files). 9/9 visual prod test green (end-to-end SSO + PIN-unlock + input-value assertion + DB cleanup) on get-coffee config 7 with $117.00 carry-over confirmed. 37/37 effective regression sweep (cash-carryover 9/9 + launchpad-fix 8/8 + r7 14/14 [solo retry; cold-worker on first dashboard hit] + multishop 6/6). Lesson: prod data shapes are richer than local seeds — visual prod tests with real fixtures catch DAO edge cases that synthetic local data misses.
2026-05-08 on prod. Carryforward from launchpad-fix-prod follow-ups. app/(authed)/dashboard/page.tsx and app/(authed)/reports/page.tsx each rendered their own <ShopSelector> redundantly with the layout-level one (which already gates on shops.length > 1 || isStarter). On Pro+1-shop tenants (demo) the page-level selector showed even with no second shop to switch to — broke test-phase2-cafe-multishop-prod at 5/6. On Pro+>1-shops or Starter the user saw two stacked selectors. Fix: remove the page-level renders; the layout is the single source of truth (still gated). 1 cafe commit (b1782bd, +4/-20 across 2 files). 28/28 prod green: phase2-cafe-multishop now 6/6 (↑5/6 — canonical proof), r7 14/14 (dashboard h1 + ranking-of-pos + Live drawer all still render), launchpad-fix 8/8 (prior fix sanity). Run sequentially per new R8.2 workflow rule.
2026-05-08 on prod. Open issue from Narong end-of-day 2026-05-08 SHIPPED. "The SSO launchpad doesn't work" → recon showed the launchpad SPA renders fine (2 cards, 200 on /me on both get-coffee and lumiere-coffee). Real bug: clicking the NIX Outdoor Sales tile bounced back to the launchpad. Tenant owners created via Cpanel / seed skip both invite paths that create the outdoor_employees bridge row, so Outdoor's SSO middleware fails the lookup, returns 401, and the SPA bounces to Commerce login → already authed → back to / (launchpad). Not R8 fallout — pre-existing bug Narong only just noticed. Fix: lazy-create the bridge in tryNixSessionCookie (gated to active outdoor_sales sub + owner role OR explicit nix_user_product_roles; race-safe via ON CONFLICT (tenant_id, email) DO UPDATE SET tenant_user_id). 1 backend commit (69b2fd2). 8/8 prod test green; 48/49 with regression sweep — 14/14 R7 + 4/4 R8 + 11/11 phase1 + 6/6 phase2-sso-outdoor + 5/6 phase2-cafe-multishop (the one fail is a PRE-EXISTING R7 dashboard regression: app/(authed)/dashboard/page.tsx renders ShopSelector unconditionally without the shops > 1 || isStarter gate the layout has — surface for next session). NEW LESSON: R8.2 single-session-per-user enforcement now means parallel prod tests sharing creds (e.g. narongix@gmail.com) mutually kick each other out — must run sequentially. Prod DB confirmed: narongix oe_id=8, lumiere owner oe_id=9, demo owner oe_id=10 all auto-bridged on first /auth/me probe.
2026-05-08 on prod. Narong's 2026-05-07 auth trio: R8.1 per-tenant token scope (the shared nix_session cookie was leaking across tenant subdomains; auth() now reads x-nix-tenant-host set by nix-router-worker and rejects mismatch), R8.2 single-session-per-user enforcement on web (backend mints fresh UUID on login + writes to tenant_users.active_session_token; JWT carries it as sid; cafe auth() validates), R8.3 view/reset PIN for cashiers (recon found this already shipped during NIX-OS-67 R1.1 — no code change needed; visual proof captured in the gallery), R8.4 single-session-per-PIN-identity (same shape as R8.2 but on commerce.pin_identities against the lockable register cookie). Mid-arc bug: initial R8.1 used x-forwarded-host, which Cloudflare's edge fetch clobbers to the upstream pages.dev hostname when nix-router proxies. Diagnosed via a temp /api/debug-r8 dump endpoint, switched to a custom x-nix-tenant-host header that CF leaves alone. 2 backend commits (3b6233d migrations + JWT sid) + 4 cafe commits (50164a9 main, a6509c6 router-host fix, 4c4be5a debug cleanup, 2e1f136 gitignore) + 1 router-worker deploy via wrangler. 4/4 R8 prod green; 14/14 R7 + 10/10 phase4 maintained throughout. NULL-default safe rollout — existing sessions keep working until first fresh login post-deploy.
2026-05-08 on prod. Multi-batch session shipping the R7 arc (NIX-OS-86 dashboard rework + net-new manager sidebar per spec §10.1) PLUS three R7-followup commits PLUS three carried-forward NIX-OS-89 follow-ups. R7.1-7.6 main arc: bankToday plumbing → /dashboard recent-orders trim → Ranking of POS + Top Products cards on /dashboard → /reports + /reports/daily parity → manager sidebar drawer (always-visible "Live" pulse-dot trigger in top strip gated on nix_cafe.dashboard.view; 440px right drawer with shop-grouped cards showing state + Cash + Bank + Sold; click → register in new tab; 30s polling) — main commit f616460 (+937/-76 across 14 files). R7-followups: 490db70 refactor consolidates pos/page.tsx inline buildLanding into the shared lib/db/manager_live module (-133 LOC); fa3e88a extends Odoo-master bankToday via two extra RPCs (pos.payment + pos.payment.method) + adds drawer refresh button + Page Visibility API (skip polling when document.hidden, refetch on visibility regain) + footer "Updated Xs ago". NIX-OS-89 carried-forward (same session): R6.7 cafe.product_categories Odoo mirror with backfill script (6 categories mirrored + 6 products linked to category UUIDs on get-coffee), R6.8 product image data URLs (image_url column populated from Odoo image_128 base64 — skipped R2 since current scale is fine), R6.3c cafe.orders.cafe_customer_id UUID FK + same-migration backfill (linked 2 prod rows). 14/14 R7 prod green + 52/52 with regression sweep (nix-os-87 11/11, receipt-fixes 6/6, phase1 11/11, phase4 10/10 solo). All commits across nix-cafe + nix-outdoor-sales-backend.
2026-05-08 on prod. Arc complete (8 of 8 sub-phases shipped 2026-05-07 + 2026-05-08). get-coffee Pro reads now hit zero Odoo round-trips on the critical path — products / customers / payment methods / pos.configs / POS landing summaries all served from cafe.* tables. Three per-tenant master flags on cafe.tenant_config (products_master, customers_master, pos_master) gate read swaps; default 'odoo' so the migration is no-op for everyone. R6.1 reverted the wrong-direction R5.7 reverse-sync; R6.2a/b mirrored cafe.products + flipped reads; R6.3a/b mirrored cafe.customers + flipped /cafe/customers + picker GET; R6.4 added Cafe→Odoo product + customer push pipeline (sync metadata on both tables, worker drains creates/updates per cafe-master tenant, picker POST writes cafe.customers first then pushes synchronously, Pro /products admin in cafe-master mode mounts the editable client); R6.5 added cafe.pos_configs + Odoo cols on cafe.payment_methods + pos_master flag covering both, with read-flip on 5 fetchPosConfigs sites + 2 fetchPaymentMethods sites; R6.6 replaced fetchSessionSummariesForConfigs with a cafe-side aggregator over cafe.sessions + cafe.orders. 14 commits across nix-cafe + nix-outdoor-sales-backend; 38/38 regression at every Gate 2; 10/10 final walkthrough on get-coffee with all 3 master flags = 'cafe'. Pattern lessons firmed up across the arc: shape-fakeable mirror DAOs return the existing remote-system shape so consumers don't branch past the import boundary; per-tenant flag with default 'odoo' is the safest rollout vehicle; discriminator filter MUST be in the read DAO from day 1 (R6.2a + R6.5 shipped follow-up fixes for tests cross-flipping tier on the same tenant); phase4-prod first-parallel-hit flake reproduces every time and resolves on solo retry.
2026-05-07. First slice of the NIX-OS-89 Cafe-as-master arc. R5.7 added an Odoo→Cafe reverse-sync worker (Pro tenants pulled operator edits from Odoo backoffice into cafe.orders every 15 min). Per Narong's 2026-05-07 architectural confirmation, that direction is now wrong — Cafe is master, Odoo receives. R6.1 removes the reverse sync in full: cron disabled (cron-trigger Worker redeployed via manual wrangler deploy), route + DAO deleted (~410 LOC), watermark column dropped from cafe.tenant_config (knex migration + migrate.js entry), and the close-shift Telegram block (the only remaining caller of the deleted fetchSessionOrders) swapped to read from cafe.orders directly via listOrdersForSession. Side-benefit fix: the old Odoo Telegram math counted refunds twice; the cafe-side version uses explicit state='refunded' rows so it's honest. Net 858 LOC removed across 8 cafe files. Forward sync (R5.5, every minute) untouched and verified working via phase4-prod ring+pay flow. 38/38 prod sweep green. Commits 9c6266d (backend) + f4790b6 (cafe).
2026-05-07. Phase 5 ship — Narong's "Settings content should load on the right instead of jumping to a different page" complaint. Lifted the nav rail + page-level "Settings" h1 into a new Next.js shared layout (app/(authed)/settings/layout.tsx); clicking any built sub-page (Payment Methods, Payment Diff Reasons, Customer Display) now swaps only the right pane — the rail stays mounted across navigations. Active state derives from usePathname() in a new client SettingsNav component (with /cafe basePath stripped so it works in the deployed Worker). Stripped redundant per-page h1+subtitle chrome from each sub-page; demoted to h2 text-lg so the layout's h1 is the visual root. Permission + tenant gating consolidated into the layout — sub-pages no longer re-check requirePermission("nix_cafe.settings.view") or re-fetch the tenant. 11/11 prod test green via DOM-identity assertion (rail tagged with data-test-rail-id on first visit, re-asserted after every nav). 38/38 with regression sweep. Commit 7d24500; nix-cafe only.
2026-05-07 polish round before Phase 5. Two surgical fixes to the thermal receipt: (1) Tax ID line removed (taxId? field stays on the type for forward compat; just not rendered). (2) Print iframe and in-app preview now match pixel-for-pixel — root cause was the print path captured innerHTML which carried Tailwind class names (text-muted, font-extrabold) and styled-jsx hashed classes that didn't resolve inside the iframe document. Fix: refactored <KhmerReceipt> to use inline styles only with design tokens copied from globals.css; trimmed openPrintWindow's iframe stylesheet to just @page + font fetch (the receipt root owns its own 80mm width + 3mm padding). 6/6 prod test green via DOM-equivalence assertion (preview rendered HTML and print iframe srcdoc both inspected). 27/27 with non-stale regression. Manual QA on the actual thermal printer recommended to confirm 80mm fill + Khmer glyph rendering. Commit 35a0dbb; nix-cafe only, no backend, no DB migration.
2026-05-08 Gate 1. Five-task bundle from Narong's 2026-05-05 demo: NIX-OS-78 (Cash Received now defaults empty so cashier must type the actual amount; Validate stays disabled with "Cash short — add more"), NIX-OS-79 (Close Shift dialog gains an "Other…" inline option that creates a permanent reason via a new cashier-callable action gated on session_close instead of settings.edit; both Pro and Starter dialogs get the picker), NIX-OS-80 (Bank Count input removed; Expected Cash switched from gross sales to cash-only via existing sumCashPaymentsForSession DAO threaded through page → LockableShell as cashSalesTotal; same fix to CashMovementDialog "Drawer so far" + Telegram session-closed math; endingBank dropped from closeShiftAction params), post-order summary popup (Starter switched from auto-print banner to the same SuccessModal Pro uses; modal gained items+total+paid+change card above Print), searchable category dropdown (native <select> → popover combobox in product-grid.tsx with search input + filterable list, Esc/outside-click close, Enter picks first match). 17/17 local checks green; typecheck clean. No DB migration; nix-cafe only.
2026-05-07 on prod. Reverses the 2026-05-02 hide-call: Pro-only features on Cafe Starter (multi-shop ShopSelector, Orders / Cash / Kitchen buttons in the register top bar, Kitchen Display route) now render visibly but disabled with an "Available on Pro" tooltip + lock-icon corner badge, instead of being removed from the UI. Drives upsell — Starter users see what they'd unlock by upgrading. New <LockedForTier> primitive + tierLockProps() helper in nix-cafe/components/locked-for-tier.tsx. Backend cap on POST /admin/tenants/:id/shops blocks Starter at ≥1 active shop with 403. Lumière re-seeded to 1 active shop (BKK1, the flagship) for the Starter demo; the other 3 shops + 1 default are soft-disabled with their order history preserved. Kitchen Display route on Starter now renders a friendly upsell page instead of notFound() (defense-in-depth for stale bookmarks). Deferred: POS landing register-list unification (Starter still routes to StarterRegisterPage, skipping the Pro PosLanding) — depends on NIX-OS-89 architectural shift confirmed by Narong same day. 3 commits: 60ccfb8 backend cap, dee4afb cafe primitive + ShopSelector + lockable shell + Kitchen route, 2acc638 in-app StarterTopBar follow-up.
2026-05-04 on prod. Closes the deferred half of R5. Every-15-min reverse-sync worker pulls operator-side edits from Odoo backoffice into cafe.orders so the Cafe ledger doesn't drift away from the financial source of truth. Three delta types in scope: (a) operator created a refund counter-order in Odoo — flip origin's cafe row to refunded + stamp odoo_refund_synced_at so forward sync doesn't try to push our own. (b) Operator marked an order cancel in Odoo — cafe row to refunded with reason 'Cancelled in Odoo'. (c) Customer attached/changed — update customer_name + odoo_partner_id. Out of scope (deliberately): line edits, total/payment changes — soft-warning logged on amount drift, never overwrites. Per-tenant odoo_reverse_sync_watermark on cafe.tenant_config; advances to MAX(write_date) of the batch; idempotent on re-run. Caught one real drift on first run — Odoo order #94 (rang 2026-04-18) had been cancelled in backoffice but cafe.orders said paid. R5.7 flipped it to refunded with the historical timestamp. 7/7 in-process Gate 2 steps green.
2026-05-04 on prod. Two follow-ups to the May-03 R5 ship. (1) Daily report now joins listSessionsForDay × aggregateNativeDailyReport.sessions on cafe.sessions UUID — each card shows opener / closer / beginning + ending cash / diff + reason / paid-order count + gross. Replaces the minimal "session-id, orders, gross" table that R5.3 had as a placeholder; mirrors what Pro had pre-R5.3. (2) New SyncDeadLetterBanner on /cafe/orders surfaces orders the every-minute sync worker has given up on after 5 retries. Hidden when queue is empty (no noise on the happy path). Click expands a table with order_number / time / total / last error / Retry button. Retry server action gated on nix_cafe.orders.refund — resets odoo_sync_retries=0 + clears odoo_next_attempt_at so the next cron firing picks the order back up. listDeadLetterOrders wraps in db.transaction to bypass Hyperdrive's 60s cache (else freshly-stuck rows wouldn't surface for a minute+). 7/7 prod walkthrough green. No schema changes — all polish landed against the columns from R5.1.
2026-05-03 on prod. Pro tier no longer hits Odoo on the cashier's critical path. Orders write to cafe.orders (same table as Starter); a cron-trigger Worker fires every minute and drains pending rows into Odoo as eventual consistency. Reads (/cafe/orders, dashboard, reports, in-POS Orders view) are NIX-native — no JSON-RPC round trips. Dashboard load went from 3–6s (Odoo) to single-digit ms (Hyperdrive). Driven by Narong's "PRO should just be an upgrade on Starter — should still work without Odoo API or connection." 3 migrations on backend (batches 6/7/8): odoo_order_id/odoo_partner_id/pos_config_id on cafe.orders, odoo_product_id on lines, odoo_payment_method_id on payments + 2 partial indexes. Pro adapter rewritten to POST /cafe/api/cafe/orders. Legacy /api/odoo/{order, order/refund, session, dashboard} routes deleted; orders detail page collapsed into the Starter modal preview. Sync worker (POST /cafe/api/cafe/cron/odoo-sync) with exponential backoff (1m→2m→4m→8m→16m), dead-letter at 5 retries, refund counter-orders. Backfill script pulled all 131 historical pos.orders for get-coffee — reconstructed 18 closed cafe.sessions; idempotent on re-run. End-to-end timing in observed runs: ringup → cafe.orders insert → cron fires → Odoo push → odoo_order_id stamped in 10–88s. 7/7 prod walkthrough checks green.
2026-05-02 on prod. Replaces the two divergent register UIs (register.tsx 1364 LOC, starter-register-client.tsx 1543 LOC) with a single <RegisterShell> driven by capability flags. Per-tier handler injection via _adapters/{pro,starter}-handlers.ts maps the shared OrderPayload to each backend (Odoo POS / cafe.orders) — shell never knows about Odoo. Pro inherits Starter's polish (per-line discount UI, image cards, Khmer search hook, category filter dropdown) for free. Starter joins the lockable PIN-gated fullscreen mode (option b) — same LOCKED→PRESHIFT→OPEN state machine, simplified chrome (no Orders/Cash movements/Kitchen since those stay Pro-only). LockScreen + PreShiftScreen extracted into _components/lock-screens.tsx for reuse. fetchProducts upgraded to pull image_128 + pos_categ_ids so the Pro grid finally has visuals + category filter. Multi-shop nav (<ShopSelector>) hidden on Starter; Kitchen Display 404s for Starter at the route level (defense in depth). Net code change: −609 LOC overall (4342 LOC of dup deleted, 2866 LOC of shared shell + adapters + mounts added). 13/13 prod walkthrough checks green.
2026-04-29 on prod. R3 fully closed across 6 sub-phases + 4 same-day follow-ups. Identity migration is the headline: auth source-of-truth moved to tenant_users (R3.6β), JWT now dual-keys legacy userId + tenantUserId for gentle rollover, /me PATCH writes tenant_users.password_hash directly (R3.6γ), and the legacy users table itself was renamed to outdoor_employees with all 13 source files rewritten + back-compat VIEW dropped (R3.6δ.1/3/4/5). Plus: Outdoor self-checkout UI removed (tenants subscribe via NIX sales now), standalone User Management dropped (Commerce /team is canonical CRUD with /users* redirect), real Outdoor RBAC roles seeded for Commerce Team modal (Owner/Manager/Salesperson per tenant + 32 permissions), tenantResolve middleware now prefers Origin/Referer headers over X-Tenant-Code (browser-native cross-origin tenant identification). Same-day follow-ups: defensive Odoo HTML-response handling, outdoor_employees bridge on Commerce invite, new test-cafe-pro-dashboard-prod.mjs that doesn't flip plan_code → exercises Pro/Odoo path (closes the CI gap the 04-29 Odoo outage exposed). 17 commits across 5 repos + 4 prod migrations. SSO 6/6 ✓ + R4.1 9/9 ✓ + Pro-path 2/2 ✓ verified after every prod migration.
2026-04-29 on prod. Final R4 sub-phase — closes Phase 5 (NIX Cpanel). Cafe's getTenant() now joins subscriptions(product_code='nix_cafe') to derive tier (cafe_starter/pro/master → starter/pro/enterprise); PayWay webhook stops writing the deprecated tenants.plan_code; seed-supabase-getcoffee.ts upserts both outdoor_sales + nix_cafe subscription rows for fresh DBs (closing the gap that bit us in R4.x and R4.3). New cafePlanController helper in test-utils.mjs centralized the capture/flip/restore pattern across 12 prod test files (bulk-migrated via 2-pass regex script). Migration drops tenants.plan_code column + tenant_limits table. 107/107 prod tests green BOTH before AND after migration applied — no remaining consumer of the dropped column. R4 done.
2026-04-29 on prod. Cpanel TenantDetailView Subscriptions card now shows a live usage bar per (tenant, product). Backend: GET /admin/tenants/:t/subscriptions/:productCode/usage branches Cafe→COUNT cafe.orders WHERE state='paid' AND date_trunc('month',created_at)=current month, Outdoor→COUNT activities WHERE activity_type='visit' this month, others→404. Limit from subscriptions.limits_json (0/missing→null/∞). New UsageBar.vue auto-fetches on subscription expand, renders green/amber/red on 70%/90% thresholds. Surfaced + fixed mid-Gate-2: get-coffee + demo had no nix_cafe subscription rows post-Supabase-migration (operator backfill via SQL); test cleanup used JSON-through-bash for SQL escape that failed (rewrote to SELECT-into-UPDATE referencing the plan's defaults). 8/8 new prod + 99/99 regression sweep green; 107/107 total.
2026-04-29 on prod. Admin-initiated tenant_user impersonation across 5 repos. Cpanel "Sign in as" → backend issues 30-min JWT with imp claim referencing new commerce.impersonation_sessions row, sets shared nix_session cookie on .nixtech.app, returns redirect → admin lands on tenant subdomain authenticated as target user. Sticky amber banner on every authed page in Cafe + Commerce + Outdoor (3 banner components, Next server component + 2 Vue) shows admin email + End button. Tenant-end and admin-revoke both mark row ended_at + ended_by; JWT validation joins on the row, so revoked sessions immediately stop authenticating. Admin's Cpanel session preserved (separate Bearer scope on admin.nixtech.app). Audit log writes started + ended events to tenant_audit_logs. New ActiveImpersonationsView in Cpanel lists live sessions with 30s auto-refresh + revoke. 9/9 new prod + 90/90 regression sweep green; 99/99 total.
2026-04-29 on prod. Two non-blocking follow-ups from R4.1 bundled together. (A) Idempotent admin_operators seed in nix-outdoor-sales-backend/migrate.js — any future fresh DB (Supabase Pro upgrade, dev clone, full re-migration) gets Cpanel access from minute zero, no more silent 24h lockout like the one R4.1 caught. Idempotency verified by running migrate.js against live prod Supabase before push: existing row untouched, "= admin_operators already populated (1 rows) — skipping seed". (B) is_active filter on the two remaining shop-read surfaces — kitchen display secret (saved URL stops authenticating when ops disables the shop) and createCashierAction shop validation (can't assign cashier to retired shop). Deliberately skipped pin_identities.listPinIdentities leftJoin — Team page wants the historical shop name for context. No new test (small surgical fixes); verified by R4.1 9/9 sanity + full regression sweep clean. 90/90 total green.
2026-04-29 on prod. First sub-phase of R4 (Cpanel finish). Drove the full multi-shop activation path on get-coffee — admin API → create 2nd shop "annex" → cashier-side ShopPicker dropdown verified visible (single-shop label gone) → switch shops via UI → Cpanel soft-disable → picker collapses back to label without 60s lag. Two real findings landed: (1) admin_operators table was empty after Supabase migration — Cpanel had been silently locked out for 24h+; restored one operator row out-of-band. (2) listAccessibleShops didn't filter on is_active AND wasn't wrapped in db.transaction() — soft-disabled shops still appeared in the picker, and even with the filter, Hyperdrive's ~60s SELECT cache made disables lag. Both fixed in lib/db/shops.ts. 9/9 new prod + 81/81 regression sweep green; 90/90 total.
2026-04-29 on prod. Two operator-feedback follow-ups bundled into one Gate cycle. (A) Success-banner Print fast-path: post-sale Print fires the print iframe directly, no modal hop — operators just rang up the order and don't need a second confirmation. /orders per-row Print still goes through the preview modal (older-order reprint sanity). (B) Daily report splits totalDiscount into totalLineDiscount + totalOrderDiscount in the DAO + UI — operators can see per-line comp/promo ($1) vs whole-ticket override ($0.30) separately. End-to-end seeded scenario verified DB ground truth on both discount_usd columns + iframe-no-modal on banner Print + split testids in the report. 5/5 new prod + 76/76 stable regression suite (receipt-preview rewritten /orders-only 6/6 + r2-followups3 7/7 + r2-followups2 11/11 + r2-followups 19/19 + r2-4b 12/12 + 70-2 10/10 + phase1 11/11) green; 81/81 total.
2026-04-28 on prod. Operator-requested in-shell receipt preview before print — new shared ReceiptPreviewModal renders KhmerReceipt with Cancel + Print + Esc-to-close + backdrop-click. Wired into Starter register success banner and /orders per-row Print. Bundled with the full Render→Supabase prod database migration after Render's free tier auto-suspended: 99 knex migrations + migrate.js + tenant seed (get-coffee + demo) + Cafe Worker secret + Hyperdrive update — all on session-mode pooler (port 5432; transaction mode 6543 broke Drizzle prepared statements). 8/8 prod + 70/70 stable regression suite green; 78/78 total.
2026-04-26 on prod. Two ergonomics polish items. Wired the R2 GC route to the existing nix-cafe-cron-trigger Worker (redeployed with second cron expression "30 18 * * *" — first fire tonight 01:30 Phnom Penh). Added a 10% / 20% / 50% / Comp quick-percent picker to the per-line discount editor — verified end-to-end on a $10 line: 10% → $9, 50% → $5, Comp → $0, X clear → $10. 7/7 prod + 63/63 stable regression suite (round 2 11/11 + round 1 19/19 + R2.4b 12/12 + 70.2 10/10 + phase1 11/11) green.
2026-04-26 on prod. Three small follow-ups bundled. R2 GC cron route (POST /cafe/api/cafe/cron/r2-gc with X-Cron-Secret + ?dry=1 + 500-key safety cap, auth boundary verified 401/401), per-line discount on cart (cafe.order_lines.discount_usd column + inline cart editor + Customer Display lineTotal field — verified end-to-end with $1 line discount + $0.30 order discount → $5.70 total + DB ground truth on both columns), and receipt printing via hidden iframe (no popup, verified by Print click → iframe in DOM + zero popup windows). 11/11 prod + 52/52 stable regression suite (round 1 19/19 + R2.4b 12/12 + 70.2 10/10 + phase1 11/11) green.
2026-04-26 on prod. Five Starter-tier follow-ups bundled into one Gate cycle. UploadButton in Starter products (uploads SVG → R2 URL → DB persist → public fetch round-trip), multi-shop register selector (single-shop tenant collapses to label), Customer Display BroadcastChannel sync (cashier rings → display flips idle → cart in real time), receipt printing (Print on success banner + per-row in /orders, GET /api/cafe/orders/:id), and discount + multi-payment splits in the rebuilt PayDialog (10% discount on $7 → $6.30 → split $4 cash + $2.30 card → DB has discount_usd=0.70). 19/19 prod + 33/33 stable regression suite (R2.4b 12/12 + 70.2 10/10 + phase1 11/11) green.
2026-04-24 on prod. Three follow-ons: (1) optimistic-UI fix for the cashier deactivate race in team/cashier-tab.tsx — Active/Inactive chip flips within 500ms on click with rollback on failure; the first prod run caught a real bug (handler read from server prop instead of optimisticActive on rapid clicks) and the fix landed same session; (2) AGENTS.md gains a "Local dev gotchas" section with 5 anchors; (3) r1-2 rewritten for popup contract (now 8/8 — was 7/9 for weeks) and r1-3 adds cashier-picker click (now 13/13 — was 6/13 for days). 8/8 prod + 15 stale regression checks turned green.
2026-04-24 on prod. Three small wins shipped together: (1) settings sidebar wires real Link components for Display Branding, Payment Methods, Diff Reasons (operators no longer have to type URLs); (2) Render/knex divergence — orphan create migration deleted, migrate.js gains an idempotent re-drop entry so cafe.user_pos_access can't permanently resurrect (verified clean on prod); (3) NIX Cash Chrome extension README marked DEPRECATED with a feature-mapping table to R1.7 / R1.8 / NIX-OS-70 native replacements. 8/8 prod + 101/101 stable regression suite green.
2026-04-24 on prod. Tenant-set branding for the secondary customer screen — logo URL, primary color, promo text, promo image URL. New /cafe/settings/display admin page with two-column layout (form + live PreviewIdle that updates as you type). Public Customer Display idle screen renders the saved logo, primary-color gradient, promo text card, and promo image. End-to-end click-through: type branding → save → server action persists → DB ground-truth match → public display renders the saved branding. Bad-hex rejection surfaces a clear error. 10/10 prod + 98/98 stable regression suite green.
2026-04-24 on prod. Second sub-phase of NIX-OS-69. Public Kitchen Display route at /cafe/kitchen/[shopId]?t={secret} with three columns (New / Preparing / Ready), 5s polling, optimistic Start/Ready/Served buttons. GET + POST API endpoints with secret + cross-shop scope guards (404 / 400 / 409 / 200). POS TopBar ChefHat pill is now a clickable button that opens the display in a new window. End-to-end click-through on prod: full state machine (Start → Preparing → Ready → Served) verified via real UI clicks + DB ground-truth assertions on every transition. 11/11 prod + 96/96 stable regression suite green.
2026-04-24 on prod. First sub-phase of NIX-OS-69. New cafe.kitchen_orders table + DAO with new→preparing→ready→served state machine, per-shop kitchen_display_secret on commerce.shops, POS order-validate hook enqueuing a denormalized payload, and POS TopBar ChefHat pill showing queue depth. End-to-end click-through: real Odoo order validated → kitchen_orders row lands with POSID-stamped order_name and non-empty payload → TopBar ticks from 0 to 1. 10/10 prod checks + 84/84 stable regression suite green. Kitchen Display route + status transitions ship in 69.2.
2026-04-23 on prod. session_closed + new_order events now fire to opted-in groups; daily cron upgraded to use fetchDailyReport (NIX-OS-72). Central notify dispatcher with audit log + HTML-safe formatters + fire-and-forget so Telegram outages never block the POS flow. 11/11 click-through + 77/77 regression = 88/88 prod tests green.
2026-04-23 on prod. Mid-shift cash drawer adjustments — tips, supplies, petty cash, float top-ups, bank runs. New cafe.cash_movements table with CHECK constraints + FK cascade. 💰 Cash button in POS TopBar opens two-column dialog: record form + running ledger + recent-movements list. Folded into Close Shift expected-cash math and Daily Sale Report per-session cards. 12/12 click-through + 65/65 regression = 77/77 prod tests green.
2026-04-23 on prod. New /cafe/reports/daily page — totals strip, tender breakdown, top 15 products, per-session cards (cashier, opened→closed, beginning/ending cash, tender split, diff + reason), date picker + prev/today/next nav, print-friendly CSS for A4 / 80mm thermal. 11/11 click-through + 54/54 regression = 65/65 prod tests green.
2026-04-23 on prod. Final R1 sub-phase. Read-only Payment Methods admin page at /cafe/settings/payment-methods + 6 USD ($1/$5/$10/$20/$50/$100) + 8 KHR (500→100,000) denomination buttons in the POS cash entry modal + ↻ Reset cash button. Replaces the retired NIX Cash Chrome extension with a fully native flow. 10/10 click-through + 44/44 regression = 54/54 prod tests green. Closes all 8 R1 sub-phases.
2026-04-23 on prod. Secondary customer-facing display synced via same-origin BroadcastChannel. Three states (idle / cart / paid) auto-rotating. Public route gated by per-session display_secret. 11/11 click-through + 33/33 regression = 44/44 prod tests green.
2026-04-22 on prod. Cash count + optional bank count + auto-computed diff + optional difference reason dropdown (admin CRUD at /cafe/settings/payment-diff-reasons). All closure fields land on cafe.sessions. 11/11 click-through + 33/33 regression = 44/44 prod tests green.
2026-04-22 on prod. POS{configId}-{seq} daily-reset order numbers (spec §8.8). Manually verified: POS06-0001 and POS06-0002 printed successfully, cafe.pos_sequences next_seq=3 confirms two atomic advances. Gate 2 was a firefight — seven Cafe-Worker/Hyperdrive gotchas fixed, including the 405 from missing /cafe basePath on client fetches (affecting 6 call sites). 42/42 regression green.
2026-04-22. Order numbers follow spec §8.8: POS{configId}-{seq}, zero-padded, reset at midnight in tenant's local timezone. Atomic advance via UPSERT+RETURNING survives concurrent cashiers. 3/3 Gate 1 checks (format, advance, daily reset, 5-way concurrency). Prod click-through at Gate 2.
2026-04-22 on prod. Park button + parked-orders strip in cart panel. Carts persist to cafe.draft_orders (JSONB, FK CASCADE). Survives tab crash / device handover. 9/9 R1.4 + 33/33 regression = 42/42 prod tests green.
2026-04-22. Cashier can park a cart and start a new one in parallel. Parked carts persist to cafe.draft_orders (JSONB payload, FK CASCADE to cafe.sessions). Survives tab crash / browser restart. New Park button + parked-orders strip in the cart panel. 3/3 Gate 1 checks. Prod click-through at Gate 2.
2026-04-22 on prod. Four QA gaps closed: PIN-collision; Manager/Owner unlock via "Unlock as {name}"; mandatory separate POS PIN (tenant_users.pos_pin_hash); Commerce-password requirement on PIN-set to prevent cashier escalation. 14/14 R1.3.1 + 33/33 regression = 47/47 prod tests green.
2026-04-22. Three QA gaps closed: (1) PIN-collision → picker-then-PIN; (2) Manager/Owner couldn't unlock → "Unlock as {name}" button; (3) Unattended-terminal risk → Manager requires separate POS PIN (new tenant_users.pos_pin_hash, inline "Set your PIN" dialog on first use). 4/4 Gate 1. Prod at Gate 2.
2026-04-22 on prod. Third cycle of R1: fullscreen new-tab POS route with three-phase state machine (Locked → PreShift → Open), signed active-cashier cookie, full resume flow after Lock. 13/13 R1.3 + 33/33 regression = 46/46 prod tests green.
2026-04-21. Third cycle of R1: new fullscreen route /cafe/pos/register/[configId] opens in its own tab. Three-phase state machine (Locked → PreShift → Open), nix_active_cashier JWT cookie separate from web session, unlock/openShift/lock/closeShift server actions, NIX cafe.sessions row mirrors Odoo pos.session. 4/4 Gate 1 checks. Prod click-through at Gate 2.
2026-04-21 on prod. Second cycle of R1: cafe.sessions + payment_diff_reasons + pos_sequences migrations live on Render. POS landing now groups registers by shop with Odoo session summary per card. 9/9 R1.2 + 33/33 regression = 42/42 prod tests green.
2026-04-21. Second cycle of R1: cafe.sessions (Odoo pos.session mirror with PIN + cash-count fields), cafe.payment_diff_reasons (R1.6), cafe.pos_sequences (R1.5). POS page now groups registers by shop with session-state summary cards. 8/8 Gate 1 checks. Prod UI click-through at Gate 2.
2026-04-21 on prod. First cycle of R1: PBKDF2 PIN hashing + Cashier tab. Full server-action click-through (login → create → PIN reveal → reset → deactivate). 9/9 R1.1 + 11/11 phase 1 regression green. Mid-gate fix: db.transaction() wrap to bypass Hyperdrive cache.
2026-04-21. First cycle of R1: PBKDF2 PIN hashing, commerce.pin_identities DAO, cashier CRUD server actions, Cashier tab on Team page. 5/5 local Gate 1 checks passed — unit tests, schema shape, DAO round-trip, SSR smoke. Prod click-through at Gate 2.
2026-04-21 on prod. Encrypted bot token + groups + webhook + cron live. 8/8 M3 + 33/33 regression = 41/41 prod tests green.
2026-04-21. Per-tenant Telegram bot: encrypted token, groups CRUD, inbound /report + /chatid webhook, scheduled daily push. 7/7 local checks passed.
2026-04-21 on prod. Shop-scope helper + OR-fallback live, validated on get-coffee.nixtech.app/cafe (real Odoo data). 10/10 M1 + 23/23 regression = 33/33 prod tests green.
2026-04-21. Dashboard/Orders/Reports in nix-cafe now shop-scope their Odoo order queries through the shared getPosConfigFilterForSelection helper, with an OR-fallback for unmapped configs so empty-mapping tenants don't blank out. 8/8 Playwright + unit checks passed.
2026-04-21 bundle on prod. Outdoor frontend persistedstate v4 fix + commerce.pin_identities live on Render nix-db. 6/6 bundle + 23/23 regression = 29/29 prod tests green.
2026-04-21 bundle. Outdoor frontend persistedstate v4 key fix + commerce.pin_identities schema (Phase 3 POS prereq). 8/8 Playwright checks passed; schema snapshot inline.
2026-04-20 build. Launchpad, read-only subscriptions, team management, invoices stub, branding self-service. 11 step-by-step screenshots. 13/13 Playwright checks passed.
2026-04-20 refactor. Tenants subscribe to multiple products with per-(tenant, product) plan/limits/billing. 13 step-by-step screenshots. 14/14 Playwright checks passed.
Attendance, leave, expenses, settlements, salary, employee records. 8 pages × 2 resolutions.
Content capped at 1400px on ultrawide screens. 6 resolutions × 5 pages. Captured from demo.nixtech.app.
Same test against local dev environment.