[object Object]

← back to Dw Collections Viewer

dw-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

15a2c8fefb535ed7685fcf15c86fdf997e7f288d · 2026-05-06 20:24:39 -0700 · Steve

Files touched

Diff

commit 15a2c8fefb535ed7685fcf15c86fdf997e7f288d
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 20:24:39 2026 -0700

    dw-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
---
 data/site-status.json | 92 +++++++++++++++++++++++++--------------------------
 public/index.html     | 13 ++++++--
 server.js             | 14 ++++++++
 3 files changed, 71 insertions(+), 48 deletions(-)

diff --git a/data/site-status.json b/data/site-status.json
index 3cc2e1e..29e6a7a 100644
--- a/data/site-status.json
+++ b/data/site-status.json
@@ -2,231 +2,231 @@
   "silkwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.733Z"
+    "probed_at": "2026-05-07T02:47:32.852Z"
   },
   "linenwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.734Z"
+    "probed_at": "2026-05-07T02:47:32.949Z"
   },
   "raffiawallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.889Z"
+    "probed_at": "2026-05-07T02:47:33.056Z"
   },
   "corkwallcovering": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.903Z"
+    "probed_at": "2026-05-07T02:47:33.159Z"
   },
   "jutewallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.913Z"
+    "probed_at": "2026-05-07T02:47:33.004Z"
   },
   "silkwallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.940Z"
+    "probed_at": "2026-05-07T02:47:32.882Z"
   },
   "textilewallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.980Z"
+    "probed_at": "2026-05-07T02:47:33.289Z"
   },
   "fabricwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:14.999Z"
+    "probed_at": "2026-05-07T02:47:33.287Z"
   },
   "raffiawalls": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.004Z"
+    "probed_at": "2026-05-07T02:47:33.167Z"
   },
   "silverleafwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.008Z"
+    "probed_at": "2026-05-07T02:47:33.418Z"
   },
   "vinylwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.129Z"
+    "probed_at": "2026-05-07T02:47:33.545Z"
   },
   "metallicwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.147Z"
+    "probed_at": "2026-05-07T02:47:33.416Z"
   },
   "micawallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.195Z"
+    "probed_at": "2026-05-07T02:47:33.516Z"
   },
   "mylarwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.262Z"
+    "probed_at": "2026-05-07T02:47:33.656Z"
   },
   "suedewallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.314Z"
+    "probed_at": "2026-05-07T02:47:33.749Z"
   },
   "madagascarwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.321Z"
+    "probed_at": "2026-05-07T02:47:33.656Z"
   },
   "stringwallpaper": {
     "http_status": 301,
     "redirect_target": "linenwallpaper.com",
-    "probed_at": "2026-05-07T00:33:15.432Z"
+    "probed_at": "2026-05-07T02:47:33.777Z"
   },
   "embroideredwallpaper": {
     "http_status": 301,
     "redirect_target": "fabricwallpaper.com",
-    "probed_at": "2026-05-07T00:33:15.438Z"
+    "probed_at": "2026-05-07T02:47:33.811Z"
   },
   "1800swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.528Z"
+    "probed_at": "2026-05-07T02:47:33.902Z"
   },
   "1890swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.533Z"
+    "probed_at": "2026-05-07T02:47:33.878Z"
   },
   "1900swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.561Z"
+    "probed_at": "2026-05-07T02:47:33.978Z"
   },
   "1930swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.591Z"
+    "probed_at": "2026-05-07T02:47:34.107Z"
   },
   "1920swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.637Z"
+    "probed_at": "2026-05-07T02:47:34.042Z"
   },
   "1940swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.640Z"
+    "probed_at": "2026-05-07T02:47:34.204Z"
   },
   "1960swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.659Z"
+    "probed_at": "2026-05-07T02:47:34.331Z"
   },
   "1970swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.715Z"
+    "probed_at": "2026-05-07T02:47:34.384Z"
   },
   "1950swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.724Z"
+    "probed_at": "2026-05-07T02:47:34.262Z"
   },
   "1980swallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.839Z"
+    "probed_at": "2026-05-07T02:47:34.411Z"
   },
   "wallpapersback": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.840Z"
+    "probed_at": "2026-05-07T02:47:34.517Z"
   },
   "retrowalls": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.861Z"
+    "probed_at": "2026-05-07T02:47:34.476Z"
   },
   "agedwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.880Z"
+    "probed_at": "2026-05-07T02:47:34.579Z"
   },
   "museumwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.938Z"
+    "probed_at": "2026-05-07T02:47:34.726Z"
   },
   "handcraftedwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.960Z"
+    "probed_at": "2026-05-07T02:47:34.645Z"
   },
   "pastelwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:15.996Z"
+    "probed_at": "2026-05-07T02:47:34.786Z"
   },
   "restorationwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.076Z"
+    "probed_at": "2026-05-07T02:47:34.697Z"
   },
   "glitterwalls": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.155Z"
+    "probed_at": "2026-05-07T02:47:34.828Z"
   },
   "greenwallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.232Z"
+    "probed_at": "2026-05-07T02:47:34.895Z"
   },
   "contractwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.318Z"
+    "probed_at": "2026-05-07T02:47:35.096Z"
   },
   "naturalwallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.319Z"
+    "probed_at": "2026-05-07T02:47:34.959Z"
   },
   "recycledwallpaper": {
     "http_status": 405,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.361Z"
+    "probed_at": "2026-05-07T02:47:35.042Z"
   },
   "hospitalitywallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.438Z"
