← back to Dw Photo Capture
Add /selfcheck self-check: server-side health report (catalog/index/queue/token/data) + auto-cleans stale photo refs (kills the dead-link class); HTML report; version pill links to it. selfcheck.sh runs server + deep Playwright UI crawl
8cdab9be47ab6e2c846314ac6834d1d6c9f99625 · 2026-06-25 07:47:56 -0700 · steve@designerwallcoverings.com
Files touched
M data/build.jsonM public/index.htmlA selfcheck-ui.cjsA selfcheck.shM server.js
Diff
commit 8cdab9be47ab6e2c846314ac6834d1d6c9f99625
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Thu Jun 25 07:47:56 2026 -0700
Add /selfcheck self-check: server-side health report (catalog/index/queue/token/data) + auto-cleans stale photo refs (kills the dead-link class); HTML report; version pill links to it. selfcheck.sh runs server + deep Playwright UI crawl
---
data/build.json | 6 ++++--
public/index.html | 4 ++--
selfcheck-ui.cjs | 40 ++++++++++++++++++++++++++++++++++++++++
selfcheck.sh | 7 +++++++
server.js | 37 +++++++++++++++++++++++++++++++++++++
5 files changed, 90 insertions(+), 4 deletions(-)
diff --git a/data/build.json b/data/build.json
index 9054d2e..17b1a54 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
{
- "next": 24,
+ "next": 26,
"map": {
"578af86f": 2,
"bc95fdb0": 3,
@@ -22,6 +22,8 @@
"c3efeca8": 20,
"859c4943": 21,
"a455891f": 22,
- "b6ea475b": 23
+ "b6ea475b": 23,
+ "cf63df2c": 24,
+ "3c666b73": 25
}
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 5d4d178..eacf2e8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -19,7 +19,7 @@
header{position:sticky;top:0;z-index:10;background:rgba(15,14,12,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:max(10px,env(safe-area-inset-top)) 14px 10px}
h1{font:600 17px/1.1 "SF Pro Display",sans-serif;letter-spacing:.04em;margin:0 0 8px;text-transform:uppercase}
h1 span{color:var(--gold)}
- h1 .ver{font:600 10px/1 ui-monospace,Menlo,monospace;color:#1b1407;background:var(--green);border-radius:99px;padding:3px 7px;letter-spacing:.04em;vertical-align:middle;text-transform:none;font-style:normal;margin-left:6px}
+ h1 .ver{font:600 10px/1 ui-monospace,Menlo,monospace;text-decoration:none;color:#1b1407;background:var(--green);border-radius:99px;padding:3px 7px;letter-spacing:.04em;vertical-align:middle;text-transform:none;font-style:normal;margin-left:6px}
.bar{height:8px;border-radius:99px;background:#2a261f;overflow:hidden;margin:6px 0}
.bar i{display:block;height:100%;background:linear-gradient(90deg,var(--gold),#e7c878);width:0;transition:width .3s}
.meta{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
@@ -120,7 +120,7 @@
</head>
<body>
<header>
- <h1>Mobile DW SKU <span>Photos</span> <em class="ver">__VER__</em></h1>
+ <h1>Mobile DW SKU <span>Photos</span> <a href="/selfcheck" class="ver" title="tap for self-check">__VER__</a></h1>
<div class="bar"><i id="prog"></i></div>
<div class="meta"><span id="count">…</span><span id="remain"></span></div>
<div class="controls">
diff --git a/selfcheck-ui.cjs b/selfcheck-ui.cjs
new file mode 100644
index 0000000..441787b
--- /dev/null
+++ b/selfcheck-ui.cjs
@@ -0,0 +1,40 @@
+const { chromium } = require('/Users/stevestudio2/.npm-global/lib/node_modules/playwright/index.js');
+const BASE='http://127.0.0.1:9890';
+(async()=>{
+const fails=[], errs=[], notes=[];
+const b=await chromium.launch();
+const ctx=await b.newContext({ httpCredentials:{username:'admin',password:'DW2024!'}, viewport:{width:430,height:932} });
+const pg=await ctx.newPage();
+pg.on('console',m=>{ if(m.type()==='error') errs.push(m.text().slice(0,160)); });
+pg.on('requestfailed',r=>fails.push(`${r.method()} ${r.url()} — ${(r.failure()||{}).errorText}`));
+pg.on('response',r=>{ if(r.status()>=400) fails.push(`HTTP ${r.status()} ${r.url().replace(BASE,'')}`); });
+const log=(m)=>notes.push(m);
+await pg.goto(BASE+'/?fresh',{waitUntil:'networkidle',timeout:30000});
+log('version pill = '+await pg.$eval('.ver',e=>e.textContent).catch(()=>'(none)'));
+const chips=await pg.$$eval('.chip',els=>els.map(e=>e.dataset.f));
+log('chips present: '+chips.join(', '));
+for(const c of ['need','live','done','new','all']){
+ const chip=await pg.$(`.chip[data-f="${c}"]`); if(!chip){ log(`MISSING chip ${c}`); continue; }
+ await chip.click(); await pg.waitForTimeout(900);
+ log(`chip ${c}: count="${await pg.$eval('#count',e=>e.textContent).catch(()=>'?')}" cards=${await pg.$$eval('.card',e=>e.length)}`);
+}
+await pg.click('.chip[data-f="any"]'); await pg.waitForTimeout(400);
+await pg.fill('#q','maranello'); await pg.waitForTimeout(1300);
+log('any/maranello cards='+await pg.$$eval('.card',e=>e.length));
+await pg.click('.chip[data-f="shop"]'); await pg.waitForTimeout(400);
+await pg.fill('#q','natural cork'); await pg.waitForTimeout(3500);
+log('shop/"natural cork" cards='+await pg.$$eval('.card',e=>e.length));
+const galBtn=await pg.$('.gal');
+if(galBtn){ await galBtn.click(); await pg.waitForTimeout(1800);
+ log(`gallery opened=${await pg.$eval('#gallery',e=>!e.hidden).catch(()=>false)} tiles=${await pg.$$eval('.gtile',e=>e.length).catch(()=>0)}`);
+ const cl=await pg.$('#galClose'); if(cl){await cl.click();log('gallery close OK');} else log('MISSING galClose');
+} else log('no .gal button in current view');
+await pg.click('.chip[data-f="need"]'); await pg.waitForTimeout(500);
+await pg.$eval('#dens',e=>{e.value=3;e.dispatchEvent(new Event('input'));e.dispatchEvent(new Event('change'));}); await pg.waitForTimeout(400);
+log('density=3 -> columns='+await pg.$eval('main',e=>getComputedStyle(e).gridTemplateColumns.split(' ').length));
+await b.close();
+console.log('=== NOTES ==='); notes.forEach(n=>console.log(' •',n));
+const uf=[...new Set(fails)];
+console.log('\n=== FAILED REQUESTS / DEAD LINKS ('+uf.length+') ==='); uf.forEach(f=>console.log(' x',f));
+console.log('\n=== CONSOLE ERRORS ('+[...new Set(errs)].length+') ==='); [...new Set(errs)].slice(0,15).forEach(e=>console.log(' !',e));
+})();
diff --git a/selfcheck.sh b/selfcheck.sh
new file mode 100755
index 0000000..42e71b7
--- /dev/null
+++ b/selfcheck.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Quick health: server-side checks + auto-clean stale photo refs
+echo "--- /selfcheck (server-side) ---"
+curl -s -u admin:DW2024! http://127.0.0.1:9890/selfcheck | python3 -m json.tool 2>/dev/null
+# Deep: headless click-through for dead links (needs global playwright)
+echo "\n--- deep UI click-through (dead links) ---"
+GP=$(npm root -g 2>/dev/null); NODE_PATH="$GP" node "$(dirname "$0")/selfcheck-ui.cjs" 2>&1 | sed -n '/FAILED REQUESTS/,$p'
diff --git a/server.js b/server.js
index e89534e..9030824 100644
--- a/server.js
+++ b/server.js
@@ -188,6 +188,43 @@ const server = http.createServer((req, res) => {
if (u.pathname === '/healthz') return send(res, 200, { ok: true });
+ // /selfcheck — re-runnable health report. Verifies data integrity + AUTO-CLEANS stale
+ // photo refs (progress entries pointing at a deleted /photos file = the dead-link class).
+ if (u.pathname === '/selfcheck') {
+ const checks = [];
+ const ck = (name, ok, detail) => checks.push({ name, ok: !!ok, detail });
+ ck('server', true, 'up');
+ ck('shopify token', !!TOKEN, TOKEN ? 'present' : 'MISSING — creates/photos will fail');
+ ck('catalog indexed', CATALOG.length > 0, `${CATALOG.length} live Fentucci`);
+ ck('lookup index', INDEX.length > 0, `${INDEX.length} total (incl. ${INDEX.filter(x => x.needs_create).length} new-from-sheet)`);
+ ck('sheet GRS loaded', SHEET.length > 0, `${SHEET.length} GRS rows`);
+ let queueN = -1; try { queueN = loadJSON(QUEUE_FILE, []).length; } catch (e) {}
+ ck('worklist queue', queueN >= 0, `${queueN} need a photo`);
+ // stale local-photo refs → auto-clean so no card 404s
+ let stale = 0, fixed = 0;
+ for (const [sku, e] of Object.entries(progress)) {
+ if (e.photo && e.photo.startsWith('/photos/') && !fs.existsSync(path.join(PHOTOS, path.basename(e.photo)))) {
+ stale++; if (e.shopify_pushed || e.live || e.created) e.photo = null; else delete progress[sku]; fixed++;
+ }
+ }
+ if (fixed) saveProgress();
+ ck('stale photo refs', true, stale ? `cleaned ${fixed} dead thumbnail ref(s)` : 'none');
+ const ok = checks.every(c => c.ok);
+ const wantsHtml = (req.headers.accept || '').includes('text/html');
+ if (!wantsHtml) return send(res, 200, { ok, version: buildLabel(), checks, ts: new Date().toISOString() });
+ const rows = checks.map(c => `<tr><td>${c.ok ? '✅' : '❌'}</td><td>${c.name}</td><td class="d">${c.detail || ''}</td></tr>`).join('');
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
+ return res.end(`<!doctype html><meta name=viewport content="width=device-width,initial-scale=1"><style>
+ body{font:15px -apple-system,sans-serif;background:#0f0e0c;color:#f3efe7;margin:0;padding:20px}
+ h1{font-size:18px;color:#c8a24a;letter-spacing:.04em} .big{font-size:22px;font-weight:800;margin:8px 0}
+ table{width:100%;border-collapse:collapse;margin-top:10px} td{padding:9px 6px;border-bottom:1px solid #2e2a25;vertical-align:top}
+ .d{color:#9a9184;font-size:13px} a{color:#c8a24a}</style>
+ <h1>DW SKU Photos — Self-Check</h1>
+ <div class="big">${ok ? '✅ All systems go' : '❌ Issues found'} · ${buildLabel()}</div>
+ <table>${rows}</table>
+ <p class="d">${new Date().toLocaleString()} · <a href="/selfcheck">re-run</a> · <a href="/">← app</a></p>`);
+ }
+
if (u.pathname === '/' || u.pathname === '/index.html') {
// no-store: the UI evolves fast and is read fresh per request — never let a
// browser serve a stale cached page (that hid the photo editor after a deploy).
← f90170b Make PWA icon + manifest public (no auth) so iOS Add-to-Home
·
back to Dw Photo Capture
·
auto-save: 2026-06-25T08:32:54 (1 files) — server.js 8ca15e9 →