[object Object]

← 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

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 →