← 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);
}