← 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
M data/site-status.jsonM public/index.htmlM server.js
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 (<30)</button>
+ <button class="btn" onclick="selectByCount('stale')">Stale (>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 →