[object Object]

← 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

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 &lt;id&gt;</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','&quot;')}</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 →