[object Object]

← back to Pattern Vault

fernwick: add uploader health-check monitor + record the first 3 published designs

9efb375ee886be14c1d19d549d353ad3516e1d5e · 2026-07-08 08:41:47 -0700 · Steve Abrams

The whole pipeline was silently broken for a long time (0 uploads) with TWO invisible
failures — expired session AND Spoonflower moving the upload page — and nothing caught it.
- check_health.js: read-only monitor that verifies (a) session valid (not bounced to /login)
  and (b) the /artists/designs/new file input is present. Exits 0=HEALTHY / 2=DEGRADED with a
  reason. Catches both silent-failure classes; publishes nothing. Verified HEALTHY.
- daily_run.sh: runs check_health as a pre-flight every run, logs uploader-health + writes
  health.json to the run dir — so a silent break is now loud in cron.out.log.
- spoonflower-uploads.json: recorded the first 3 real uploads (22712805/06/09, Fernwick Studio,
  PRIVATE) that the direct upload_sf.js run didn't log.

Files touched

Diff

commit 9efb375ee886be14c1d19d549d353ad3516e1d5e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 08:41:47 2026 -0700

    fernwick: add uploader health-check monitor + record the first 3 published designs
    
    The whole pipeline was silently broken for a long time (0 uploads) with TWO invisible
    failures — expired session AND Spoonflower moving the upload page — and nothing caught it.
    - check_health.js: read-only monitor that verifies (a) session valid (not bounced to /login)
      and (b) the /artists/designs/new file input is present. Exits 0=HEALTHY / 2=DEGRADED with a
      reason. Catches both silent-failure classes; publishes nothing. Verified HEALTHY.
    - daily_run.sh: runs check_health as a pre-flight every run, logs uploader-health + writes
      health.json to the run dir — so a silent break is now loud in cron.out.log.
    - spoonflower-uploads.json: recorded the first 3 real uploads (22712805/06/09, Fernwick Studio,
      PRIVATE) that the direct upload_sf.js run didn't log.
---
 data/spoonflower-uploads.json                   | 146 +++++-------------------
 whimsical-compare/daily/scripts/check_health.js |  64 +++++++++++
 whimsical-compare/daily/scripts/daily_run.sh    |   7 ++
 3 files changed, 99 insertions(+), 118 deletions(-)

