[object Object]

← back to Wallco Ai

admin: /admin/mural-masters local review gallery for room-size scenic murals [yolo T1, DTD-A, local-only]

4cb93d5764be616c69bf83a6301228d4877845a3 · 2026-06-03 14:32:36 -0700 · Steve Abrams

Admin-gated local surface to review the 12×11ft mural masters built by
build-mural-master.py (masters live on Henry, Mac2-local not prod). API scans the
masters dir, reads each summary.json + master file size/mtime, returns dims +
per-width panel counts + created timestamp + contact-sheet availability. HTML grid
shows contact-sheet thumb, 24/36/54in panel chips, size, created date+time chip
(standing rule), Sort + Density controls (standing rule), 30s auto-refresh as the
batch lands more. Contact-sheet served via /api/admin/mural-masters/:id/sheet.
node -c clean; real-browser validated 13 cards render with all controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4cb93d5764be616c69bf83a6301228d4877845a3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 3 14:32:36 2026 -0700

    admin: /admin/mural-masters local review gallery for room-size scenic murals [yolo T1, DTD-A, local-only]
    
    Admin-gated local surface to review the 12×11ft mural masters built by
    build-mural-master.py (masters live on Henry, Mac2-local not prod). API scans the
    masters dir, reads each summary.json + master file size/mtime, returns dims +
    per-width panel counts + created timestamp + contact-sheet availability. HTML grid
    shows contact-sheet thumb, 24/36/54in panel chips, size, created date+time chip
    (standing rule), Sort + Density controls (standing rule), 30s auto-refresh as the
    batch lands more. Contact-sheet served via /api/admin/mural-masters/:id/sheet.
    node -c clean; real-browser validated 13 cards render with all controls.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 YOLO_BACKLOG.md                 |   6 ++
 public/admin/mural-masters.html | 129 ++++++++++++++++++++++++++++++++++++++++
 server.js                       |  54 +++++++++++++++++
 3 files changed, 189 insertions(+)

diff --git a/YOLO_BACKLOG.md b/YOLO_BACKLOG.md
index a9dd66c..8a7107b 100644
--- a/YOLO_BACKLOG.md
+++ b/YOLO_BACKLOG.md
@@ -139,3 +139,9 @@ $ per mural + the low-res demotion is customer-facing; both await Steve's go.
 - T5 · 2026-06-03 ~10:2x · fliepaper-bugs date-chip — ALREADY COMPLIANT (fmtDate has hour+minute + 🕓 chip line 175; first-pass grep mis-flagged). No change. Decided in-process (no DTD — DTD spends Codex $, against no-spend envelope).
 
 ✅ STOP CONDITION REACHED 2026-06-03 ~10:2x — sort+density already fleet-complete (2026-06-02 loop), admin date+time chip rule satisfied across all record grids, mural prep exhausted. No non-gated local work remains → stopped scheduling (no busywork). Session-recap: memory/session_recap_2026-06-03_yolo_mural_pipeline.md. STEVE: paid mural batch (--estimate 12 → ~$3.30 → manifest batch cmd) + 644-row low-res demotion both await your go.
