← back to Wallco Ai
subject-mismatch: enrich /api with dominant_hex+created_at (batched PG lookup) → finish standing-rule compliance: color-by-hue sort + date+time swatch chip
3910ddc94beaff9039ec6a44e4a3c5a48a4c3d81 · 2026-06-03 07:45:35 -0700 · Steve Abrams
Files touched
M public/subject-mismatch.htmlM server.js
Diff
commit 3910ddc94beaff9039ec6a44e4a3c5a48a4c3d81
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 3 07:45:35 2026 -0700
subject-mismatch: enrich /api with dominant_hex+created_at (batched PG lookup) → finish standing-rule compliance: color-by-hue sort + date+time swatch chip
---
public/subject-mismatch.html | 29 +++++++++++++++++++++++++++--
server.js | 23 +++++++++++++++++++++++
2 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/public/subject-mismatch.html b/public/subject-mismatch.html
index 641cbdd..90b145a 100644
--- a/public/subject-mismatch.html
+++ b/public/subject-mismatch.html
@@ -33,6 +33,8 @@
.card .meta .prompt { color:var(--muted); font-size:11.5px; line-height:1.4; margin:6px 0 4px; font-style:italic; }
.card .meta .saw { color:var(--fg); font-size:11.5px; line-height:1.4; padding:6px 8px; background:#fbf6e8; border-left:3px solid var(--warn); border-radius:0 4px 4px 0; }
.card .meta .saw b { color:var(--warn); }
+ .card .meta .when { margin-top:8px; display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--gold); font-variant-numeric:tabular-nums; }
+ .card .meta .when .sw { width:11px; height:11px; border-radius:50%; border:1px solid rgba(0,0,0,.18); display:inline-block; }
.empty { padding:60px; text-align:center; color:var(--muted); }
</style>
</head>
@@ -46,9 +48,12 @@
<label>Sort</label>
<select id="sortBy">
<option value="default">Default (scan order)</option>
+ <option value="newest">Newest</option>
+ <option value="oldest">Oldest</option>
+ <option value="color">Color (by hue)</option>
+ <option value="subject">Subject A→Z</option>
<option value="id_desc">Root ID ↓</option>
<option value="id">Root ID ↑</option>
- <option value="subject">Subject A→Z</option>
</select>
<label>Density</label>
<input type="range" id="density" min="280" max="600" step="20" title="card size">
@@ -62,7 +67,8 @@ const $ = (id) => document.getElementById(id);
let ALL = [];
// sort + density persist (Steve standing rule: every card grid; both persist).
-// Note: this QA API exposes no created_at / dominant_hex, so no date chip + no color sort.
+// The /api/subject-mismatch route now enriches each row with the design's real
+// dominant_hex + created_at (PG lookup), so color-sort + the date chip are live.
$('sortBy').value = localStorage.getItem('subjMismatch_sort') || 'default';
$('density').value = localStorage.getItem('subjMismatch_density') || '360';
document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
@@ -72,10 +78,28 @@ $('density').addEventListener('input', () => {
document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
});
+function hexHue(hex) {
+ const m = /^#?([0-9a-fA-F]{6})$/.exec(hex || ''); if (!m) return 1e6;
+ const n = parseInt(m[1], 16), r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, b = (n & 255) / 255;
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min; let h = 0;
+ if (d === 0) h = 0; else if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4;
+ h *= 60; if (h < 0) h += 360;
+ const s = max === 0 ? 0 : d / max;
+ if (s < 0.12) return 1e5 + (1 - max);
+ return h;
+}
+function fmtDate(s) {
+ if (!s) return '—';
+ const d = new Date(s); if (isNaN(d)) return '—';
+ return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
+}
function sorted(rows) {
const r = rows.slice(), m = $('sortBy').value;
if (m === 'id_desc') r.sort((a, b) => b.root_id - a.root_id);
else if (m === 'id') r.sort((a, b) => a.root_id - b.root_id);
+ else if (m === 'newest') r.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')) || (b.root_id - a.root_id));
+ else if (m === 'oldest') r.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')) || (a.root_id - b.root_id));
+ else if (m === 'color') r.sort((a, b) => hexHue(a.dominant_hex) - hexHue(b.dominant_hex) || (b.root_id - a.root_id));
else if (m === 'subject') r.sort((a, b) => String(a.primary_subject || '').localeCompare(String(b.primary_subject || '')) || (b.root_id - a.root_id));
return r; // 'default' keeps scan order
}
@@ -99,6 +123,7 @@ function render() {
</div>
<div class="prompt">"${p.prompt_head || ''}"</div>
<div class="saw"><b>Vision saw:</b> ${p.what_you_see || '—'}</div>
+ <div class="when" title="created ${p.created_at || ''}">${p.dominant_hex ? `<span class="sw" style="background:${p.dominant_hex}"></span>` : ''}🕓 ${fmtDate(p.created_at)}</div>
</div>
</div>
`).join('');
diff --git a/server.js b/server.js
index 074f573..0262952 100644
--- a/server.js
+++ b/server.js
@@ -20856,6 +20856,29 @@ app.get('/api/subject-mismatch', (req, res) => {
});
} catch {}
}
+ // Enrich with each design's real dominant_hex + created_at so the page can
+ // offer a color-by-hue sort and the standing admin date+time chip. One
+ // batched lookup; root_ids come from the trusted scanner JSONL (ints only).
+ // Best-effort: on the sparse prod DB some rows are absent → null, and the
+ // page degrades gracefully (chip shows '—', color-sort puts nulls last).
+ if (flagged.length) {
+ try {
+ const ids = [...new Set(flagged.map(f => parseInt(f.root_id, 10)).filter(Number.isFinite))];
+ if (ids.length) {
+ const raw = psqlQuery(
+ `SELECT COALESCE(json_agg(json_build_object('id', id, 'hex', dominant_hex, 'ca', created_at)), '[]')
+ FROM all_designs WHERE id IN (${ids.join(',')});`
+ );
+ const meta = new Map();
+ for (const m of JSON.parse(raw || '[]')) meta.set(m.id, m);
+ for (const f of flagged) {
+ const m = meta.get(parseInt(f.root_id, 10));
+ f.dominant_hex = m ? m.hex : null;
+ f.created_at = (m && m.ca) ? m.ca : (f.ts || null);
+ }
+ }
+ } catch { /* enrichment is best-effort; the page handles missing fields */ }
+ }
res.json({ count: flagged.length, scanned, items: flagged });
} catch (e) {
res.status(500).json({ error: e.message });
← 9b4f2b9 yolo-backlog: T6 — N7 etsy-bucket + N8 elements done, LOOP C
·
back to Wallco Ai
·
yolo audit: APPLIED+DEPLOYED 3 unpublish + 51 kind-flip + 12 b4692f9 →