All tasks

Slice M follow-up -- saveDisplayBranding UPSERT PROD -- GATE 2

Shipped 2026-05-18. Pre-existing bug surfaced by Slice M's first prod run: saveDisplayBranding did a naked UPDATE WHERE tenant_id=... which silently no-oped for tenants without a cafe.tenant_config row. Starter tenants (lumiere-coffee on prod) don't get a row at provisioning time -- getTenant synthesizes a fallback. Net effect: those tenants couldn't save any display branding (logo, primary color, promo text, slideshow, screensaver).

5/5 prod test on lumiere-coffee (the originally-broken tenant) -- 51/51 regression green -- 56/56 total. Slice M's prod test now passes on lumiere -- the first save creates the row via the new UPSERT, subsequent saves UPDATE in place. The same Slice M code that needed a get-coffee workaround at first ship now works end-to-end on Starter.

What shipped

Single file change. saveDisplayBranding in lib/db/display_branding.ts rewritten from db.update(...).set(...).where(...) to db.insert(...).onConflictDoUpdate(...).

INSERT path populates: tenantId (PK), storeName (looked up from tenants.name, fallback "NIX Cafe"), empty-string defaults for required Odoo fields (odooUrl, odooDatabase, odooLogin, odooApiKeyEnc) -- Starter tenants don't use Odoo so empty is correct. Plus all the display_* branding fields the caller wants to save.

UPDATE path set clause is display-only -- Pro tenants' Odoo creds + currency + posMaster + bypass_odoo_pos all preserved. Verified by a Gate-1 source assertion that the set: { ... } block doesn't include any non-display fields.

Checks -- 5/5 prod test (on lumiere-coffee)

Raw: result.json

Regression sweep -- 51/51 green

test-phase1-prod.mjs11/11
test-phase2-sso-outdoor-prod.mjs6/6
test-phase2-cafe-multishop-prod.mjs6/6
test-m1-prod.mjs (shop scoping)10/10
test-r7-prod.mjs (dashboard + manager-live)14/14
test-r8-prod.mjs (auth/security trio)4/4

Side benefit

All Slice G features (slideshow images, slideshow speed, slideshow enabled) and Slice M (screensaver) now also work for Starter tenants without a pre-existing cafe.tenant_config row. Before this fix, lumiere-coffee couldn't save ANY of these settings -- the Settings/Display form appeared to work in the UI but the DB never updated.

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

Modified: nix-cafe/lib/db/display_branding.ts -- saveDisplayBranding rewritten as db.insert(...).onConflictDoUpdate(...).

Commit: 3d2c32b