← back to Wallco Ai
edges-agent: unpublish 11,274 seam-defect designs + admin red-outline review
57869a4c1611ee383942dff4049959013010e08c · 2026-05-26 07:49:58 -0700 · Steve Abrams
Ran the 6-lens seamless-tile scanner (~/.claude/skills/edges-agent) across
all 27,267 published designs. 41% failed — primarily SDXL internal-grid
leaks (7,333), with 2,883 catastrophic both-axis fails and 1,058 edge-wrap
regressions. 38 scan errors (missing/corrupt local PNG).
PG writes (via apply-unpublish.py):
- is_published=false on 11,312 flagged ids
- notes appended with ` | EDGES_FAIL: lens=... max=... grid=... ts=...`
(or EDGES_SCAN_ERROR for missing files), preserving prior provenance
Server changes:
- loadDesigns() now drops is_published=false rows from the in-memory
DESIGNS array. The stale line-215 sitemap comment ("renders them all")
is fixed — gating happens at the loader.
- /admin/edges-review serves the new admin grid page (red-outlined cards
colored by defect class: orange=mids-only, red=edges-only, dark=both,
grey=scan-err). Per-card hover tooltip shows the 6 lens ΔE scores.
- /api/admin/edges-review/flagged returns parsed lens scores from the
structured EDGES_FAIL: tag in notes — 11,312 items in one shot.
- refresh_designs_snapshot.py now also emits `unpublish_reason` (the
extracted EDGES_FAIL tag) so future admin views don't need a PG hit.
Direct /design/:id access still works for the now-unpublished rows via the
existing lazy PG lookup at line ~9128 — only list views drop them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A public/admin/edges-review.htmlM scripts/refresh_designs_snapshot.pyM server.js
Diff
commit 57869a4c1611ee383942dff4049959013010e08c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 26 07:49:58 2026 -0700
edges-agent: unpublish 11,274 seam-defect designs + admin red-outline review
Ran the 6-lens seamless-tile scanner (~/.claude/skills/edges-agent) across
all 27,267 published designs. 41% failed — primarily SDXL internal-grid
leaks (7,333), with 2,883 catastrophic both-axis fails and 1,058 edge-wrap
regressions. 38 scan errors (missing/corrupt local PNG).
PG writes (via apply-unpublish.py):
- is_published=false on 11,312 flagged ids
- notes appended with ` | EDGES_FAIL: lens=... max=... grid=... ts=...`
(or EDGES_SCAN_ERROR for missing files), preserving prior provenance
Server changes:
- loadDesigns() now drops is_published=false rows from the in-memory
DESIGNS array. The stale line-215 sitemap comment ("renders them all")
is fixed — gating happens at the loader.
- /admin/edges-review serves the new admin grid page (red-outlined cards
colored by defect class: orange=mids-only, red=edges-only, dark=both,
grey=scan-err). Per-card hover tooltip shows the 6 lens ΔE scores.
- /api/admin/edges-review/flagged returns parsed lens scores from the
structured EDGES_FAIL: tag in notes — 11,312 items in one shot.
- refresh_designs_snapshot.py now also emits `unpublish_reason` (the
extracted EDGES_FAIL tag) so future admin views don't need a PG hit.
Direct /design/:id access still works for the now-unpublished rows via the
existing lazy PG lookup at line ~9128 — only list views drop them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
public/admin/edges-review.html | 330 ++++++++++++++++++++++++++++++++++++
scripts/refresh_designs_snapshot.py | 22 ++-
server.js | 107 +++++++++++-
3 files changed, 450 insertions(+), 9 deletions(-)
diff --git a/public/admin/edges-review.html b/public/admin/edges-review.html
new file mode 100644
index 0000000..7f99ce4
--- /dev/null
+++ b/public/admin/edges-review.html
@@ -0,0 +1,330 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="robots" content="noindex,nofollow">
+<title>Edges Review — wallco.ai admin</title>
+<style>
+ :root {
+ --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
+ --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
+ --green:#1f6a2c; --red:#b3261e; --orange:#c0660b; --grey:#888;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
+ --serif:'Playfair Display','Didot',Georgia,serif;
+ }
+ * { box-sizing:border-box; }
+ body { margin:0; background:var(--bg); color:var(--ink); font:14px var(--sans); }
+ header {
+ padding:12px 22px; border-bottom:1px solid var(--line); background:#fff;
+ display:flex; gap:16px; align-items:baseline; position:sticky; top:0; z-index:5;
+ }
+ header h1 { font:400 22px/1.2 var(--serif); margin:0; }
+ header .meta { color:var(--faint); font-size:12px; letter-spacing:.04em; }
+ header .stats { margin-left:auto; display:flex; gap:16px; font:600 12px ui-monospace,Menlo,monospace; }
+ header .stats span b { color:var(--ink); }
+ header .stats .mids b { color:var(--orange); }
+ header .stats .edges b { color:var(--red); }
+ header .stats .both b { color:#7a1818; }
+ header .stats .err b { color:var(--grey); }
+
+ main { max-width:1500px; margin:0 auto; padding:14px 18px 80px; }
+
+ .controls {
+ display:flex; gap:14px; align-items:center; padding:9px 14px;
+ background:#fff; border:1px solid var(--line); border-radius:8px;
+ margin-bottom:12px; flex-wrap:wrap; font-size:12px;
+ }
+ .controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
+ text-transform:uppercase; color:var(--faint); }
+ .controls select { padding:5px 9px; border:1px solid var(--line);
+ border-radius:4px; background:#fff; font:12px var(--sans); }
+ .controls input[type=range] { vertical-align:middle; width:160px; }
+ .controls .density-val { font:600 11px ui-monospace,Menlo,monospace;
+ color:var(--faint); min-width:32px; display:inline-block; }
+ .controls .legend { margin-left:auto; font:11px var(--sans); color:var(--faint); }
+ .controls .legend .sw {
+ display:inline-block; width:10px; height:10px; vertical-align:-1px;
+ margin-right:4px; border-radius:2px;
+ }
+ .controls .legend .sw.mids { background:var(--orange); }
+ .controls .legend .sw.edges { background:var(--red); }
+ .controls .legend .sw.both { background:#7a1818; }
+ .controls .legend .sw.err { background:var(--grey); }
+
+ .grid {
+ display:grid;
+ grid-template-columns:repeat(auto-fill,minmax(var(--card-min,200px),1fr));
+ gap:10px;
+ }
+ .tile {
+ position:relative; aspect-ratio:1; background:#f3eee2;
+ border-radius:6px; overflow:hidden;
+ cursor:pointer; transition:transform .12s, box-shadow .12s;
+ outline:2px solid var(--red); outline-offset:-2px;
+ }
+ .tile.grid-mids { outline-color:var(--orange); }
+ .tile.grid-edges { outline-color:var(--red); }
+ .tile.grid-both { outline-color:#7a1818; }
+ .tile.grid-err { outline-color:var(--grey); }
+ .tile img { width:100%; height:100%; object-fit:cover; display:block; }
+ .tile:hover { transform:translateY(-2px); box-shadow:0 6px 16px rgba(0,0,0,.18); }
+
+ .badge {
+ position:absolute; top:6px; left:6px;
+ background:var(--red); color:#fff;
+ font:700 9px var(--sans); letter-spacing:.16em; text-transform:uppercase;
+ padding:3px 7px; border-radius:3px;
+ box-shadow:0 1px 3px rgba(0,0,0,.25);
+ }
+ .tile.grid-mids .badge { background:var(--orange); }
+ .tile.grid-both .badge { background:#7a1818; }
+ .tile.grid-err .badge { background:var(--grey); }
+
+ .id-tag {
+ position:absolute; bottom:6px; right:6px;
+ background:rgba(0,0,0,.65); color:#fff;
+ font:600 10px ui-monospace,Menlo,monospace;
+ padding:2px 6px; border-radius:3px;
+ }
+
+ /* Hover tooltip — shows lens scores */
+ .tooltip {
+ position:absolute; bottom:30px; left:6px; right:6px;
+ background:rgba(20,12,4,.92); color:#fff;
+ font:11px ui-monospace,Menlo,monospace;
+ padding:6px 8px; border-radius:4px;
+ opacity:0; pointer-events:none; transition:opacity .14s;
+ z-index:2; line-height:1.4;
+ }
+ .tooltip .row { display:flex; justify-content:space-between; }
+ .tooltip .row .k { color:#c9a14b; }
+ .tooltip .row .v { color:#fff; }
+ .tooltip .row .v.fail { color:#ff8a7a; font-weight:700; }
+ .tooltip .row .v.warn { color:#ffd68a; }
+ .tile:hover .tooltip { opacity:1; }
+
+ .empty {
+ text-align:center; padding:60px 20px; color:var(--faint);
+ font:14px var(--sans);
+ }
+ .loading { text-align:center; padding:40px; color:var(--faint); }
+
+ /* Pagination */
+ .pager { margin-top:18px; display:flex; gap:6px; justify-content:center;
+ flex-wrap:wrap; font:12px var(--sans); }
+ .pager a, .pager span {
+ padding:5px 10px; border:1px solid var(--line); border-radius:4px;
+ color:var(--ink); text-decoration:none; background:#fff;
+ }
+ .pager a:hover { background:#f3eee2; }
+ .pager .active { background:var(--ink); color:#fff; border-color:var(--ink); }
+</style>
+</head>
+<body>
+<header>
+ <h1>Edges Review</h1>
+ <span class="meta">designs unpublished because the 6-lens seamless scan failed</span>
+ <div class="stats" id="stats">
+ <span class="mids">mids-only <b>—</b></span>
+ <span class="edges">edges-only <b>—</b></span>
+ <span class="both">both <b>—</b></span>
+ <span class="err">scan-err <b>—</b></span>
+ <span>total <b id="total-count">—</b></span>
+ </div>
+</header>
+
+<main>
+ <div class="controls">
+ <label for="sort">Sort</label>
+ <select id="sort">
+ <option value="max_desc">Worst ΔE first</option>
+ <option value="max_asc">Best ΔE first</option>
+ <option value="id_desc">Newest id</option>
+ <option value="id_asc">Oldest id</option>
+ <option value="grid">By defect type</option>
+ </select>
+
+ <label for="filter">Filter</label>
+ <select id="filter">
+ <option value="all">All defects</option>
+ <option value="mids_only">SDXL grid-leak (mids only)</option>
+ <option value="edges_only">Edge-wrap defect</option>
+ <option value="both">Catastrophic (both)</option>
+ <option value="err">Scan error / missing file</option>
+ </select>
+
+ <label for="density">Density</label>
+ <input id="density" type="range" min="120" max="380" step="20" value="200">
+ <span class="density-val" id="density-val">200</span>
+
+ <div class="legend">
+ <span><span class="sw mids"></span>mids-only</span>
+
+ <span><span class="sw edges"></span>edges-only</span>
+
+ <span><span class="sw both"></span>both</span>
+
+ <span><span class="sw err"></span>scan-err</span>
+ </div>
+ </div>
+
+ <div id="grid" class="grid"><div class="loading">loading flagged designs…</div></div>
+ <div id="pager" class="pager"></div>
+</main>
+
+<script>
+(function(){
+ const PER = 200;
+ const state = {
+ sort: localStorage.getItem('edges-review-sort') || 'max_desc',
+ filter: localStorage.getItem('edges-review-filter') || 'all',
+ density: parseInt(localStorage.getItem('edges-review-density') || '200', 10),
+ page: 1,
+ items: [],
+ };
+
+ function classifyGrid(item) {
+ if (!item.ok) return 'err';
+ const e = item.edges_max || 0, m = item.mids_max || 0;
+ if (e > 12 && m > 12) return 'both';
+ if (m > 12) return 'mids';
+ if (e > 12) return 'edges';
+ return 'err';
+ }
+
+ function applyDensity(px) {
+ document.documentElement.style.setProperty('--card-min', px + 'px');
+ document.getElementById('density-val').textContent = px;
+ }
+
+ function render() {
+ const grid = document.getElementById('grid');
+ const items = state.items.slice();
+ // Filter
+ const filtered = items.filter(it => {
+ if (state.filter === 'all') return true;
+ const g = classifyGrid(it);
+ return (state.filter === 'mids_only' && g === 'mids')
+ || (state.filter === 'edges_only' && g === 'edges')
+ || (state.filter === 'both' && g === 'both')
+ || (state.filter === 'err' && g === 'err');
+ });
+ // Sort
+ filtered.sort((a, b) => {
+ if (state.sort === 'max_desc') return (b.max_de || 0) - (a.max_de || 0);
+ if (state.sort === 'max_asc') return (a.max_de || 0) - (b.max_de || 0);
+ if (state.sort === 'id_desc') return b.id - a.id;
+ if (state.sort === 'id_asc') return a.id - b.id;
+ if (state.sort === 'grid') {
+ const order = { both:0, edges:1, mids:2, err:3 };
+ return (order[classifyGrid(a)] || 9) - (order[classifyGrid(b)] || 9);
+ }
+ return 0;
+ });
+
+ // Update stats
+ const stats = { mids:0, edges:0, both:0, err:0 };
+ items.forEach(it => { stats[classifyGrid(it)]++; });
+ document.querySelector('.stats .mids b').textContent = stats.mids;
+ document.querySelector('.stats .edges b').textContent = stats.edges;
+ document.querySelector('.stats .both b').textContent = stats.both;
+ document.querySelector('.stats .err b').textContent = stats.err;
+ document.getElementById('total-count').textContent = items.length;
+
+ const pages = Math.max(1, Math.ceil(filtered.length / PER));
+ if (state.page > pages) state.page = 1;
+ const slice = filtered.slice((state.page - 1) * PER, state.page * PER);
+
+ if (slice.length === 0) {
+ grid.innerHTML = '<div class="empty">no flagged designs match this filter</div>';
+ } else {
+ grid.innerHTML = slice.map(item => {
+ const g = classifyGrid(item);
+ const badge = g === 'err' ? 'SCAN ERR' : 'NOT ACTIVE';
+ const lenses = item.lenses || {};
+ const lensRows = ['top','bottom','left','right','h_mid','v_mid'].map(k => {
+ const v = lenses[k];
+ if (!v) return '';
+ const cls = v.verdict === 'FAIL' ? 'fail' : (v.verdict === 'WARN' ? 'warn' : '');
+ 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');
+ 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>
+ <div class="tooltip">
+ ${lensRows || `<div class="row"><span class="k">error</span><span class="v fail">${item.error || 'unknown'}</span></div>`}
+ </div>
+ </a>`;
+ }).join('');
+ }
+
+ const pager = document.getElementById('pager');
+ if (pages > 1) {
+ const links = [];
+ for (let p = 1; p <= pages; p++) {
+ links.push(p === state.page
+ ? `<span class="active">${p}</span>`
+ : `<a href="#" data-page="${p}">${p}</a>`);
+ }
+ pager.innerHTML = links.join('');
+ pager.querySelectorAll('a').forEach(a => {
+ a.addEventListener('click', e => {
+ e.preventDefault();
+ state.page = parseInt(a.dataset.page, 10);
+ render();
+ window.scrollTo(0, 0);
+ });
+ });
+ } else {
+ pager.innerHTML = '';
+ }
+ }
+
+ // Wire controls
+ document.getElementById('sort').value = state.sort;
+ document.getElementById('filter').value = state.filter;
+ document.getElementById('density').value = state.density;
+ applyDensity(state.density);
+
+ document.getElementById('sort').addEventListener('change', e => {
+ state.sort = e.target.value;
+ localStorage.setItem('edges-review-sort', state.sort);
+ state.page = 1;
+ render();
+ });
+ document.getElementById('filter').addEventListener('change', e => {
+ state.filter = e.target.value;
+ localStorage.setItem('edges-review-filter', state.filter);
+ state.page = 1;
+ render();
+ });
+ document.getElementById('density').addEventListener('input', e => {
+ state.density = parseInt(e.target.value, 10);
+ localStorage.setItem('edges-review-density', String(state.density));
+ 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>`;
+ });
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
index 4e04d59..6ff40b3 100644
--- a/scripts/refresh_designs_snapshot.py
+++ b/scripts/refresh_designs_snapshot.py
@@ -73,14 +73,26 @@ def title_for(cat, hex_, id_):
rows = psql_json(
"SELECT COALESCE(json_agg(t),'[]'::json) FROM ("
"SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
- "is_published, created_at, motifs, room_mockups, "
+ "is_published, created_at, motifs, room_mockups, notes, "
"user_removed, user_color_good, user_style_good, user_scale_good, user_stars "
# 2026-05-22 — added 'comfy' so Mac1 ComfyUI-generated designs (drunk-monkeys-v2 etc)
# land in the snapshot. Previously these were silently excluded, making the live
# viewer show 0 results for any cat= filter pointing at a comfy-generated category.
+ # 2026-05-26 — added 'notes' so the edges-agent EDGES_FAIL: tag rides along into
+ # designs.json. Admin "edges-fail review" surface reads it without an extra PG hit.
"FROM spoon_all_designs WHERE generator IN ('replicate','gemini-2.5-flash-image-edit','comfy') ORDER BY id) t;"
)
+import re as _re
+_EDGES_FAIL_RE = _re.compile(r'\| (EDGES_FAIL|EDGES_SCAN_ERROR): ([^|]+)')
+def extract_edges_tag(notes):
+ if not notes:
+ return None
+ m = _EDGES_FAIL_RE.search(notes)
+ if not m:
+ return None
+ return f"{m.group(1)}: {m.group(2).strip()}"
+
out = []
for r in rows:
h, l, s = hex_to_hls(r['dominant_hex'] or '')
@@ -114,7 +126,13 @@ for r in rows:
"user_stars": r.get('user_stars'),
# Render-ready URL list — server-side disk paths flipped to /designs/room/:id/:room.
# Snapshot only stores room types; server resolves the file from disk at request time.
- "room_mockups": sorted(list((r.get('room_mockups') or {}).keys()))
+ "room_mockups": sorted(list((r.get('room_mockups') or {}).keys())),
+ # Extracted edges-agent flag (NULL when clean). Drives the admin red-outline
+ # treatment + the per-design "why this is not active" tooltip. Format:
+ # "EDGES_FAIL: lens=top:78.6,bottom:78.6 max=78.6 grid=mids_only ts=..."
+ # Customer-safe — no Mac2 paths, no vendor names. Public /api/designs filter
+ # is_published before serializing anyway, so this field never reaches public.
+ "unpublish_reason": extract_edges_tag(r.get('notes'))
})
OUT.write_text(json.dumps(out, indent=2))
diff --git a/server.js b/server.js
index 55922b2..f708b18 100644
--- a/server.js
+++ b/server.js
@@ -212,10 +212,10 @@ app.get('/sitemap.xml', (req, res) => {
for (const p of staticPages) {
urls.push(` <url><loc>https://${SITE}${p.loc}</loc><changefreq>${p.freq}</changefreq><priority>${p.priority}</priority><lastmod>${today}</lastmod></url>`);
}
- // Note: is_published is currently false for every row in designs.json (admin
- // hasn't run the publish step yet) but /designs renders them all — so the
- // sitemap mirrors the live catalog. If/when a "draft" flag is introduced,
- // gate here.
+ // 2026-05-26: DESIGNS is now filtered to is_published=true at loadDesigns,
+ // so the sitemap automatically mirrors only the live catalog. No extra
+ // gate needed here. Unpublished rows (edges-fail, settlement, bleed-ghost,
+ // user_removed) are excluded by construction.
for (const d of DESIGNS) {
const sat = Math.max(0, Math.min(1, Number(d.saturation) || 0));
const hasRoom = (d.room_mockups || []).length > 0 ? 0.1 : 0;
@@ -289,9 +289,14 @@ function passesAestheticGate(d) {
function loadDesigns() {
try {
const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
- // Hide soft-removed designs, then drop anything that fails the aesthetic
- // gate (neon / >3 hues / rainbow tags).
- const beforeGate = raw.filter(d => !d.user_removed);
+ // Hide soft-removed designs, drop unpublished (is_published=false — covers
+ // edges-fail/settlement/bleed-ghost/admin-unpublished), then drop anything
+ // that fails the aesthetic gate (neon / >3 hues / rainbow tags).
+ // 2026-05-26: added is_published gate — the comment at the sitemap loop
+ // saying "/designs renders them all" is now stale. Unpublished rows are
+ // off-grid; admin views fetch from PG via /admin/edges-fail-review and
+ // direct /design/:id access still works via lazy PG lookup at line ~9128.
+ const beforeGate = raw.filter(d => !d.user_removed && d.is_published !== false);
DESIGNS = beforeGate.filter(passesAestheticGate);
const dropped = beforeGate.length - DESIGNS.length;
if (dropped > 0) console.log(`[aesthetic-gate] dropped ${dropped} designs at load (neon/multi-hue/rainbow)`);
@@ -1058,6 +1063,94 @@ app.get('/admin/ghost-review', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin', 'ghost-review.html'));
});
+// ── /admin/edges-review — 6-lens seamless-tile defect review.
+// Shows every design that was unpublished by the edges-agent sweep
+// (notes ~ '| EDGES_FAIL:' or '| EDGES_SCAN_ERROR:'). Red-outlined chips
+// with hover tooltip showing the per-lens ΔE. Click → /design/:id.
+app.get('/admin/edges-review', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ res.sendFile(path.join(__dirname, 'public', 'admin', 'edges-review.html'));
+});
+
+app.get('/api/admin/edges-review/flagged', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ try {
+ // Pull every row that the edges-agent flagged. notes column carries the
+ // 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 " +
+ "WHERE notes LIKE '%EDGES_FAIL:%' OR notes LIKE '%EDGES_SCAN_ERROR:%' " +
+ "ORDER BY id DESC" +
+ ") t;"
+ );
+ const rows = JSON.parse(rawJson || '[]');
+
+ // Parse "EDGES_FAIL: lens=top:78.6,bottom:78.6 max=78.6 grid=mids_only ts=..."
+ const FAIL_RE = /\| EDGES_FAIL:\s*lens=([^ ]+)\s+max=([\d.]+)\s+grid=(\w+)\s+ts=([^|]+?)(?:\s*\|.*)?$/;
+ const ERR_RE = /\| EDGES_SCAN_ERROR:\s*([^ ]+)\s+ts=([^|]+?)(?:\s*\|.*)?$/;
+
+ const items = rows.map(r => {
+ const m = r.notes && r.notes.match(FAIL_RE);
+ if (m) {
+ const lensStr = m[1], maxDe = parseFloat(m[2]), grid = m[3], ts = m[4].trim();
+ const lenses = {};
+ let edgesMax = 0, midsMax = 0;
+ lensStr.split(',').forEach(kv => {
+ const [k, v] = kv.split(':');
+ const score = parseFloat(v);
+ 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;
+ }
+ });
+ return {
+ id: r.id,
+ ok: true,
+ image_url: `/designs/img/by-id/${r.id}`,
+ verdict: 'FAIL',
+ lenses,
+ edges_max: edgesMax,
+ mids_max: midsMax,
+ max_de: maxDe,
+ grid,
+ ts,
+ reason: `${grid} @ max ΔE ${maxDe}`,
+ };
+ }
+ const e = r.notes && r.notes.match(ERR_RE);
+ if (e) {
+ return {
+ id: r.id,
+ ok: false,
+ image_url: `/designs/img/by-id/${r.id}`,
+ error: e[1],
+ ts: e[2].trim(),
+ reason: `scan error: ${e[1]}`,
+ };
+ }
+ // Notes matched the LIKE filter but regex didn't — surface so we don't
+ // silently drop edge cases.
+ return {
+ id: r.id,
+ ok: false,
+ image_url: `/designs/img/by-id/${r.id}`,
+ error: 'unparseable_tag',
+ reason: 'edges_fail tag present but unparseable',
+ };
+ });
+
+ res.json({ ok: true, 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
← 1d8b4c6 add mkdir-based mutex wrapper for cron ticks (macOS has no f
·
back to Wallco Ai
·
fliepaper-bugs: scrub category-leak in generator + YOLO loop 6e609a5 →