+
+---
+
+# YOLO loop run — 2026-06-03 PM (Steve away · DTD-decided · mural batch building in bg)
+Envelope: LOCAL COMMITS ONLY, no deploy/publish/prod/sends. The mural BATCH itself is Steve-approved spend (separate); the loop's own work is local/no-spend. DTD decides forks.
+- T1 · /admin/mural-masters review gallery (DTD verdict A, 2/2 valid — Claude+Codex A, Qwen ERR). server.js routes + public/admin/mural-masters.html. Lists every built master on Henry: contact-sheet thumb + 12×11ft dims + 24/36/54in panel counts + size + created date+time chip (standing rule) + Sort+Density (standing rule). Real-browser validated 13 cards. CNCP pending.
diff --git a/public/admin/mural-masters.html b/public/admin/mural-masters.html
new file mode 100644
index 0000000..be06eb4
--- /dev/null
+++ b/public/admin/mural-masters.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Mural Masters — room-size scenic murals</title>
+<style>
+  :root { --bg:#fafaf8; --fg:#1a1a1a; --muted:#6a6a6a; --card:#fff; --border:#e8e6e1; --accent:#7a5c2e; --gold:#c9a14b; --card-min:360px; }
+  * { box-sizing:border-box; }
+  body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system,BlinkMacSystemFont,"Helvetica Neue",sans-serif; }
+  header { padding:22px 28px 16px; border-bottom:1px solid var(--border); background:var(--card); position:sticky; top:0; z-index:10; }
+  header h1 { margin:0 0 4px; font-size:20px; font-weight:600; letter-spacing:-0.01em; }
+  header h1 .badge { display:inline-block; padding:2px 8px; font-size:11px; font-weight:600; border-radius:10px; background:var(--accent); color:#fff; margin-left:8px; vertical-align:middle; }
+  header .stats { color:var(--muted); font-size:13px; }
+  header .stats code { background:#f0eee8; padding:1px 5px; border-radius:3px; }
+  .filters { margin-top:12px; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
+  .filters select, .filters input { padding:6px 10px; border:1px solid var(--border); border-radius:4px; background:var(--card); font:inherit; }
+  .filters label { font-size:12px; color:var(--muted); margin-right:4px; }
+  .filters input[type=range] { accent-color:var(--gold); vertical-align:middle; padding:0; }
+  main { padding:22px 28px 64px; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--card-min),1fr)); gap:20px; }
+  .card { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; position:relative; }
+  .card .sheet { background:#efece6; overflow:hidden; position:relative; min-height:120px; }
+  .card .sheet img { width:100%; display:block; }
+  .card .sheet.nosheet { padding:34px; text-align:center; color:var(--muted); font-size:12px; font-style:italic; }
+  .card .meta { padding:12px 14px; }
+  .card .meta .row1 { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:6px; }
+  .card .meta .row1 .id { font-weight:700; color:var(--accent); font-size:14px; }
+  .card .meta .row1 .dims { color:var(--muted); font-size:11.5px; font-variant-numeric:tabular-nums; }
+  .card .meta .panels { display:flex; gap:6px; flex-wrap:wrap; margin:6px 0; }
+  .card .meta .panels .p { padding:2px 8px; background:#f3efe6; border:1px solid var(--border); border-radius:999px; font-size:11px; color:var(--fg); font-variant-numeric:tabular-nums; }
+  .card .meta .panels .p b { color:var(--accent); }
+  .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; }
+  .empty { padding:60px; text-align:center; color:var(--muted); }
+</style>
+</head>
+<body>
+<header>
+  <h1>Mural Masters <span class="badge" id="count">…</span></h1>
+  <div class="stats">Room-size 12×11ft scenic-mural print masters (21,600×19,800 px @ 150 DPI) built by
+    <code>scripts/build-mural-master.py</code> — SDXL outpaint bleed → tiled ESRGAN → vertical panel slicing.
+    Local review only; masters live on <code id="dir">Henry</code>.</div>
+  <div class="filters">
+    <label>Sort</label>
+    <select id="sortBy">
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+      <option value="id">ID ↑</option>
+      <option value="id_desc">ID ↓</option>
+      <option value="size_desc">Size ↓</option>
+      <option value="size">Size ↑</option>
+    </select>
+    <label>Density</label>
+    <input type="range" id="density" min="260" max="640" step="20" title="card size">
+  </div>
+</header>
+<main>
+  <div class="grid" id="grid"><div class="empty">Loading…</div></div>
+</main>
+<script>
+const $ = id => document.getElementById(id);
+let ALL = [];
+
+// Sort + density persist (Steve standing rule).
+$('sortBy').value = localStorage.getItem('muralMasters_sort') || 'newest';
+$('density').value = localStorage.getItem('muralMasters_density') || '360';
+document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+$('sortBy').addEventListener('change', () => { localStorage.setItem('muralMasters_sort', $('sortBy').value); render(); });
+$('density').addEventListener('input', () => {
+  localStorage.setItem('muralMasters_density', $('density').value);
+  document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+});
+
+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 fmtMB(b) { return b ? (b / 1e6).toFixed(0) + ' MB' : '—'; }
+
+function sorted(rows) {
+  const r = rows.slice(), m = $('sortBy').value;
+  if (m === 'id') r.sort((a,b) => (a.id|0) - (b.id|0));
+  else if (m === 'id_desc') r.sort((a,b) => (b.id|0) - (a.id|0));
+  else if (m === 'size_desc') r.sort((a,b) => (b.bytes||0) - (a.bytes||0));
+  else if (m === 'size') r.sort((a,b) => (a.bytes||0) - (b.bytes||0));
+  else if (m === 'oldest') r.sort((a,b) => String(a.created_at||'').localeCompare(String(b.created_at||'')));
+  else r.sort((a,b) => String(b.created_at||'').localeCompare(String(a.created_at||'')));  // newest
+  return r;
+}
+
+function render() {
+  const grid = $('grid');
+  if (!ALL.length) { grid.innerHTML = '<div class="empty">No mural masters built yet. Run <code>scripts/build-mural-master.py &lt;id&gt;</code>.</div>'; return; }
+  grid.innerHTML = sorted(ALL).map(m => {
+    const dims = (m.master_px && m.master_px.length === 2) ? `${m.master_px[0].toLocaleString()}×${m.master_px[1].toLocaleString()} px` : '';
+    const ftw = m.print_ft ? `${m.print_ft}×${m.height_ft}ft` : '';
+    const panels = Object.keys(m.panels || {}).map(k => `<span class="p"><b>${m.panels[k]}</b> × ${k.replace('in','&quot;')}</span>`).join('');
+    const sheet = m.has_sheet
+      ? `<div class="sheet"><img loading="lazy" src="/api/admin/mural-masters/${m.id}/sheet" alt="contact sheet ${m.id}"></div>`
+      : `<div class="sheet nosheet">contact-sheet pending</div>`;
+    return `<div class="card">
+      ${sheet}
+      <div class="meta">
+        <div class="row1"><div class="id">#${m.id}</div><div class="dims">${ftw} · ${dims} · ${fmtMB(m.bytes)}</div></div>
+        <div class="panels">${panels}</div>
+        <div class="when" title="master built ${m.created_at || ''}">🕓 ${fmtDate(m.created_at)}</div>
+      </div>
+    </div>`;
+  }).join('');
+}
+
+async function load() {
+  try {
+    const r = await fetch('/api/admin/mural-masters', { credentials:'same-origin' });
+    const d = await r.json();
+    $('count').textContent = d.total || 0;
+    if (d.dir) $('dir').textContent = d.dir;
+    ALL = d.items || [];
+    render();
+  } catch (e) {
+    $('grid').innerHTML = '<div class="empty">Failed: ' + e.message + '</div>';
+  }
+}
+load();
+setInterval(load, 30000);  // refresh as the batch + finisher land more masters
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index b8fb5c9..77e9c38 100644
--- a/server.js
+++ b/server.js
@@ -1958,6 +1958,60 @@ app.get('/api/admin/needs-tif', requireAdmin, (req, res) => {
   }
 });
 
