← back to Wallco Ai
feat(cactus): 10 11ft × 7.5ft single-cactus murals + hide Pattern-scale in MURAL mode
b4d4d1621ed629accd12b0f625351ee7feb379bf · 2026-05-21 14:22:58 -0700 · Steve Abrams
- scripts/generate-cactus-11ft.js — Pass 3 of the cactus brief. 10 designs,
each a distinct cactus species (saguaro 6-arm/2-arm/asymmetric/bare,
organ-pipe, prickly-pear, agave, ocotillo, cholla, golden barrel) × a
designer Tucson colorway. SDXL native 832×1216 portrait, print spec
90in × 132in (7.5 ft × 11 ft), single-panel. Loud anti-tile/-tessellation
prompt + SDXL anti-prompt unreliability noted (per
feedback_sdxl_antiprompts_unreliable — outputs landed as photoreal
desert scenes, not the flat-ink silhouette the prompt asked for; Steve
approved 'publish as-is — they look like luxury hospitality murals').
- scripts/publish-cactus-11ft.js — companion script: lists unpublished
rows, flips is_published=TRUE, accepts --dry-run / --id / --reject.
- server.js Pattern-scale slider + 24in repeat readout now hide in MURAL
mode (level === 0), same reasoning as the earlier ghosting-button hide
(5f15c01) — a 20×11 ft single-mural canvas has no repeat distance, so
showing a 'Pattern scale' control is misleading. Wrapped the
.scale-controls div in id='scale-controls' and toggle display in the
mural-stepper apply() function.
Files touched
A scripts/generate-cactus-11ft.jsA scripts/publish-cactus-11ft.jsM server.js
Diff
commit b4d4d1621ed629accd12b0f625351ee7feb379bf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 21 14:22:58 2026 -0700
feat(cactus): 10 11ft × 7.5ft single-cactus murals + hide Pattern-scale in MURAL mode
- scripts/generate-cactus-11ft.js — Pass 3 of the cactus brief. 10 designs,
each a distinct cactus species (saguaro 6-arm/2-arm/asymmetric/bare,
organ-pipe, prickly-pear, agave, ocotillo, cholla, golden barrel) × a
designer Tucson colorway. SDXL native 832×1216 portrait, print spec
90in × 132in (7.5 ft × 11 ft), single-panel. Loud anti-tile/-tessellation
prompt + SDXL anti-prompt unreliability noted (per
feedback_sdxl_antiprompts_unreliable — outputs landed as photoreal
desert scenes, not the flat-ink silhouette the prompt asked for; Steve
approved 'publish as-is — they look like luxury hospitality murals').
- scripts/publish-cactus-11ft.js — companion script: lists unpublished
rows, flips is_published=TRUE, accepts --dry-run / --id / --reject.
- server.js Pattern-scale slider + 24in repeat readout now hide in MURAL
mode (level === 0), same reasoning as the earlier ghosting-button hide
(5f15c01) — a 20×11 ft single-mural canvas has no repeat distance, so
showing a 'Pattern scale' control is misleading. Wrapped the
.scale-controls div in id='scale-controls' and toggle display in the
mural-stepper apply() function.
---
scripts/generate-cactus-11ft.js | 253 ++++++++++++++++++++++++++++++++++++++++
scripts/publish-cactus-11ft.js | 163 ++++++++++++++++++++++++++
server.js | 79 ++++++++++++-
3 files changed, 494 insertions(+), 1 deletion(-)
diff --git a/scripts/generate-cactus-11ft.js b/scripts/generate-cactus-11ft.js
new file mode 100644
index 0000000..4fe7229
--- /dev/null
+++ b/scripts/generate-cactus-11ft.js
@@ -0,0 +1,253 @@
+#!/usr/bin/env node
+// Pass 3 — eleven-foot single-cactus murals.
+//
+// Brief (Steve, 2026-05-21): "create at scale similar items and scale. not
+// small repeated items. 1 big cactus."
+//
+// What's different from Pass 1 (generate-cactus.js — seamless tile repeats)
+// and Pass 2 (generate-saguaro-arms-v2.js — 8 ft × 4.25 ft single specimens,
+// one colorway only):
+//
+// - Each output is ONE big isolated cactus, dead-center of a portrait
+// mural canvas, scaled to read at 132 inches (11 ft) tall in the room.
+// - SDXL native portrait 832 × 1216 (aspect 0.684 matches 90"×132" target).
+// - Print spec passed to generate_designs.js: --width 90 --height 132
+// (= 7.5 ft × 11 ft mural panel, single panel, kind=mural).
+// - 10 designs, each a distinct cactus species × colorway combination from
+// the Omni Tucson / Sonoran luxury palette.
+// - Settlement gate: single isolated subject → A1 fails (no directional
+// repeating leaves), A2 fails (no tessellation), A3 fails (single ink
+// figure layer). Cactus is not in Part B. Gate passes by construction.
+//
+// To run:
+// cd ~/Projects/wallco-ai && node scripts/generate-cactus-11ft.js
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+
+// 10 distinct cactus mural compositions
+const DESIGNS = [
+ {
+ slug: 'saguaro-six-arm-candelabra',
+ species: 'saguaro',
+ desc:
+ 'A single mature giant saguaro cactus rising from the ground to fill the entire canvas. ' +
+ 'Six arms in a dense candelabra arrangement — three on the left side at staggered heights ' +
+ '(25, 50, 75 percent up the trunk) and three on the right (35, 60, 80 percent). ' +
+ 'Trunk thick at the base, ribs visible, tapering very slightly toward the crown. ' +
+ 'Reads as a Sonoran-desert elder saguaro, 30+ feet tall in real life.',
+ colorway: 'mesquite-on-bone',
+ },
+ {
+ slug: 'saguaro-two-arm-classic',
+ species: 'saguaro',
+ desc:
+ 'A single classic two-armed saguaro cactus — the iconic silhouette. Trunk rises straight ' +
+ 'from the bottom. Two symmetric arms curve up from each side of the trunk at roughly 40 percent ' +
+ 'trunk height, arm tips reaching to about 75 percent total height. Heroic, balanced, ' +
+ 'photographic but rendered as flat ink illustration.',
+ colorway: 'catalina-dusk',
+ },
+ {
+ slug: 'saguaro-asymmetric-tall',
+ species: 'saguaro',
+ desc:
+ 'A single tall slender saguaro with a single very long arm curving up on the right side from ' +
+ 'about 30 percent trunk height, its tip reaching nearly the top of the trunk, and a stubby ' +
+ 'short arm budding on the left at about 20 percent trunk height. Asymmetric and graceful.',
+ colorway: 'adobe-sundown',
+ },
+ {
+ slug: 'saguaro-bare-column',
+ species: 'saguaro',
+ desc:
+ 'A single tall slender bare saguaro column — NO arms at all. A young mature saguaro that ' +
+ 'has not yet branched. Pure vertical silhouette, ribbed trunk, tapered crown. ' +
+ 'Reads as quiet, monumental, minimalist.',
+ colorway: 'caliche-limestone',
+ },
+ {
+ slug: 'organ-pipe-cluster',
+ species: 'organ-pipe',
+ desc:
+ 'A single dense organ-pipe-cactus cluster — fifteen to twenty parallel vertical stems rising ' +
+ 'from a shared base, varying in height, all roughly the same diameter, like a pipe organ ' +
+ 'standing in the desert. Tightest stems in the center, slightly shorter stems fanning out ' +
+ 'to either side.',
+ colorway: 'saguaro-patina',
+ },
+ {
+ slug: 'prickly-pear-tower',
+ species: 'prickly-pear',
+ desc:
+ 'A single very large prickly-pear cactus tower — paddle-shaped pads stacked on top of each ' +
+ 'other in a vertical zigzag, growing wider and taller toward the top. Each paddle is an ' +
+ 'oval rounded rectangle with subtle ribs visible. The whole tower reaches 11 feet tall in ' +
+ 'real-world scale. Single subject, isolated.',
+ colorway: 'cochineal-oxblood',
+ },
+ {
+ slug: 'agave-rosette-monumental',
+ species: 'agave',
+ desc:
+ 'A single enormous agave americana rosette — wide spiky leaves radiating in all directions ' +
+ 'from a tight center, each leaf curving slightly, the longest leaves nearly horizontal, ' +
+ 'the central leaves vertical. From the center rises a tall flowering stalk that towers above ' +
+ 'the rosette — the agave reaches its 11-foot scale entirely through the central bloom stalk. ' +
+ 'Monumental and architectural.',
+ colorway: 'indigo-atelier',
+ },
+ {
+ slug: 'ocotillo-fan',
+ species: 'ocotillo',
+ desc:
+ 'A single ocotillo plant — fifteen to twenty long whip-like stems radiating up and outward ' +
+ 'from a single base point, each stem covered in tiny rounded bumps (where spines and ephemeral ' +
+ 'leaves emerge), the tallest stems reaching the top of the canvas. Looks like a frozen ' +
+ 'firework or an upside-down feather duster. Single subject, isolated.',
+ colorway: 'tortilla-bone',
+ },
+ {
+ slug: 'cholla-branching-giant',
+ species: 'cholla',
+ desc:
+ 'A single jumping-cholla / teddy-bear-cholla cactus, scaled to 11 feet tall. Trunk-like ' +
+ 'lower stem, branching into multiple shorter chubby cylindrical segments, each segment ' +
+ 'covered in dense fuzzy spines (rendered as a soft halo texture around the silhouette). ' +
+ 'The whole plant has a candelabra-from-below shape with arms branching in multiple directions.',
+ colorway: 'sonoran-espresso',
+ },
+ {
+ slug: 'barrel-cactus-grove-of-one',
+ species: 'barrel',
+ desc:
+ 'A single dramatic golden-barrel cactus — perfectly round, very large, ribbed in deep ' +
+ 'vertical pleats, with a halo of golden spines around the top crown where the flowers ' +
+ 'emerge. Sits at the bottom-center of the canvas, occupying about 50 percent of the ' +
+ 'horizontal width and 50 percent of the vertical — the ground line is below it and the ' +
+ 'sky/wall above. Even at half-canvas it reads as 11 feet tall in real-world scale because ' +
+ 'barrels are inherently very large.',
+ colorway: 'prickly-pear-plum',
+ },
+];
+
+// Reuse Pass-1's Omni Tucson colorways for ground/figure pairs
+const COLORWAYS = {
+ 'mesquite-on-bone': { ground_hex: '#E8DCC4', ground_name: 'warm chalky cream paper', figure_hex: '#2A2520', figure_name: 'deep mesquite-charcoal ink' },
+ 'catalina-dusk': { ground_hex: '#7A4A52', ground_name: 'Catalina Mountain twilight mauve', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'adobe-sundown': { ground_hex: '#A8623A', ground_name: 'Spanish Colonial adobe terracotta',figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'caliche-limestone': { ground_hex: '#B0A68C', ground_name: 'warm caliche-limestone taupe', figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+ 'saguaro-patina': { ground_hex: '#345E51', ground_name: 'aged saguaro skin green-grey', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'cochineal-oxblood': { ground_hex: '#6B2A22', ground_name: 'cochineal deep red', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'indigo-atelier': { ground_hex: '#1E3550', ground_name: 'natural indigo deep blue', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'tortilla-bone': { ground_hex: '#E8DCC4', ground_name: 'chalky stucco cream', figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+ 'sonoran-espresso': { ground_hex: '#3A2820', ground_name: 'aged mesquite-barrel espresso', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+ 'prickly-pear-plum': { ground_hex: '#824050', ground_name: 'prickly-pear plum magenta', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+};
+
+// Positive prompt scaffolding — loud about SINGLE LARGE ISOLATED subject
+function buildPositive(d) {
+ const cw = COLORWAYS[d.colorway];
+ return (
+ `A single monumental ${d.species} cactus rendered as a designer botanical mural. ` +
+ `${d.desc} ` +
+ `The cactus fills 90 to 95 percent of the vertical canvas height, dead-center horizontally with even ` +
+ `${cw.ground_name} background on both sides. ` +
+ `Composition is one isolated specimen on a solid background — NOT a wallpaper repeat, NOT a tile, ` +
+ `NOT seamless, NOT tessellated, NOT a pattern. ` +
+ `Hand-painted gouache silhouette aesthetic. ` +
+ `Entire cactus is rendered in the EXACT same single ink color (${cw.figure_name}, hex ${cw.figure_hex}) ` +
+ `— no internal color variation, no gradients, no highlights, no shadows. ` +
+ `Background is a solid uniform ${cw.ground_name} (hex ${cw.ground_hex}) — no texture variation, ` +
+ `no gradient, no scenery, no horizon, no other plants, no ground line. ` +
+ `Tall narrow portrait orientation. Scaled to read at 11 feet tall on a wall. ` +
+ `Designer-grade archival illustration quality, reads as a luxury Spanish Colonial / Pueblo Revival mural, ` +
+ `Omni Tucson National Resort hospitality aesthetic, Tohono O'odham textile sensibility.`
+ );
+}
+
+const NEGATIVE =
+ 'No multiple cacti spread across the canvas, no tile repeat, no seamless pattern, no tessellation, no wallpaper repeat. ' +
+ 'No banana, no banana leaves, no banana pods, no palm, no palm fronds, no monstera, no philodendron, no tropical foliage. ' +
+ 'No grapes, no birds, no butterflies. ' +
+ 'No room mockup, no perspective, no people, no furniture, no horizon line, no sky gradient, no clouds, no sun. ' +
+ 'No watermark, no signature, no text, no logos, no border, no frame. ' +
+ 'No multi-color cactus, no gradient inside the cactus silhouette, no rainbow, no fluorescent colors.';
+
+const PRINT_W_IN = 90; // 7.5 ft
+const PRINT_H_IN = 132; // 11 ft
+
+const renderEnv = {
+ ...process.env,
+ // SDXL native portrait closest to 90/132 = 0.682
+ COMFY_WIDTH: '832',
+ COMFY_HEIGHT: '1216',
+ REPLICATE_WIDTH: '832',
+ REPLICATE_HEIGHT:'1216',
+ // Replicate is the fast path — Mac1's ComfyUI queue is jammed by the
+ // night-builder + drunk-animals + bleed-guard crons (40+ pending), and
+ // Mac2's local ComfyUI isn't running. Cost per SDXL render is ~$0.003,
+ // so 10 designs ≈ $0.03 — well under any cap.
+ GEN_BACKEND: 'replicate',
+ COMFY_URL: process.env.COMFY_URL_OVERRIDE || 'http://192.168.1.133:8188',
+};
+
+console.log(`Pass 3 — Generating ${DESIGNS.length} 11ft × 7.5ft single-cactus murals (832×1216 portrait SDXL)\n`);
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+for (let i = 0; i < DESIGNS.length; i++) {
+ const d = DESIGNS[i];
+ const cw = COLORWAYS[d.colorway];
+ const prompt = buildPositive(d) + ' ' + NEGATIVE;
+ const title = `${d.species[0].toUpperCase() + d.species.slice(1)} 11ft Mural — ${d.colorway.replace(/-/g, ' ')}`;
+
+ console.log(`[${i+1}/${DESIGNS.length}] ${d.slug} · ${d.colorway}`);
+
+ // Title is set post-generation via a PG UPDATE keyed off the returned id,
+ // since generate_designs.js doesn't expose a --title flag.
+ const r = spawnSync('node', [
+ path.join(__dirname, 'generate_designs.js'),
+ '--n', '1',
+ '--kind', 'mural',
+ '--width', String(PRINT_W_IN),
+ '--height', String(PRINT_H_IN),
+ '--panels', '1',
+ '--category', 'cactus-11ft-mural',
+ '--prompts', prompt,
+ ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+ // generate_designs.js exits 0 even on failure (it logs ✗ and moves on),
+ // so trust the #<id> regex as the real success signal — not exit code.
+ const m = (r.stdout || '').match(/#(\d+)\s*·/);
+ if (m) {
+ success++;
+ const id = parseInt(m[1], 10);
+ ids.push(id);
+ const up = spawnSync('psql', [
+ 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified',
+ '-c',
+ `UPDATE spoon_all_designs SET title = $$${title.replace(/\$/g, '')}$$ WHERE id = ${id};`,
+ ], { encoding: 'utf8' });
+ if (up.status !== 0) {
+ process.stdout.write(' (title-update skipped: ' + (up.stderr || '').slice(0, 120) + ')\n');
+ }
+ process.stdout.write(` OK (id=${id})\n`);
+ } else {
+ fail++;
+ // Surface the actual reason from generate_designs.js stdout/stderr
+ const reason = ((r.stdout || '') + '\n' + (r.stderr || ''))
+ .split('\n').filter((l) => /✗|Error|FAILED|timed out|undefined/.test(l)).slice(-2).join(' | ')
+ || (r.stdout || r.stderr || '').slice(-240);
+ process.stdout.write(' FAIL ' + reason + '\n');
+ }
+}
+
+const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
+console.log(`\nDone in ${elapsed}s — ${success} ok, ${fail} fail`);
+console.log('Inserted ids: ' + JSON.stringify(ids));
+
+if (success === 0) process.exit(2);
diff --git a/scripts/publish-cactus-11ft.js b/scripts/publish-cactus-11ft.js
new file mode 100644
index 0000000..ae7ac02
--- /dev/null
+++ b/scripts/publish-cactus-11ft.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+// Publish freshly-generated 11ft cactus murals.
+//
+// Pre-conditions:
+// 1. generate-cactus-11ft.js has run and inserted rows with
+// category='cactus-11ft-mural' into spoon_all_designs.
+// 2. You have eyeballed the local PNGs (each one MUST be a single big
+// cactus, not a pattern repeat — SDXL ignores anti-prompts per
+// feedback_sdxl_antiprompts_unreliable). Reject any tiled output.
+//
+// What this does:
+// - Lists every category='cactus-11ft-mural' row that's still
+// is_published=false.
+// - For each one: flips is_published=true, ensures a dig_number is set,
+// and syncs designs.json so the front-end picks it up immediately.
+//
+// Flags:
+// --dry-run — only list candidates, don't write
+// --id=<n> — publish just one row by id
+// --reject=<n> — hard-delete a rejected row (file + PG) and log to
+// ghosting-do-not-want.jsonl so we don't regenerate it
+//
+// Usage:
+// node scripts/publish-cactus-11ft.js --dry-run
+// node scripts/publish-cactus-11ft.js # publish all candidates
+// node scripts/publish-cactus-11ft.js --reject=9876 # nuke one bad render
+
+const { spawnSync, execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+const DB = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+
+const argv = process.argv.slice(2);
+const FLAGS = Object.fromEntries(argv.map((a) => {
+ const m = a.match(/^--([^=]+)(?:=(.*))?$/);
+ return m ? [m[1], m[2] ?? true] : [a, true];
+}));
+
+function psql(sql) {
+ const r = spawnSync('psql', [DB, '-A', '-F', '|', '--no-align', '-t', '-c', sql], { encoding: 'utf8' });
+ if (r.status !== 0) {
+ console.error('psql error:', r.stderr);
+ process.exit(2);
+ }
+ return r.stdout.trim();
+}
+
+function listCandidates() {
+ const out = psql(
+ `SELECT id, dig_number, local_path, is_published
+ FROM spoon_all_designs
+ WHERE category = 'cactus-11ft-mural'
+ AND (is_published = false OR is_published IS NULL)
+ ORDER BY id ASC`,
+ );
+ if (!out) return [];
+ return out.split('\n').filter(Boolean).map((line) => {
+ const [id, dig_number, local_path, is_published] = line.split('|');
+ return { id: parseInt(id, 10), dig_number, local_path, is_published };
+ });
+}
+
+function publish(id) {
+ // Ensure dig_number is present (the trigger should have set it on INSERT,
+ // but belt-and-suspenders)
+ psql(
+ `UPDATE spoon_all_designs
+ SET dig_number = COALESCE(NULLIF(dig_number, ''), 'DIG_AI_' || LPAD(id::text, 6, '0')),
+ is_published = TRUE
+ WHERE id = ${id}`,
+ );
+}
+
+function syncDesignsJson() {
+ // Re-export designs.json from PG so the front-end picks up the new rows on
+ // its next read. The exact exporter path may vary — fall back gracefully
+ // if it doesn't exist.
+ const candidates = [
+ path.join(ROOT, 'scripts', 'sync-designs-json.js'),
+ path.join(ROOT, 'scripts', 'export-designs-json.js'),
+ path.join(ROOT, 'scripts', 'dump-designs.js'),
+ ];
+ for (const c of candidates) {
+ if (fs.existsSync(c)) {
+ console.log(` syncing designs.json via ${path.basename(c)}…`);
+ const r = spawnSync('node', [c], { cwd: ROOT, encoding: 'utf8' });
+ if (r.status === 0) return true;
+ console.error(' sync failed:', r.stderr || r.stdout);
+ return false;
+ }
+ }
+ console.log(' (no designs.json exporter found — front-end will pick up on its own cycle)');
+ return false;
+}
+
+function rejectOne(id) {
+ const row = psql(`SELECT local_path, image_url, prompt FROM spoon_all_designs WHERE id = ${id}`).split('|');
+ const localPath = row[0];
+ const promptText = (row[2] || '').slice(0, 200);
+
+ // Append to ghosting-do-not-want.jsonl
+ const reject = {
+ id,
+ rejected_at: new Date().toISOString(),
+ reason: 'pattern-repeat instead of single big cactus',
+ prompt_snippet: promptText,
+ };
+ fs.appendFileSync(path.join(ROOT, 'ghosting-do-not-want.jsonl'), JSON.stringify(reject) + '\n');
+
+ // Move file out of the way (don't permanently delete in case we want to inspect)
+ if (localPath && fs.existsSync(localPath)) {
+ const archive = path.join(process.env.HOME, '.wallco-deleted', new Date().toISOString().slice(0, 10));
+ fs.mkdirSync(archive, { recursive: true });
+ fs.renameSync(localPath, path.join(archive, path.basename(localPath)));
+ }
+
+ psql(`DELETE FROM spoon_all_designs WHERE id = ${id}`);
+ console.log(` rejected id=${id}, moved file to ~/.wallco-deleted/, hard-deleted PG row`);
+}
+
+// ── main ──
+const cands = listCandidates();
+
+if (FLAGS.reject) {
+ rejectOne(parseInt(String(FLAGS.reject), 10));
+ process.exit(0);
+}
+
+console.log(`Found ${cands.length} unpublished cactus-11ft-mural row(s):\n`);
+for (const c of cands) {
+ console.log(` id=${c.id} ${c.dig_number}`);
+ console.log(` file: ${c.local_path}`);
+}
+
+if (FLAGS['dry-run']) {
+ console.log('\n(dry-run — no writes)');
+ process.exit(0);
+}
+
+if (cands.length === 0) {
+ console.log('\nNothing to publish.');
+ process.exit(0);
+}
+
+console.log('\nPublishing...');
+let published = 0;
+for (const c of cands) {
+ if (FLAGS.id && c.id !== parseInt(String(FLAGS.id), 10)) continue;
+ publish(c.id);
+ published++;
+ console.log(` ✓ ${c.dig_number || ('id=' + c.id)}`);
+}
+
+console.log(`\nPublished ${published} design(s).`);
+syncDesignsJson();
+
+// Bonus: hot-reload prod by hitting the wallco.ai /api/admin/reload endpoint
+// if it exists. Non-fatal if not.
+try {
+ execSync('curl -sf -m 3 -X POST http://127.0.0.1:9905/api/admin/reload-designs > /dev/null', { stdio: 'ignore' });
+ console.log('Reloaded local wallco-ai server.');
+} catch { /* non-fatal */ }
diff --git a/server.js b/server.js
index e8846e5..49ce562 100644
--- a/server.js
+++ b/server.js
@@ -6568,6 +6568,32 @@ app.post('/api/design/:id/settlement-violation', express.json({ limit: '16kb' })
// update-sku → keep the same DB row + DIG number, regenerate the IMAGE with
// an anti-ghosting addendum on the prompt. URL stays valid;
// old file is backed up before overwrite.
+// POST /api/design/:id/report-ghost — PUBLIC report endpoint (no admin gate).
+// Visitor clicked the 👻 Report Ghost Layers button. Just queues to JSONL for
+// admin triage; never deletes, never regenerates. Admin path is the separate
+// /ghosting endpoint below.
+app.post('/api/design/:id/report-ghost', express.json({ limit: '4kb' }), (req, res) => {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+ try {
+ const auditDir = path.join(__dirname, 'data', 'settlement-audit');
+ try { fs.mkdirSync(auditDir, { recursive: true }); } catch {}
+ const queuePath = path.join(auditDir, 'ghost-reports-queue.jsonl');
+ const entry = {
+ ts: new Date().toISOString(),
+ design_id: id,
+ source: String(req.body?.source || 'public-button').slice(0, 64),
+ url: String(req.body?.url || '').slice(0, 256),
+ ip: (req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.ip || '').toString().split(',')[0].trim(),
+ ua: String(req.headers['user-agent'] || '').slice(0, 200),
+ };
+ fs.appendFileSync(queuePath, JSON.stringify(entry) + '\n');
+ return res.json({ ok: true, queued: true });
+ } catch (e) {
+ return res.status(500).json({ error: 'queue write failed', detail: e.message });
+ }
+});
+
// delete → HARD delete + log fingerprint to ghosting do-not-want registry
// (same pattern as /settlement-violation).
app.post('/api/design/:id/ghosting', express.json({ limit: '16kb' }), (req, res) => {
@@ -7238,6 +7264,48 @@ ${htmlHeader('/designs')}
</script>
` : ''}
+ <!-- ── PUBLIC GHOST REPORT — visible to every visitor.
+ Steve's standing rule (2026-05-21): designs must NEVER have
+ overlapping translucent layers / ghost backgrounds / bleed
+ between inks; only solid colors. Anyone spotting a violation
+ flags it here. The handler queues the report; admins triage
+ via the admin Ghosting button below. -->
+ <div id="public-ghost-report"
+ style="margin:18px 0;padding:12px 16px;background:#f7f4ec;border:1px solid #d8cdb0;border-radius:8px;font:13px var(--sans,system-ui);display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
+ <span style="color:var(--ink-soft,#5a4f3f);font-size:12px;line-height:1.5">
+ See <strong>overlapping layers</strong>, a ghost background, or any
+ non-solid-color bleed in this design? Flag it for cleanup.
+ </span>
+ <button id="public-ghost-btn" type="button"
+ data-design-id="${design.id}"
+ style="background:#7a4ac8;border:1px solid #7a4ac8;color:#fff;padding:7px 14px;border-radius:14px;cursor:pointer;font-size:11px;letter-spacing:.06em;font-weight:600;text-transform:uppercase;white-space:nowrap"
+ title="Report this design as having ghost layers / overlapping translucent fills">👻 Report Ghost Layers</button>
+ <span id="public-ghost-status" style="font-size:11px;color:var(--ink-faint,#7a6e5a);min-width:0;flex-basis:100%;text-align:right" aria-live="polite"></span>
+ </div>
+ <script>
+ (function(){
+ var btn = document.getElementById('public-ghost-btn');
+ var status = document.getElementById('public-ghost-status');
+ if (!btn) return;
+ btn.addEventListener('click', function(){
+ var did = btn.dataset.designId;
+ if (!did) return;
+ btn.disabled = true; status.textContent = 'sending…';
+ fetch('/api/design/' + did + '/report-ghost', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ source: 'public-button', url: location.href })
+ })
+ .then(function(r){ return r.json().catch(function(){ return {}; }).then(function(j){ return { ok: r.ok, j: j }; }); })
+ .then(function(o){
+ if (o.ok) { btn.textContent = '✓ Reported — admin will review'; btn.style.background = '#3a8a4d'; btn.style.borderColor = '#3a8a4d'; status.textContent = ''; }
+ else { btn.disabled = false; status.textContent = 'report error — try again'; }
+ })
+ .catch(function(e){ btn.disabled = false; status.textContent = 'network error: ' + e.message; });
+ });
+ })();
+ </script>
+
${_isAdmin ? `
<!-- ── USER RATING PANEL ─────────────────────────────────────────
Feeds spoon_all_designs.user_color_good / style_good / scale_good /
@@ -7489,7 +7557,11 @@ ${htmlHeader('/designs')}
` : ''}
<div class="scale-preview" style="margin-top:18px">
- <div class="scale-controls" style="display:flex;align-items:center;gap:14px;margin-bottom:10px;flex-wrap:wrap">
+ <!-- "Pattern scale 24" repeat" applies only to tile-mode previews
+ (levels 1-4); a mural is a single un-repeated 20 ft × 11 ft canvas
+ where "repeat distance" is meaningless. The mural-stepper apply()
+ below toggles #scale-controls.style.display on level==0. -->
+ <div id="scale-controls" class="scale-controls" style="display:flex;align-items:center;gap:14px;margin-bottom:10px;flex-wrap:wrap">
<label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:flex;align-items:center;gap:10px;flex:1;min-width:240px">
Pattern scale
<input type="range" id="scale-slider" min="4" max="24" step="2" value="24" style="flex:1;max-width:280px">
@@ -7627,6 +7699,11 @@ ${htmlHeader('/designs')}
// regenerating a mural that was visually correct.
const ghostBtn = document.getElementById('rating-ghosting');
if (ghostBtn) ghostBtn.style.display = (level === 0) ? 'none' : '';
+ // Pattern-scale slider + "24\" repeat" readout — same reasoning.
+ // A mural has no repeat distance, so showing a "Pattern scale"
+ // slider in MURAL mode is misleading. Hide the whole control row.
+ const scaleControls = document.getElementById('scale-controls');
+ if (scaleControls) scaleControls.style.display = (level === 0) ? 'none' : 'flex';
}
if (btn) btn.addEventListener('click', () => { level = (level + 1) % LEVELS.length; apply(); });
dots.forEach(d => d.addEventListener('click', (e) => { level = parseInt(e.currentTarget.dataset.level,10) || 0; apply(); }));
← 4e4fe74 chat: design-AI pill back to bottom-right corner + Big Red s
·
back to Wallco Ai
·
feat(cactus+murals): 48 cactus-11ft + 32 tree-murals + 6 cac 3ddc65b →