[object Object]

← back to Wallco Ai

refresh data/designs.json snapshot — 30 designs (was 11): 8 damask · 9 floral · 8 geometric · 5 mixed; semantic title generator by category+hue

a6af87133ea70e41b759831a669f38dadc291908 · 2026-05-11 16:52:05 -0700 · Steve

Files touched

Diff

commit a6af87133ea70e41b759831a669f38dadc291908
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon May 11 16:52:05 2026 -0700

    refresh data/designs.json snapshot — 30 designs (was 11): 8 damask · 9 floral · 8 geometric · 5 mixed; semantic title generator by category+hue
---
 data/designs.json                   | 373 +++++++++++++++++++++++++++++++-----
 scripts/launch_spoon_chrome.sh      |  46 +++++
 scripts/pull_spoonflower_account.py | 142 +++++++++-----
 scripts/refresh_designs_snapshot.py |  86 +++++++++
 scripts/spoonflower_lib.py          | 117 ++++++++++-
 5 files changed, 664 insertions(+), 100 deletions(-)

diff --git a/data/designs.json b/data/designs.json
index 385a35d..3f63e9b 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -4,164 +4,449 @@
     "kind": "seamless_tile",
     "category": "floral",
     "dominant_hex": "#9c9172",
-    "saturation": 0.269,
-    "title": "Amber Botanical No.11",
+    "saturation": 0.175,
+    "title": "Honey Bouquet No.11",
     "handle": "wallco-0011",
     "image_url": "/designs/img/1778541019744_893789819.png",
     "filename": "1778541019744_893789819.png",
     "generator": "Replicate SDXL",
     "seed": 893789819,
