← back to Wallco Ai
fliepaper-bugs: collection page + admin grid HTML (placeholder-aware, 36 cells)
8e4960d6b5476c42c421376e3c8364aa1fb134d8 · 2026-05-13 16:50:31 -0700 · Steve
Files touched
A public/admin/fliepaper-bugs.htmlA public/collections/fliepaper-bugs.html
Diff
commit 8e4960d6b5476c42c421376e3c8364aa1fb134d8
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 13 16:50:31 2026 -0700
fliepaper-bugs: collection page + admin grid HTML (placeholder-aware, 36 cells)
---
public/admin/fliepaper-bugs.html | 242 ++++++++++++++++++
public/collections/fliepaper-bugs.html | 451 +++++++++++++++++++++++++++++++++
2 files changed, 693 insertions(+)
diff --git a/public/admin/fliepaper-bugs.html b/public/admin/fliepaper-bugs.html
new file mode 100644
index 0000000..2ab7872
--- /dev/null
+++ b/public/admin/fliepaper-bugs.html
@@ -0,0 +1,242 @@
+<!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; }
+
+ #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])); }
+
+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>
+ ${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>
+</body>
+</html>
diff --git a/public/collections/fliepaper-bugs.html b/public/collections/fliepaper-bugs.html
new file mode 100644
index 0000000..c0e6671
--- /dev/null
+++ b/public/collections/fliepaper-bugs.html
@@ -0,0 +1,451 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>fliepaper × wallco — Gucci Resort Edition</title>
+<meta name="viewport" content="width=device-width,initial-scale=1" />
+<meta name="description" content="Six luxury insect wallcoverings — scarab, luna moth, dragonfly, stag beetle, honeybee, firefly — in six Gucci-resort colorways. 36 SKUs from wallco.ai." />
+<link rel="stylesheet" href="/marketplace/_layout.css" />
+<style>
+ :root {
+ --fb-bg: #faf7f1;
+ --fb-fg: #1a1814;
+ --fb-muted: #6b665c;
+ --fb-line: rgba(0,0,0,.08);
+ --fb-card: #fff;
+ --fb-accent: #0F4F38;
+ --fb-cols: 3;
+ }
+ html[data-theme="dark"] {
+ --fb-bg: #0e0d0b;
+ --fb-fg: #f3eee2;
+ --fb-muted: #a39c8b;
+ --fb-line: rgba(255,255,255,.10);
+ --fb-card: #191714;
+ --fb-accent: #B8914A;
+ }
+ html,body { background: var(--fb-bg); color: var(--fb-fg); margin:0; padding:0;
+ font-family: ui-sans-serif, system-ui, "Helvetica Neue", Arial, sans-serif;
+ transition: background 200ms ease, color 200ms ease;
+ }
+ a { color: inherit; }
+
+ /* ─────── HERO (Gucci-style) ─────── */
+ .fb-hero {
+ position: relative;
+ height: min(96vh, 1080px);
+ min-height: 540px;
+ overflow: hidden;
+ background: #14110d;
+ }
+ .fb-hero-img {
+ position: absolute; inset:0;
+ background-size: cover; background-position: center;
+ transition: opacity 800ms ease;
+ opacity: 0;
+ }
+ .fb-hero-img.is-active { opacity: 1; }
+ .fb-hero-fallback {
+ position: absolute; inset:0;
+ background:
+ radial-gradient(ellipse at 30% 20%, rgba(184,145,74,.30), transparent 55%),
+ radial-gradient(ellipse at 70% 80%, rgba(15,79,56,.45), transparent 55%),
+ linear-gradient(135deg, #0F4F38 0%, #1A1F3C 50%, #3D1F52 100%);
+ }
+ .fb-hero-overlay {
+ position: absolute; inset:0;
+ background: linear-gradient(180deg, rgba(0,0,0,.20) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,.55) 100%);
+ pointer-events: none;
+ }
+
+ /* logo UL */
+ .fb-logo {
+ position: absolute; top: 20px; left: 28px; z-index: 5;
+ color: #fff; font-weight: 700; letter-spacing: .12em; font-size: 14px;
+ text-decoration: none; mix-blend-mode: difference;
+ }
+ /* hamburger UR ≡ MENU */
+ .fb-menu {
+ position: absolute; top: 18px; right: 24px; z-index: 5;
+ display: inline-flex; align-items: center; gap: 8px;
+ color: #fff; cursor: pointer; padding: 8px 4px;
+ background: transparent; border: 0;
+ font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
+ letter-spacing: .22em; text-transform: uppercase;
+ mix-blend-mode: difference;
+ }
+ .fb-menu .bars { display:inline-flex; flex-direction:column; gap:4px; }
+ .fb-menu .bars span { width: 22px; height: 1px; background: currentColor; display:block; }
+
+ /* dark/light toggle */
+ .fb-theme-toggle {
+ position: absolute; top: 20px; right: 110px; z-index: 5;
+ background: transparent; border: 0; cursor: pointer;
+ color: #fff; mix-blend-mode: difference;
+ font-size: 18px; padding: 6px 8px;
+ }
+
+ /* wordmark BELOW the photo (i.e. centered, low in the hero) */
+ .fb-wordmark {
+ position: absolute; left:0; right:0; bottom: 22%;
+ z-index: 4; text-align: center; color: #fff;
+ text-shadow: 0 2px 28px rgba(0,0,0,.45);
+ pointer-events: none;
+ }
+ .fb-wordmark .brand {
+ font-family: "Times New Roman", Didot, Georgia, serif;
+ font-size: clamp(48px, 9vw, 132px);
+ font-weight: 400; letter-spacing: .02em; line-height: 1;
+ }
+ .fb-wordmark .tag {
+ margin-top: 14px; font-style: italic; font-size: clamp(14px, 1.6vw, 20px);
+ letter-spacing: .18em; text-transform: uppercase; opacity: .92;
+ }
+ /* white Enter pill — centered bottom of hero */
+ .fb-enter {
+ position: absolute; left: 50%; bottom: 6%; transform: translateX(-50%);
+ z-index: 5;
+ background: #fff; color: #111;
+ padding: 14px 56px; border-radius: 4px;
+ font: 600 13px/1 ui-sans-serif, system-ui, sans-serif;
+ letter-spacing: .22em; text-transform: uppercase;
+ text-decoration: none;
+ transition: transform 200ms ease, box-shadow 200ms ease;
+ }
+ .fb-enter:hover { transform: translateX(-50%) translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,.20); }
+
+ /* ─────── BODY ─────── */
+ .fb-main { max-width: 1480px; margin: 0 auto; padding: 56px 28px 100px; }
+
+ .fb-intro {
+ text-align: center; max-width: 720px; margin: 0 auto 36px;
+ }
+ .fb-intro h2 {
+ font-family: "Times New Roman", Didot, Georgia, serif; font-weight: 400;
+ font-size: clamp(28px, 3.6vw, 44px); margin: 0 0 12px;
+ }
+ .fb-intro p { color: var(--fb-muted); line-height: 1.55; margin: 0; }
+
+ /* color-dot row */
+ .fb-palette {
+ display: flex; gap: 18px; justify-content: center; align-items: center;
+ margin: 28px auto 48px; flex-wrap: wrap;
+ }
+ .fb-palette .swatch {
+ display: flex; flex-direction: column; align-items:center; gap: 8px;
+ cursor: pointer; min-width: 84px;
+ }
+ .fb-palette .dot {
+ width: 44px; height: 44px; border-radius: 50%;
+ border: 1px solid var(--fb-line);
+ box-shadow: inset 0 0 0 3px var(--fb-bg);
+ }
+ .fb-palette .swatch.is-active .dot { box-shadow: inset 0 0 0 3px var(--fb-bg), 0 0 0 2px var(--fb-fg); }
+ .fb-palette .swatch .name { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--fb-muted); }
+
+ /* controls (sort + density) */
+ .fb-controls {
+ display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
+ margin: 0 0 28px; padding: 14px 16px;
+ background: var(--fb-card);
+ border: 1px solid var(--fb-line); border-radius: 12px;
+ }
+ .fb-controls label { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--fb-muted); }
+ .fb-controls select {
+ padding: 8px 14px; border-radius: 999px; border: 1px solid var(--fb-line);
+ background: var(--fb-bg); color: var(--fb-fg); font-size: 14px;
+ }
+ .fb-controls .pill {
+ margin-left: auto; font-size: 12px; color: var(--fb-muted);
+ letter-spacing: .08em; text-transform: uppercase;
+ }
+
+ /* pattern grid */
+ .fb-grid {
+ display: grid; gap: 26px;
+ grid-template-columns: repeat(var(--fb-cols), 1fr);
+ }
+ @media (max-width: 980px) { :root { --fb-cols: 2; } }
+ @media (max-width: 600px) { :root { --fb-cols: 1; } }
+
+ .fb-card {
+ background: var(--fb-card); border: 1px solid var(--fb-line);
+ border-radius: 14px; overflow: hidden;
+ display: flex; flex-direction: column;
+ transition: transform 180ms ease, box-shadow 180ms ease;
+ }
+ .fb-card:hover { transform: translateY(-3px); box-shadow: 0 12px 28px rgba(0,0,0,.10); }
+ .fb-card .pat-img {
+ aspect-ratio: 1 / 1.15;
+ background-size: cover; background-position: center;
+ background-color: #1a1814; position: relative;
+ }
+ .fb-card .pat-pending {
+ position: absolute; top: 12px; left: 12px;
+ background: rgba(0,0,0,.55); color: #fff;
+ font: 600 10px/1 ui-sans-serif, system-ui;
+ letter-spacing: .2em; text-transform: uppercase;
+ padding: 6px 10px; border-radius: 999px;
+ }
+ .fb-card .body { padding: 16px 18px 18px; }
+ .fb-card .name {
+ font-family: "Times New Roman", Didot, Georgia, serif; font-size: 22px;
+ margin: 0 0 4px;
+ }
+ .fb-card .bug-sub { color: var(--fb-muted); font-size: 13px; margin: 0 0 12px;
+ letter-spacing: .04em; text-transform: capitalize; }
+ .fb-card .chips { display: flex; gap: 6px; flex-wrap: wrap; }
+ .fb-card .chip {
+ width: 22px; height: 22px; border-radius: 50%;
+ border: 1px solid var(--fb-line);
+ cursor: pointer; position: relative;
+ }
+ .fb-card .chip.is-missing::after {
+ content: ""; position: absolute; inset: 4px;
+ border: 1px dashed rgba(0,0,0,.30); border-radius: 50%;
+ }
+ .fb-card .meta-row {
+ display: flex; gap: 10px; justify-content: space-between;
+ align-items: center; margin-top: 14px;
+ font-size: 12px; color: var(--fb-muted);
+ }
+
+ .fb-footer {
+ margin-top: 80px; padding: 28px 0; border-top: 1px solid var(--fb-line);
+ text-align: center; color: var(--fb-muted); font-size: 12px; letter-spacing: .12em;
+ text-transform: uppercase;
+ }
+ .fb-footer a { color: inherit; text-decoration: underline; text-underline-offset: 4px; }
+</style>
+</head>
+<body data-theme="light">
+ <!-- ───────────── HERO ───────────── -->
+ <section class="fb-hero" id="fb-hero">
+ <div class="fb-hero-fallback" id="fb-hero-fallback"></div>
+ <a href="/" class="fb-logo">wallco.ai</a>
+ <button class="fb-theme-toggle" id="fb-theme" title="Toggle light/dark">◐</button>
+ <button class="fb-menu" type="button" onclick="document.dispatchEvent(new Event('wallco-open-menu'))" aria-label="Open menu">
+ <span class="bars"><span></span><span></span><span></span></span>
+ MENU
+ </button>
+ <div class="fb-hero-overlay"></div>
+ <div class="fb-wordmark">
+ <div class="brand">fliepaper × wallco</div>
+ <div class="tag">Gucci Resort Edition · 6 patterns · 6 colorways · 36 SKUs</div>
+ </div>
+ <a class="fb-enter" href="#collection">Enter</a>
+ </section>
+
+ <!-- ───────────── BODY ───────────── -->
+ <main class="fb-main" id="collection">
+ <section class="fb-intro">
+ <h2>Six bugs. Six Gucci-resort colorways.</h2>
+ <p>Scarab, luna moth, dragonfly, stag beetle, honeybee, firefly — six luxury insect wallcoverings rendered in six Gucci-coordinated colorways: emerald, papyrus, midnight, antique horsebit, deep plum, oxblood. Designed in-house by Wallco House Studio. Every pattern generated through proprietary Wallco AI.</p>
+ </section>
+
+ <!-- Color dot row -->
+ <div class="fb-palette" id="fb-palette"></div>
+
+ <!-- Sort + density -->
+ <div class="fb-controls">
+ <label for="fb-sort">Sort</label>
+ <select id="fb-sort">
+ <option value="newest">Newest</option>
+ <option value="color">Color</option>
+ <option value="pattern">Pattern A→Z</option>
+ <option value="sku">SKU A→Z</option>
+ </select>
+
+ <label for="fb-density">Density</label>
+ <input type="range" id="fb-density" min="1" max="4" value="3" step="1" />
+
+ <span class="pill" id="fb-meta">loading…</span>
+ </div>
+
+ <div class="fb-grid" id="fb-grid"></div>
+ </main>
+
+ <footer class="fb-footer">
+ <a href="/">wallco.ai</a> · Fliepaper Bugs — Gucci Resort Edition · <span id="fb-status-count">0/36</span> SKUs generated
+ </footer>
+
+<script>
+const STORAGE_KEY = 'wallco.fliepaper-bugs.prefs';
+const FB = { spec: null, cells: [], colorwayFilter: null };
+
+function loadPrefs() {
+ try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; }
+}
+function savePrefs() {
+ const p = {
+ sort: document.getElementById('fb-sort').value,
+ density: document.getElementById('fb-density').value,
+ theme: document.body.dataset.theme,
+ };
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch {}
+}
+
+function applyDensity() {
+ const v = parseInt(document.getElementById('fb-density').value, 10);
+ // 1→1col, 2→2col, 3→3col, 4→4col
+ document.documentElement.style.setProperty('--fb-cols', v);
+}
+
+function applyTheme(theme) {
+ document.documentElement.setAttribute('data-theme', theme);
+ document.body.dataset.theme = theme;
+ document.getElementById('fb-theme').textContent = theme === 'dark' ? '☀' : '◐';
+}
+
+function placeholderPoster(hex, label) {
+ // hsl-gradient style placeholder — never a 404, never a broken image.
+ // Two-stop gradient seeded from the colorway hex so each pattern×colorway
+ // pair gets a distinct placeholder.
+ const seed = label.split('').reduce((s,c)=>s+c.charCodeAt(0),0);
+ const hueShift = (seed % 60) - 30;
+ return `linear-gradient(135deg,
+ ${hex} 0%,
+ color-mix(in oklab, ${hex} 65%, #000 35%) 100%)`;
+}
+
+function escHtml(s){ return String(s||'').replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); }
+
+async function fetchCollection() {
+ try {
+ const r = await fetch('/api/collections/fliepaper-bugs');
+ if (!r.ok) throw new Error('http ' + r.status);
+ const j = await r.json();
+ FB.spec = j.spec;
+ FB.cells = j.cells || [];
+ renderPalette();
+ renderGrid();
+ updateMeta();
+ } catch (e) {
+ document.getElementById('fb-meta').textContent = 'load error: ' + e.message;
+ }
+}
+
+function renderPalette() {
+ const el = document.getElementById('fb-palette');
+ el.innerHTML = '';
+ for (const c of FB.spec.palette) {
+ const w = document.createElement('div');
+ w.className = 'swatch' + (FB.colorwayFilter === c.slug ? ' is-active' : '');
+ w.innerHTML = `
+ <div class="dot" style="background:${c.hex}"></div>
+ <div class="name">${escHtml(c.name)}</div>
+ `;
+ w.addEventListener('click', () => {
+ FB.colorwayFilter = (FB.colorwayFilter === c.slug) ? null : c.slug;
+ renderPalette();
+ renderGrid();
+ });
+ el.appendChild(w);
+ }
+}
+
+function sortPatterns(patterns) {
+ const mode = document.getElementById('fb-sort').value;
+ const arr = patterns.slice();
+ if (mode === 'pattern') arr.sort((a,b) => a.name.localeCompare(b.name));
+ else if (mode === 'sku') arr.sort((a,b) => a.slug.localeCompare(b.slug));
+ else if (mode === 'color') {
+ // group by dominant colorway present — placeholder until art lands, sort by first-cell hex
+ arr.sort((a,b) => a.slug.localeCompare(b.slug));
+ }
+ // 'newest' = spec order (the order yolo-runner generates in)
+ return arr;
+}
+
+function renderGrid() {
+ const grid = document.getElementById('fb-grid');
+ const patterns = sortPatterns(FB.spec.patterns);
+ grid.innerHTML = '';
+
+ for (const p of patterns) {
+ const cellsForPat = FB.cells.filter(c => c.pattern_slug === p.slug);
+ const visibleCells = FB.colorwayFilter
+ ? cellsForPat.filter(c => c.colorway_slug === FB.colorwayFilter)
+ : cellsForPat;
+
+ if (FB.colorwayFilter && visibleCells.length === 0) continue;
+
+ // Hero image for the card — prefer a generated cell that matches the
+ // active colorway filter, else first GENERATED cell, else placeholder.
+ const heroCell = (FB.colorwayFilter
+ ? visibleCells.find(c => c.status === 'GENERATED')
+ : null)
+ || cellsForPat.find(c => c.status === 'GENERATED')
+ || cellsForPat[0];
+
+ const heroHex = (heroCell && heroCell.colorway_hex) || '#1A1F3C';
+ const heroIsReal = heroCell && heroCell.status === 'GENERATED' && heroCell.thumbnail_url;
+
+ const card = document.createElement('article');
+ card.className = 'fb-card';
+ card.innerHTML = `
+ <div class="pat-img" style="${heroIsReal
+ ? `background-image:url('${heroCell.thumbnail_url}')`
+ : `background-image:${placeholderPoster(heroHex, p.slug)}`}">
+ ${heroIsReal ? '' : `<span class="pat-pending">Pending</span>`}
+ </div>
+ <div class="body">
+ <h3 class="name">${escHtml(p.name)}</h3>
+ <div class="bug-sub">${escHtml(p.bug)} · ${escHtml(p.scale)}</div>
+ <div class="chips" data-pat="${p.slug}"></div>
+ <div class="meta-row">
+ <span>${visibleCells.filter(c => c.status === 'GENERATED').length}/${visibleCells.length} colorways live</span>
+ <span>${p.composition.split(';')[0]}</span>
+ </div>
+ </div>
+ `;
+
+ // chip row — one per colorway
+ const chips = card.querySelector('.chips');
+ for (const c of FB.spec.palette) {
+ const cell = cellsForPat.find(x => x.colorway_slug === c.slug);
+ const span = document.createElement('span');
+ span.className = 'chip' + (cell?.status === 'GENERATED' ? '' : ' is-missing');
+ span.style.background = c.hex;
+ span.title = `${c.name} — ${cell?.status || 'PENDING'}`;
+ span.addEventListener('click', () => {
+ FB.colorwayFilter = (FB.colorwayFilter === c.slug) ? null : c.slug;
+ renderPalette();
+ renderGrid();
+ });
+ chips.appendChild(span);
+ }
+ grid.appendChild(card);
+ }
+}
+
+function updateMeta() {
+ const generated = FB.cells.filter(c => c.status === 'GENERATED').length;
+ const total = FB.cells.length;
+ document.getElementById('fb-meta').textContent =
+ `${generated}/${total} SKUs generated · ${FB.spec.patterns.length} patterns · ${FB.spec.palette.length} colorways`;
+ document.getElementById('fb-status-count').textContent = `${generated}/${total}`;
+}
+
+// ─── Init ───
+(function init() {
+ const prefs = loadPrefs();
+ if (prefs.sort) document.getElementById('fb-sort').value = prefs.sort;
+ if (prefs.density) document.getElementById('fb-density').value = prefs.density;
+ applyDensity();
+ applyTheme(prefs.theme === 'dark' ? 'dark' : 'light');
+
+ document.getElementById('fb-sort').addEventListener('change', () => { savePrefs(); renderGrid(); });
+ document.getElementById('fb-density').addEventListener('input', () => { applyDensity(); savePrefs(); });
+ document.getElementById('fb-theme').addEventListener('click', () => {
+ applyTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
+ savePrefs();
+ });
+
+ fetchCollection();
+ // Re-poll every 30s so newly generated cells swap in without reload.
+ setInterval(fetchCollection, 30000);
+})();
+</script>
+</body>
+</html>
← 4bc1561 fliepaper-bugs: DB seed — mp_collections row + fliepaper_bug
·
back to Wallco Ai
·
fliepaper-bugs: API + server.js mount — settlement-gated reg b5d6362 →