[object Object]

← back to Dw Hero Admin

fleet: honor _shared/data/dw-tossed-domains.json — exclude tossed from sidebar

a2f7a8c8d97632aea6424fad457a57a670df27bc · 2026-05-14 09:06:46 -0700 · Steve

Files touched

Diff

commit a2f7a8c8d97632aea6424fad457a57a670df27bc
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu May 14 09:06:46 2026 -0700

    fleet: honor _shared/data/dw-tossed-domains.json — exclude tossed from sidebar
---
 public/index.html | 59 ++++++++++++++++++++++++++++++++++++++++++++++++-------
 server.js         |  9 +++++++++
 2 files changed, 61 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index 2ae840f..abb8be8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -59,10 +59,14 @@
   .toolbar button.primary:hover { background:#c0b090; }
   /* ─ products grid ─ */
   .products-wrap { flex:1; overflow-y:auto; padding:18px 24px; }
-  .products-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
+  .products-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; gap:12px; flex-wrap:wrap; }
   .products-header h3 { margin:0; font-size:12px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted); font-weight:500; }
-  .product-search input { padding:6px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; outline:none; border-radius:3px; width:260px; font-size:12px; }
-  .products { display:grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap:10px; }
+  .products-controls { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
+  .products-controls label { display:inline-flex; align-items:center; gap:6px; font-size:11px; letter-spacing:0.1em; text-transform:uppercase; color:var(--muted); }
+  .products-controls select { padding:5px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; font-size:12px; outline:none; border-radius:3px; }
+  .products-controls input[type=range] { width:120px; accent-color:var(--accent); }
+  .product-search input { padding:6px 10px; background:transparent; border:1px solid var(--line); color:var(--paper); font:inherit; outline:none; border-radius:3px; width:220px; font-size:12px; }
+  .products { display:grid; grid-template-columns: repeat(auto-fill, minmax(var(--card-w, 140px), 1fr)); gap:10px; }
   .product { position:relative; aspect-ratio:1/1.15; background:var(--slot-bg); border:1px solid var(--line); overflow:hidden; cursor:grab; transition:border-color .14s; }
   .product:active { cursor:grabbing; }
   .product:hover { border-color:var(--accent); }
@@ -130,7 +134,16 @@
 <script>
 (function () {
   'use strict';
-  var state = { site: null, products: [], grid: null, filter: '' };
+  var LS = 'dw-hero-admin.';
+  var state = {
+    site: null,
+    products: [],
+    grid: null,
+    filter: '',
+    sort: localStorage.getItem(LS + 'sort') || 'newest',
+    cardW: parseInt(localStorage.getItem(LS + 'cardW') || '140', 10),
+  };
+  document.documentElement.style.setProperty('--card-w', state.cardW + 'px');
   var els = {
     siteList: document.getElementById('siteList'),
     siteSearch: document.getElementById('siteSearch'),
@@ -257,8 +270,21 @@
   function renderProductsHeader() {
     els.productsWrap.innerHTML = '\
       <div class="products-header">\
-        <h3 id="prodCount">All products — newest first</h3>\
-        <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(state.filter || '') + '"></div>\
+        <h3 id="prodCount">All products</h3>\
+        <div class="products-controls">\
+          <label>Sort\
+            <select id="prodSort">\
+              <option value="newest">Newest first</option>\
+              <option value="title">Title A→Z</option>\
+              <option value="sku">SKU A→Z</option>\
+              <option value="vendor">Vendor A→Z</option>\
+            </select>\
+          </label>\
+          <label>Size\
+            <input id="prodDensity" type="range" min="90" max="320" step="10" value="' + state.cardW + '">\
+          </label>\
+          <div class="product-search"><input id="prodFilter" type="search" placeholder="Filter pattern, vendor, tag..." value="' + escapeHtml(state.filter || '') + '"></div>\
+        </div>\
       </div>\
       <div class="products" id="prodGrid"></div>\
     ';
@@ -267,6 +293,19 @@
       state.filter = pf.value;
       renderProductsGrid();
     });
+    var ps = document.getElementById('prodSort');
+    ps.value = state.sort;
+    ps.addEventListener('change', function () {
+      state.sort = ps.value;
+      localStorage.setItem(LS + 'sort', state.sort);
+      renderProductsGrid();
+    });
+    var pd = document.getElementById('prodDensity');
+    pd.addEventListener('input', function () {
+      state.cardW = parseInt(pd.value, 10);
+      localStorage.setItem(LS + 'cardW', state.cardW);
+      document.documentElement.style.setProperty('--card-w', state.cardW + 'px');
+    });
   }
 
   // Only re-renders the grid + count — never touches the header DOM, so
@@ -277,8 +316,14 @@
       if (!q) return true;
       return ((p.title || '') + ' ' + (p.sku || '') + ' ' + (p.vendor || '') + ' ' + (p.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
     });
+    var cmp = function (a, b, key) { return (a[key] || '').localeCompare(b[key] || ''); };
+    if (state.sort === 'title') items = items.slice().sort(function (a, b) { return cmp(a, b, 'title'); });
+    else if (state.sort === 'sku') items = items.slice().sort(function (a, b) { return cmp(a, b, 'sku'); });
+    else if (state.sort === 'vendor') items = items.slice().sort(function (a, b) { return cmp(a, b, 'vendor') || cmp(a, b, 'title'); });
+    // newest = leave api ordering (server already reversed products.json)
+    var label = ({ newest:'newest first', title:'title A→Z', sku:'SKU A→Z', vendor:'vendor A→Z' })[state.sort] || 'newest first';
     var count = document.getElementById('prodCount');
-    if (count) count.textContent = 'All products — newest first (' + items.length + ')';
+    if (count) count.textContent = 'All products — ' + label + ' (' + items.length + ')';
     var grid = document.getElementById('prodGrid');
     if (!grid) return;
     grid.innerHTML = items.slice(0, 400).map(function (p) {
diff --git a/server.js b/server.js
index f942154..3dd4a41 100644
--- a/server.js
+++ b/server.js
@@ -22,11 +22,20 @@ const REMOTE_HOST = process.env.REMOTE_HOST || 'root@45.61.58.125';
 // Canonical 53-site list — same source as /tmp/dw-sites.txt (auto-discovers
 // every project under ~/Projects/ whose public/index.html has nsContactOpen
 // or .ns-modal markers — i.e. the DW family).
+function loadTossedDomains() {
+  try {
+    const buf = fs.readFileSync(path.join(PROJECTS_DIR, '_shared', 'data', 'dw-tossed-domains.json'), 'utf8');
+    return new Set((JSON.parse(buf).tossed || []).map(t => t.domain.replace(/\.com$/, '')));
+  } catch (e) { return new Set(); }
+}
+
 function discoverSites() {
+  const tossed = loadTossedDomains();
   const sites = [];
   const entries = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true });
   for (const e of entries) {
     if (!e.isDirectory()) continue;
+    if (tossed.has(e.name)) continue; // honor toss list
     const idx = path.join(PROJECTS_DIR, e.name, 'public', 'index.html');
     if (!fs.existsSync(idx)) continue;
     try {

← 6224e99 fix(mobile): products grid pushed off-screen below the 2x2 h  ·  back to Dw Hero Admin  ·  feat: per-site fashion-house style guide picker with color d 74b3917 →