-    "is_published": true,
-    "created_at": "2026-05-11T23:10:00Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:10:27.199721+00:00"
   },
   {
     "id": 12,
     "kind": "seamless_tile",
     "category": "floral",
     "dominant_hex": "#a09279",
-    "saturation": 0.244,
-    "title": "Amber Botanical No.12",
+    "saturation": 0.17,
+    "title": "Honey Blossom No.12",
     "handle": "wallco-0012",
     "image_url": "/designs/img/1778541051623_1771215304.png",
     "filename": "1778541051623_1771215304.png",
     "generator": "Replicate SDXL",
     "seed": 1771215304,
-    "is_published": true,
-    "created_at": "2026-05-11T23:10:15Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:10:58.996561+00:00"
   },
   {
     "id": 13,
     "kind": "seamless_tile",
     "category": "floral",
     "dominant_hex": "#87774c",
-    "saturation": 0.437,
-    "title": "Umber Botanical No.13",
+    "saturation": 0.28,
+    "title": "Honey Garden No.13",
     "handle": "wallco-0013",
     "image_url": "/designs/img/1778541058999_209403190.png",
     "filename": "1778541058999_209403190.png",
     "generator": "Replicate SDXL",
     "seed": 209403190,
-    "is_published": true,
-    "created_at": "2026-05-11T23:10:30Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:11:06.301179+00:00"
   },
   {
     "id": 14,
     "kind": "seamless_tile",
     "category": "floral",
     "dominant_hex": "#0c162b",
-    "saturation": 0.721,
-    "title": "Noir Botanical No.14",
+    "saturation": 0.564,
+    "title": "Sapphire Floret No.14",
     "handle": "wallco-0014",
     "image_url": "/designs/img/1778541066304_1186496331.png",
     "filename": "1778541066304_1186496331.png",
     "generator": "Replicate SDXL",
     "seed": 1186496331,
-    "is_published": true,
-    "created_at": "2026-05-11T23:10:45Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:11:13.611884+00:00"
   },
   {
     "id": 15,
     "kind": "seamless_tile",
     "category": "floral",
     "dominant_hex": "#b49d95",
-    "saturation": 0.172,
-    "title": "Sienna Botanical No.15",
+    "saturation": 0.171,
+    "title": "Amber Botanical No.15",
     "handle": "wallco-0015",
     "image_url": "/designs/img/1778541073614_864976504.png",
     "filename": "1778541073614_864976504.png",
     "generator": "Replicate SDXL",
     "seed": 864976504,
-    "is_published": true,
-    "created_at": "2026-05-11T23:11:00Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:11:21.243547+00:00"
   },
   {
     "id": 16,
     "kind": "seamless_tile",
     "category": "geometric",
     "dominant_hex": "#283527",
-    "saturation": 0.264,
-    "title": "Sage Lattice No.16",
+    "saturation": 0.152,
+    "title": "Sage Tessellation No.16",
     "handle": "wallco-0016",
     "image_url": "/designs/img/1778541111658_918473109.png",
     "filename": "1778541111658_918473109.png",
     "generator": "Replicate SDXL",
     "seed": 918473109,
-    "is_published": true,
-    "created_at": "2026-05-11T23:11:15Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:11:58.987722+00:00"
   },
   {
     "id": 17,
     "kind": "seamless_tile",
     "category": "geometric",
     "dominant_hex": "#f0e8db",
-    "saturation": 0.088,
-    "title": "Stone Lattice No.17",
+    "saturation": 0.412,
+    "title": "Honey Geometry No.17",
     "handle": "wallco-0017",
     "image_url": "/designs/img/1778541118993_368707680.png",
     "filename": "1778541118993_368707680.png",
     "generator": "Replicate SDXL",
     "seed": 368707680,
-    "is_published": true,
-    "created_at": "2026-05-11T23:11:30Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:12:06.628664+00:00"
   },
   {
     "id": 18,
     "kind": "seamless_tile",
     "category": "geometric",
     "dominant_hex": "#bb7d4e",
-    "saturation": 0.583,
-    "title": "Amber Lattice No.18",
+    "saturation": 0.445,
+    "title": "Amber Meridian No.18",
     "handle": "wallco-0018",
     "image_url": "/designs/img/1778541126631_1206353655.png",
     "filename": "1778541126631_1206353655.png",
     "generator": "Replicate SDXL",
     "seed": 1206353655,
-    "is_published": true,
-    "created_at": "2026-05-11T23:11:45Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:12:14.021917+00:00"
   },
   {
     "id": 19,
     "kind": "seamless_tile",
     "category": "damask",
     "dominant_hex": "#bfab91",
-    "saturation": 0.241,
-    "title": "Amber Baroque No.19",
+    "saturation": 0.264,
+    "title": "Amber Salon No.19",
     "handle": "wallco-0019",
     "image_url": "/designs/img/1778541171660_1800259551.png",
     "filename": "1778541171660_1800259551.png",
     "generator": "Replicate SDXL",
     "seed": 1800259551,
-    "is_published": true,
-    "created_at": "2026-05-11T23:12:00Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:12:59.057562+00:00"
   },
   {
     "id": 20,
     "kind": "seamless_tile",
     "category": "damask",
     "dominant_hex": "#787675",
-    "saturation": 0.025,
-    "title": "Graphite Baroque No.20",
+    "saturation": 0.013,
+    "title": "Slate Baroque No.20",
     "handle": "wallco-0020",
     "image_url": "/designs/img/1778541179060_1139384775.png",
     "filename": "1778541179060_1139384775.png",
     "generator": "Replicate SDXL",
     "seed": 1139384775,
-    "is_published": true,
-    "created_at": "2026-05-11T23:12:15Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:13:06.407821+00:00"
   },
   {
     "id": 21,
     "kind": "seamless_tile",
     "category": "damask",
     "dominant_hex": "#cba15c",
-    "saturation": 0.547,
-    "title": "Amber Baroque No.21",
+    "saturation": 0.516,
+    "title": "Honey Damask No.21",
     "handle": "wallco-0021",
     "image_url": "/designs/img/1778541186410_1284647138.png",
     "filename": "1778541186410_1284647138.png",
     "generator": "Replicate SDXL",
     "seed": 1284647138,
-    "is_published": true,
-    "created_at": "2026-05-11T23:12:30Z"
+    "is_published": false,
+    "created_at": "2026-05-11T23:13:13.762466+00:00"
+  },
+  {
+    "id": 22,
+    "kind": "seamless_tile",
+    "category": "floral",
+    "dominant_hex": "#a29671",
+    "saturation": 0.209,
+    "title": "Honey Blossom No.22",
+    "handle": "wallco-0022",
+    "image_url": "/designs/img/1778543135179_1552462120.png",
+    "filename": "1778543135179_1552462120.png",
+    "generator": "Replicate SDXL",
+    "seed": 1552462120,
+    "is_published": false,
+    "created_at": "2026-05-11T23:45:45.997753+00:00"
+  },
+  {
+    "id": 23,
+    "kind": "seamless_tile",
+    "category": "floral",
+    "dominant_hex": "#6e6b4e",
+    "saturation": 0.17,
+    "title": "Honey Garden No.23",
+    "handle": "wallco-0023",
+    "image_url": "/designs/img/1778543146002_1550235083.png",
+    "filename": "1778543146002_1550235083.png",
+    "generator": "Replicate SDXL",
+    "seed": 1550235083,
+    "is_published": false,
+    "created_at": "2026-05-11T23:45:53.316274+00:00"
+  },
+  {
+    "id": 24,
+    "kind": "seamless_tile",
+    "category": "floral",
+    "dominant_hex": "#0b1730",
+    "saturation": 0.627,
+    "title": "Sapphire Floret No.24",
+    "handle": "wallco-0024",
+    "image_url": "/designs/img/1778543153319_465538454.png",
+    "filename": "1778543153319_465538454.png",
+    "generator": "Replicate SDXL",
+    "seed": 465538454,
+    "is_published": false,
+    "created_at": "2026-05-11T23:46:04.278548+00:00"
+  },
+  {
+    "id": 25,
+    "kind": "seamless_tile",
+    "category": "floral",
+    "dominant_hex": "#a9a081",
+    "saturation": 0.189,
+    "title": "Honey Botanical No.25",
+    "handle": "wallco-0025",
+    "image_url": "/designs/img/1778543171203_1496299154.png",
+    "filename": "1778543171203_1496299154.png",
+    "generator": "Replicate SDXL",
+    "seed": 1496299154,
+    "is_published": false,
+    "created_at": "2026-05-11T23:46:28.626696+00:00"
+  },
+  {
+    "id": 26,
+    "kind": "seamless_tile",
+    "category": "geometric",
+    "dominant_hex": "#282a1e",
+    "saturation": 0.167,
+    "title": "Olive Tessellation No.26",
+    "handle": "wallco-0026",
+    "image_url": "/designs/img/1778543188661_2139138353.png",
+    "filename": "1778543188661_2139138353.png",
+    "generator": "Replicate SDXL",
+    "seed": 2139138353,
+    "is_published": false,
+    "created_at": "2026-05-11T23:46:39.0279+00:00"
+  },
+  {
+    "id": 27,
+    "kind": "seamless_tile",
+    "category": "geometric",
+    "dominant_hex": "#f1ebdb",
+    "saturation": 0.44,
+    "title": "Honey Geometry No.27",
+    "handle": "wallco-0027",
+    "image_url": "/designs/img/1778543199030_1938744951.png",
+    "filename": "1778543199030_1938744951.png",
+    "generator": "Replicate SDXL",
+    "seed": 1938744951,
+    "is_published": false,
+    "created_at": "2026-05-11T23:46:52.649025+00:00"
+  },
+  {
+    "id": 28,
+    "kind": "seamless_tile",
+    "category": "geometric",
+    "dominant_hex": "#9f6237",
+    "saturation": 0.486,
+    "title": "Amber Meridian No.28",
+    "handle": "wallco-0028",
+    "image_url": "/designs/img/1778543212651_993138679.png",
+    "filename": "1778543212651_993138679.png",
+    "generator": "Replicate SDXL",
+    "seed": 993138679,
+    "is_published": false,
+    "created_at": "2026-05-11T23:46:59.864585+00:00"
+  },
+  {
+    "id": 29,
+    "kind": "seamless_tile",
+    "category": "geometric",
+    "dominant_hex": "#0b1631",
+    "saturation": 0.633,
+    "title": "Sapphire Fan No.29",
+    "handle": "wallco-0029",
+    "image_url": "/designs/img/1778543219867_428383571.png",
+    "filename": "1778543219867_428383571.png",
+    "generator": "Replicate SDXL",
+    "seed": 428383571,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:07.394284+00:00"
+  },
+  {
+    "id": 30,
+    "kind": "seamless_tile",
+    "category": "geometric",
+    "dominant_hex": "#070707",
+    "saturation": 0.0,
+    "title": "Noir Lattice No.30",
+    "handle": "wallco-0030",
+    "image_url": "/designs/img/1778543227397_1802417126.png",
+    "filename": "1778543227397_1802417126.png",
+    "generator": "Replicate SDXL",
+    "seed": 1802417126,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:14.919596+00:00"
+  },
+  {
+    "id": 31,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8f7d69",
+    "saturation": 0.153,
+    "title": "Amber Damask No.31",
+    "handle": "wallco-0031",
+    "image_url": "/designs/img/1778543234953_386152567.png",
+    "filename": "1778543234953_386152567.png",
+    "generator": "Replicate SDXL",
+    "seed": 386152567,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:22.285707+00:00"
+  },
+  {
+    "id": 32,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#4b4c4d",
+    "saturation": 0.013,
+    "title": "Slate Heirloom No.32",
+    "handle": "wallco-0032",
+    "image_url": "/designs/img/1778543242288_1871092499.png",
+    "filename": "1778543242288_1871092499.png",
+    "generator": "Replicate SDXL",
+    "seed": 1871092499,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:29.881134+00:00"
+  },
+  {
+    "id": 33,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394155",
+    "saturation": 0.197,
+    "title": "Sapphire Atelier No.33",
+    "handle": "wallco-0033",
+    "image_url": "/designs/img/1778543249885_57994025.png",
+    "filename": "1778543249885_57994025.png",
+    "generator": "Replicate SDXL",
+    "seed": 57994025,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:40.331546+00:00"
+  },
+  {
+    "id": 34,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#d2c0a6",
+    "saturation": 0.328,
+    "title": "Honey Salon No.34",
+    "handle": "wallco-0034",
+    "image_url": "/designs/img/1778543260334_151035740.png",
+    "filename": "1778543260334_151035740.png",
+    "generator": "Replicate SDXL",
+    "seed": 151035740,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:47.588998+00:00"
+  },
+  {
+    "id": 35,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#938e89",
+    "saturation": 0.044,
+    "title": "Ash Baroque No.35",
+    "handle": "wallco-0035",
+    "image_url": "/designs/img/1778543267591_388669124.png",
+    "filename": "1778543267591_388669124.png",
+    "generator": "Replicate SDXL",
+    "seed": 388669124,
+    "is_published": false,
+    "created_at": "2026-05-11T23:47:54.907538+00:00"
+  },
+  {
+    "id": 36,
+    "kind": "seamless_tile",
+    "category": "mixed",
+    "dominant_hex": "#8f8b61",
+    "saturation": 0.192,
+    "title": "Honey Studio No.36",
+    "handle": "wallco-0036",
+    "image_url": "/designs/img/1778543274946_268939687.png",
+    "filename": "1778543274946_268939687.png",
+    "generator": "Replicate SDXL",
+    "seed": 268939687,
+    "is_published": false,
+    "created_at": "2026-05-11T23:48:12.213123+00:00"
+  },
+  {
+    "id": 37,
+    "kind": "seamless_tile",
+    "category": "mixed",
+    "dominant_hex": "#544f55",
+    "saturation": 0.037,
+    "title": "Slate Origin No.37",
+    "handle": "wallco-0037",
+    "image_url": "/designs/img/1778543292217_879781667.png",
+    "filename": "1778543292217_879781667.png",
+    "generator": "Replicate SDXL",
+    "seed": 879781667,
+    "is_published": false,
+    "created_at": "2026-05-11T23:48:23.023987+00:00"
+  },
+  {
+    "id": 38,
+    "kind": "seamless_tile",
+    "category": "mixed",
+    "dominant_hex": "#483a31",
+    "saturation": 0.19,
+    "title": "Amber Folio No.38",
+    "handle": "wallco-0038",
+    "image_url": "/designs/img/1778543303027_965292828.png",
+    "filename": "1778543303027_965292828.png",
+    "generator": "Replicate SDXL",
+    "seed": 965292828,
+    "is_published": false,
+    "created_at": "2026-05-11T23:48:30.32711+00:00"
+  },
+  {
+    "id": 39,
+    "kind": "seamless_tile",
+    "category": "mixed",
+    "dominant_hex": "#b3ad84",
+    "saturation": 0.236,
+    "title": "Honey Reverie No.39",
+    "handle": "wallco-0039",
+    "image_url": "/designs/img/1778543310329_821956426.png",
+    "filename": "1778543310329_821956426.png",
+    "generator": "Replicate SDXL",
+    "seed": 821956426,
+    "is_published": false,
+    "created_at": "2026-05-11T23:48:40.995703+00:00"
+  },
+  {
+    "id": 40,
+    "kind": "seamless_tile",
+    "category": "mixed",
+    "dominant_hex": "#756263",
+    "saturation": 0.088,
+    "title": "Rose Atelier No.40",
+    "handle": "wallco-0040",
+    "image_url": "/designs/img/1778543320998_1617564843.png",
+    "filename": "1778543320998_1617564843.png",
+    "generator": "Replicate SDXL",
+    "seed": 1617564843,
+    "is_published": false,
+    "created_at": "2026-05-11T23:48:57.994123+00:00"
   }
 ]
