← back to Wallco Ai
edges-review: sort by status (removed/active first) + dynamic REMOVED/ACTIVE/NOT-ACTIVE badge; show human failure reason in audit tooltip (e.g. 'Mismatch at horizontal midline (ΔE 12.69 at h_mid)')
017a7119b91d61498d78bcab998c7b6861f656da · 2026-05-27 09:21:11 -0700 · Steve Abrams
Files touched
M public/admin/edges-review.htmlM server.js
Diff
commit 017a7119b91d61498d78bcab998c7b6861f656da
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 09:21:11 2026 -0700
edges-review: sort by status (removed/active first) + dynamic REMOVED/ACTIVE/NOT-ACTIVE badge; show human failure reason in audit tooltip (e.g. 'Mismatch at horizontal midline (ΔE 12.69 at h_mid)')
---
public/admin/edges-review.html | 19 +++++++++++++++++--
server.js | 28 ++++++++++++++++++++++++----
2 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/public/admin/edges-review.html b/public/admin/edges-review.html
index 0dfa73f..f6312e1 100644
--- a/public/admin/edges-review.html
+++ b/public/admin/edges-review.html
@@ -80,6 +80,8 @@
.tile.grid-mids .badge { background:var(--orange); }
.tile.grid-both .badge { background:#7a1818; }
.tile.grid-err .badge { background:var(--grey); }
+ .badge.st-removed { background:#444 !important; } /* deleted */
+ .badge.st-active { background:var(--green) !important; } /* still published */
.id-tag {
position:absolute; top:6px; right:6px;
@@ -97,6 +99,8 @@
opacity:0; pointer-events:none; transition:opacity .14s;
z-index:2; line-height:1.4;
}
+ .tooltip .why { color:#ffd68a; font-weight:700; margin-bottom:4px; padding-bottom:3px;
+ border-bottom:1px solid rgba(255,255,255,.18); white-space:normal; line-height:1.3; }
.tooltip .row { display:flex; justify-content:space-between; }
.tooltip .row .k { color:#c9a14b; }
.tooltip .row .v { color:#fff; }
@@ -209,6 +213,8 @@
<option value="id_desc">Newest id</option>
<option value="id_asc">Oldest id</option>
<option value="grid">By defect type</option>
+ <option value="removed_first">Status: removed first</option>
+ <option value="active_first">Status: active first</option>
</select>
<label for="filter">Filter</label>
@@ -312,6 +318,10 @@
const order = { both:0, edges:1, mids:2, err:3 };
return (order[classifyGrid(a)] || 9) - (order[classifyGrid(b)] || 9);
}
+ // Status rank: removed (2) > unpublished/flagged (1) > active/published (0)
+ const rank = it => it.removed ? 2 : (it.published ? 0 : 1);
+ if (state.sort === 'removed_first') return rank(b) - rank(a) || b.id - a.id;
+ if (state.sort === 'active_first') return rank(a) - rank(b) || b.id - a.id;
return 0;
});
@@ -333,7 +343,11 @@
} else {
grid.innerHTML = slice.map(item => {
const g = classifyGrid(item);
- const badge = g === 'err' ? 'SCAN ERR' : 'NOT ACTIVE';
+ // Status badge: removed (deleted) / active (still published) / flagged (unpublished)
+ const badge = item.removed ? 'REMOVED'
+ : item.published ? 'ACTIVE'
+ : (g === 'err' ? 'SCAN ERR' : 'NOT ACTIVE');
+ const badgeCls = item.removed ? 'st-removed' : item.published ? 'st-active' : '';
const lenses = item.lenses || {};
const lensRows = ['top','bottom','left','right','h_mid','v_mid'].map(k => {
const v = lenses[k];
@@ -351,10 +365,11 @@
<a class="thumb" href="/design/${item.id}" target="_blank" rel="noopener">
<img src="/designs/img/by-id/${item.id}" loading="lazy" alt="">
</a>
- <span class="badge">${badge}</span>
+ <span class="badge ${badgeCls}">${badge}</span>
<span class="id-tag">#${item.id}</span>
${whenChip}
<div class="tooltip">
+ <div class="why">${reason}</div>
${lensRows || `<div class="row"><span class="k">error</span><span class="v fail">${item.error || 'unknown'}</span></div>`}
</div>
</div>`;
diff --git a/server.js b/server.js
index 1043010..10cb1c9 100644
--- a/server.js
+++ b/server.js
@@ -1350,6 +1350,21 @@ app.get('/api/admin/elements/items', (req, res) => {
}
});
+// Human-readable failure reason from the failing lenses — "show why it failed"
+// (e.g. "Mismatch at horizontal midline"). Maps the 6 lens keys to plain phrases.
+function edgesFailReason(lenses, maxDe) {
+ const failed = Object.entries(lenses || {})
+ .filter(([, v]) => v && v.verdict === 'FAIL').map(([k]) => k);
+ if (!failed.length) return `Seam ΔE ${maxDe}`;
+ const phrases = [];
+ if (failed.includes('h_mid')) phrases.push('horizontal midline');
+ if (failed.includes('v_mid')) phrases.push('vertical midline');
+ if (failed.includes('top') || failed.includes('bottom')) phrases.push('top–bottom wrap');
+ if (failed.includes('left') || failed.includes('right')) phrases.push('left–right wrap');
+ const worst = failed.reduce((m, k) => (lenses[k].score > (lenses[m]?.score || 0) ? k : m), failed[0]);
+ return `Mismatch at ${phrases.join(' + ')} (ΔE ${lenses[worst].score} at ${worst})`;
+}
+
app.get('/api/admin/edges-review/flagged', (req, res) => {
if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
try {
@@ -1357,7 +1372,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, created_at FROM spoon_all_designs " +
+ "SELECT id, notes, created_at, is_published, user_removed FROM spoon_all_designs " +
"WHERE notes LIKE '%EDGES_FAIL:%' OR notes LIKE '%EDGES_SCAN_ERROR:%' " +
"ORDER BY id DESC" +
") t;"
@@ -1393,13 +1408,14 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
image_url: `/designs/img/by-id/${r.id}`,
verdict: 'FAIL',
created_at: r.created_at,
+ removed: !!r.user_removed, published: !!r.is_published,
lenses,
edges_max: edgesMax,
mids_max: midsMax,
max_de: maxDe,
grid,
ts,
- reason: `${grid} @ max ΔE ${maxDe}`,
+ reason: edgesFailReason(lenses, maxDe),
};
}
const e = r.notes && r.notes.match(ERR_RE);
@@ -1410,6 +1426,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
image_url: `/designs/img/by-id/${r.id}`,
error: e[1],
created_at: r.created_at,
+ removed: !!r.user_removed, published: !!r.is_published,
ts: e[2].trim(),
reason: `scan error: ${e[1]}`,
};
@@ -1422,6 +1439,7 @@ app.get('/api/admin/edges-review/flagged', (req, res) => {
image_url: `/designs/img/by-id/${r.id}`,
error: 'unparseable_tag',
created_at: r.created_at,
+ removed: !!r.user_removed, published: !!r.is_published,
reason: 'edges_fail tag present but unparseable',
};
});
@@ -1475,7 +1493,8 @@ app.get('/api/admin/edges-review/snapshot-flagged', (req, res) => {
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}`,
+ removed: !!d.user_removed, published: !!d.is_published,
+ reason: edgesFailReason(lenses, maxDe),
});
continue;
}
@@ -1484,7 +1503,8 @@ app.get('/api/admin/edges-review/snapshot-flagged', (req, res) => {
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]}`,
+ removed: !!d.user_removed, published: !!d.is_published,
+ title: d.title || null, reason: `Scan error — ${e[1]} (file missing or unreadable)`,
});
}
}
← d042eec bad_examples registry: dedicated anti-pattern DB ('keep all
·
back to Wallco Ai
·
add /admin/defect-registry web viewer over wallco_defect_reg a1cbb55 →