[object Object]

← back to Dw Collections Viewer

save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule

71f733796d067fdb9759bc41af063ba824fabadd · 2026-05-06 17:42:44 -0700 · Steve

Files touched

Diff

commit 71f733796d067fdb9759bc41af063ba824fabadd
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 17:42:44 2026 -0700

    save+deploy: sort selector (textures/newest/title/sku/priceUp/priceDown/random) wired through API → renderSite; per CLAUDE.md grid sort+density rule
---
 data/site-status.json | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html     |  39 +++++++--
 server.js             |  30 +++++--
 3 files changed, 285 insertions(+), 16 deletions(-)

diff --git a/data/site-status.json b/data/site-status.json
new file mode 100644
index 0000000..3cc2e1e
--- /dev/null
+++ b/data/site-status.json
@@ -0,0 +1,232 @@
+{
+  "silkwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.733Z"
+  },
+  "linenwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.734Z"
+  },
+  "raffiawallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.889Z"
+  },
+  "corkwallcovering": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.903Z"
+  },
+  "jutewallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.913Z"
+  },
+  "silkwallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.940Z"
+  },
+  "textilewallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.980Z"
+  },
+  "fabricwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:14.999Z"
+  },
+  "raffiawalls": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.004Z"
+  },
+  "silverleafwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.008Z"
+  },
+  "vinylwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.129Z"
+  },
+  "metallicwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.147Z"
+  },
+  "micawallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.195Z"
+  },
+  "mylarwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.262Z"
+  },
+  "suedewallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.314Z"
+  },
+  "madagascarwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.321Z"
+  },
+  "stringwallpaper": {
+    "http_status": 301,
+    "redirect_target": "linenwallpaper.com",
+    "probed_at": "2026-05-07T00:33:15.432Z"
+  },
+  "embroideredwallpaper": {
+    "http_status": 301,
+    "redirect_target": "fabricwallpaper.com",
+    "probed_at": "2026-05-07T00:33:15.438Z"
+  },
+  "1800swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.528Z"
+  },
+  "1890swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.533Z"
+  },
+  "1900swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.561Z"
+  },
+  "1930swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.591Z"
+  },
+  "1920swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.637Z"
+  },
+  "1940swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.640Z"
+  },
+  "1960swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.659Z"
+  },
+  "1970swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.715Z"
+  },
+  "1950swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.724Z"
+  },
+  "1980swallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.839Z"
+  },
+  "wallpapersback": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.840Z"
+  },
+  "retrowalls": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.861Z"
+  },
+  "agedwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.880Z"
+  },
+  "museumwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.938Z"
+  },
+  "handcraftedwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.960Z"
+  },
+  "pastelwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:15.996Z"
+  },
+  "restorationwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.076Z"
+  },
+  "glitterwalls": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.155Z"
+  },
+  "greenwallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.232Z"
+  },
+  "contractwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.318Z"
+  },
+  "naturalwallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.319Z"
+  },
+  "recycledwallpaper": {
+    "http_status": 405,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.361Z"
+  },
+  "hospitalitywallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.438Z"
+  },
+  "hotelwallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.473Z"
+  },
+  "healthcarewallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.476Z"
+  },
+  "restaurantwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.483Z"
+  },
+  "saloonwallpaper": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.541Z"
+  },
+  "architecturalwallcoverings": {
+    "http_status": 200,
+    "redirect_target": null,
+    "probed_at": "2026-05-07T00:33:16.601Z"
+  }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 4d584df..702ccda 100644
--- a/public/index.html
+++ b/public/index.html
@@ -763,10 +763,26 @@ function renderDetail() {
 
     <div class="actions">
       <button class="btn" onclick="saveColls()">Save collections</button>
+      <label class="deploy-sort" style="display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.06em;color:#999;text-transform:uppercase">
+        Sort:
+        <select id="deploySortSel" onchange="localStorage.setItem('dwc_deploy_sort', this.value)" style="background:#0e0e10;color:#f4f1ea;border:1px solid #2a2a32;padding:5px 8px;font-family:inherit;font-size:11px">
+          <option value="textures">textures first (default)</option>
+          <option value="newest">newest</option>
+          <option value="title">title A→Z</option>
+          <option value="sku">SKU A→Z</option>
+          <option value="priceUp">price ↑</option>
+          <option value="priceDown">price ↓</option>
+          <option value="random">random</option>
+        </select>
+      </label>
       <button class="btn go" onclick="saveAndDeploy()" ${assigned.length === 0 ? 'disabled' : ''}>💾 Save &amp; Deploy</button>
       <button class="btn" onclick="renderSite()" ${assigned.length === 0 ? 'disabled' : ''}>Render now (live)</button>
       <a class="btn" href="https://${s.domain}" target="_blank" style="text-decoration:none">Open site ↗</a>
     </div>
+    <script>
+      // Restore saved deploy sort
+      (function(){ const v = localStorage.getItem('dwc_deploy_sort'); if (v) { const sel = document.getElementById('deploySortSel'); if (sel) sel.value = v; } })();
+    </script>
   `;
   // Hydrate the products slider after the detail HTML is in the DOM.
   renderProductsSlider();
@@ -856,16 +872,22 @@ async function saveColls() {
   toast(`saved · ${j.collections.length} collections assigned to ${CURRENT}`);
 }
 
+function getDeploySort() {
+  const sel = document.getElementById('deploySortSel');
+  return (sel && sel.value) || localStorage.getItem('dwc_deploy_sort') || 'textures';
+}
+
 async function renderSite() {
-  if (!confirm(`Re-render ${CURRENT} from ${SITES[CURRENT].collections.length} Shopify collection(s)? This will overwrite the current products.json + scp + pm2 restart.`)) return;
-  toast(`rendering ${CURRENT}…`);
+  const sort = getDeploySort();
+  if (!confirm(`Re-render ${CURRENT} from ${SITES[CURRENT].collections.length} Shopify collection(s), sorted "${sort}"? Overwrites products.json + scp + pm2 restart.`)) return;
+  toast(`rendering ${CURRENT} (sort=${sort})…`);
   const r = await fetch(`api/site/${CURRENT}/render`, {
     method: 'POST', headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({ deploy: true })
+    body: JSON.stringify({ deploy: true, sort })
   });
   const j = await r.json();
   if (!r.ok) return toast('render failed: ' + (j.error || r.status), false);
-  toast(`${CURRENT} rendered · ${j.total_unique} unique products · deployed: ${j.deployed}`);
+  toast(`${CURRENT} rendered · ${j.total_unique} products · sort=${sort} · deployed: ${j.deployed}`);
   await loadSites();
   renderDetail();
 }
@@ -875,7 +897,8 @@ async function saveAndDeploy() {
   const slug = CURRENT;
   const colls = SITES[slug].collections || [];
   if (!colls.length) return toast('no collections assigned', false);
-  if (!confirm(`Save & deploy ${slug}? Persists ${colls.length} collection(s), re-renders products.json from Shopify, and pm2-restarts the live site.`)) return;
+  const sort = getDeploySort();
+  if (!confirm(`Save & deploy ${slug} (sort: ${sort})? Persists ${colls.length} collection(s), re-renders products.json from Shopify in "${sort}" order, scp + pm2 restart.`)) return;
   toast(`saving ${slug}…`);
   const r1 = await fetch(`api/site/${slug}/collections`, {
     method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -883,14 +906,14 @@ async function saveAndDeploy() {
   });
   const j1 = await r1.json();
   if (!r1.ok) return toast('save failed: ' + (j1.error || r1.status), false);
-  toast(`saved · deploying ${slug}…`);
+  toast(`saved · deploying ${slug} (sort=${sort})…`);
   const r2 = await fetch(`api/site/${slug}/render`, {
     method: 'POST', headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({ deploy: true }),
+    body: JSON.stringify({ deploy: true, sort }),
   });
   const j2 = await r2.json();
   if (!r2.ok) return toast('deploy failed: ' + (j2.error || r2.status), false);
-  toast(`${slug} live · ${j2.total_unique} products · deployed: ${j2.deployed}`);
+  toast(`${slug} live · ${j2.total_unique} products · sort=${sort} · deployed: ${j2.deployed}`);
   await loadSites();
   renderDetail();
 }
diff --git a/server.js b/server.js
index 2a768eb..7420cc8 100644
--- a/server.js
+++ b/server.js
@@ -272,12 +272,22 @@ async function renderSite(slug, opts = {}) {
     }
   }
 
-  // Sort textures-first → patterns. If hero_sku is pinned, bubble it to position 0.
-  const arr = Array.from(merged.values()).sort((a, b) => {
-    const sa = texScore(a), sb = texScore(b);
-    if (sa !== sb) return sb - sa;
-    return (a.title || '').localeCompare(b.title || '');
-  }).slice(0, 600);
+  // Sort: caller picks via opts.sort. Default = textures-first → patterns (legacy behavior).
+  // Available: 'textures' (default), 'newest', 'sku', 'title', 'priceUp', 'priceDown', 'random'.
+  const all = Array.from(merged.values());
+  const sortMode = String(opts.sort || rule.sort || 'textures').toLowerCase();
+  const sorters = {
+    textures:  (a, b) => (texScore(b) - texScore(a)) || (a.title || '').localeCompare(b.title || ''),
+    newest:    (a, b) => String(b.published_at || b.created_at || '').localeCompare(String(a.published_at || a.created_at || '')),
+    sku:       (a, b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')),
+    title:     (a, b) => String(a.title || '').localeCompare(String(b.title || '')),
+    priceUp:   (a, b) => (a.max_price || 0) - (b.max_price || 0),
+    priceDown: (a, b) => (b.max_price || 0) - (a.max_price || 0),
+    random:    () => Math.random() - 0.5,
+  };
+  const cmp = sorters[sortMode] || sorters.textures;
+  const arr = all.sort(cmp).slice(0, 600);
+  console.log(`[render] ${slug} sort=${sortMode} → ${arr.length} products`);
   if (rule.hero_sku) {
     const i = arr.findIndex(p => p.sku === rule.hero_sku);
     if (i > 0) { const [pin] = arr.splice(i, 1); arr.unshift(pin); }
@@ -440,12 +450,13 @@ app.post('/api/auto-seed', async (req, res) => {
 app.post('/api/render-bulk', async (req, res) => {
   const slugs = Array.isArray(req.body.slugs) ? req.body.slugs.filter(validSlug).slice(0, 50) : [];
   const deploy = !!req.body.deploy;
+  const sort = String(req.body.sort || '').toLowerCase() || undefined;
   if (!slugs.length) return res.status(400).json({ error: 'no slugs' });
   const results = [];
   for (let i = 0; i < slugs.length; i++) {
     const s = slugs[i];
     try {
-      const out = await renderSite(s, { deploy });
+      const out = await renderSite(s, { deploy, sort });
       results.push({ slug: s, ok: true, total_unique: out.total_unique, deployed: out.deployed });
     } catch (e) {
       results.push({ slug: s, ok: false, error: e.message });
@@ -468,7 +479,10 @@ app.post('/api/site/:slug/collections', (req, res) => {
 
 app.post('/api/site/:slug/render', async (req, res) => {
   try {
-    const out = await renderSite(req.params.slug, { deploy: !!req.body.deploy });
+    const out = await renderSite(req.params.slug, {
+      deploy: !!req.body.deploy,
+      sort: String(req.body.sort || '').toLowerCase() || undefined,
+    });
     res.json({ ok: true, ...out });
   } catch (e) { res.status(500).json({ error: e.message }); }
 });

← aa01c45 Probe live HTTP status; flag redirected/parked sites in the  ·  back to Dw Collections Viewer  ·  Hero resolution probe + HD/MED/SD badges + upscale to larges dd68026 →