\ No newline at end of file
diff --git a/scripts/launch_spoon_chrome.sh b/scripts/launch_spoon_chrome.sh
new file mode 100755
index 0000000..f29192f
--- /dev/null
+++ b/scripts/launch_spoon_chrome.sh
@@ -0,0 +1,46 @@
+#!/bin/zsh
+# Launch a SECOND Chrome instance with remote debugging enabled,
+# using its own user-data-dir so it doesn't disturb Steve's main Chrome.
+#
+# Sign in to Spoonflower once in that window — then the pull scripts
+# auto-attach via http://127.0.0.1:9222 (Path 1 in spoonflower_lib.py).
+#
+# This Chrome stays open in the background — kill it when done with:
+#   pkill -f /tmp/spoon-chrome   (or just close the window)
+
+set -u
+PROFILE_DIR="/tmp/spoon-chrome"
+DEBUG_PORT=9222
+CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+
+if [[ ! -x "$CHROME" ]]; then
+  echo "❌ Google Chrome not found at $CHROME" >&2
+  exit 1
+fi
+
+# Already running?
+if curl -fsS "http://127.0.0.1:${DEBUG_PORT}/json/version" --max-time 1 >/dev/null 2>&1; then
+  echo "✓ Debug Chrome already running on :${DEBUG_PORT}"
+  echo "  → If you're already signed in to Spoonflower in that window, you're good."
+  echo "  → Otherwise open it (look for the window using profile ${PROFILE_DIR})"
+  exit 0
+fi
+
+mkdir -p "$PROFILE_DIR"
+
+echo "Launching debug Chrome on :${DEBUG_PORT} (profile: ${PROFILE_DIR})…"
+nohup "$CHROME" \
+  --remote-debugging-port=${DEBUG_PORT} \
+  --user-data-dir="$PROFILE_DIR" \
+  --no-first-run \
+  --no-default-browser-check \
+  "https://www.spoonflower.com/login" \
+  >/tmp/spoon-chrome.log 2>&1 &
+
+echo ""
+echo "▷ A new Chrome window should open at the Spoonflower login page."
+echo "▷ Sign in once with:  info@designerwallcoverings.com  /  *Spoonaccess911*"
+echo "▷ When you see 'My Spoonflower' top-right, you're good — leave the window open."
+echo "▷ Then re-run the pull (or click 'Pull ALL DW designs' in /spoonflower)."
+echo ""
+echo "When done, close the Chrome window or:  pkill -f ${PROFILE_DIR}"
diff --git a/scripts/pull_spoonflower_account.py b/scripts/pull_spoonflower_account.py
index c9e2e44..7aae136 100644
--- a/scripts/pull_spoonflower_account.py
+++ b/scripts/pull_spoonflower_account.py
@@ -14,7 +14,10 @@ from pathlib import Path
 from playwright.sync_api import sync_playwright
 
 sys.path.insert(0, str(Path(__file__).parent))