diff --git a/data/spoonflower-uploads.json b/data/spoonflower-uploads.json
index 6c6b147..42c9c84 100644
--- a/data/spoonflower-uploads.json
+++ b/data/spoonflower-uploads.json
@@ -1,119 +1,29 @@
-{
-  "shop": "Fernwick Studio",
-  "handle": "fernwick_studio",
-  "account": "900854229",
-  "login": "info@printmurals.com",
-  "_seller_account_going_forward": "info@designerwallcoverings.com — 2026-07-07: ALL future uploads switch to this account; info@printmurals.com becomes a designer persona. This record is the initial printmurals/Fernwick batch (account 900854229).",
-  "uploaded_at": "2026-07-02",
-  "state": "Private (not for sale — payout/tax setup is the remaining switch)",
-  "designs": [
-    {
-      "n": 1,
-      "id": "22668196",
-      "title": "Arden Botanical — Heritage Trellis",
-      "url": "https://www.spoonflower.com/artists/designs/22668196",
-      "proofed": true
-    },
-    {
-      "n": 2,
-      "id": "22668205",
-      "title": "Nocturne Bloom — Dark Moody Floral",
-      "url": "https://www.spoonflower.com/artists/designs/22668205",
-      "proofed": true
-    },
-    {
-      "n": 3,
-      "id": "22668206",
-      "title": "Gilded Bough — Heritage Noir",
-      "url": "https://www.spoonflower.com/artists/designs/22668206",
-      "proofed": true
-    },
-    {
-      "n": 4,
-      "id": "22668210",
-      "title": "Rosewater Trellis — Heritage Soft",
-      "url": "https://www.spoonflower.com/artists/designs/22668210",
-      "proofed": false,
-      "hold_reason": "4-way mirror repeat fault (graphic-designer HOLD)"
-    },
-    {
-      "n": 5,
-      "id": "22668217",
-      "title": "Amethyst Nocturne — Moody Bloom",
-      "url": "https://www.spoonflower.com/artists/designs/22668217",
-      "proofed": true
-    },
-    {
-      "n": 6,
-      "id": "22668219",
-      "title": "Emerald Nocturne — Camellia",
-      "url": "https://www.spoonflower.com/artists/designs/22668219",
-      "proofed": true
-    },
-    {
-      "n": 7,
-      "id": "22668220",
-      "title": "Atomic Meadow — Mid-Century Retro",
-      "url": "https://www.spoonflower.com/artists/designs/22668220",
-      "proofed": false,
-      "hold_reason": "column-to-tile-edge seam misalignment (graphic-designer HOLD)"
-    },
-    {
-      "n": 8,
-      "id": "22668245",
-      "title": "Tide Lines — Coastal Stripe",
-      "url": "https://www.spoonflower.com/artists/designs/22668245",
-      "proofed": true
-    },
-    {
-      "n": 9,
-      "id": "22668247",
-      "title": "Folk Tile — Block Print Geometry",
-      "url": "https://www.spoonflower.com/artists/designs/22668247",
-      "proofed": true
-    },
-    {
-      "n": 10,
-      "id": "22668250",
-      "title": "Calico Vine — Heritage Ditsy",
-      "url": "https://www.spoonflower.com/artists/designs/22668250",
-      "proofed": true
-    },
-    {
-      "n": 11,
-      "id": "22668252",
-      "title": "Cottage Wash — Painterly Floral",
-      "url": "https://www.spoonflower.com/artists/designs/22668252",
-      "proofed": true
-    },
-    {
-      "n": 12,
-      "id": "22668255",
-      "title": "Indigo Trellis — Heritage Vine",
-      "url": "https://www.spoonflower.com/artists/designs/22668255",
-      "proofed": true
-    },
-    {
-      "n": 13,
-      "id": "22668266",
-      "title": "Crimson Nocturne — Moody Rose",
-      "url": "https://www.spoonflower.com/artists/designs/22668266",
-      "proofed": true
-    },
-    {
-      "n": 14,
-      "id": "22668271",
-      "title": "Teal Nocturne — Magnolia Bloom",
-      "url": "https://www.spoonflower.com/artists/designs/22668271",
-      "proofed": true
-    }
-  ],
-  "proofing": {
-    "reviewed_by": "graphic-designer agent",
-    "date": "2026-07-02",
-    "proofed": 12,
-    "held": 2,
-    "method": "Spoonflower digital proofing (For Sale > Proof > guided repeat review > Looks Good)"
+[
+  {
+    "id": "22712805",
+    "title": "Fern Wink — Rainbows",
+    "url": "https://www.spoonflower.com/artists/designs/22712805",
+    "account": "info@designerwallcoverings.com",
+    "shop": "Fernwick Studio",
+    "private": true,
+    "uploadedAt": "2026-07-08T15:40:50.287Z"
   },
-  "remaining_switch": "Account verification (tax ID + payout) required before any design can go For Sale — Steves manual step. Product coverage (fabric, wallpaper, home goods) is automatic once verified."
-}
\ No newline at end of file
+  {
+    "id": "22712806",
+    "title": "Clover Trellis — Rolling Hills",
+    "url": "https://www.spoonflower.com/artists/designs/22712806",
+    "account": "info@designerwallcoverings.com",
+    "shop": "Fernwick Studio",
+    "private": true,
+    "uploadedAt": "2026-07-08T15:40:50.287Z"
+  },
+  {
+    "id": "22712809",
+    "title": "Foxglove Ditty — Garden Snails",
+    "url": "https://www.spoonflower.com/artists/designs/22712809",
+    "account": "info@designerwallcoverings.com",
+    "shop": "Fernwick Studio",
+    "private": true,
+    "uploadedAt": "2026-07-08T15:40:50.287Z"
+  }
+]
\ No newline at end of file
diff --git a/whimsical-compare/daily/scripts/check_health.js b/whimsical-compare/daily/scripts/check_health.js
new file mode 100644
index 0000000..125e5ce
--- /dev/null
+++ b/whimsical-compare/daily/scripts/check_health.js
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+/**
+ * Fernwick → Spoonflower UPLOADER HEALTH CHECK (read-only, publishes nothing).
+ *
+ * Born 2026-07-07: the pipeline generated designs daily but published 0 for a long
+ * time and NOTHING noticed — two independent silent failures (a) the stored session
+ * expired, (b) Spoonflower moved the upload page to /artists/designs/new so the file
+ * input was "not found". This check catches BOTH classes before a real run:
+ *   - session VALID?  (logged in, not bounced to /login)
+ *   - upload page REACHABLE?  (the /artists/designs/new file input is present)
+ * Exits 0 = HEALTHY, 2 = DEGRADED (with a reason). Wire to a canary/cron to alert.
+ *
+ *   node check_health.js            # human-readable
+ *   node check_health.js --json     # {healthy, session, uploadPage, reason}
+ */
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const STATE = process.env.SF_STATE || path.join(os.homedir(), '.config/pattern-vault/sf-state.json');
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/126 Safari/537.36';
+const JSON_OUT = process.argv.includes('--json');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+  const out = { healthy: false, session: false, uploadPage: false, reason: '', checkedAt: new Date().toISOString() };
+  if (!fs.existsSync(STATE)) { out.reason = 'no session file at ' + STATE; return done(out); }
+
+  let b;
+  try {
+    b = await chromium.launch({ headless: true });
+    const ctx = await b.newContext({ storageState: STATE, userAgent: UA });
+    const pg = await ctx.newPage();
+
+    // 1. session valid?
+    await pg.goto('https://www.spoonflower.com/en/dashboard', { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
+    await sleep(3000);
+    if (pg.url().includes('/login')) { out.reason = 'session expired (bounced to /login) — run capture_sf_session.js --headful'; return done(out, b); }
+    out.session = true;
+
+    // 2. upload page reachable? (the exact thing that silently rotted)
+    await pg.goto('https://www.spoonflower.com/artists/designs/new', { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
+    await sleep(4000);
+    if (pg.url().includes('/login')) { out.reason = 'upload page bounced to /login'; return done(out, b); }
+    const fi = await pg.waitForSelector('input[type=file]', { state: 'attached', timeout: 8000 }).catch(() => null);
+    if (!fi) { out.reason = 'upload page has NO file input — Spoonflower may have moved it again (recalibrate openUploader URLs/selectors)'; return done(out, b); }
+    out.uploadPage = true;
+
+    out.healthy = true;
+    out.reason = 'session valid + upload page reachable';
+    return done(out, b);
+  } catch (e) {
+    out.reason = 'check error: ' + e.message;
+    return done(out, b);
+  }
+})();
+
+async function done(out, b) {
+  if (b) await b.close().catch(() => {});
+  if (JSON_OUT) console.log(JSON.stringify(out));
+  else console.log(`${out.healthy ? '✅ HEALTHY' : '❌ DEGRADED'} — ${out.reason}  (session=${out.session} uploadPage=${out.uploadPage})`);
+  process.exit(out.healthy ? 0 : 2);
+}
diff --git a/whimsical-compare/daily/scripts/daily_run.sh b/whimsical-compare/daily/scripts/daily_run.sh
index 669c6e8..cdb7588 100755
--- a/whimsical-compare/daily/scripts/daily_run.sh
+++ b/whimsical-compare/daily/scripts/daily_run.sh
@@ -46,6 +46,13 @@ SEED_BASE=$(( NOW_EPOCH % 1000000 ))
 mkdir -p "$STATE/$TODAY"
 MANIFEST="$STATE/$TODAY/manifest.json"
 
+# 0. pre-flight uploader health (born 2026-07-07 — the pipeline was silently broken for a long
+# time: the stored session expired AND Spoonflower moved the upload page to /artists/designs/new,
+# and NOTHING noticed). This read-only check surfaces both failure classes in every run's log.
+HEALTH=$(node "$HERE/check_health.js" --json 2>/dev/null || echo '{"healthy":false,"reason":"health check crashed"}')
+echo "uploader-health: $HEALTH"
+echo "$HEALTH" > "$STATE/$TODAY/health.json"
+
 # 1. generate
 python3 "$HERE/gen_daily.py" --n $N --date "$TODAY" --seed-base "$SEED_BASE" > "$STATE/$TODAY/gen.stdout" 2> "$STATE/$TODAY/gen.stderr" || true
 [ -f "$MANIFEST" ] || { echo "no manifest — gen failed"; cat "$STATE/$TODAY/gen.stderr" | tail; exit 1; }

← 6603026 auto-save: 2026-07-08T08:34:49 (1 files) — package-lock.json  ·  back to Pattern Vault  ·  fernwick: surface uploader-DEGRADED in the daily email (both 3f91374 →