[object Object]

← 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

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 →