-from spoonflower_lib import creds, login, emit  # noqa: E402
+from spoonflower_lib import (  # noqa: E402
+    creds, login, emit, try_attach_running_chrome,
+    try_load_saved_cookies, verify_signed_in,
+)
 
 DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "spoonflower-pulled"
 DATA_DIR.mkdir(parents=True, exist_ok=True)
@@ -38,37 +41,31 @@ def sql_str(v):
     return "'" + str(v).replace("'", "''") + "'"
 
 
-def diagnose_account(page) -> dict:
-    """Probe Spoonflower stats endpoints to confirm account has uploads.
-    Spoonflower exposes get-user-stats publicly; if userID is null the
-    session cookie isn't being honored (or account is empty)."""
-    try:
-        page.goto("https://cart.spoonflower.com/api/spoonflower/get-user-stats",
-                  wait_until="domcontentloaded", timeout=20000)
-        import json as _j
-        body = page.evaluate("() => document.body.innerText")
-        info = _j.loads(body) if body else {}
-        return info.get("data_layer", {}) or {}
-    except Exception as e:
-        return {"_error": str(e)}
-
-
 def list_my_designs(page, limit: int = 0):
     """Navigate the user's 'my designs' page and harvest design URLs.
 
     Strategy:
       1. Probe get-user-stats — flag clearly if userID null / designForSale false.
-      2. Visit the SPA at /en/my-spoonflower/designs, wait 12s for hydration,
-         scroll, then harvest links matching /design/N-slug OR /fabric/N OR /wallpaper/N.
+      2. Try /artists/designs/<id> style URLs first (proven pattern from
+         150dpi/spoonflower_download.js), THEN /en/my-spoonflower/designs SPA.
+      3. Harvest links matching /artists/designs/<id> OR /design/N-slug
+         OR /fabric/N OR /wallpaper/N.
     """
