2026-05-24 — Gate 2. Both bugs from Narong's Telegram report fixed and verified live on lumiere-coffee. 9/9 prod + 51/51 regression = 60/60.
Root cause: The NIX Cash Chrome extension Narong has installed (from the legacy Odoo POS days) registers a content script on <all_urls> and monkey-patches window.print + watches every iframe added to the document. When nix-cafe's openPrintWindow creates the hidden print iframe with our paid-order receipt, the extension's findReceiptEl() fallback regex /ticket|total|order|receipt|served by/i matches the "Order: BKK1-0002" + "Served by" text, and overrideReceipt() replaces the iframe's innerHTML with its own buildKhmerReceiptHTML template — which on a cold Cafe-side DOM (no Odoo selectors to parse) renders the empty fallback "NIX Store" + "(no items)" + "Powered by NIX" exactly as in the screenshot. Confirmed via 100% structural match: every line of the screenshot maps to a specific line in content.js lines 365–428.
Fix: 17-line hostname guard prepended to content.js. Throws on any *.nixtech.app host before any monkey-patch / iframe-observer / DOM-rewrite runs. Legacy Odoo POS testing (on non-nixtech hosts like get-coffee1.odoo.com) is unaffected.
Narong action required: reload the unpacked extension in chrome://extensions to pick up the patch, OR disable/uninstall it entirely (Cafe POS replaces it). Until one of those happens, the bug persists in his current Chrome session.
Root cause: The shared nix_session JWT had a 30-minute TTL. Commerce's SPA has a 401-driven silent refresh in its apiClient, but nix-cafe is a separate Cloudflare Worker that just reads the cookie — it has no refresh loop. Cashiers behind the register for >30 min lose the cookie silently; the next navigation or server-action redirects them to /login.
Fix — belt-and-suspenders:
ACCESS_TOKEN_EXPIRY in tenant.auth.service.ts, now exported) + matching COOKIE_MAX_AGE_MS in tenant.auth.route.ts. Worst-case background-tab lifetime; matches the nix_active_cashier cookie TTL.POST /tenant/auth/extend-session — verifies the existing JWT (signature + exp still valid), re-signs the same claims with a fresh expiresIn, sets the rotated cookie. Returns 401 with TOKEN_EXPIRED / UNAUTHORIZED / NO_TOKEN_PROVIDED for the three failure modes so the client knows to redirect.<SessionKeepAlive /> — mounted at the top of (authed)/layout.tsx. Fires the ping once on mount + every 25 min while the tab is visible + on visibilitychange. Skips when document.hidden so abandoned tabs let their sessions expire naturally. Reloads on 401 so the server-side login redirect chain runs.Active /cafe/* user — session never expires. Abandoned tab — expires at the 8h cap.
| ✓ | SSO-login lumiere owner |
| ✓ | nix_session cookie is present after login |
| ✓ | JWT TTL is now ~8h (exp − iat ≈ 28800s), not 30m (was 1800s) |
| ✓ | POST /tenant/auth/extend-session with cookie → 200 + new Set-Cookie |
| ✓ | Cookie in jar rotated to a fresh JWT with later exp; identity claims preserved |
| ✓ | POST /tenant/auth/extend-session with NO cookie → 401 NO_TOKEN_PROVIDED |
| ✓ | POST /tenant/auth/extend-session with garbage cookie → 401 UNAUTHORIZED |
| ✓ | SessionKeepAlive fires extend-session ping within 5s of /cafe/dashboard mount |
| ✓ | Cookie continues to rotate after the in-page ping (multi-extend safe; iat advances) |
| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-phase2-cafe-multishop-prod.mjs | 6/6 (solo retry — first run had a 1-second flake on POS picker render; unrelated to bundle) |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
deployments list + endpoint probe both confirmed before the test run.resp.json() after await ctx.close() errors. Always read body BEFORE closing the context.waitForTimeout(1100) between rotations.chrome://extensions → find the NIX Cash unpacked dev extension → click Reload to pick up the patch, OR Disable / Remove it entirely since Cafe POS replaces it. To verify: print a receipt from /cafe/pos — should now show the correct receipt with the big ORDER # block. Quick test: Chrome Incognito (extensions off by default) — print should work without any extension reload.