← back to Wallco Ai
fliepaper-bugs: API + server.js mount — settlement-gated regen, yolo job queue, GET /api/collections endpoint
b5d6362d72498cdc147c76776fe7a4c947078296 · 2026-05-13 16:54:29 -0700 · Steve
Files touched
M server.jsA src/fliepaper-bugs.js
Diff
commit b5d6362d72498cdc147c76776fe7a4c947078296
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 13 16:54:29 2026 -0700
fliepaper-bugs: API + server.js mount — settlement-gated regen, yolo job queue, GET /api/collections endpoint
---
server.js | 7 ++
src/fliepaper-bugs.js | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 262 insertions(+)
diff --git a/server.js b/server.js
index 3d76da7..10228f0 100644
--- a/server.js
+++ b/server.js
@@ -781,6 +781,13 @@ setInterval(() => load(true), 30000);
// check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
const { isAdmin, requireAdmin } = require('./src/admin-gate');
+// ── Fliepaper Bugs — Gucci Resort Edition (6 patterns × 6 colorways = 36 cells)
+// Settlement-gated regen routes a job file into data/fliepaper-bugs/queue/ that
+// yolo-runner picks up. Public collection page + admin grid; placeholders show
+// while yolo-runner generates art.
+try { require('./src/fliepaper-bugs').mount(app, { isAdmin }); }
+catch (e) { console.error('Fliepaper-bugs mount failed:', e.message); }
+
// ── /admin/reload-designs — re-read data/designs.json without a pm2 reload.
// Localhost-admin per src/admin-gate. Used by drunk-animals tick + future
// generators to surface freshly persisted designs in the live grid without
diff --git a/src/fliepaper-bugs.js b/src/fliepaper-bugs.js
new file mode 100644
index 0000000..436b86f
--- /dev/null
+++ b/src/fliepaper-bugs.js
@@ -0,0 +1,255 @@
+// fliepaper-bugs collection API
+// Mounts:
+// GET /api/collections/fliepaper-bugs — public, returns spec.json + per-cell status
+// POST /api/collections/fliepaper-bugs/cell/regenerate — admin, settlement-gated, queues yolo regen
+// GET /collections/fliepaper-bugs — public HTML
+// GET /admin/fliepaper-bugs — admin HTML (admin-gated)
+//
+// Data model:
+// - spec.json: /data/fliepaper-bugs/spec.json (6 patterns × 6 colorways)
+// - PG fliepaper_bugs_cells: status, design_id (FK spoon_all_designs.id), thumbnail_url, last_error
+// - PG mp_collections row exists (slug='fliepaper-bugs', designer Wallco House)
+//
+// Settlement skill is invoked BEFORE every regenerate. The fliepaper-bugs prompts
+// are insect-only (no foliage/banana/butterfly) so they should always pass —
+// but the gate runs as a hard rule, never bypassed.
+
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const { spawn, execSync } = require('child_process');
+
+const SPEC_PATH = path.join(__dirname, '..', 'data', 'fliepaper-bugs', 'spec.json');
+
+// ── PG access — reuse the parent project's PSQL_CMD convention.
+const DB_NAME = process.env.WALLCO_DB || 'dw_unified';
+const PSQL_CMD = (process.platform === 'linux')
+ ? `sudo -n -u postgres psql ${DB_NAME} -At -q`
+ : `psql ${DB_NAME} -At -q`;
+
+function psqlQuery(sql) {
+ return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+}
+function pgEsc(v) {
+ if (v === null || v === undefined) return 'NULL';
+ if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'NULL';
+ if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
+ return "'" + String(v).replace(/'/g, "''") + "'";
+}
+
+// ── Spec loader (cached; spec.json is hand-edited)
+let SPEC_CACHE = null;
+let SPEC_MTIME = 0;
+function loadSpec() {
+ try {
+ const stat = fs.statSync(SPEC_PATH);
+ if (!SPEC_CACHE || stat.mtimeMs > SPEC_MTIME) {
+ SPEC_CACHE = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
+ SPEC_MTIME = stat.mtimeMs;
+ }
+ return SPEC_CACHE;
+ } catch (e) {
+ return null;
+ }
+}
+
+// ── Settlement gate (inline implementation of the binding rules from
+// ~/.claude/skills/settlement/SKILL.md). For fliepaper-bugs the only
+// risk surface is if a regen request adds foliage to the prompt; the
+// insect motifs themselves are not prohibited. We block prompts that
+// name any of the Part B elements (bananas/grapes/birds/butterflies)
+// or any leaf/frond term. Returns { verdict, reason, suggestion? }.
+function settlementCheck(prompt) {
+ let p = String(prompt || '').toLowerCase();
+
+ // Strip negative-clause tails — they often LIST prohibited elements as
+ // anti-prompts ("no birds, no butterflies, no bananas") and would
+ // false-positive on a substring scan. We check only the affirmative prompt.
+ // Anything from a "no <word>", "without", "excluding", "no foliage" cue to
+ // end-of-sentence is removed before the substring scan runs.
+ p = p
+ .replace(/\bno foliage[^.]*\./g, '.')
+ .replace(/\binsect-only motif[^.]*\./g, '.')
+ .replace(/\bwithout\b[^.]*\./g, '.')
+ .replace(/\bexcluding\b[^.]*\./g, '.')
+ .replace(/\b(?:no|not)\s+[^.,;]*(?:banana|bird|butterfl|grape|leaf|leaves|frond|palm|foliage|tropical)[^.]*\./g, '.')
+ .replace(/\bcorrections:[^.]*\./g, '.');
+
+ // Part B — specific prohibited elements (any one = BLOCK).
+ // Word-boundary regex avoids substring false-positives.
+ const partB = [
+ { rx: /\bbanana(?:s|\s+pods?)?\b/, label: 'bananas / banana pods' },
+ { rx: /\bplantains?\b/, label: 'plantain (banana family)' },
+ { rx: /\bgrapes?\b/, label: 'grapes' },
+ { rx: /\bbirds?\b/, label: 'birds' },
+ { rx: /\bbutterfl(?:y|ies)\b/, label: 'butterflies' },
+ ];
+ for (const e of partB) {
+ if (e.rx.test(p)) {
+ return { verdict: 'BLOCK', reason: `Part B prohibited element: ${e.label}`,
+ suggestion: `Remove "${e.label}" from the prompt. Fliepaper-bugs is insect-only (scarab, luna moth, dragonfly, stag beetle, honeybee, firefly).` };
+ }
+ }
+
+ // Part A — directional foliage with negative space and >1 ink color.
+ // Heuristic flag: any mention of leaves/fronds/palm/foliage/tropical.
+ const foliage = /(\bleaves?\b|fronds?|\bpalm\b|monstera|philodendron|\bfoliage\b|\btropical\b)/.test(p);
+ if (foliage) {
+ return { verdict: 'NEEDS_REVIEW',
+ reason: 'Foliage terms detected. Fliepaper-bugs prompts should be insect-focused; foliage risks Part A directional-leaf coverage.',
+ suggestion: 'Replace foliage with the pattern\'s specified composition (trellis, damask, lacquer panels) or add a tree trunk / clear branch to qualify as Acceptable Tropical Design.' };
+ }
+
+ // Insect-only prompt — clean.
+ return { verdict: 'OK', reason: 'No prohibited elements detected.' };
+}
+
+// ── Compose the generator prompt for a cell from spec composition + colorway.
+function composeCellPrompt(pattern, colorway) {
+ return `${pattern.name} — ${pattern.composition}. Colorway: ${colorway.name} (${colorway.hex}, ${colorway.gucci_ref}); role=${colorway.role}. Seamless tile repeat, luxury wallcovering scale ${pattern.scale}, brand reference Gucci Resort. Insect-only motif (no foliage, no birds, no butterflies, no bananas).`;
+}
+
+// ── Read all 36 cells joined with spoon_all_designs for thumbnails.
+function readCells() {
+ const sql = `
+ SELECT row_to_json(t) FROM (
+ SELECT c.id, c.pattern_slug, c.pattern_name, c.bug, c.scale, c.colorway_slug,
+ c.colorway_name, c.colorway_hex, c.colorway_role, c.gucci_ref, c.status,
+ c.design_id, c.thumbnail_url, c.last_error, c.regen_count, c.generated_at,
+ c.settlement_verdict, c.composition,
+ CASE WHEN d.local_path IS NOT NULL
+ THEN '/designs/img/' || regexp_replace(d.local_path, '^.*/', '')
+ ELSE NULL END AS design_image_url,
+ d.is_published AS design_is_published
+ FROM fliepaper_bugs_cells c
+ LEFT JOIN spoon_all_designs d ON d.id = c.design_id
+ WHERE c.collection_slug='fliepaper-bugs'
+ ORDER BY c.pattern_slug, c.colorway_slug
+ ) t;
+ `;
+ const raw = psqlQuery(sql);
+ if (!raw) return [];
+ return raw.split('\n').filter(Boolean).map(line => {
+ try {
+ const r = JSON.parse(line);
+ // Prefer the snapshot thumbnail_url; fall back to design's served image
+ if (!r.thumbnail_url && r.design_image_url) r.thumbnail_url = r.design_image_url;
+ return r;
+ } catch { return null; }
+ }).filter(Boolean);
+}
+
+function mount(app, { isAdmin }) {
+ const path_ = path;
+ const pagesPublic = path_.join(__dirname, '..', 'public', 'collections', 'fliepaper-bugs.html');
+ const pagesAdmin = path_.join(__dirname, '..', 'public', 'admin', 'fliepaper-bugs.html');
+
+ // ─── HTML — public collection page
+ app.get('/collections/fliepaper-bugs', (_req, res) => {
+ res.setHeader('Cache-Control', 'no-store, must-revalidate');
+ res.sendFile(pagesPublic);
+ });
+
+ // ─── HTML — admin grid (admin-gated)
+ app.get('/admin/fliepaper-bugs', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ res.setHeader('Cache-Control', 'no-store, must-revalidate');
+ res.sendFile(pagesAdmin);
+ });
+
+ // ─── API — collection state (spec + per-cell status)
+ app.get('/api/collections/fliepaper-bugs', (_req, res) => {
+ try {
+ const spec = loadSpec();
+ if (!spec) return res.status(500).json({ error: 'spec.json missing' });
+ const cells = readCells();
+ res.json({ ok: true, spec, cells });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+ });
+
+ // ─── API — regenerate a cell (admin, settlement-gated, queues yolo agent)
+ app.post('/api/collections/fliepaper-bugs/cell/regenerate', (req, res) => {
+ if (!isAdmin(req)) return res.status(403).json({ ok: false, error: 'admin only' });
+
+ const cellId = parseInt(req.body?.cell_id, 10);
+ if (!Number.isFinite(cellId) || cellId < 1) {
+ return res.status(400).json({ ok: false, error: 'cell_id required' });
+ }
+
+ try {
+ // Look up the cell
+ const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT * FROM fliepaper_bugs_cells WHERE id=${cellId}) t;`);
+ if (!raw) return res.status(404).json({ ok: false, error: 'cell not found' });
+ const cell = JSON.parse(raw);
+
+ const spec = loadSpec();
+ if (!spec) return res.status(500).json({ ok: false, error: 'spec.json missing' });
+ const pattern = spec.patterns.find(p => p.slug === cell.pattern_slug);
+ const colorway = spec.palette.find(c => c.slug === cell.colorway_slug);
+ if (!pattern || !colorway) {
+ return res.status(500).json({ ok: false, error: 'pattern/colorway mismatch with spec' });
+ }
+
+ // Compose prompt + run settlement gate BEFORE any generation
+ const prompt = composeCellPrompt(pattern, colorway);
+ const verdict = settlementCheck(prompt);
+
+ // Persist the verdict on the cell regardless of outcome
+ psqlQuery(`UPDATE fliepaper_bugs_cells
+ SET settlement_verdict=${pgEsc(JSON.stringify(verdict))}::jsonb,
+ updated_at=now()
+ WHERE id=${cellId};`);
+
+ if (verdict.verdict === 'BLOCK') {
+ psqlQuery(`UPDATE fliepaper_bugs_cells
+ SET status='SETTLEMENT_BLOCKED',
+ last_error=${pgEsc('Settlement BLOCK: ' + verdict.reason)},
+ updated_at=now()
+ WHERE id=${cellId};`);
+ return res.json({ ok: false, settlement: verdict, message: 'Settlement gate BLOCKED the regenerate request.' });
+ }
+
+ // Queue the yolo-agent / generator. yolo-runner watches
+ // data/fliepaper-bugs/queue/*.json — we drop a job file there.
+ const queueDir = path_.join(__dirname, '..', 'data', 'fliepaper-bugs', 'queue');
+ try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
+ const jobId = `${Date.now()}_${cellId}`;
+ const jobFile = path_.join(queueDir, `${jobId}.json`);
+ const job = {
+ job_id: jobId,
+ cell_id: cellId,
+ pattern: { slug: pattern.slug, name: pattern.name, bug: pattern.bug, scale: pattern.scale, composition: pattern.composition },
+ colorway: { slug: colorway.slug, name: colorway.name, hex: colorway.hex, role: colorway.role, gucci_ref: colorway.gucci_ref },
+ prompt,
+ settlement: verdict,
+ queued_at: new Date().toISOString(),
+ previous_design_id: cell.design_id || null,
+ };
+ fs.writeFileSync(jobFile, JSON.stringify(job, null, 2));
+
+ // Mark cell PENDING + bump regen_count
+ psqlQuery(`UPDATE fliepaper_bugs_cells
+ SET status='PENDING',
+ last_error=NULL,
+ regen_count=regen_count+1,
+ updated_at=now()
+ WHERE id=${cellId};`);
+
+ res.json({
+ ok: true,
+ settlement: verdict,
+ job_file: jobFile,
+ cell_id: cellId,
+ note: 'Queued for yolo-runner. yolo-agent watches data/fliepaper-bugs/queue/. Cell flipped to PENDING.',
+ });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+ });
+
+ console.log(' Fliepaper-bugs collection mounted (/collections/fliepaper-bugs, /admin/fliepaper-bugs, /api/collections/fliepaper-bugs)');
+}
+
+module.exports = { mount, settlementCheck, composeCellPrompt };
← 8e4960d fliepaper-bugs: collection page + admin grid HTML (placehold
·
back to Wallco Ai
·
wallco: fliepaper-bugs tick generator script 7d5c357 →