[object Object]

← 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

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 →