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