← back to Wallco Ai
admin/mural-masters: show in-progress builds as live 'building…' cards [yolo, local-only]
7513978052e2be352da5afd3d5898e9555983231 · 2026-06-03 20:52:38 -0700 · Steve Abrams
API surfaces master dirs without summary.json as {building:true}; gallery renders
them as pulsing 'building…' cards and the header shows 'N done · M building'. Lets
Steve watch a batch land in real time (30s auto-refresh) instead of only seeing
finished masters.
Files touched
M public/admin/mural-masters.htmlM server.js
Diff
commit 7513978052e2be352da5afd3d5898e9555983231
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 3 20:52:38 2026 -0700
admin/mural-masters: show in-progress builds as live 'building…' cards [yolo, local-only]
API surfaces master dirs without summary.json as {building:true}; gallery renders
them as pulsing 'building…' cards and the header shows 'N done · M building'. Lets
Steve watch a batch land in real time (30s auto-refresh) instead of only seeing
finished masters.
---
public/admin/mural-masters.html | 13 ++++++++++++-
server.js | 8 +++++++-
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/public/admin/mural-masters.html b/public/admin/mural-masters.html
index be06eb4..0b4875f 100644
--- a/public/admin/mural-masters.html
+++ b/public/admin/mural-masters.html
@@ -23,6 +23,9 @@
.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.building { opacity:.85; }
+ .card .sheet.build { padding:40px; text-align:center; color:var(--accent); font-size:13px; font-weight:600; background:repeating-linear-gradient(45deg,#f3efe6,#f3efe6 10px,#efe9dd 10px,#efe9dd 20px); animation:pulse 1.4s ease-in-out infinite; }
+ @keyframes pulse { 0%,100%{opacity:.6} 50%{opacity:1} }
.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; }
@@ -93,6 +96,13 @@ 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 => {
+ if (m.building) {
+ return `<div class="card building">
+ <div class="sheet build">⏳ building…</div>
+ <div class="meta"><div class="row1"><div class="id">#${m.id}</div><div class="dims">12×11ft master rendering</div></div>
+ <div class="when">🕓 started ${fmtDate(m.created_at)}</div></div>
+ </div>`;
+ }
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('');
@@ -114,7 +124,8 @@ 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;
+ const building = (d.items || []).filter(i => i.building).length;
+ $('count').textContent = building ? `${(d.total||0) - building} done · ${building} building` : (d.total || 0);
if (d.dir) $('dir').textContent = d.dir;
ALL = d.items || [];
render();
diff --git a/server.js b/server.js
index 4ac27e2..28150c1 100644
--- a/server.js
+++ b/server.js
@@ -2026,7 +2026,13 @@ app.get('/api/admin/mural-masters', requireAdmin, (req, res) => {
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; }
+ try { summary = JSON.parse(fs.readFileSync(path.join(dir, 'summary.json'), 'utf-8')); } catch (e) {
+ // No summary.json yet → an in-progress build. Surface it as a "building"
+ // card so a running batch fills in live.
+ let bAt = null; try { bAt = fs.statSync(dir).birthtime.toISOString(); } catch (_) {}
+ items.push({ id: parseInt(d.name, 10) || d.name, building: true, created_at: bAt });
+ 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;
← 3ca5a95 curate-monterey-batch: exclude already-built masters + batch
·
back to Wallco Ai
·
yolo ledger: T4 live building cards + batch-2 recovery 94fd03d →