← back to Pattern Vault
spoonflower daily: add headful session-capture script + move session to persistent path (~/.config, survives reboot) — session now captured for info@designerwallcoverings.com
e978c1f9153447fb2b4eae9b1488101d6a89e635 · 2026-07-07 07:25:08 -0700 · Steve Abrams
Files touched
A whimsical-compare/daily/scripts/capture_sf_session.jsM whimsical-compare/daily/scripts/upload_sf.js
Diff
commit e978c1f9153447fb2b4eae9b1488101d6a89e635
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 7 07:25:08 2026 -0700
spoonflower daily: add headful session-capture script + move session to persistent path (~/.config, survives reboot) — session now captured for info@designerwallcoverings.com
---
.../daily/scripts/capture_sf_session.js | 103 +++++++++++++++++++++
whimsical-compare/daily/scripts/upload_sf.js | 4 +-
2 files changed, 106 insertions(+), 1 deletion(-)
diff --git a/whimsical-compare/daily/scripts/capture_sf_session.js b/whimsical-compare/daily/scripts/capture_sf_session.js
new file mode 100644
index 0000000..8457dec
--- /dev/null
+++ b/whimsical-compare/daily/scripts/capture_sf_session.js
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/**
+ * Capture a logged-in Spoonflower session (storageState) for the UPLOAD/SELLER
+ * account, so upload_sf.js can post without re-authing each run.
+ *
+ * Account = info@designerwallcoverings.com (2026-07-07 switch; printmurals is now
+ * a designer persona). Creds are read from env, falling back to the resize-it .env
+ * — NEVER hardcoded here.
+ *
+ * Writes storageState to SF_STATE (default ~/.config/pattern-vault/sf-state.json),
+ * the SAME persistent path upload_sf.js now reads. Run headful with --headful to
+ * watch / clear a rare challenge.
+ *
+ * node capture_sf_session.js [--headful]
+ */
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+// --- creds: env first, then resize-it/.env (real values live there) ---
+function loadEnvFile(p) {
+ const out = {};
+ try {
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
+ if (m) out[m[1]] = m[2].replace(/^"(.*)"$/, '$1');
+ }
+ } catch {}
+ return out;
+}
+const envFile = loadEnvFile(path.join(os.homedir(), '.claude/skills/resize-it/.env'));
+const EMAIL = process.env.SPOONFLOWER_EMAIL || envFile.SPOONFLOWER_EMAIL;
+const PASSWORD = process.env.SPOONFLOWER_PASSWORD || envFile.SPOONFLOWER_PASSWORD;
+const STATE = process.env.SF_STATE || path.join(os.homedir(), '.config/pattern-vault/sf-state.json');
+const HEADFUL = process.argv.includes('--headful');
+
+if (!EMAIL || !PASSWORD) { console.error('❌ missing SPOONFLOWER_EMAIL/PASSWORD (env or resize-it/.env)'); process.exit(2); }
+console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);
+
+(async () => {
+ fs.mkdirSync(path.dirname(STATE), { recursive: true });
+ const b = await chromium.launch({ headless: !HEADFUL });
+ const ctx = await b.newContext({ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/126 Safari/537.36' });
+ const pg = await ctx.newPage();
+ try {
+ await pg.goto('https://www.spoonflower.com/login', { waitUntil: 'networkidle', timeout: 60000 });
+ // Dismiss the cookie-consent banner — it overlays and intercepts the Sign In button.
+ await pg.waitForTimeout(2000);
+ // Cookie consent may render in an IFRAME; search every frame for an accept/reject button.
+ let cookieDismissed = false;
+ for (const fr of pg.frames()) {
+ for (const name of [/accept all/i, /reject all/i, /accept/i]) {
+ try { const b = fr.getByRole('button', { name }).first();
+ if (await b.isVisible({ timeout: 800 })) { await b.click({ force: true }); cookieDismissed = true; break; } } catch {}
+ }
+ if (cookieDismissed) break;
+ }
+ // Fallback: known consent IDs in main frame
+ if (!cookieDismissed) { try { const b = pg.locator('#onetrust-accept-btn-handler, [id*="accept" i][id*="cookie" i], button[aria-label*="accept" i]').first();
+ if (await b.isVisible({ timeout: 800 })) { await b.click({ force: true }); cookieDismissed = true; } } catch {} }
+ await pg.waitForTimeout(800);
+ console.log(` cookie banner dismissed: ${cookieDismissed}`);
+ // Type char-by-char so the form's onChange validation fires and enables Sign In.
+ const emailEl = pg.locator('input[type="email"], input[name*="email" i], input[placeholder*="email" i]').first();
+ await emailEl.click(); await emailEl.fill(''); await emailEl.pressSequentially(EMAIL, { delay: 30 });
+ const pwEl = pg.locator('input[type="password"]').first();
+ await pwEl.click(); await pwEl.fill(''); await pwEl.pressSequentially(PASSWORD, { delay: 30 });
+ await pwEl.blur().catch(() => {});
+ await pg.waitForTimeout(500);
+ if (HEADFUL) {
+ console.log('\n👉 Browser window is open with email+password pre-filled.');
+ console.log(' Do two things in the window: (1) solve the reCAPTCHA checkbox, (2) click "Sign in".');
+ console.log(' Waiting up to 3 min for login to complete…\n');
+ let ok = false;
+ for (let i = 0; i < 90; i++) { await pg.waitForTimeout(2000); if (!pg.url().includes('/login')) { ok = true; break; } }
+ if (!ok) { console.error('❌ timed out (still on /login) — captcha/login not completed.'); await b.close(); process.exit(1); }
+ } else {
+ const submit = pg.locator('button[type="submit"], button:has-text("Log In"), button:has-text("Sign In")').first();
+ await submit.scrollIntoViewIfNeeded().catch(() => {});
+ await submit.click({ timeout: 15000 });
+ await pg.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
+ await pg.waitForTimeout(4000);
+ if (pg.url().includes('/login')) {
+ await pg.screenshot({ path: '/tmp/sf-capture-fail.png' });
+ console.error(`❌ still on /login after submit — captcha/challenge. Screenshot: /tmp/sf-capture-fail.png`);
+ await b.close(); process.exit(1);
+ }
+ }
+ const url = pg.url();
+ // Confirm which account we're in (best-effort)
+ let who = '';
+ try { who = await pg.evaluate(() => document.querySelector('[data-testid*="account" i], a[href*="/profile"], a[href*="/account"]')?.textContent?.trim() || ''); } catch {}
+ await ctx.storageState({ path: STATE });
+ const cookies = JSON.parse(fs.readFileSync(STATE, 'utf8')).cookies?.length || 0;
+ console.log(`✅ session saved (${cookies} cookies). landed: ${url}${who ? ' | ' + who : ''}`);
+ await b.close();
+ } catch (e) {
+ try { await pg.screenshot({ path: '/tmp/sf-capture-fail.png' }); } catch {}
+ console.error('❌ capture error:', e.message, '(screenshot /tmp/sf-capture-fail.png)');
+ await b.close(); process.exit(1);
+ }
+})();
diff --git a/whimsical-compare/daily/scripts/upload_sf.js b/whimsical-compare/daily/scripts/upload_sf.js
index 174e35a..31508ee 100755
--- a/whimsical-compare/daily/scripts/upload_sf.js
+++ b/whimsical-compare/daily/scripts/upload_sf.js
@@ -24,7 +24,9 @@ const fs = require('fs');
const MANIFEST = process.argv[2];
const ARMED = process.argv.includes('--armed');
-const STATE = '/tmp/sf-state.json';
+// Persistent session path (was /tmp/sf-state.json — /tmp is wiped on reboot, which
+// silently expired the Spoonflower session every restart). Override with SF_STATE.
+const STATE = process.env.SF_STATE || require('path').join(require('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 rnd = (a, b) => Math.floor(a + Math.random() * (b - a));
const sleep = ms => new Promise(r => setTimeout(r, ms));
← cb3baaa spoonflower: switch upload/seller account to info@designerwa
·
back to Pattern Vault
·
chore: v0.2.1 (session close) 3007363 →