← back to Wallco Ai
admin: live before/after Fixes Feed for every repair event
080cc68e5a5f059f4d219cf9a597d4c754f911e7 · 2026-05-23 23:55:53 -0700 · Steve Abrams
NEW PAGE /admin/fixes-feed — auto-polling gallery (10s) showing every
successful repair, newest first:
- BEFORE | → | AFTER side-by-side cards
- Color-coded kind pill (blue=fix, red=remove, purple=regen, green=wand)
- Filter chips: All / Surgical Fix / Remove+Fill / Reverse-Regen / Wand
- Pulsing LIVE badge + tab-title flash when new fixes land
- Quick links per card: ← review · open old · open fixed
- 60s+ cache-free polling so it stays current while Steve labels
NEW EVENT SINK — every fix endpoint now appends to
data/fixes-feed.jsonl via appendFixEvent({ts, kind, src_id, new_id, ...}):
- crop-fix logs region + fix_kind (ghost-layer/overlap/composition/remove-fill)
- regenerate-reverse logs the new reverse-engineered prompt
- magic-wand save-edited logs bytes + summary
NEW LINK on /admin/ghost-review — "📡 Before/After Feed →" in the header
so Steve can open the feed in a side tab as he labels and watch each
repair as it commits.
Endpoint: GET /api/fixes-feed → { ok, items[], total }
Files touched
A public/admin/fixes-feed.html
Diff
commit 080cc68e5a5f059f4d219cf9a597d4c754f911e7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 23 23:55:53 2026 -0700
admin: live before/after Fixes Feed for every repair event
NEW PAGE /admin/fixes-feed — auto-polling gallery (10s) showing every
successful repair, newest first:
- BEFORE | → | AFTER side-by-side cards
- Color-coded kind pill (blue=fix, red=remove, purple=regen, green=wand)
- Filter chips: All / Surgical Fix / Remove+Fill / Reverse-Regen / Wand
- Pulsing LIVE badge + tab-title flash when new fixes land
- Quick links per card: ← review · open old · open fixed
- 60s+ cache-free polling so it stays current while Steve labels
NEW EVENT SINK — every fix endpoint now appends to
data/fixes-feed.jsonl via appendFixEvent({ts, kind, src_id, new_id, ...}):
- crop-fix logs region + fix_kind (ghost-layer/overlap/composition/remove-fill)
- regenerate-reverse logs the new reverse-engineered prompt
- magic-wand save-edited logs bytes + summary
NEW LINK on /admin/ghost-review — "📡 Before/After Feed →" in the header
so Steve can open the feed in a side tab as he labels and watch each
repair as it commits.
Endpoint: GET /api/fixes-feed → { ok, items[], total }
---
public/admin/fixes-feed.html | 296 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 296 insertions(+)
diff --git a/public/admin/fixes-feed.html b/public/admin/fixes-feed.html
new file mode 100644
index 0000000..cc8aac1
--- /dev/null
+++ b/public/admin/fixes-feed.html
@@ -0,0 +1,296 @@
+<!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>Fixes Feed — wallco.ai admin</title>
+<style>
+ :root {
+ --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
+ --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
+ --red:#b3261e; --green:#1f6a2c; --orange:#c0660b; --purple:#7a4ab8; --blue:#3b78d8;
+ --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:14px 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:18px; font:600 12px ui-monospace,Menlo,monospace; }
+ header .stats b { color:var(--ink); }
+ header .live {
+ font:600 10.5px var(--sans); color:var(--green); letter-spacing:.1em;
+ margin-left:auto; padding-left:16px;
+ }
+ header .live::before {
+ content:'●'; margin-right:6px; animation:pulse 1.4s ease-in-out infinite;
+ }
+ @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
+
+ main { max-width:1500px; margin:0 auto; padding:18px; }
+
+ .controls {
+ display:flex; gap:14px; align-items:center; padding:10px 14px;
+ background:#fff; border:1px solid var(--line); border-radius:8px;
+ margin-bottom:14px; font:12px var(--sans); flex-wrap:wrap;
+ }
+ .controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
+ text-transform:uppercase; color:var(--faint); }
+ .controls .filter button {
+ border:1px solid var(--line); background:#fff; border-radius:14px;
+ padding:5px 11px; cursor:pointer; font:600 11px var(--sans);
+ color:var(--faint); letter-spacing:.04em; margin-right:6px;
+ }
+ .controls .filter button.active { background:var(--ink); color:#fff; border-color:var(--ink); }
+ .controls .ledger {
+ margin-left:auto; font:600 11px ui-monospace,Menlo,monospace; color:var(--faint);
+ }
+
+ .card {
+ background:var(--card); border:1px solid var(--line); border-radius:10px;
+ margin-bottom:18px; overflow:hidden;
+ transition:box-shadow .15s;
+ }
+ .card:hover { box-shadow:0 6px 22px rgba(0,0,0,.06); }
+ .card .ribbon {
+ display:flex; gap:10px; align-items:center; padding:9px 14px;
+ border-bottom:1px solid var(--line); background:#fcfaf4;
+ font:11px var(--sans);
+ }
+ .card .kind-pill {
+ font:700 10px var(--sans); letter-spacing:.08em;
+ padding:3px 8px; border-radius:11px; color:#fff;
+ text-transform:uppercase;
+ }
+ .card .kind-pill.k-crop-fix { background:var(--blue); }
+ .card .kind-pill.k-crop-fix-remove { background:var(--red); }
+ .card .kind-pill.k-regenerate-reverse { background:var(--purple); }
+ .card .kind-pill.k-magic-wand { background:var(--green); }
+ .card .ts { color:var(--faint); font-family:ui-monospace,Menlo,monospace; }
+ .card .elapsed { color:var(--faint); margin-left:6px; }
+ .card .links { margin-left:auto; display:flex; gap:8px; }
+ .card .links a {
+ color:var(--faint); text-decoration:none; font:600 11px var(--sans);
+ padding:3px 8px; border:1px solid var(--line); border-radius:4px;
+ }
+ .card .links a:hover { background:var(--ink); color:#fff; border-color:var(--ink); }
+
+ .pair {
+ display:grid; grid-template-columns:1fr auto 1fr; gap:0;
+ align-items:stretch;
+ }
+ .pair .side {
+ padding:14px 18px; display:flex; flex-direction:column; gap:8px;
+ min-width:0;
+ }
+ .pair .side.before { border-right:1px solid var(--line); background:#fdfbf5; }
+ .pair .side .label {
+ font:700 10px var(--sans); letter-spacing:.16em;
+ text-transform:uppercase; color:var(--faint);
+ display:flex; align-items:center; gap:8px;
+ }
+ .pair .side.before .label::before { content:'◀'; color:#b3261e; }
+ .pair .side.after .label::before { content:'▶'; color:var(--green); }
+ .pair .side .id-row {
+ font:600 14px ui-monospace,Menlo,monospace; color:var(--ink);
+ }
+ .pair .img-frame {
+ aspect-ratio:1; background:#f3eee2;
+ border-radius:6px; overflow:hidden; position:relative;
+ border:1px solid var(--line);
+ }
+ .pair .img-frame img {
+ width:100%; height:100%; object-fit:contain; display:block;
+ background:#000;
+ }
+ .pair .img-frame.before img { opacity:1; }
+ .pair .arrow {
+ display:flex; align-items:center; justify-content:center;
+ width:62px; background:var(--bg);
+ border-left:1px solid var(--line); border-right:1px solid var(--line);
+ font:24px var(--sans); color:var(--faint);
+ }
+
+ .pair .side .meta-row {
+ display:flex; flex-wrap:wrap; gap:6px 12px;
+ font:11px ui-monospace,Menlo,monospace; color:var(--faint);
+ }
+ .pair .side .meta-row b { color:var(--ink); font-weight:600; }
+
+ .empty {
+ text-align:center; padding:80px 20px; color:var(--faint);
+ font:14px var(--sans);
+ }
+ .empty h2 { font:400 28px var(--serif); color:var(--ink); margin-bottom:10px; }
+ .empty code {
+ background:#f3eee2; padding:2px 6px; border-radius:3px; font:12px ui-monospace,Menlo,monospace;
+ }
+
+ @media (max-width: 720px) {
+ .pair { grid-template-columns:1fr; }
+ .pair .side.before { border-right:none; border-bottom:1px solid var(--line); }
+ .pair .arrow { width:auto; height:42px; border-left:none; border-right:none; border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
+ }
+</style>
+</head>
+<body>
+
+<header>
+ <h1>Fixes Feed</h1>
+ <span class="meta">every successful repair, newest first</span>
+ <div class="stats">
+ <span>FIX <b id="stat-fix">0</b></span>
+ <span>REMOVE <b id="stat-remove">0</b></span>
+ <span>REGEN <b id="stat-regen">0</b></span>
+ <span>WAND <b id="stat-wand">0</b></span>
+ <span>TOTAL <b id="stat-total">0</b></span>
+ </div>
+ <span class="live">LIVE</span>
+</header>
+
+<main>
+ <div class="controls">
+ <label>Filter</label>
+ <div class="filter">
+ <button class="active" data-f="all">All</button>
+ <button data-f="crop-fix">Surgical Fix</button>
+ <button data-f="crop-fix-remove">Remove + Fill</button>
+ <button data-f="regenerate-reverse">Reverse-Regen</button>
+ <button data-f="magic-wand">Magic Wand</button>
+ </div>
+ <span class="ledger" id="ledger">—</span>
+ </div>
+
+ <div id="feed"></div>
+ <div id="empty" class="empty" style="display:none">
+ <h2>No fixes yet</h2>
+ <p>Go to <code>/admin/ghost-review</code>, mark a defect, hit a Fix button. The before/after will appear here automatically.</p>
+ </div>
+</main>
+
+<script>
+(() => {
+ const $ = (id) => document.getElementById(id);
+ let ITEMS = [];
+ let FILTER = 'all';
+ let SEEN_TOTAL = 0;
+
+ const KIND_LABEL = {
+ 'crop-fix': 'Surgical Fix',
+ 'crop-fix-remove': 'Remove + Fill',
+ 'regenerate-reverse': 'Reverse-Regen',
+ 'magic-wand': 'Magic Wand',
+ };
+
+ document.querySelectorAll('.filter button').forEach(b => {
+ b.onclick = () => {
+ document.querySelectorAll('.filter button').forEach(x => x.classList.remove('active'));
+ b.classList.add('active');
+ FILTER = b.dataset.f;
+ render();
+ };
+ });
+
+ function fmtKind(item) {
+ if (item.kind === 'crop-fix' && item.fix_kind === 'remove-fill') return 'crop-fix-remove';
+ return item.kind;
+ }
+
+ function updateStats() {
+ const counts = { 'crop-fix':0, 'crop-fix-remove':0, 'regenerate-reverse':0, 'magic-wand':0 };
+ ITEMS.forEach(i => { const k = fmtKind(i); counts[k] = (counts[k] || 0) + 1; });
+ $('stat-fix').textContent = counts['crop-fix'];
+ $('stat-remove').textContent = counts['crop-fix-remove'];
+ $('stat-regen').textContent = counts['regenerate-reverse'];
+ $('stat-wand').textContent = counts['magic-wand'];
+ $('stat-total').textContent = ITEMS.length;
+ }
+
+ function render() {
+ const feed = $('feed');
+ feed.innerHTML = '';
+ const empty = $('empty');
+ const filtered = FILTER === 'all' ? ITEMS : ITEMS.filter(i => fmtKind(i) === FILTER);
+ if (!filtered.length) { empty.style.display = ''; return; }
+ empty.style.display = 'none';
+ const frag = document.createDocumentFragment();
+ for (const it of filtered) {
+ const k = fmtKind(it);
+ const card = document.createElement('div');
+ card.className = 'card';
+ const tsShort = new Date(it.ts).toLocaleString();
+ const sub =
+ k === 'crop-fix' ? `region (${it.region?.x?.toFixed(1)}%, ${it.region?.y?.toFixed(1)}%, ${it.region?.w?.toFixed(1)}%, ${it.region?.h?.toFixed(1)}%)` :
+ k === 'crop-fix-remove' ? `removed region (${it.region?.w?.toFixed(1)}% × ${it.region?.h?.toFixed(1)}%)` :
+ k === 'regenerate-reverse' ? `<span title="${(it.new_prompt || '').replace(/"/g,'"')}">new prompt: ${(it.new_prompt || '').slice(0, 80)}…</span>` :
+ k === 'magic-wand' ? `${it.summary || 'browser edit'} · ${(it.bytes/1024).toFixed(0)} KB` :
+ '';
+ card.innerHTML = `
+ <div class="ribbon">
+ <span class="kind-pill k-${k}">${KIND_LABEL[k] || k}</span>
+ <span class="ts">${tsShort}</span>
+ ${it.elapsed_s ? `<span class="elapsed">· ${it.elapsed_s}s</span>` : ''}
+ <span class="links">
+ <a href="/admin/ghost-review" title="back to review">← review</a>
+ <a href="/design/${it.src_id}" target="_blank" title="open old">#${it.src_id}</a>
+ <a href="/design/${it.new_id}" target="_blank" title="open fixed">→ #${it.new_id}</a>
+ </span>
+ </div>
+ <div class="pair">
+ <div class="side before">
+ <div class="label">Before · #${it.src_id} (unpublished)</div>
+ <div class="img-frame before">
+ <img loading="lazy" src="/designs/img/by-id/${it.src_id}" alt="before #${it.src_id}">
+ </div>
+ <div class="meta-row"><b>id</b> ${it.src_id}</div>
+ </div>
+ <div class="arrow">→</div>
+ <div class="side after">
+ <div class="label">After · #${it.new_id} (live)</div>
+ <div class="img-frame">
+ <img loading="lazy" src="/designs/img/by-id/${it.new_id}" alt="after #${it.new_id}">
+ </div>
+ <div class="meta-row">
+ <span><b>id</b> ${it.new_id}</span>
+ <span><b>kind</b> ${KIND_LABEL[k] || k}</span>
+ ${sub ? `<span>${sub}</span>` : ''}
+ </div>
+ </div>
+ </div>
+ `;
+ frag.appendChild(card);
+ }
+ feed.appendChild(frag);
+ }
+
+ async function refresh() {
+ try {
+ const r = await fetch('/api/fixes-feed');
+ const j = await r.json();
+ if (!j.ok) return;
+ // Pulse the LIVE badge briefly when new items arrive
+ if (j.total > SEEN_TOTAL && SEEN_TOTAL > 0) {
+ document.title = `(${j.total - SEEN_TOTAL} new) Fixes Feed`;
+ setTimeout(() => { document.title = 'Fixes Feed — wallco.ai admin'; }, 4000);
+ }
+ SEEN_TOTAL = j.total;
+ ITEMS = j.items;
+ $('ledger').textContent = `${j.items.length} shown · ${j.total} total events`;
+ updateStats();
+ render();
+ } catch (e) {
+ console.warn('refresh failed:', e.message);
+ }
+ }
+
+ refresh();
+ setInterval(refresh, 10000); // poll every 10s for new fixes
+})();
+</script>
+</body>
+</html>
← a3c2b00 add ghost-rescore-flagged.js — quantify detector-change resc
·
back to Wallco Ai
·
ghost-review: SHIFT+drag = KEEP element (green dotted) · fix a9fbc70 →