← back to Pattern Vault

whimsical-compare/daily/scripts/report.js

81 lines

#!/usr/bin/env node
/**
 * Email steve-office the daily Fernwick Studio upload report via George (local :9850).
 * Inlines each design thumbnail (base64) + title + Spoonflower link + settlement verdict.
 * Internal operational mail → George gmail_send is allowed (not draft-only).
 *
 * Usage: node report.js <manifest.json> <upload-result.json>
 */
const fs = require('fs');
const http = require('http');
const path = require('path');

const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const up = process.argv[3] && fs.existsSync(process.argv[3]) ? JSON.parse(fs.readFileSync(process.argv[3], 'utf8')) : { uploaded: [], failed: [], sessionExpired: false, armed: false };

// pre-flight uploader health (written by daily_run.sh check_health) — surfaces BOTH silent-
// failure classes (expired session AND a moved upload page) as one loud alert, not just the log.
let health = null;
try { const hp = path.join(path.dirname(process.argv[2]), 'health.json'); if (fs.existsSync(hp)) health = JSON.parse(fs.readFileSync(hp, 'utf8')); } catch (e) {}
const degraded = health && !health.healthy;

const GEORGE = 'http://127.0.0.1:9850';
const TO = 'steve@designerwallcoverings.com';
const FROM_ACCT = 'steve-office';

const byTitle = {};
(up.uploaded || []).forEach(u => byTitle[u.title] = u);

const rows = manifest.designs.map(d => {
  const u = byTitle[d.title] || {};
  const b64 = fs.existsSync(d.file) ? fs.readFileSync(d.file).toString('base64') : '';
  const link = u.url ? `<a href="${u.url}">${u.url}</a>` : (up.armed ? '<i>upload pending/failed</i>' : '<i>dry-run — not uploaded</i>');
  const thumb = b64 ? `<img src="data:image/png;base64,${b64}" width="160" height="160" style="border-radius:8px;object-fit:cover;border:1px solid #ddd">` : '—';
  return `<tr>
    <td style="padding:8px;vertical-align:top">${thumb}</td>
    <td style="padding:8px;vertical-align:top;font:14px -apple-system,sans-serif">
      <b>${d.n}. ${d.title}</b><br>
      <span style="color:#777">${d.style} · ${d.motif}</span><br>
      <span style="color:#777">palette: ${d.palette}</span><br>
      settlement: <b style="color:#2e5b3f">${d.settlement}</b><br>
      ${link}
    </td></tr>`;
}).join('');

const armedNote = up.sessionExpired
  ? '<p style="color:#a33"><b>⚠ Spoonflower session expired — nothing uploaded.</b> Re-auth needed (run the login capture) before the next drop.</p>'
  : up.armed
    ? `<p style="color:#2e5b3f"><b>LIVE:</b> ${(up.uploaded || []).length} uploaded to fernwick_studio (Private).</p>`
    : '<p style="color:#8a6d00"><b>DRY RUN:</b> designs generated + settlement-passed, but NOT uploaded (uploader not armed). Flip the arm switch to go live.</p>';

const healthBanner = degraded
  ? `<p style="background:#a33;color:#fff;padding:10px 12px;border-radius:6px"><b>❌ UPLOADER DEGRADED — fix before it wastes another day:</b> ${health.reason}</p>`
  : '';

const html = `<div style="max-width:640px;font:14px -apple-system,sans-serif;color:#222">
  <h2 style="margin:0 0 4px">🌿 Fernwick Studio — Daily Uploads · ${manifest.date}</h2>
  ${healthBanner}
  ${armedNote}
  <p style="color:#555">${manifest.count} original whimsical designs · all settlement-passed · est. gen cost <b>$${manifest.est_cost_usd}</b> (Replicate SDXL).</p>
  ${up.failed && up.failed.length ? `<p style="color:#a33">${up.failed.length} failed: ${up.failed.map(f => f.title).join(', ')}</p>` : ''}
  <table style="border-collapse:collapse;width:100%">${rows}</table>
  <p style="color:#999;font-size:12px;margin-top:16px">Compare tool: the top-100 whimsical bestsellers vs. our versions runs locally (pattern-vault/whimsical-compare).</p>
</div>`;

const subj = up.sessionExpired
  ? `⚠ Fernwick daily — session expired (${manifest.date})`
  : degraded
    ? `❌ Fernwick daily — UPLOADER DEGRADED (${manifest.date})`
    : `Fernwick Studio daily uploads — ${manifest.count} designs (${manifest.date})`;

const payload = JSON.stringify({ account: FROM_ACCT, to: TO, subject: subj, body: html, source: 'fernwick-daily' });

const req = http.request(GEORGE + '/api/send', {
  method: 'POST', headers: { 'content-type': 'application/json', 'authorization': 'Basic ' + Buffer.from('admin:').toString('base64') }
}, resp => {
  let d = ''; resp.on('data', c => d += c);
  resp.on('end', () => { console.log('george', resp.statusCode, d.slice(0, 200)); });
});
req.on('error', e => { console.error('george send error', e.message); process.exit(1); });
req.write(payload); req.end();