in ALL JSON-LD (fleet-wide injection fix, Cody-hardened)","b":""},{"h":"41049f5","d":"2026-08-03","s":"auto-save: 2026-08-03T11:23:33 (1 files) — server.js","b":""},{"h":"1bd3992","d":"2026-08-03","s":"security: response headers + non-breaking CSP (frame-ancestors/object-src/base-uri) + patch /api/render error-message leak (Cody-hardened)","b":""},{"h":"c34917c","d":"2026-08-03","s":"snapshot before deploy: in-tree security headers (nosniff/frame/referrer/permissions, drop x-powered-by) + don't leak raw error to /api/render client","b":""},{"h":"81d733e","d":"2026-08-03","s":"refine: shared fmtStamp (storefront+admin, NaN/future-safe), sortable products table (Newest|Most clicks), post-migration schema assertion — v0.4.1","b":""},{"h":"3f3869c","d":"2026-08-03","s":"a11y: working skip-to-content link (focus-moving, tabindex=-1) + prefers-reduced-motion support (Cody-hardened)","b":""},{"h":"85a3efa","d":"2026-08-03","s":"seo+integrity: per-page OG images + saved rooms/guides/catalog fully honor suppression & exclude wall paints (Cody-hardened)","b":""},{"h":"303f890","d":"2026-08-03","s":"chore: sync package-lock to v0.4.0","b":""},{"h":"7c3f401","d":"2026-08-03","s":"chore: v0.4.0 (session close — date+time-added on store cards, admin analytics, deploy schema-migration safety)","b":"Co-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"c8362bb","d":"2026-08-03","s":"auto-save: 2026-08-03T10:53:14 (2 files) — lib/rooms.js server.js","b":""},{"h":"181743c","d":"2026-08-03","s":"deploy: schema-migration step + /shop smoke test (stop code-ahead-of-schema)","b":"Prevents a repeat of the 2026-08-03 regression where shipped code referenced a\nprod-missing column (products.suppressed) + tables (affiliate_settings,\nsuppress_rules) and /shop 500'd while the DB-blind /healthz smoke test stayed\ngreen.\n\n- db/schema.sql: explicit ADD COLUMN IF NOT EXISTS guards (inline, before\n dependent indexes) so the file is a complete idempotent migration on a fresh\n OR drifted DB, not just a fresh-create.\n- scripts/migrate.sh: applies schema.sql with ON_ERROR_STOP; loads DATABASE_URL\n from the remote's own .env.\n- .deploy.conf: BUILD_CMD runs the migration on the remote after install/before\n reload; HEALTH_URL now hits /shop (exercises the real catalog query, not the\n DB-blind /healthz); INSTALL_CMD bakes in the sharp-safe (scripts-enabled) install.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"ffa8079","d":"2026-08-03","s":"chore: v0.3.0 — keyword/id suppression feature (session close)","b":"Author: session that shipped 72708fc (admin Suppress page). Version bump only;\nworking-tree edits to lib/rooms.js/routes/admin.js/server.js + new lib/ids.js belong\nto a concurrent session and are intentionally left uncommitted."},{"h":"39571bf","d":"2026-08-03","s":"robustness: bigint-safe id guard across all client-value->id paths (/go, product_ids, wall_paint_id, admin+suppress routes) — no more 500 on malformed/overflow ids","b":""},{"h":"aae117f","d":"2026-08-03","s":"harden: intOrNull() guard on client-supplied ids reaching bigint columns","b":"Non-numeric wall_paint_id (and similar) now coerces to null instead of\nNumber()->NaN reaching a bigint query and throwing a 500. Applied at\n/api/rooms and /api/render call sites. (Pre-existing working-tree change,\nsnapshotted before deploy so shipped code == committed code.)\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"78c986b","d":"2026-08-03","s":"store: date + time added on product cards — New badge + 'Added …' line","b":"Every storefront product card now shows when it was added to the store: a NEW\nbadge over the image for items added in the last 14 days (exact date+time in the\nhover title), plus a muted, dated 'Added Mon D, YYYY, H:MM AM' line under the\nprice. New shared addedStamp() helper in render.js mirrors the admin when()\nformat; card-new + card-added styles added to site.css. Chosen treatment per\n2026-08-03 decision (overrides the usual 'customer cards omit created date').\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"72708fc","d":"2026-08-03","s":"admin: keyword/id suppression rules (Suppress page) + Vacuum re-apply + Refresh","b":"- New suppress_rules table (durable keyword/id hide rules, distinct from source-level\n affiliate_settings and the raw suppressed flag).\n- /admin/suppress: enter keywords (and/or ids) -> immediately flip suppressed=TRUE on\n every current match (title/brand/advertiser). Reuses the pervasive NOT suppressed gate\n so hides propagate to /shop, /rooms, guides, and the sitemap.\n- Vacuum re-applies all rules (catches products ingested later matching an old keyword);\n Refresh reloads/recounts; View shop -> /shop?room=decor&style=modern.\n- Per-rule live match/hidden counts, created date+time, Re-apply, Remove, Remove+unhide.\n- Verified end-to-end: add velvet (7 hidden), storefront drop confirmed, vacuum idempotent,\n remove+unhide restores state; CSRF guard returns 403."},{"h":"82f2795","d":"2026-08-03","s":"admin: per-product click analytics column (total, 7d, last-clicked date)","b":"Fold click aggregates into the products query via one grouped LEFT JOIN and\nrender a Clicks column per row — lifetime total, a 7-day sub-count, and the\nlast-clicked timestamp (dated with the standard when() helper). No-click\nproducts show a muted — instead of fake zeros.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"5b5102f","d":"2026-08-03","s":"seo: breadcrumbs (nav + BreadcrumbList) across facet/room/guide/looks pages, multi-JSON-LD layout (Cody-hardened)","b":""},{"h":"ecc8fe0","d":"2026-08-03","s":"nav: promote kitchen/bathroom to rooms + upper-right room hamburger (collapsed on load, all room types)","b":""},{"h":"6a15502","d":"2026-08-03","s":"seo: indexable facet landing pages — sitemap + self-canonical whitelist, ratio-gated, orphan rooms 404 (Cody-hardened)","b":""},{"h":"3e86ee5","d":"2026-08-03","s":"IDS admin: implement EZ-Join DTD verdict (C+relabel), fix smart-quote regression, sync rooms.hotspots schema (v0.2.1)","b":"- EZ-Join verdict C-as-superset-of-A: joinTarget() resolves a per-network\n marketplace SEARCH deep-link (brand pre-filled) where searchVerified, else the\n plain landing (safe fallback). All networks land today (unverified); flip\n searchVerified per network after a live test. Honest relabel 'EZ Join' ->\n 'Join via '.\n- Fix: refactor pass had introduced curly quotes as HTML attr delimiters on the\n join button (class=\"ezjoin\"/href broke styling+link) — restored straight quotes.\n- db/rooms.sql: declare + idempotently ALTER-add the hotspots JSONB column that\n createRoom() writes (schema file was drifted from the live DB).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"562d723","d":"2026-08-03","s":"chore: lint, refactor, v0.2.0 (session close)","b":"Lint: colon-safe Basic-auth parse. Refactor (behavior-preserving): move\nAFFILIATE_ENABLED const above buildWhere; NUL->KEY_SEP; chip param key->netKey;\ndedup joinedNets label. Verified: auth 401/200, /shop + affiliates + suggested\n200, toggle round-trip. Minor bump — session shipped the affiliate on/off admin.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"33bf504","d":"2026-08-03","s":"IDS admin: confirm CJ PID 101848244, drop bogus 1062321 (TK-10171)","b":".env CJ_WEBSITE_ID=101848244 matches the PID in the live tracking links, so CJ\nattribution is correct. 1062321 was identified as a FileMaker customer acct\n(client Claire, Shopify order #32749), not a CJ id — removed from the record.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"8fb23cd","d":"2026-08-03","s":"IDS admin: record CJ account identifiers on Affiliates tab (TK-10171)","b":"Adds a CJ Account reference bar to /admin/affiliates from data/cj-account.json:\nlogin steve@designerwallcoverings.com, CID 7896980, live-link PID 101848244\n(all evidence-confirmed). 1062321 shown as 'unverified' — it matches neither the\nCID nor the PID in the live click-[PID]-[AID] tracking links, so it is NOT wired\ninto anything pending confirmation under CJ Account > Websites.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"770915b","d":"2026-08-03","s":"admin: keyword line-hider with preview — catch a line across reseller-mislabeled brands (Honiture gap)","b":""},{"h":"dcba584","d":"2026-08-03","s":"admin: brand-level line selector — hide/show a whole brand across every advertiser","b":""},{"h":"99f7508","d":"2026-08-03","s":"auto-save: 2026-08-03T08:52:19 (7 files) — db/schema.sql lib/catalog.js lib/render.js lib/rooms.js public/css/site.css","b":""},{"h":"95495fd","d":"2026-08-03","s":"IDS admin: 'Suggested lines to join' affiliate tab (TK-10171)","b":"Adds a second tab under /admin/affiliates: a curated gap-finder of on-brand\nhome/decor affiliate programs across CJ + other networks. Flags lines already\nin the catalog as Joined, gives an EZ Join deep-link into the right network's\nmarketplace for the rest, filterable by network. Roster is data-driven\n(data/suggested-affiliates.json), read live so it extends without a restart.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"e72a006","d":"2026-08-03","s":"moodboard drawer: Pieces tab defaults open (board visible on open); Project/Create stay collapsed; explicit collapse still persists","b":""},{"h":"d44578c","d":"2026-08-03","s":"Switch Honiture affiliate OFF (kept, hidden) + drop its ingest keyword","b":""},{"h":"81400f7","d":"2026-08-03","s":"moodboard drawer: in-drawer project switcher + collapsible Project/Pieces/Create tabs (collapsed on load, state persisted)","b":""},{"h":"590fca7","d":"2026-08-03","s":"shop: collapse facet rail on load, 4-across grid + columns slider, mood-board project select","b":"- facet rail -> native
, all collapsed on load (auto-opens only the group holding an active filter)\n- grid controls: columns slider (2-6, default 4) driving #grid via --gcols; grid starts 4-across even pre-JS\n- sort: date/color/style/brand/title/price all present (Newest relabelled 'Date - Newest')\n- mood board -> named PROJECTS: #mbProjectSel switcher, create/switch, per-project item lists; legacy single board migrated to 'My Board'"},{"h":"f14bc91","d":"2026-08-03","s":"refactor: replace all SELECT * with explicit column lists (lib/cols.js) — clears the pre-deploy lint block so the standard deploy.sh path works for IDS (no more surgical-rsync workaround); result sets identical","b":"Co-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"28e228f","d":"2026-08-03","s":"feat(moodboard): client-side spend guard on Create Room Setting — two-step confirm (\"Generate room ~$0.039? Tap again\") + per-session cap of 8 renders, so a public visitor can't silently rack up Gemini spend (server per-IP limit still applies)","b":"Co-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"e5ef1c4","d":"2026-08-03","s":"feat: shop-time Mood Board — collect pieces while shopping, assemble, one button turns them into a photoreal room setting","b":"- public/js/moodboard.js: self-contained client feature (mirrors cart.js). Injects a Mood Board button on every product card, floating FAB + slide-in drawer that assembles all selections, a star-Primary toggle, room-type picker, and a Create Room Setting button that POSTs the selected ids to the existing /api/render (Gemini 2.5 Flash Image, ~$0.039) and shows the rendered room + shoppable pieces inline\n- lib/render.js: load moodboard.js globally so selection works site-wide (shop, rooms, home, looks)\n- Primary piece is sent first so it anchors the room (scene refs the first 4 pieces)\n\nCo-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"713ee0d","d":"2026-08-03","s":"fix(render): match bigint product ids type-safe — /api/render was silently ignoring explicit product_ids (string p.id === number id never matched), rendering a generic room; also fixes /build explicit-piece selection","b":"Co-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"96f59a1","d":"2026-08-02","s":"ingest: partner-scoped CJ sweeps (partnerIds) + TWOPAGES program joined 2026-08-02 — keyword search can't reach niche joined advertisers; feed indexes 24-48h post-join, re-runs pick it up","b":""},{"h":"b8892c3","d":"2026-08-02","s":"ingest: add robot/cordless vacuum + honiture brand sweep — Honiture CJ program joined 2026-08-02 (56 products, all tracked, $20-$260)","b":"Co-Authored-By: Claude Fable 5 "},{"h":"53a67e4","d":"2026-08-02","s":"fix .deploy.conf: IDS runs on 9876 (9861 belongs to staged.dw) — smoke test was probing the wrong app","b":""},{"h":"8898a07","d":"2026-08-02","s":"docs: go-live runbook for auto-guides (gated)","b":""},{"h":"213ca8b","d":"2026-08-02","s":"feat: auto guide generator — gen-guide.js builds a room (shared lib/roomgen) + authors an editorial guide around its exact pieces (local qwen3:14b, template fallback); room-type rotation for variety; 5 guides published locally","b":""},{"h":"325e399","d":"2026-08-02","s":"add tracked-link coverage canary: samples live /go redirects, alerts on worsening CJ-tracking coverage (CNCP), daily 3:15am","b":"Co-Authored-By: Claude Opus 4.8 (1M context) "},{"h":"64a5449","d":"2026-08-01","s":"feat: affiliate cart — collect pieces, checkout opens each product in its own tab (per-merchant). Server-rendered add-cart buttons + client cart.js (localStorage list, floating cart button, slide-in drawer, tab-fanout checkout with popup-block fallback)","b":""},{"h":"47e1b8d","d":"2026-08-01","s":"room page: 3x-larger click-to-shop thumbnails (info + Buy on click); big thumbs are the shop surface on scene-less rooms so they no longer dead-end","b":"- roomThumbs: each thumb is now a