+    "probed_at": "2026-05-07T02:47:35.233Z"
   },
   "hotelwallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.473Z"
+    "probed_at": "2026-05-07T02:47:35.074Z"
   },
   "healthcarewallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.476Z"
+    "probed_at": "2026-05-07T02:47:35.233Z"
   },
   "restaurantwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.483Z"
+    "probed_at": "2026-05-07T02:47:35.375Z"
   },
   "saloonwallpaper": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.541Z"
+    "probed_at": "2026-05-07T02:47:35.383Z"
   },
   "architecturalwallcoverings": {
     "http_status": 200,
     "redirect_target": null,
-    "probed_at": "2026-05-07T00:33:16.601Z"
+    "probed_at": "2026-05-07T02:47:35.487Z"
   }
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index a4fcae1..d608951 100644
--- a/public/index.html
+++ b/public/index.html
@@ -48,6 +48,10 @@ main { flex: 1; display: grid; grid-template-columns: 1fr 480px; min-height: 0 }
 .card .stats .pill { display: inline-block; padding: 2px 6px; margin-left: 4px;
   background: var(--rule); color: var(--gold); border-radius: 0 }
 .card .stats .pill.zero { color: var(--red) }
+.card .stats .pill.pill-stale {
+  color: #c66c4d; border: 1px solid #c66c4d; background: rgba(198,108,77,0.10);
+  font-weight: 500; cursor: help;
+}
 
 aside { border-left: 1px solid var(--rule); background: var(--bg2);
   display: flex; flex-direction: column; min-height: 0 }
@@ -272,6 +276,7 @@ body.bulk .card.selected { border-color: var(--gold) }
       <button class="btn" onclick="selectNone()">Clear</button>
       <button class="btn" onclick="selectByCount('zero')">With 0 products</button>
       <button class="btn" onclick="selectByCount('thin')">Thin (&lt;30)</button>
+      <button class="btn" onclick="selectByCount('stale')">Stale (&gt;30 days)</button>
       <button class="btn go" onclick="bulkRender()">Render selected</button>
     </div>
     <div class="grid" id="grid"></div>
@@ -347,7 +352,7 @@ function renderGrid() {
         </div>
         <div class="info">
           <div class="name">${esc(slug)}${pinned}</div>
-          <div class="stats">${s.domain}<span class="pill${cls}">${s.product_count}</span><span class="pill">${collCt}c</span></div>
+          <div class="stats">${s.domain}<span class="pill${cls}">${s.product_count}</span><span class="pill">${collCt}c</span>${s.is_stale ? `<span class="pill pill-stale" title="Last rendered ${s.age_days} days ago — needs refresh">⟲ ${s.age_days}d</span>` : ''}</div>
         </div>
       </div>`;
   }).join('');
@@ -386,7 +391,11 @@ function selectAllVisible() {
 function selectNone() { BULK_SELECTED.clear(); document.getElementById('bulkCount').textContent = 0; renderGrid(); }
 function selectByCount(mode) {
   Object.entries(SITES).forEach(([slug, s]) => {
-    if ((mode === 'zero' && s.product_count === 0) || (mode === 'thin' && s.product_count > 0 && s.product_count < 30)) BULK_SELECTED.add(slug);
+    if (
+      (mode === 'zero'  && s.product_count === 0) ||
+      (mode === 'thin'  && s.product_count > 0 && s.product_count < 30) ||
+      (mode === 'stale' && s.is_stale)
+    ) BULK_SELECTED.add(slug);
   });
   document.getElementById('bulkCount').textContent = BULK_SELECTED.size;
   renderGrid();
diff --git a/server.js b/server.js
index 92c6bd9..0f3fc43 100644
--- a/server.js
+++ b/server.js
@@ -120,12 +120,23 @@ function listSites() {
   const rules = loadRules();
   const status = loadSiteStatus();
   const sites = {};
+  const STALE_DAYS = 30;
+  const now = Date.now();
   for (const slug of Object.keys(rules.sites)) {
     const rule = rules.sites[slug];
     const fp = path.join(HOME, 'Projects', slug, 'data', 'products.json');
     const products = readJsonSafe(fp) || [];
     const hero = products[0] || {};
     const st = status[slug] || {};
+    let lastRenderIso = null, ageDays = null, isStale = false;
+    if (fs.existsSync(fp)) {
+      try {
+        const m = fs.statSync(fp).mtimeMs;
+        lastRenderIso = new Date(m).toISOString();
+        ageDays = Math.floor((now - m) / 86400000);
+        isStale = ageDays >= STALE_DAYS;
+      } catch {}
+    }
     sites[slug] = {
       slug,
       domain: rule.domain,
@@ -137,6 +148,9 @@ function listSites() {
       hero_sku: rule.hero_sku || null,
       cover_image_url: rule.cover_image_url || null,  // manual drag-drop override
       product_count: products.length,
+      last_render_iso: lastRenderIso,
+      age_days: ageDays,
+      is_stale: isStale,
       hero: hero.image_url ? {
         image_url: hero.image_url,
         title: hero.title || '',

← f3e88a0 Fix HTML tokenizer breaking outer script — escape nested </s  ·  back to Dw Collections Viewer  ·  dw-collections-viewer: 'Preview sort' button — re-render wit cbb7a20 →