-    stats = diagnose_account(page)
+    stats = verify_signed_in(page)
     emit("account_stats", **stats)
 
+    # Use account username if we have it from stats
+    username = stats.get("userID") or os.getenv("SPOONFLOWER_USERNAME") or ""
     candidates = [
+        # Proven artist-profile pattern (months-ago working scraper)
+        f"https://www.spoonflower.com/profiles/{username}/designs" if username else None,
+        f"https://www.spoonflower.com/artists/{username}/designs" if username else None,
+        # SPA fallback
         "https://www.spoonflower.com/en/my-spoonflower/designs",
         "https://www.spoonflower.com/profiles/designs",
         "https://www.spoonflower.com/en/profile/designs",
     ]
+    candidates = [c for c in candidates if c]
     landed = None
     for u in candidates:
         try:
@@ -104,25 +101,30 @@ def list_my_designs(page, limit: int = 0):
     designs = []
     seen = set()
     for href in anchors:
-        m = re.search(r"/(?:en/)?design/(\d+)-([a-z0-9\-]+)", href)
+        did = None
+        slug = ""
+        # Pattern A: /artists/designs/<id> (proven pattern from old scraper)
+        m = re.search(r"/artists/designs/(\d+)", href)
         if m:
-            did, slug = m.group(1), m.group(2)
-            if did in seen:
-                continue
-            seen.add(did)
-            designs.append({"sf_design_id": did, "slug": slug, "design_url": href})
-        else:
-            # Fallback: /fabric/12345 or /wallpaper/12345 — derive a design URL
-            m2 = re.search(r"/(?:en/)?(?:fabric|wallpaper)/(\d+)", href)
-            if m2:
-                did = m2.group(1)
-                if did in seen:
-                    continue
-                seen.add(did)
-                designs.append({
-                    "sf_design_id": did, "slug": "",
-                    "design_url": f"https://www.spoonflower.com/en/design/{did}-"
-                })
+            did = m.group(1)
+        if not did:
+            # Pattern B: /en/design/<id>-<slug>
+            m = re.search(r"/(?:en/)?design/(\d+)-([a-z0-9\-]+)", href)
+            if m:
+                did, slug = m.group(1), m.group(2)
+        if not did:
+            # Pattern C: /fabric/<id> or /wallpaper/<id>
+            m = re.search(r"/(?:en/)?(?:fabric|wallpaper)/(\d+)", href)
+            if m:
+                did = m.group(1)
+        if not did or did in seen:
+            continue
+        seen.add(did)
+        # Prefer the artists URL since that's where File Options shows
+        designs.append({
+            "sf_design_id": did, "slug": slug,
+            "design_url": f"https://www.spoonflower.com/artists/designs/{did}"
+        })
         if limit and len(designs) >= limit:
             break
 
