← back to Wallco Ai
design page: sticky admin toolbar (Unpublish / Smart-fix / Sell-on-Etsy-bucket) at top of /design/:id + POST /api/design/:id/unpublish toggle. Local only — wallco.ai deploy gated.
88d2c1faad92ce035fe2d02d1c9b701b9270db0e · 2026-05-28 09:53:20 -0700 · Steve Abrams
Files touched
Diff
commit 88d2c1faad92ce035fe2d02d1c9b701b9270db0e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 09:53:20 2026 -0700
design page: sticky admin toolbar (Unpublish / Smart-fix / Sell-on-Etsy-bucket) at top of /design/:id + POST /api/design/:id/unpublish toggle. Local only — wallco.ai deploy gated.
---
server.js | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
diff --git a/server.js b/server.js
index 600d248..c3e30cb 100644
--- a/server.js
+++ b/server.js
@@ -10656,6 +10656,81 @@ app.get('/design/:id', (req, res) => {
<body>
${htmlHeader('/designs')}
<main class="detail-main">
+${_isAdmin ? `
+<!-- Admin quick-action toolbar — Steve 2026-05-28: surface unpublish/fix/etsy
+ prominently on the front-end instead of buried in the Distribute drawer.
+ Sticky so it stays in reach while scrolling. Admin-gated, never shown
+ to anonymous buyers. -->
+<div id="admin-toolbar" style="position:sticky;top:0;z-index:80;display:flex;gap:10px;align-items:center;padding:10px 14px;background:#1a1816;border-bottom:1px solid #3a2f1c;color:#f0e6cc;font:13px var(--sans,system-ui);box-shadow:0 2px 6px rgba(0,0,0,.18)">
+ <span style="font:600 10px ui-sans-serif,system-ui;letter-spacing:.18em;text-transform:uppercase;color:#c98a00">Admin</span>
+ <code style="font:11px ui-monospace,Menlo,monospace;color:#a08b6a">#${design.id}</code>
+ <span id="atb-state" style="font:11px ui-sans-serif,system-ui;color:${design.is_published ? '#7ad48a' : '#d8a060'};letter-spacing:.04em">${design.is_published ? '● live' : '○ unpublished'}</span>
+ <div style="flex:1"></div>
+ <button type="button" id="atb-unpub" data-id="${design.id}" data-state="${design.is_published ? 'live' : 'down'}"
+ style="padding:7px 14px;border:1px solid #d8a060;border-radius:5px;background:transparent;color:#d8a060;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em">
+ ${design.is_published ? '🔻 Unpublish' : '🔼 Republish'}
+ </button>
+ <button type="button" id="atb-fix" data-id="${design.id}"
+ style="padding:7px 14px;border:1px solid #c98a00;border-radius:5px;background:transparent;color:#c98a00;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em">
+ 🔧 Smart-fix
+ </button>
+ <button type="button" id="atb-etsy" data-id="${design.id}"
+ style="padding:7px 14px;border:1px solid #6c8a4c;border-radius:5px;background:transparent;color:#9bc46b;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em"
+ title="Add to wallco_etsy_bucket with eligibility check + DPI/upscale plan. Does NOT call the Etsy API.">
+ 🛒 Sell on Etsy
+ </button>
+ <span id="atb-status" style="margin-left:6px;font:11px ui-sans-serif,system-ui;color:#a08b6a;min-width:160px"></span>
+</div>
+<script>
+(function(){
+ var stat = document.getElementById('atb-status');
+ function setStatus(msg, cls){ stat.textContent = msg; stat.style.color = cls === 'err' ? '#e08070' : cls === 'ok' ? '#9bc46b' : '#a08b6a'; }
+ function onClick(btnId, handler){
+ var b = document.getElementById(btnId); if (!b) return;
+ b.addEventListener('click', async function(){
+ b.disabled = true; var orig = b.style.opacity; b.style.opacity = '0.5';
+ try { await handler(b); } catch(e){ setStatus('error: '+e.message, 'err'); }
+ finally { b.disabled = false; b.style.opacity = orig || '1'; }
+ });
+ }
+ onClick('atb-unpub', async function(b){
+ var live = b.dataset.state === 'live';
+ if (!confirm(live ? 'Unpublish #' + b.dataset.id + ' from the live storefront?' : 'Republish #' + b.dataset.id + ' to the live storefront?')) return;
+ setStatus(live ? 'Unpublishing…' : 'Republishing…');
+ var r = await fetch('/api/design/' + b.dataset.id + '/unpublish', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ unpublish: live }) });
+ var j = await r.json();
+ if (r.ok && j.ok) {
+ setStatus((j.is_published ? '● live' : '○ unpublished') + ' (saved)', 'ok');
+ // Update toolbar state without a full reload
+ document.getElementById('atb-state').textContent = j.is_published ? '● live' : '○ unpublished';
+ document.getElementById('atb-state').style.color = j.is_published ? '#7ad48a' : '#d8a060';
+ b.textContent = j.is_published ? '🔻 Unpublish' : '🔼 Republish';
+ b.dataset.state = j.is_published ? 'live' : 'down';
+ } else setStatus('failed: ' + (j.error || r.status), 'err');
+ });
+ onClick('atb-fix', async function(b){
+ if (!confirm('Run smart-fix on #' + b.dataset.id + '? Generates a corrected child design — original stays put.')) return;
+ setStatus('Smart-fix running… (~30–60s)');
+ var r = await fetch('/api/design/' + b.dataset.id + '/smart-fix', { method:'POST', headers:{'Content-Type':'application/json'}, body: '{}' });
+ var j = await r.json();
+ if (r.ok && j.ok) setStatus('✓ smart-fix → #' + (j.new_id || j.id || '?') + (j.kind ? ' (' + j.kind + ')' : ''), 'ok');
+ else setStatus('smart-fix failed: ' + (j.error || j.reason || r.status), 'err');
+ });
+ onClick('atb-etsy', async function(b){
+ if (!confirm('Queue #' + b.dataset.id + ' in wallco_etsy_bucket (eligibility-checked, upscaled to 300 DPI)? Does NOT create a live Etsy listing.')) return;
+ setStatus('Queueing for Etsy bucket…');
+ var r = await fetch('/api/etsy-bucket/add', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ id: parseInt(b.dataset.id, 10) }) });
+ var j = await r.json();
+ if (r.ok && j.ok) {
+ var first = (j.results && j.results[0]) || {};
+ if (first.ok && first.already_in) setStatus('already in bucket (' + first.already_in + ')', 'ok');
+ else if (first.ok) setStatus('✓ queued · ' + first.target_dpi + ' DPI · ' + first.upscale_px + 'px upscale', 'ok');
+ else setStatus('skipped: ' + (first.reason || 'not eligible'), 'err');
+ } else setStatus('etsy queue failed: ' + (j.error || r.status), 'err');
+ });
+})();
+</script>
+` : ''}
<style>
/* Collapse-pane — tappable header row + chevron, closed by default.
Wraps non-essential panels (specs / calculator / coordinates / room
@@ -18342,6 +18417,34 @@ app.post('/api/spoonflower/upload', requireAdmin, (req, res) => {
res.json({ ok: true, job_id: jobId, dry, log_url: `/api/spoonflower/status/${jobId}` });
});
+// ── POST /api/design/:id/unpublish — admin toggle for is_published.
+// body: { unpublish: true } → set is_published=FALSE
+// body: { unpublish: false } → set is_published=TRUE (republish)
+// body: {} → just flip whatever the current state is
+// Used by the front-end admin toolbar. Does NOT delete or quarantine the row;
+// re-pub is fully reversible. Hot-reloads the in-memory DESIGNS entry so the
+// next render reflects the new state without a full server restart.
+app.post('/api/design/:id/unpublish', requireAdmin, express.json({ limit: '1kb' }), (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 cur = psqlQuery(`SELECT is_published FROM all_designs WHERE id=${id};`);
+ if (!cur) return res.status(404).json({ error: 'design not found' });
+ const isLive = String(cur).trim() === 't';
+ const targetUnpub = (typeof req.body?.unpublish === 'boolean') ? req.body.unpublish : isLive;
+ const next = targetUnpub ? false : true;
+ psqlExecLocal(`UPDATE all_designs SET is_published=${next ? 'TRUE' : 'FALSE'}, updated_at=NOW() WHERE id=${id};`);
+ // Bump in-memory snapshot too so subsequent renders agree
+ try {
+ const d = DESIGNS.find(x => x.id === id);
+ if (d) d.is_published = next;
+ } catch {}
+ res.json({ ok: true, id, is_published: next });
+ } catch (e) {
+ res.status(500).json({ error: String(e.message || e).slice(0, 200) });
+ }
+});
+
// ── POST /api/design/:id/etsy-tag — mark a design for Etsy digital-file resale
//
// Admin-only. Tags the row with 'etsy-digital-file' and appends to data/etsy-queue.jsonl
← 54a2c78 trade-login: wire PureLyMail SMTP (smtp.purelymail.com:465 S
·
back to Wallco Ai
·
unpublish endpoint: drop updated_at column (doesn't exist on 696e739 →