← back to Wallco Ai
public/admin/fliepaper-bugs.html
247 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Fliepaper Bugs · Admin · wallco.ai</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<style>
:root {
--bg: #f8f6f0; --fg: #15130f; --muted: #6a6358; --line: rgba(0,0,0,.10);
--card: #fff; --ok: #2f8f5f; --warn: #c98415; --bad: #c0392b; --blocked: #6c4cb8;
}
body { font-family: ui-sans-serif, system-ui, "Helvetica Neue", Arial, sans-serif;
background: var(--bg); color: var(--fg); margin:0; padding: 24px 32px 80px; }
h1 { font-family: "Times New Roman", Didot, Georgia, serif; font-weight: 400;
font-size: 30px; margin: 0 0 6px; }
.sub { color: var(--muted); margin: 0 0 24px; }
.topbar { display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-bottom: 22px; }
.topbar .stats { display:flex; gap: 18px; font-size: 13px; }
.topbar .stats b { font-weight: 700; }
.topbar button {
background: var(--fg); color: var(--bg); border: 0; padding: 8px 16px;
border-radius: 999px; font: 600 12px/1 ui-sans-serif, system-ui;
letter-spacing: .12em; text-transform: uppercase; cursor: pointer;
}
.topbar button:disabled { opacity: .45; cursor: not-allowed; }
.legend { display:flex; gap:14px; font-size: 12px; color: var(--muted); }
.legend .dot { display: inline-block; width:10px; height:10px; border-radius:50%; vertical-align: middle; margin-right: 4px; }
table.grid { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--card);
border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
table.grid th, table.grid td {
border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
padding: 10px; vertical-align: top;
}
table.grid th { background: #f1ede4; font: 600 11px/1.2 ui-sans-serif, system-ui;
text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
table.grid th.pat-head { text-align: left; padding-left: 14px; min-width: 160px; }
table.grid td.pat-cell { font-family: "Times New Roman", Didot, Georgia, serif;
font-size: 16px; padding: 12px 14px; background: #faf7f0; }
table.grid td.pat-cell small { display:block; font-family: ui-sans-serif, system-ui;
font-size: 11px; color: var(--muted); letter-spacing: .04em; margin-top: 2px; }
table.grid th.cw-head { min-width: 130px; }
.cw-dot { display:inline-block; width:18px; height:18px; border-radius: 50%;
border: 1px solid var(--line); margin-right: 6px; vertical-align: middle; }
.cell { position: relative; min-height: 120px; }
.cell .thumb {
width: 100%; aspect-ratio: 1/1; background-size: cover; background-position: center;
background-color: #1a1814; border-radius: 6px; margin-bottom: 6px;
border: 1px solid var(--line);
}
.cell .thumb.pending {
background: repeating-linear-gradient(135deg, #ece5d4 0 8px, #f5efde 8px 16px);
display: flex; align-items: center; justify-content: center;
color: var(--muted); font: 600 10px/1 ui-sans-serif, system-ui;
letter-spacing: .14em; text-transform: uppercase;
}
.cell .status-pill {
display: inline-block; padding: 2px 8px; border-radius: 999px;
font: 600 10px/1.6 ui-sans-serif, system-ui;
text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px;
}
.status-PENDING { background: #f0eada; color: #6a6358; }
.status-GENERATED { background: #d9f0e3; color: var(--ok); }
.status-SETTLEMENT_BLOCKED { background: #ede0ff; color: var(--blocked); }
.status-FAILED { background: #fbd9d4; color: var(--bad); }
.cell .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.cell .actions button {
flex: 1 1 auto; padding: 4px 6px; font-size: 10px; letter-spacing: .04em;
background: var(--fg); color: var(--bg); border: 0; border-radius: 4px; cursor: pointer;
}
.cell .actions button.danger { background: var(--bad); }
.cell .err { font-size: 10px; color: var(--bad); margin-top: 4px; line-height: 1.3; max-height: 36px; overflow: hidden; }
.cell .when { font-size: 10px; color: #8a8378; margin-top: 4px; white-space: nowrap; }
#toast {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: #1a1814; color: #fff; padding: 10px 18px; border-radius: 999px;
font-size: 13px; display: none; z-index: 100; max-width: 80vw;
}
#toast.show { display: block; }
#toast.err { background: var(--bad); }
</style>
</head>
<body>
<h1>Fliepaper Bugs — Admin</h1>
<p class="sub">Steve-owned · brand_reference Gucci · 6 patterns × 6 colorways = 36 cells. yolo-runner generates art; this grid surfaces status + per-cell controls.</p>
<div class="topbar">
<div class="stats" id="stats">loading…</div>
<div class="legend">
<span><span class="dot" style="background:#d9f0e3"></span>Generated</span>
<span><span class="dot" style="background:#f0eada"></span>Pending</span>
<span><span class="dot" style="background:#ede0ff"></span>Settlement-blocked</span>
<span><span class="dot" style="background:#fbd9d4"></span>Failed</span>
</div>
<button id="refresh" type="button">Refresh</button>
<a href="/collections/fliepaper-bugs" target="_blank" rel="noopener">
<button type="button">View public page →</button>
</a>
</div>
<table class="grid" id="grid"></table>
<div id="toast"></div>
<script>
const ADMIN_TOKEN = new URLSearchParams(location.search).get('admin') || '';
const AUTH_QS = ADMIN_TOKEN ? ('?admin=' + encodeURIComponent(ADMIN_TOKEN)) : '';
function toast(msg, isErr) {
const t = document.getElementById('toast');
t.textContent = msg; t.className = isErr ? 'show err' : 'show';
setTimeout(() => t.className = '', 4500);
}
function escHtml(s){ return String(s||'').replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); }
function fmtDate(ts){ if(!ts) return ''; const d=new Date(ts); if(isNaN(d)) return ''; return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
async function loadData() {
const r = await fetch('/api/collections/fliepaper-bugs' + AUTH_QS);
if (!r.ok) { toast('load failed (' + r.status + ' — admin token required)', true); return null; }
return r.json();
}
function tally(cells) {
return cells.reduce((a,c) => { a[c.status] = (a[c.status]||0)+1; return a; }, {});
}
function render(data) {
const { spec, cells } = data;
const cellByKey = {};
for (const c of cells) cellByKey[c.pattern_slug + '|' + c.colorway_slug] = c;
const t = tally(cells);
document.getElementById('stats').innerHTML = `
<span><b>${t.GENERATED || 0}</b> generated</span>
<span><b>${t.PENDING || 0}</b> pending</span>
<span><b>${t.SETTLEMENT_BLOCKED || 0}</b> blocked</span>
<span><b>${t.FAILED || 0}</b> failed</span>
<span><b>${cells.length}</b> total</span>
`;
const grid = document.getElementById('grid');
let html = '<thead><tr><th class="pat-head">Pattern \\ Colorway</th>';
for (const cw of spec.palette) {
html += `<th class="cw-head"><span class="cw-dot" style="background:${cw.hex}"></span>${escHtml(cw.name)}</th>`;
}
html += '</tr></thead><tbody>';
for (const p of spec.patterns) {
html += `<tr><td class="pat-cell">${escHtml(p.name)}<small>${escHtml(p.bug)} · ${escHtml(p.scale)}</small></td>`;
for (const cw of spec.palette) {
const cell = cellByKey[p.slug + '|' + cw.slug];
if (!cell) {
html += `<td><div class="cell"><span class="status-pill status-PENDING">missing row</span></div></td>`;
continue;
}
const thumb = (cell.status === 'GENERATED' && cell.thumbnail_url)
? `<div class="thumb" style="background-image:url('${cell.thumbnail_url}')"></div>`
: `<div class="thumb pending">${cell.status}</div>`;
const errLine = cell.last_error
? `<div class="err" title="${escHtml(cell.last_error)}">${escHtml(cell.last_error)}</div>`
: '';
html += `<td>
<div class="cell">
${thumb}
<span class="status-pill status-${cell.status}">${cell.status}</span>
<div class="actions">
<button data-action="regen" data-id="${cell.id}">Regen</button>
${cell.design_id
? `<button data-action="violation" data-design-id="${cell.design_id}" data-cell-id="${cell.id}" class="danger">⚠ Violation</button>`
: `<button disabled title="No design yet">⚠ Violation</button>`}
</div>
${cell.created_at ? `<div class="when" title="${escHtml(cell.created_at)}">🕓 ${escHtml(fmtDate(cell.created_at))}</div>` : ''}
${errLine}
</div>
</td>`;
}
html += '</tr>';
}
html += '</tbody>';
grid.innerHTML = html;
// wire buttons
grid.querySelectorAll('button[data-action="regen"]').forEach(btn => {
btn.addEventListener('click', () => regen(btn.dataset.id));
});
grid.querySelectorAll('button[data-action="violation"]').forEach(btn => {
btn.addEventListener('click', () => violation(btn.dataset.designId, btn.dataset.cellId));
});
}
async function regen(cellId) {
if (!confirm('Queue regeneration for this cell? Settlement check runs FIRST.')) return;
try {
const r = await fetch('/api/collections/fliepaper-bugs/cell/regenerate' + AUTH_QS, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cell_id: parseInt(cellId, 10) }),
});
const j = await r.json();
if (j.settlement && j.settlement.verdict === 'BLOCK') {
toast('Settlement BLOCK: ' + (j.settlement.reason || '(no reason)'), true);
} else if (j.ok) {
toast('Regen queued. Settlement: ' + (j.settlement?.verdict || 'OK'));
setTimeout(refresh, 1500);
} else {
toast('Regen failed: ' + (j.error || 'unknown'), true);
}
} catch (e) { toast('Regen error: ' + e.message, true); }
}
async function violation(designId, cellId) {
const reason = prompt('Settlement violation reason (e.g. "banana leaves", "monstera bleed", "trademark"):');
if (!reason || !reason.trim()) return;
if (!confirm(`HARD delete design #${designId}? URL goes 404, logged to do-not-want registry, fresh regen queued.`)) return;
try {
const r = await fetch('/api/design/' + designId + '/settlement-violation' + AUTH_QS, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason.trim(), cell_id: parseInt(cellId, 10) }),
});
const j = await r.json();
if (j.ok) {
toast('Violation logged. Cell flipped to SETTLEMENT_BLOCKED.');
setTimeout(refresh, 1000);
} else {
toast('Violation failed: ' + (j.error || 'unknown'), true);
}
} catch (e) { toast('Violation error: ' + e.message, true); }
}
async function refresh() {
const data = await loadData();
if (data) render(data);
}
document.getElementById('refresh').addEventListener('click', refresh);
refresh();
// 20s auto-refresh — admin grid should reflect yolo-runner progress live.
setInterval(refresh, 20000);
</script>
<script src="/admin/admin-modal-kit.js" defer></script>
</body>
</html>