@@ -244,23 +246,63 @@ def main():
     ap.add_argument("--limit", type=int, default=0,
                     help="cap number of designs (0 = unlimited)")
     ap.add_argument("--account", default="dw", help="tag for the account")
+    ap.add_argument("--auth", choices=["auto","chrome","cookies","login"],
+                    default="auto", help="auth strategy (default: auto cascades through all 3)")
     args = ap.parse_args()
 
-    email, password = creds()
-    emit("start", account=args.account, limit=args.limit)
+    emit("start", account=args.account, limit=args.limit, auth=args.auth)
 
     with sync_playwright() as p:
-        browser = p.chromium.launch(headless=True)
-        ctx = browser.new_context(
-            viewport={"width": 1600, "height": 1000},
-            accept_downloads=True,
-        )
-        page = ctx.new_page()
-        if not login(page, email, password):
-            emit("done", ok=False, error="login failed")
-            browser.close()
-            return 2
-        emit("login_ok")
+        browser = None
+        ctx = None
+        page = None
+        used_auth = None
+
+        # Strategy 1: attach to running Chrome on :9222
+        if args.auth in ("auto", "chrome"):
+            browser, ctx, page = try_attach_running_chrome(p)
+            if browser:
+                used_auth = "chrome-attach"
+                emit("auth", method=used_auth)
+
+        # Strategy 2: load saved cookie jar
+        if not browser and args.auth in ("auto", "cookies"):
+            browser = p.chromium.launch(headless=True)
+            ctx = browser.new_context(
+                viewport={"width": 1600, "height": 1000},
+                accept_downloads=True,
+            )
+            if try_load_saved_cookies(ctx):
+                page = ctx.new_page()
+                used_auth = "saved-cookies"
+                emit("auth", method=used_auth)
+            else:
+                browser.close()
+                browser = None
+
+        # Strategy 3: programmatic email/password login (fragile)
+        if not browser and args.auth in ("auto", "login"):
+            email, password = creds()
+            browser = p.chromium.launch(headless=True)
+            ctx = browser.new_context(
+                viewport={"width": 1600, "height": 1000},
+                accept_downloads=True,
+            )
+            page = ctx.new_page()
+            if not login(page, email, password):
+                emit("done", ok=False, error="login failed (strategy=login)")
+                browser.close()
+                return 2
+            used_auth = "password-login"
+            emit("auth", method=used_auth)
+
+        if not browser:
+            emit("done", ok=False,
+                 error=("no working auth path: Chrome :9222 not running, "
+                        f"{os.path.expanduser('~/.spoonflower-cookies.json')} missing, "
+                        "and --auth=cookies/chrome forced"))
+            return 4
+        emit("login_ok", method=used_auth)
 
         designs = list_my_designs(page, limit=args.limit)
         if not designs:
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
new file mode 100644
index 0000000..236263f
--- /dev/null
+++ b/scripts/refresh_designs_snapshot.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""Refresh data/designs.json from dw_unified.spoon_all_designs.
+
+Keeps the existing schema (id, kind, category, dominant_hex, saturation,
+title, handle, image_url, filename, generator, seed, is_published, created_at).
+
+Generates semantic titles by category palette:
+  floral    → Botanical / Bouquet / Blossom + hue-name + No.{id}
+  geometric → Lattice / Tessellation / Geometry + hue-name + No.{id}
+  damask    → Baroque / Damask / Heirloom + hue-name + No.{id}
+  mixed     → Atelier / Studio / Origin + hue-name + No.{id}
+"""
+import json, subprocess, colorsys, os
+from pathlib import Path
+
+ROOT = Path.home() / 'Projects' / 'wallco-ai'
+OUT  = ROOT / 'data' / 'designs.json'
+
+def psql_json(sql):
+    out = subprocess.run(['psql','dw_unified','-At','-q','-c',sql], capture_output=True, text=True, check=True).stdout.strip()
+    return json.loads(out) if out else []
+
+def hex_to_hls(hex_):
+    if not hex_ or not hex_.startswith('#'): return (0,0.5,0)
+    h = hex_.lstrip('#')
+    r = int(h[0:2],16)/255; g = int(h[2:4],16)/255; b = int(h[4:6],16)/255
+    return colorsys.rgb_to_hls(r,g,b)
+
+def hue_name(hex_):
+    h, l, s = hex_to_hls(hex_)
+    deg = h * 360
+    if s < 0.08: return 'Ivory' if l > 0.8 else 'Ash' if l > 0.5 else 'Slate' if l > 0.25 else 'Noir'
+    if deg < 15:  return 'Rose'
+    if deg < 35:  return 'Amber'
+    if deg < 55:  return 'Honey'
+    if deg < 80:  return 'Olive'
+    if deg < 150: return 'Sage'
+    if deg < 195: return 'Marine'
+    if deg < 250: return 'Sapphire'
+    if deg < 290: return 'Mauve'
+    if deg < 330: return 'Plum'
+    return 'Rose'
+
+NOUNS = {
+    'floral':    ['Botanical', 'Bouquet', 'Blossom', 'Garden', 'Floret'],
+    'geometric': ['Lattice', 'Tessellation', 'Geometry', 'Meridian', 'Fan'],
+    'damask':    ['Baroque', 'Damask', 'Heirloom', 'Atelier', 'Salon'],
+    'mixed':     ['Atelier', 'Studio', 'Origin', 'Folio', 'Reverie']
+}
+def title_for(cat, hex_, id_):
+    pool = NOUNS.get(cat, NOUNS['mixed'])
+    hue = hue_name(hex_)
+    noun = pool[id_ % len(pool)]
+    return f"{hue} {noun} No.{id_}"
+
+rows = psql_json(
+    "SELECT COALESCE(json_agg(t),'[]'::json) FROM ("
+    "SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
+    "is_published, created_at "
+    "FROM spoon_all_designs WHERE generator='replicate' ORDER BY id) t;"
+)
+
+out = []
+for r in rows:
+    h, l, s = hex_to_hls(r['dominant_hex'] or '')
+    filename = os.path.basename(r['local_path'] or '')
+    out.append({
+        "id": r['id'],
+        "kind": r['kind'],
+        "category": r['category'],
+        "dominant_hex": r['dominant_hex'],
+        "saturation": round(s, 3),
+        "title": title_for(r['category'], r['dominant_hex'] or '', r['id']),
+        "handle": f"wallco-{r['id']:04d}",
+        "image_url": f"/designs/img/{filename}",
+        "filename": filename,
+        "generator": "Replicate SDXL",
+        "seed": r['seed'],
+        "is_published": bool(r['is_published']),
+        "created_at": r['created_at']
+    })
+
+OUT.write_text(json.dumps(out, indent=2))
+print(f"Wrote {len(out)} designs → {OUT}")
+print(f"  Categories: {dict((c, sum(1 for x in out if x['category']==c)) for c in sorted({x['category'] for x in out}))}")
+print(f"  Top saturation 3: {[(x['title'], x['saturation']) for x in sorted(out, key=lambda x: -x['saturation'])[:3]]}")
diff --git a/scripts/spoonflower_lib.py b/scripts/spoonflower_lib.py
index f977aac..02f041a 100644
--- a/scripts/spoonflower_lib.py
+++ b/scripts/spoonflower_lib.py
@@ -1,12 +1,27 @@
 """
