← back to Wallco Ai
edges-review: add Snapshot tab sourcing flagged designs from shipped designs.json (prod PG is sparse → live tab shows ~0); add created-date+time chip per admin-card rule
ae1089ac01cc2a8f329df04bf60fa034f4301fa8 · 2026-05-27 08:40:55 -0700 · Steve Abrams
Files touched
M public/admin/edges-review.htmlM server.js
Diff
commit ae1089ac01cc2a8f329df04bf60fa034f4301fa8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 08:40:55 2026 -0700
edges-review: add Snapshot tab sourcing flagged designs from shipped designs.json (prod PG is sparse → live tab shows ~0); add created-date+time chip per admin-card rule
---
public/admin/edges-review.html | 107 ++++++++++++++++++++++++++++++++++-------
server.js | 68 +++++++++++++++++++++++++-
2 files changed, 157 insertions(+), 18 deletions(-)
diff --git a/public/admin/edges-review.html b/public/admin/edges-review.html
index 7f99ce4..7b12095 100644
--- a/public/admin/edges-review.html
+++ b/public/admin/edges-review.html
@@ -82,7 +82,7 @@
.tile.grid-err .badge { background:var(--grey); }
.id-tag {
- position:absolute; bottom:6px; right:6px;
+ position:absolute; top:6px; right:6px;
background:rgba(0,0,0,.65); color:#fff;
font:600 10px ui-monospace,Menlo,monospace;
padding:2px 6px; border-radius:3px;
@@ -119,6 +119,27 @@
}
.pager a:hover { background:#f3eee2; }
.pager .active { background:var(--ink); color:#fff; border-color:var(--ink); }
+
+ /* Source tabs (Live DB vs shipped Snapshot) */
+ .tabs { display:flex; gap:6px; margin:0 0 12px; flex-wrap:wrap; }
+ .tabs button {
+ padding:7px 14px; border:1px solid var(--line); border-radius:999px;
+ background:#fff; color:var(--faint); font:600 12px var(--sans);
+ cursor:pointer; transition:all .12s; letter-spacing:.02em;
+ }
+ .tabs button:hover { border-color:var(--gold); color:var(--ink); }
+ .tabs button.active { background:var(--ink); color:#fff; border-color:var(--ink); }
+ .tabs button .n { font:600 11px ui-monospace,Menlo,monospace; opacity:.7; margin-left:6px; }
+ .tabs button.active .n { opacity:.85; }
+
+ /* Created date+time chip (admin-card rule) */
+ .when {
+ position:absolute; bottom:6px; left:6px; right:6px;
+ background:rgba(0,0,0,.62); color:#fff;
+ font:600 9.5px ui-monospace,Menlo,monospace;
+ padding:2px 6px; border-radius:3px;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
+ }
</style>
</head>
<body>
@@ -135,6 +156,11 @@
</header>
<main>
+ <div class="tabs" id="tabs">
+ <button data-src="live" title="spoon_all_designs rows on this server (near-empty on prod)">Live DB<span class="n" id="n-live">—</span></button>
+ <button data-src="snapshot" title="flagged designs in the shipped data/designs.json (what prod actually has)">Snapshot (shipped to prod)<span class="n" id="n-snapshot">—</span></button>
+ </div>
+
<div class="controls">
<label for="sort">Sort</label>
<select id="sort">
@@ -180,10 +206,24 @@
sort: localStorage.getItem('edges-review-sort') || 'max_desc',
filter: localStorage.getItem('edges-review-filter') || 'all',
density: parseInt(localStorage.getItem('edges-review-density') || '200', 10),
+ source: localStorage.getItem('edges-review-source') || 'snapshot',
page: 1,
items: [],
+ cache: { live: null, snapshot: null }, // per-source item cache
+ };
+ const ENDPOINT = {
+ live: '/api/admin/edges-review/flagged',
+ snapshot: '/api/admin/edges-review/snapshot-flagged',
};
+ // Created date+time chip (admin-card standing rule). Local tz, date AND time.
+ function fmtDate(iso) {
+ if (!iso) return '';
+ const d = new Date(iso);
+ if (isNaN(d)) return '';
+ return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+ }
+
function classifyGrid(item) {
if (!item.ok) return 'err';
const e = item.edges_max || 0, m = item.mids_max || 0;
@@ -250,11 +290,14 @@
return `<div class="row"><span class="k">${k}</span><span class="v ${cls}">ΔE ${v.score}</span></div>`;
}).join('');
const reason = item.reason || (item.ok === false ? (item.error || 'scan error') : 'edges_fail');
+ const when = fmtDate(item.created_at);
+ const whenChip = when ? `<span class="when" title="${item.created_at}">🕓 ${when}</span>` : '';
return `
<a class="tile grid-${g}" href="/design/${item.id}" target="_blank" rel="noopener" title="${reason}">
<img src="/designs/img/by-id/${item.id}" loading="lazy" alt="">
<span class="badge">${badge}</span>
<span class="id-tag">#${item.id}</span>
+ ${whenChip}
<div class="tooltip">
${lensRows || `<div class="row"><span class="k">error</span><span class="v fail">${item.error || 'unknown'}</span></div>`}
</div>
@@ -308,22 +351,52 @@
applyDensity(state.density);
});
- // Fetch
- fetch('/api/admin/edges-review/flagged')
- .then(r => r.json())
- .then(data => {
- if (!data.ok) {
- document.getElementById('grid').innerHTML =
- `<div class="empty">error: ${data.error || 'unknown'}</div>`;
- return;
- }
- state.items = data.items || [];
- render();
- })
- .catch(err => {
- document.getElementById('grid').innerHTML =
- `<div class="empty">fetch failed: ${err.message}</div>`;
- });
+ // ── Source tabs (Live DB vs shipped Snapshot) ──
+ function setActiveTab() {
+ document.querySelectorAll('#tabs button').forEach(b =>
+ b.classList.toggle('active', b.dataset.src === state.source));
+ }
+
+ function loadSource(src, force) {
+ state.source = src;
+ localStorage.setItem('edges-review-source', src);
+ setActiveTab();
+ state.page = 1;
+ if (!force && state.cache[src]) { state.items = state.cache[src]; render(); return; }
+ const grid = document.getElementById('grid');
+ grid.innerHTML = '<div class="loading">loading flagged designs…</div>';
+ fetch(ENDPOINT[src])
+ .then(r => r.json())
+ .then(data => {
+ if (!data.ok) {
+ grid.innerHTML = `<div class="empty">error: ${data.error || 'unknown'}</div>`;
+ return;
+ }
+ const items = data.items || [];
+ state.cache[src] = items;
+ const n = document.getElementById('n-' + src);
+ if (n) n.textContent = items.length;
+ if (state.source === src) { state.items = items; render(); }
+ })
+ .catch(err => {
+ grid.innerHTML = `<div class="empty">fetch failed: ${err.message}</div>`;
+ });
+ }
+
+ document.querySelectorAll('#tabs button').forEach(b =>
+ b.addEventListener('click', () => loadSource(b.dataset.src)));
+
+ // Initial load + background count for the inactive tab so both badges fill in.
+ setActiveTab();
+ loadSource(state.source);
+ const other = state.source === 'live' ? 'snapshot' : 'live';
+ fetch(ENDPOINT[other]).then(r => r.json()).then(d => {
+ if (d && d.ok) {
+ state.cache[other] = d.items || [];
+ const n = document.getElementById('n-' + other);
+ if (n) n.textContent = (d.items || []).length;
+ }
+ }).catch(() => {});
})();
</script>
</body>
diff --git a/server.js b/server.js
index b05c354..6737a53 100644
--- a/server.js
+++ b/server.js
@@ -1314,7 +1314,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
// structured tag — re-parse it here so the client sees clean lens scores.
const rawJson = psqlQuery(
"SELECT COALESCE(json_agg(t), '[]'::json) FROM (" +
- "SELECT id, notes FROM spoon_all_designs " +
+ "SELECT id, notes, created_at FROM spoon_all_designs " +
"WHERE notes LIKE '%EDGES_FAIL:%' OR notes LIKE '%EDGES_SCAN_ERROR:%' " +
"ORDER BY id DESC" +
") t;"
@@ -1349,6 +1349,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
ok: true,
image_url: `/designs/img/by-id/${r.id}`,
verdict: 'FAIL',
+ created_at: r.created_at,
lenses,
edges_max: edgesMax,
mids_max: midsMax,
@@ -1365,6 +1366,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
ok: false,
image_url: `/designs/img/by-id/${r.id}`,
error: e[1],
+ created_at: r.created_at,
ts: e[2].trim(),
reason: `scan error: ${e[1]}`,
};
@@ -1376,6 +1378,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
ok: false,
image_url: `/designs/img/by-id/${r.id}`,
error: 'unparseable_tag',
+ created_at: r.created_at,
reason: 'edges_fail tag present but unparseable',
};
});
@@ -1386,6 +1389,69 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
}
});
+// GET /api/admin/edges-review/snapshot-flagged — same flagged list, but sourced
+// from the SHIPPED data/designs.json snapshot instead of PG. On prod the
+// spoon_all_designs table is near-empty (~350 rows) so the PG-backed /flagged
+// returns ~0; the real ~11k edges-flagged designs only exist in the deployed
+// snapshot. This endpoint reads that file so the "Snapshot" tab can surface
+// them. Cached by file mtime so we don't re-parse 42k rows on every hit.
+let _snapFlaggedCache = { mtime: -1, items: null };
+app.get('/api/admin/edges-review/snapshot-flagged', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ try {
+ const fp = path.join(__dirname, 'data', 'designs.json');
+ const st = fs.statSync(fp);
+ if (_snapFlaggedCache.items && _snapFlaggedCache.mtime === st.mtimeMs) {
+ return res.json({ ok: true, source: 'snapshot', count: _snapFlaggedCache.items.length, items: _snapFlaggedCache.items });
+ }
+ const all = JSON.parse(fs.readFileSync(fp, 'utf8'));
+ // Snapshot notes lack the leading "| " the PG tag carries — match leniently.
+ const FAIL_RE = /EDGES_FAIL:\s*lens=([^ ]+)\s+max=([\d.]+)\s+grid=(\w+)/;
+ const ERR_RE = /EDGES_SCAN_ERROR:\s*([^ |]+)/;
+ const items = [];
+ for (const d of all) {
+ const note = String(d.notes || d.unpublish_reason || '');
+ const m = note.match(FAIL_RE);
+ if (m) {
+ const lensStr = m[1], maxDe = parseFloat(m[2]), grid = m[3];
+ const lenses = {}; let edgesMax = 0, midsMax = 0;
+ lensStr.split(',').forEach(kv => {
+ const [k, v] = kv.split(':'); const score = parseFloat(v);
+ if (!Number.isFinite(score)) return;
+ let verdict = 'PASS';
+ if (score > 12.0) verdict = 'FAIL'; else if (score > 5.0) verdict = 'WARN';
+ lenses[k] = { score, verdict };
+ if (k === 'top' || k === 'bottom' || k === 'left' || k === 'right') {
+ if (score > edgesMax) edgesMax = score;
+ } else if (k === 'h_mid' || k === 'v_mid') {
+ if (score > midsMax) midsMax = score;
+ }
+ });
+ items.push({
+ id: d.id, ok: true, image_url: `/designs/img/by-id/${d.id}`,
+ verdict: 'FAIL', created_at: d.created_at || null, lenses,
+ edges_max: edgesMax, mids_max: midsMax, max_de: maxDe, grid,
+ title: d.title || null, category: d.category || null,
+ reason: `${grid} @ max ΔE ${maxDe}`,
+ });
+ continue;
+ }
+ const e = note.match(ERR_RE);
+ if (e) {
+ items.push({
+ id: d.id, ok: false, image_url: `/designs/img/by-id/${d.id}`,
+ error: e[1], created_at: d.created_at || null,
+ title: d.title || null, reason: `scan error: ${e[1]}`,
+ });
+ }
+ }
+ _snapFlaggedCache = { mtime: st.mtimeMs, items };
+ res.json({ ok: true, source: 'snapshot', count: items.length, items });
+ } catch (err) {
+ res.status(500).json({ ok: false, error: err.message });
+ }
+});
+
// GET /api/ghost-review/design/:id — fetch ANY design's review metadata,
// even if it's not in the ghost-flagged set. Lets the viewer open arbitrary
// design ids that Steve found "ugly but not detected" so he can run the
← 998a6c8 deploy: exclude data/generated_cactus_quarantine from rsync
·
back to Wallco Ai
·
cactus-vision-score: skip user_removed designs (only score t 989c3d0 →