← back to Wallco Ai
spoonflower upload returns SF pattern#, adds Upload-to-Shopify, DIG # auto-assigned + hover overlay
92d9917901f7ff6db1ceec5461c797e403398b17 · 2026-05-13 14:59:43 -0700 · SteveStudio2
- upload_spoonflower_new.py: parse SF design_id from final_url, emit sf_design_id + sf_public_url in `done` event
- server.js POST /api/spoonflower/upload: parse log on exit, persist spoonflower_design_id + spoonflower_url onto design row
- server.js GET /api/spoonflower/status/:job: surface sf_design_id + sf_public_url for frontend polling
- server.js POST /api/design/:id/upload-shopify (admin-only): create draft Shopify product on designer-laboratory-sandbox with SKU=dig_number, image upload from local_path, metafields dwc.dig_number/dwc.wallco_design_id/dwc.spoonflower_id/dwc.spoonflower_url; persists shopify_product_id + shopify_product_url; idempotent
- /design/:id: replaced single-shot "kicked off" toast with 5s polling that renders "Spoonflower pattern #N — view ↗" on success and lights up the new "Upload to Shopify" button next to it
- /design/:id: now shows the DW Original Number under the tool buttons
- /designs grid (buyer card): hover overlay reveals DW Original Number + View / Sample buttons over the image
- /designs grid (admin review card): pattern-num pill switched from DW-WC-#### to DIG_AI_xxxxxx; same View/Sample buttons appear on hover
DB plumbing was committed earlier in this session (sql/200_dig_auto_assign.sql trigger + spoonflower_*/shopify_* columns on spoon_all_designs); this commit wires the app code to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M scripts/upload_spoonflower_new.pyM server.js
Diff
commit 92d9917901f7ff6db1ceec5461c797e403398b17
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 14:59:43 2026 -0700
spoonflower upload returns SF pattern#, adds Upload-to-Shopify, DIG # auto-assigned + hover overlay
- upload_spoonflower_new.py: parse SF design_id from final_url, emit sf_design_id + sf_public_url in `done` event
- server.js POST /api/spoonflower/upload: parse log on exit, persist spoonflower_design_id + spoonflower_url onto design row
- server.js GET /api/spoonflower/status/:job: surface sf_design_id + sf_public_url for frontend polling
- server.js POST /api/design/:id/upload-shopify (admin-only): create draft Shopify product on designer-laboratory-sandbox with SKU=dig_number, image upload from local_path, metafields dwc.dig_number/dwc.wallco_design_id/dwc.spoonflower_id/dwc.spoonflower_url; persists shopify_product_id + shopify_product_url; idempotent
- /design/:id: replaced single-shot "kicked off" toast with 5s polling that renders "Spoonflower pattern #N — view ↗" on success and lights up the new "Upload to Shopify" button next to it
- /design/:id: now shows the DW Original Number under the tool buttons
- /designs grid (buyer card): hover overlay reveals DW Original Number + View / Sample buttons over the image
- /designs grid (admin review card): pattern-num pill switched from DW-WC-#### to DIG_AI_xxxxxx; same View/Sample buttons appear on hover
DB plumbing was committed earlier in this session (sql/200_dig_auto_assign.sql trigger + spoonflower_*/shopify_* columns on spoon_all_designs); this commit wires the app code to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
scripts/upload_spoonflower_new.py | 32 +++++++++++++--
server.js | 82 +++++++++++++++++++++++++++++++++++----
2 files changed, 104 insertions(+), 10 deletions(-)
diff --git a/scripts/upload_spoonflower_new.py b/scripts/upload_spoonflower_new.py
index 8daa092..d6117e9 100644
--- a/scripts/upload_spoonflower_new.py
+++ b/scripts/upload_spoonflower_new.py
@@ -10,7 +10,7 @@ stdout is line-delimited JSON events. Final event is `done` (ok=True/False).
NOTE — defaults to dry-run (--dry). Pass without --dry to actually publish.
Steve's standing rule: no public-facing publish without explicit confirm-click.
"""
-import argparse, sys, os, time, json
+import argparse, sys, os, time, json, re
from pathlib import Path
from playwright.sync_api import sync_playwright
@@ -19,6 +19,24 @@ from spoonflower_lib import creds, login, emit # noqa: E402
UPLOAD_URL = "https://www.spoonflower.com/en/design_studio/uploads"
+# Spoonflower URLs after a successful publish look like:
+# https://www.spoonflower.com/en/shop/wallpaper/12345678-some-slug
+# https://www.spoonflower.com/en/fabric/12345678-some-slug
+# https://www.spoonflower.com/en/design_studio/designs/12345678/edit
+# Extract the first long-numeric ID segment so the caller can build the
+# public pattern URL deterministically.
+_SF_ID_RE = re.compile(r"/(?:designs|wallpaper|fabric|home_decor)(?:/[^/]+)?/(\d{5,})")
+
+
+def parse_sf_design_id(url: str) -> str | None:
+ if not url:
+ return None
+ m = _SF_ID_RE.search(url)
+ if m:
+ return m.group(1)
+ m = re.search(r"(\d{6,})", url)
+ return m.group(1) if m else None
+
def upload_new(image_path: str, title: str, tags: str = "", dry: bool = True):
if not Path(image_path).exists():
@@ -144,8 +162,16 @@ def upload_new(image_path: str, title: str, tags: str = "", dry: bool = True):
page.wait_for_load_state("networkidle", timeout=90000)
time.sleep(3)
- emit("submitted", final_url=page.url)
- emit("done", ok=True, url=page.url)
+ final_url = page.url
+ sf_design_id = parse_sf_design_id(final_url)
+ sf_public_url = (
+ f"https://www.spoonflower.com/en/shop/wallpaper/{sf_design_id}"
+ if sf_design_id else None
+ )
+ emit("submitted", final_url=final_url,
+ sf_design_id=sf_design_id, sf_public_url=sf_public_url)
+ emit("done", ok=True, url=final_url,
+ sf_design_id=sf_design_id, sf_public_url=sf_public_url)
browser.close()
return 0
diff --git a/server.js b/server.js
index e0f2ab3..d6f2cd1 100644
--- a/server.js
+++ b/server.js
@@ -3212,11 +3212,24 @@ app.get('/designs', (req, res) => {
// Style tag — category is the closest thing to a style category in the
// data model (damask, butterfly-trellis, stoned-animals, mural-scenic).
const styleTag = String(d.category || 'mixed').replace(/-/g, ' ');
+ // DW Original Number (DIG_AI_xxxxxx) — auto-assigned at DB INSERT
+ // time by trg_spoon_all_designs_assign_dig so it's always present.
+ // The fallback expression matches the trigger's exact output so we
+ // can show it on the card without re-querying PG for every row.
+ const digNum = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
return `
- <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}${isNew ? ' is-new' : ''}" aria-label="${cardAriaLabel(d)}" data-design-id="${d.id}">
+ <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}${isNew ? ' is-new' : ''}" aria-label="${cardAriaLabel(d)}" data-design-id="${d.id}" data-dig="${digNum}">
<button type="button" class="fav-btn" data-fav-id="${d.id}" aria-pressed="false" aria-label="Save ${d.title} to favorites" title="Favorite"><span class="fav-icon"></span></button>
${newBadge}
- <div class="card-img" style="background-image:url('${d.image_url}')" role="img" aria-label="${d.title} pattern thumbnail">${roomOverlay}</div>
+ <div class="card-img" style="background-image:url('${d.image_url}')" role="img" aria-label="${d.title} pattern thumbnail">${roomOverlay}
+ <div class="card-hover" aria-hidden="true">
+ <span class="card-hover-dig" title="DW Original Number">${digNum}</span>
+ <div class="card-hover-actions">
+ <a class="chov-btn chov-view" href="/design/${d.id}">View</a>
+ <a class="chov-btn chov-sample" href="/samples?design=${d.id}" onclick="event.stopPropagation()">Sample</a>
+ </div>
+ </div>
+ </div>
<div class="card-meta">
<span class="card-title">${d.title}</span>
<span class="card-sku" title="DW catalog SKU">${sku}</span>
@@ -3229,13 +3242,19 @@ app.get('/designs', (req, res) => {
</a>`;
}
// Admin review-mode card: sliders, K/R, chips, why-chip
- const _sku = `DW-WC-${String(d.id).padStart(4, '0')}`;
+ // Show the DW Original Number (DIG_AI_xxxxxx) as the pattern pill so it
+ // matches the buyer view + the Shopify SKU written by /upload-shopify.
+ const _digAdm = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
return `
- <div class="design-card review-card" data-id="${d.id}">
+ <div class="design-card review-card" data-id="${d.id}" data-dig="${_digAdm}">
<a href="/design/${d.id}" target="_blank" class="card-img" style="background-image:url('${d.image_url}')" rel="noopener noreferrer">
<span class="decision-badge"></span>
+ <div class="card-hover-actions admin-hover-actions" aria-hidden="true">
+ <a class="chov-btn chov-view" href="/design/${d.id}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View</a>
+ <a class="chov-btn chov-sample" href="/samples?design=${d.id}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">Sample</a>
+ </div>
</a>
- <span class="pattern-num" aria-hidden="true">${_sku}</span>
+ <span class="pattern-num" aria-hidden="true">${_digAdm}</span>
<div class="review-flyout">
<div class="card-meta">
<span class="card-title">${d.title}</span>
@@ -3557,6 +3576,42 @@ ${cat === 'mural-scenic' ? `
/* NEW badge — top-right corner, brand-gold pill on dark background */
.card-new-badge { position:absolute;top:10px;right:10px;z-index:5;padding:4px 10px;border-radius:999px;background:var(--accent,#c9a14b);color:var(--bg,#fff);font:600 10px/1 var(--sans,system-ui);letter-spacing:.12em;text-transform:uppercase;box-shadow:0 2px 8px rgba(0,0,0,.25) }
.design-card.is-new { box-shadow:0 0 0 1px var(--accent,#c9a14b),0 6px 22px rgba(201,161,75,.18) }
+ /* Hover layer on every catalog card — pattern number + View / Sample.
+ Sits inside .card-img so it picks up the existing aspect-ratio box.
+ pointer-events:none on the wrapper lets clicks fall through to the
+ parent <a>, except on the explicit child buttons which re-enable
+ pointer events. */
+ .design-card .card-img { position:relative }
+ .card-hover {
+ position:absolute; inset:0; z-index:4; pointer-events:none;
+ background:linear-gradient(to top, rgba(20,18,15,.78) 0%, rgba(20,18,15,.32) 45%, rgba(20,18,15,0) 70%);
+ opacity:0; transition:opacity .18s ease;
+ display:flex; flex-direction:column; justify-content:flex-end;
+ padding:10px 12px 12px;
+ color:#fff;
+ }
+ .design-card:hover .card-hover,
+ .design-card:focus-within .card-hover { opacity:1 }
+ .card-hover-dig {
+ position:absolute; top:10px; left:50%; transform:translateX(-50%);
+ background:rgba(255,255,255,.92); color:#111;
+ font:600 10px/1 ui-monospace,Menlo,monospace; letter-spacing:.06em;
+ padding:5px 9px; border-radius:3px;
+ box-shadow:0 2px 6px rgba(0,0,0,.18);
+ white-space:nowrap;
+ }
+ .card-hover-actions { display:flex; gap:8px; justify-content:center }
+ .chov-btn {
+ pointer-events:auto;
+ flex:1; max-width:120px; text-align:center;
+ padding:7px 10px; border-radius:3px;
+ font:600 11px/1 var(--sans,system-ui); letter-spacing:.08em; text-transform:uppercase;
+ text-decoration:none; transition:transform .12s, background .15s;
+ }
+ .chov-view { background:#fff; color:#111 }
+ .chov-sample { background:rgba(255,255,255,.18); color:#fff; border:1px solid rgba(255,255,255,.55) }
+ .chov-btn:hover { transform:translateY(-1px) }
+ .chov-sample:hover { background:rgba(255,255,255,.32) }
</style>
<p class="result-count">${total} design${total===1?'':'s'}${cat?' in '+cat:''}${motifQ?' with '+motifQ:''}${roomQ?' with room preview':''}${q?' matching "'+q+'"':''}<span id="fav-count-extra" style="margin-left:8px;color:var(--ink-faint)"></span></p>
@@ -4077,6 +4132,19 @@ ${_isAdmin ? `
.design-card.review-card[data-decision=reject] { border-color: var(--reject); opacity:.55; }
.design-card.review-card .card-img { position:relative; display:block; aspect-ratio:1; background:#0d0c0a no-repeat center/cover; }
.design-card.review-card .card-img .decision-badge { position:absolute; top:8px; right:8px; padding:2px 8px; font-size:10px; border-radius:2px; text-transform:uppercase; letter-spacing:.06em; display:none; pointer-events:none; }
+ /* Admin-card hover actions — View / Sample buttons rendered inside the
+ image; hidden by default, appear on hover alongside the pattern-num pill.
+ Sits at bottom of the image; pointer-events on the wrapper let clicks
+ on the underlying link work, but the buttons re-enable their own. */
+ .design-card.review-card .admin-hover-actions {
+ position:absolute; left:8px; right:8px; bottom:8px; z-index:6;
+ display:flex; gap:6px; justify-content:center;
+ opacity:0; transition:opacity .15s ease;
+ pointer-events:none;
+ }
+ .design-card.review-card:hover .admin-hover-actions,
+ .design-card.review-card:focus-within .admin-hover-actions { opacity:1 }
+ .design-card.review-card .admin-hover-actions .chov-btn { pointer-events:auto; max-width:100px; font-size:10px; padding:5px 8px }
.design-card.review-card[data-decision=keep] .decision-badge { display:inline-block; background:var(--keep); color:#0f0e0c; }
.design-card.review-card[data-decision=keep] .decision-badge::after { content:'KEEP'; }
.design-card.review-card[data-decision=reject] .decision-badge { display:inline-block; background:var(--reject); color:#fff; }
@@ -10741,7 +10809,7 @@ app.post('/api/design/:id/upload-shopify', requireAdmin, express.json({ limit: '
let d;
try {
const raw = psqlQuery(`SELECT row_to_json(t) FROM (
- SELECT id, title, ai_title, ai_pattern, ai_color_name, category, tags,
+ SELECT id, ai_title, ai_pattern, ai_color_name, category, tags,
dominant_hex, dig_number, local_path, spoonflower_design_id,
spoonflower_url, shopify_product_id, shopify_product_url
FROM spoon_all_designs WHERE id=${id}) t;`);
@@ -10763,7 +10831,7 @@ app.post('/api/design/:id/upload-shopify', requireAdmin, express.json({ limit: '
}
const dig = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
- const title = d.ai_title || d.title || `wallco.ai #${d.id}`;
+ const title = d.ai_title || `wallco.ai ${dig}`;
const tagList = [
'wallco-ai', 'wallco-original',
d.category, d.ai_pattern, d.ai_color_name,
← 67e4cc8 marketplace: designer follow + /trade/projects board listing
·
back to Wallco Ai
·
marketplace pattern page: wire 'Generate AI colorways' butto 524cc84 →