← 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
M YOLO_BACKLOG.mdA public/admin/mural-masters.htmlM server.js
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 <id></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','"')}</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 →