+// ── /admin/mural-masters — local review gallery for the room-size scenic-mural
+// masters built by scripts/build-mural-master.py. Masters live on the Henry
+// external volume (Mac2-local, NOT prod), so this is a local curation surface:
+// contact-sheet thumbnail + master dims + per-width panel counts + created
+// date+time chip (Steve standing rule). See docs/mural-pipeline.md.
+const MURAL_MASTERS_DIR = (function () {
+  const henry = '/Volumes/Henry/wallco-ai-archive/mural-masters';
+  try { if (fs.existsSync('/Volumes/Henry/wallco-ai-archive')) return henry; } catch (e) {}
+  return path.join(__dirname, 'data', 'mural-masters');
+})();
+
+app.get('/admin/mural-masters', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  res.sendFile(path.join(__dirname, 'public', 'admin', 'mural-masters.html'));
+});
+
+app.get('/api/admin/mural-masters', requireAdmin, (req, res) => {
+  try {
+    const items = [];
+    let dirs = [];
+    try { dirs = fs.readdirSync(MURAL_MASTERS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()); } catch (e) {}
+    for (const d of dirs) {
+      const dir = path.join(MURAL_MASTERS_DIR, d.name);
+      let summary = null;
+      try { summary = JSON.parse(fs.readFileSync(path.join(dir, 'summary.json'), 'utf-8')); } catch (e) { continue; }
+      const masterPath = summary.master && fs.existsSync(summary.master)
+        ? summary.master : path.join(dir, fs.readdirSync(dir).find(f => f.endsWith('.tif')) || '');
+      let bytes = 0, created = null;
+      try { const st = fs.statSync(masterPath); bytes = st.size; created = st.mtime.toISOString(); } catch (e) {}
+      const hasSheet = fs.existsSync(path.join(dir, 'contact-sheet.png'));
+      const panels = {};
+      for (const k of Object.keys(summary.panels || {})) panels[k] = (summary.panels[k] || {}).count || 0;
+      items.push({
+        id: parseInt(d.name, 10) || d.name,
+        master_px: summary.master_px, dpi: summary.dpi,
+        print_ft: summary.print_ft, height_ft: summary.height_ft, bleed_in: summary.bleed_in,
+        panels, bytes, created_at: created, has_sheet: hasSheet,
+      });
+    }
+    items.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+    res.json({ ok: true, total: items.length, dir: MURAL_MASTERS_DIR, items });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// Serve a master's contact-sheet PNG (local review only). id-validated.
+app.get('/api/admin/mural-masters/:id/sheet', requireAdmin, (req, res) => {
+  const id = String(req.params.id).replace(/[^0-9A-Za-z_-]/g, '');
+  const sheet = path.join(MURAL_MASTERS_DIR, id, 'contact-sheet.png');
+  if (!fs.existsSync(sheet)) return res.status(404).type('text/plain').send('no contact sheet');
+  res.sendFile(sheet);
+});
+
 // ── /admin/rooms — master gallery of EVERY saved room-setting render ───────
 app.get('/admin/rooms', (req, res) => {
   if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');

← 78083e9 Loop tick 9: corrected auto-curator republish premise (no li  ·  back to Wallco Ai  ·  universal search: match designs by color name (blue/navy/tea e25598a →