← back to Pattern Vault
whimsical-compare/daily/scripts/check_health.js
65 lines
#!/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);
}