in template literal","b":"renderDetail() injects a giant template-literal HTML block via\ninnerHTML; that block contains a nested \nhelper. Browsers' HTML tokenizer doesn't respect JS template literals — when\nit hits the inner , it terminates the OUTER inside backtick template literals."},{"h":"a0cea68","d":"2026-05-06","s":"save+deploy: persist chosen sort to per-site rules; UI pre-populates from server-side rules.sort with localStorage fallback","b":""},{"h":"dd68026","d":"2026-05-06","s":"Hero resolution probe + HD/MED/SD badges + upscale to largest variant","b":"Steve flagged: many hero images look blurry full-page because the source is\na small Shopify variant (e.g. 480x480). Two fixes:\n\n1. largestShopifyImage(url) — strips _NxN size suffix from any\n cdn.shopify.com / designerwallcoverings.com URL so the master is fetched\n for both card thumbnails AND drag-drop hero pins.\n\n2. Resolution probe + badges — every product card now shows a HD/MED/SD\n badge in its top-right after the natural image dimensions are probed\n client-side (concurrency 6, sessionStorage cached). Tier rules:\n ≥ 1500 px largest side → HD (green)\n ≥ 800 px → MED (gold)\n < 800 px → SD (red)\n SD cards fade to 55% opacity to push you toward HD picks.\n\n3. Hero panel banner — when a site is selected, a banner above the assigned-\n collections section reports whether the current hero is HD/MED/SD with\n the actual dimensions; SD shows a 'WILL LOOK BLURRY' warning.\n\n4. 'HD only' filter checkbox in the products bar — hides all non-HD cards.\n\n5. Products-bar count line now shows '23 HD · 12 MED · 4 SD · 8 probing'.\n\nCork live site already pushed with the upscale logic; 45 other sites\nre-patched locally pending deploy."},{"h":"71f7337","d":"2026-05-06","s":"save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule","b":""},{"h":"aa01c45","d":"2026-05-06","s":"Probe live HTTP status; flag redirected/parked sites in the grid","b":"Steve noticed several site cards showed wrong heroes — they're nginx 301/302\nredirects to consolidation domains, and the admin's screenshot probe was\nshowing the redirect target's hero. Misleading.\n\nThis change adds a HEAD probe + banner so consolidated sites are honest:\n\n- New POST /api/sites/probe — HEAD-checks every site over HTTPS, parallelism 8,\n caches { http_status, redirect_target, probed_at } in data/site-status.json\n- listSites() merges the probe cache into each site row\n- index.html: new 📡 Probe live button in the toolbar; cards with 3xx render\n a gold redirect banner overlaying the hero ('→ (301)') AND fade\n to 0.55 opacity so the redirect chain stops looking like a bug\n- Cards with HTTP 0/5xx render a red 'offline' banner\n\nVerified live: 46 sites probed in one click, 43 ok / 2 redirects (string→\nlinen, embroidered→fabric) / 1 offline. Banners render correctly."},{"h":"3a36c5f","d":"2026-05-06","s":"Drag product → cover image + sort by color/style/SKU + grid-size slider","b":"Three new affordances on the right detail panel:\n\n1. Drag any product card onto the cover image to set it as the hero.\n - .pcard is now draggable=true with a custom MIME (application/x-dwc-product)\n carrying { sku, image, title }\n - .hero-large catches dragover/drop, highlights via existing dropzone-active\n CSS, then POSTs the sku to the existing /api/site/:slug/hero endpoint\n - Optimistic local update + toast confirmation; renderDetail() re-renders\n so the cover swaps without a fetch round-trip\n\n2. Sort dropdown — newest (default), Color (clusters black→white→warm→cool→\n purple buckets via tag matching), Style (Traditional/Modern/Floral/etc.\n buckets), SKU (alpha). Selection persists in localStorage as 'dwc_sort'.\n\n3. Grid card-width slider — 80px to 260px, live-updates\n grid-template-columns:repeat(auto-fill,minmax(N,1fr)) without re-render.\n Persists as 'dwc_size' in localStorage.\n\nVerified end-to-end via headless Playwright: 4-option sort dropdown live,\n50/50 cards draggable, drop simulation flips hero img src as expected."},{"h":"f016e8f","d":"2026-05-06","s":"Click site → show all products as a responsive grid (was horizontal scroll)","b":"Previous .products-slider .row was a horizontal flex with 110px cards and\noverflow-x:auto — only ~6 cards visible at a time, lots of horizontal\nscrolling to see everything.\n\nNow: display:grid, grid-template-columns:repeat(auto-fill,minmax(140px,1fr)),\ngap:10px. Every product visible at once; cards bigger (140-200+ px) and\nlabels can wrap to 2 lines via -webkit-line-clamp:2 with min-height:32px so\nrows align. Selection, bulk-remove, copy SKUs all unchanged."},{"h":"3f6faec","d":"2026-05-06","s":"bulk product remove-from-list (server endpoint + UI button)","b":""},{"h":"8a47889","d":"2026-05-06","s":"press C anywhere to focus collection search input","b":""},{"h":"7455efd","d":"2026-05-06","s":"show SKU + published_at in product card hover tooltip","b":""},{"h":"3b2d32d","d":"2026-05-06","s":"color preview toast green/red by product-count delta","b":""},{"h":"9c53f65","d":"2026-05-06","s":"add Save & Deploy combined button (one-click save + render + deploy)","b":""},{"h":"0b73541","d":"2026-05-06","s":"add inline .env loader + Shopify pagination + all-products page","b":""},{"h":"9a73dc0","d":"2026-05-06","s":"initial scaffold","b":""}];
const q = document.getElementById('q');
const list = document.getElementById('bp-commits');
const tally = document.getElementById('bp-tally');
function applyFilter() {
const term = q.value.toLowerCase().trim();
let shown = 0;
for (const li of list.children) {
const match = !term || li.dataset.blob.includes(term);
li.style.display = match ? '' : 'none';
if (match) shown++;
}
tally.textContent = term ? `${shown} / ${DATA.length} match "${q.value}"` : `${DATA.length} commits indexed`;
// Reflect search state in URL so a filtered view is shareable. Use
// replaceState so the back button still escapes the project page.
const url = new URL(window.location.href);
if (term) url.searchParams.set('q', q.value); else url.searchParams.delete('q');
history.replaceState(null, '', url);
}
q.addEventListener('input', applyFilter);
// Apply the initial ?q= from the server-rendered value field.
if (q.value) applyFilter();
build-pages
Dw Collections Viewer
repo: ~/Projects/dw-collections-viewer · 22 commits · 0 in last 24h, 0 in last 7d ·
1d1ab8c2026-07-13similar-pattern search: per-tile ≈ button → /api/similar proxy to all.dw (creds server-side) → scored live substitutes in a shared panel below the product slider
aa7d9282026-06-10snapshot before dep reinstall + pm2 restart
61f62d92026-05-18Add rel=noopener noreferrer to Open site external link
deda82f2026-05-07dw-collections-viewer: gold ↕<sort> pill on tiles when persisted sort != default 'textures'; surfaces last_sort_applied at a glance
cbb7a202026-05-06dw-collections-viewer: 'Preview sort' button — re-render with chosen sort but skip deploy/pm2-restart so user can eyeball the order before pushing live
15a2c8f2026-05-06dw-collections-viewer: stale-products pill (>30 days mtime) on each tile + 'Stale' bulk-select option; surfaces last_render_iso/age_days/is_stale via /api/sites
f3e88a02026-05-06Fix HTML tokenizer breaking outer script — escape nested </script> in template literal
a0cea682026-05-06save+deploy: persist chosen sort to per-site rules; UI pre-populates from server-side rules.sort with localStorage fallback
dd680262026-05-06Hero resolution probe + HD/MED/SD badges + upscale to largest variant
71f73372026-05-06save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule
aa01c452026-05-06Probe live HTTP status; flag redirected/parked sites in the grid
Commits with substantial prose (≥120 chars) — the rationale behind each move.
f3e88a0 · 2026-05-06 · Fix HTML tokenizer breaking outer script — escape nested </script> in template literal
renderDetail() injects a giant template-literal HTML block via
innerHTML; that block contains a nested <script>(restore deploy-sort)</script>
helper. Browsers' HTML tokenizer doesn't respect JS template literals — when
it hits the inner </script>, it terminates the OUTER <script> block too.
Everything after that closing tag (including the outer template's closing
backtick) became raw HTML, leaving the main JS body unterminated:
'Unexpected end of input'. Page rendered shell only, no site cards.
Fix: split the open tag as <` + `script> and escape the close as <\/script>
inside the template literal. Tokenizer no longer sees a script boundary.
Verified via debate-team-fast (Claude + Kimi + Qwen3 all 3/3): correct fix.
Future refactor (ticket): move the inline restore script OUT of the template
literal entirely (data-attribute + DOMContentLoaded hook). Add a pre-commit
grep for unescaped </script> inside backtick template literals.
dd68026 · 2026-05-06 · Hero resolution probe + HD/MED/SD badges + upscale to largest variant
Steve flagged: many hero images look blurry full-page because the source is
a small Shopify variant (e.g. 480x480). Two fixes:
1. largestShopifyImage(url) — strips _NxN size suffix from any
cdn.shopify.com / designerwallcoverings.com URL so the master is fetched
for both card thumbnails AND drag-drop hero pins.
2. Resolution probe + badges — every product card now shows a HD/MED/SD
badge in its top-right after the natural image dimensions are probed
client-side (concurrency 6, sessionStorage cached). Tier rules:
≥ 1500 px largest side → HD (green)
≥ 800 px → MED (gold)
< 800 px → SD (red)
SD cards fade to 55% opacity to push you toward HD picks.
3. Hero panel banner — when a site is selected, a banner above the assigned-
collections section reports whether the current hero is HD/MED/SD with
the actual dimensions; SD shows a 'WILL LOOK BLURRY' warning.
4. 'HD only' filter checkbox in the products bar — hides all non-HD cards.
5. Products-bar count line now shows '23 HD · 12 MED · 4 SD · 8 probing'.
Cork live site already pushed with the upscale logic; 45 other sites
re-patched locally pending deploy.
aa01c45 · 2026-05-06 · Probe live HTTP status; flag redirected/parked sites in the grid
Steve noticed several site cards showed wrong heroes — they're nginx 301/302
redirects to consolidation domains, and the admin's screenshot probe was
showing the redirect target's hero. Misleading.
This change adds a HEAD probe + banner so consolidated sites are honest:
- New POST /api/sites/probe — HEAD-checks every site over HTTPS, parallelism 8,
caches { http_status, redirect_target, probed_at } in data/site-status.json
- listSites() merges the probe cache into each site row
- index.html: new 📡 Probe live button in the toolbar; cards with 3xx render
a gold redirect banner overlaying the hero ('→ <target> (301)') AND fade
to 0.55 opacity so the redirect chain stops looking like a bug
- Cards with HTTP 0/5xx render a red 'offline' banner
Verified live: 46 sites probed in one click, 43 ok / 2 redirects (string→
linen, embroidered→fabric) / 1 offline. Banners render correctly.
Three new affordances on the right detail panel:
1. Drag any product card onto the cover image to set it as the hero.
- .pcard is now draggable=true with a custom MIME (application/x-dwc-product)
carrying { sku, image, title }
- .hero-large catches dragover/drop, highlights via existing dropzone-active
CSS, then POSTs the sku to the existing /api/site/:slug/hero endpoint
- Optimistic local update + toast confirmation; renderDetail() re-renders
so the cover swaps without a fetch round-trip
2. Sort dropdown — newest (default), Color (clusters black→white→warm→cool→
purple buckets via tag matching), Style (Traditional/Modern/Floral/etc.
buckets), SKU (alpha). Selection persists in localStorage as 'dwc_sort'.
3. Grid card-width slider — 80px to 260px, live-updates
grid-template-columns:repeat(auto-fill,minmax(N,1fr)) without re-render.
Persists as 'dwc_size' in localStorage.
Verified end-to-end via headless Playwright: 4-option sort dropdown live,
50/50 cards draggable, drop simulation flips hero img src as expected.
f016e8f · 2026-05-06 · Click site → show all products as a responsive grid (was horizontal scroll)
Previous .products-slider .row was a horizontal flex with 110px cards and
overflow-x:auto — only ~6 cards visible at a time, lots of horizontal
scrolling to see everything.
Now: display:grid, grid-template-columns:repeat(auto-fill,minmax(140px,1fr)),
gap:10px. Every product visible at once; cards bigger (140-200+ px) and
labels can wrap to 2 lines via -webkit-line-clamp:2 with min-height:32px so
rows align. Selection, bulk-remove, copy SKUs all unchanged.
File tree
10 files tracked. Click any to browse the source at HEAD.