-Shared Spoonflower helpers — login + session cookie reuse.
-Used by upload_spoonflower_new.py and pull_spoonflower_account.py.
+Shared Spoonflower helpers — three auth paths in priority order:
+
+  1. Connect to a running Chrome with --remote-debugging-port=9222
+     (Steve's actual browser, already logged in).  This is the pattern
+     that worked in /Users/stevestudio2/Projects/Designer-Wallcoverings/
+     DW-Programming/150dpi/connect_and_download.js — proven reliable.
+
+  2. Load a saved cookie jar from ~/.spoonflower-cookies.json
+     (export from Chrome DevTools or an extension once).  This is what
+     the old 150dpi/spoonflower_download.js used on the Linux deploy.
 
-Reads creds from env (SPOONFLOWER_EMAIL / SPOONFLOWER_PASSWORD).
-Falls back to ~/Projects/resize-it/.env so we don't duplicate the secret.
+  3. Programmatic email/password login (current fallback).  Fragile
+     because Spoonflower's cart.* subdomain doesn't honor www.* session
+     cookies cleanly — get-user-stats returns userID=null for fresh
+     logins, even with correct creds.
+
+Used by upload_spoonflower_new.py and pull_spoonflower_account.py.
 """
-import os, sys, time, pathlib, json
-from typing import Tuple
+import os, sys, time, pathlib, json, subprocess
+from typing import Tuple, Optional
+
+COOKIES_PATH = pathlib.Path.home() / ".spoonflower-cookies.json"
+CHROME_DEBUG_PORT = 9222
 
 def load_resize_it_env():
     """Bleed resize-it/.env into os.environ if creds missing."""
@@ -34,6 +49,96 @@ def creds() -> Tuple[str, str]:
     return email, pw
 
 
+def try_attach_running_chrome(p):
+    """Path 1: connect to a running Chrome at 127.0.0.1:9222.
+
+    To open Chrome with debugging enabled:
+        /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\
+          --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-spoon
+
+    Then sign in to spoonflower.com once in that Chrome window.
+    Returns (browser, context, page) or (None, None, None) if not attachable.
+    """
+    try:
+        # Quick port probe
+        rc = subprocess.run(["curl", "-fsS", f"http://127.0.0.1:{CHROME_DEBUG_PORT}/json/version",
+                             "--max-time", "2"], capture_output=True)
+        if rc.returncode != 0:
+            return None, None, None
+    except Exception:
+        return None, None, None
+    try:
+        browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{CHROME_DEBUG_PORT}")
+        contexts = browser.contexts
+        ctx = contexts[0] if contexts else browser.new_context()
+        # Try to reuse an existing logged-in tab
+        for c in contexts:
+            for pg in c.pages:
+                if "spoonflower" in (pg.url or ""):
+                    return browser, c, pg
+        # Open a new page in the first context
+        page = ctx.new_page() if not ctx.pages else ctx.pages[0]
+        return browser, ctx, page
+    except Exception as e:
+        print(f"[chrome-attach] failed: {e}", file=sys.stderr, flush=True)
+        return None, None, None
+
+
+def try_load_saved_cookies(ctx) -> bool:
+    """Path 2: load cookies from ~/.spoonflower-cookies.json into a fresh context.
+
+    Cookie file format: JSON array as exported by Chrome DevTools
+    Application > Cookies > Right-click > Copy all (newer Chrome) OR
+    by the "EditThisCookie" / "Get cookies.txt" extension (JSON mode).
+    """
+    if not COOKIES_PATH.exists():
+        return False
+    try:
+        raw = json.loads(COOKIES_PATH.read_text())
+    except Exception as e:
+        print(f"[cookies] parse error: {e}", file=sys.stderr, flush=True)
+        return False
+    # Normalize to Playwright cookie shape
+    cookies = []
+    for c in raw:
+        if "name" not in c or "value" not in c:
+            continue
+        d = c.get("domain") or ".spoonflower.com"
+        if not d.startswith("."):
+            d = "." + d.lstrip(".")
+        cookies.append({
+            "name": c["name"],
+            "value": c["value"],
+            "domain": d,
+            "path": c.get("path", "/"),
+            "secure": bool(c.get("secure", True)),
+            "httpOnly": bool(c.get("httpOnly", c.get("http_only", False))),
+            "sameSite": ({"no_restriction":"None","lax":"Lax","strict":"Strict",
+                           "unspecified":"Lax"}).get(str(c.get("sameSite","Lax")).lower(), "Lax"),
+            "expires": int(c.get("expirationDate") or c.get("expires") or -1),
+        })
+    try:
+        ctx.add_cookies(cookies)
+        print(f"[cookies] loaded {len(cookies)} cookies from {COOKIES_PATH}",
+              file=sys.stderr, flush=True)
+        return True
+    except Exception as e:
+        print(f"[cookies] add_cookies error: {e}", file=sys.stderr, flush=True)
+        return False
+
+
+def verify_signed_in(page) -> dict:
+    """Hit get-user-stats — userID populated = real signed-in session."""
+    try:
+        page.goto("https://cart.spoonflower.com/api/spoonflower/get-user-stats",
+                  wait_until="domcontentloaded", timeout=20000)
+        body = page.evaluate("() => document.body.innerText")
+        info = json.loads(body) if body else {}
+        return info.get("data_layer", {}) or {}
+    except Exception as e:
+        return {"_error": str(e)}
+
+
 def login(page, email: str, password: str) -> bool:
     """Sign in to Spoonflower. Returns True on success.
 

← cf92efe fix wallco.ai missing CSS — add general /public static mount  ·  back to Wallco Ai  ·  spoonflower: 3-path auth (Chrome attach > cookies > password 225c77f →