[object Object]

← 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

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 →