← back to Wallco Ai

public/admin/mural-masters.html

141 lines

<!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.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; }
  .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 => {
    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('');
    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();
    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();
  } 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>