← back to Dw Monitoring
scripts/broken-image-check.js
156 lines
#!/usr/bin/env node
/**
* dw-monitoring · daily broken-image regression guard
*
* Counts shopify_products whose image_url maps to an image_hashes row with
* status != 'success'. If today's count grows >=5% above the stored baseline,
* writes an alert file (and attempts SMTP if creds are present).
*
* Read-only against dw_unified. Never modifies shopify_products / image_hashes.
*/
'use strict';
const fs = require('fs');
const path = require('path');
// Load secrets-manager .env (canonical) without overriding existing process env.
require('dotenv').config({
path: path.join(process.env.HOME, 'Projects/secrets-manager/.env'),
});
const { Client } = require('pg');
const ROOT = path.resolve(__dirname, '..');
const DATA_DIR = path.join(ROOT, 'data');
const ALERTS_DIR = path.join(ROOT, 'alerts');
const BASELINE_FILE = path.join(DATA_DIR, 'broken-image-baseline.json');
const HISTORY_FILE = path.join(DATA_DIR, 'broken-image-history.jsonl');
const GROWTH_THRESHOLD = 1.05; // 5% above baseline triggers alert
const QUERY = `
SELECT COUNT(*)::int AS broken
FROM shopify_products sp
JOIN image_hashes ih ON sp.image_url = ih.url
WHERE ih.status != 'success'
`;
function ensureDirs() {
for (const d of [DATA_DIR, ALERTS_DIR]) {
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
}
}
function readBaseline() {
if (!fs.existsSync(BASELINE_FILE)) {
const init = { count: 4989, asOf: '2026-05-19' };
fs.writeFileSync(BASELINE_FILE, JSON.stringify(init, null, 2) + '\n');
return init;
}
return JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
}
function appendHistory(entry) {
fs.appendFileSync(HISTORY_FILE, JSON.stringify(entry) + '\n');
}
function writeAlert(payload) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const file = path.join(ALERTS_DIR, `${ts}.txt`);
const body = [
'DW-MONITORING · BROKEN-IMAGE REGRESSION ALERT',
'='.repeat(60),
`Detected at: ${payload.ts}`,
`Current count: ${payload.count}`,
`Baseline: ${payload.baseline.count} (asOf ${payload.baseline.asOf})`,
`Growth: ${(payload.ratio * 100 - 100).toFixed(2)}% above baseline`,
`Threshold: +${((GROWTH_THRESHOLD - 1) * 100).toFixed(0)}%`,
'',
'Read-only monitoring. Investigate via:',
` psql -d dw_unified -c "SELECT COUNT(*) FROM shopify_products sp`,
` JOIN image_hashes ih ON sp.image_url = ih.url`,
` WHERE ih.status != 'success';"`,
'',
].join('\n');
fs.writeFileSync(file, body);
return { file, body };
}
async function trySmtpSend(subject, body) {
// Look for optional Gmail SMTP creds. If absent, return false (file-only mode).
const user = process.env.GMAIL_USER || process.env.GEORGE_GMAIL_USER;
const pass = process.env.GMAIL_APP_PASSWORD || process.env.GEORGE_PASSWORD;
if (!user || !pass) return { sent: false, reason: 'no SMTP creds' };
let nodemailer;
try {
nodemailer = require('nodemailer');
} catch (e) {
return { sent: false, reason: 'nodemailer not installed' };
}
const to = process.env.MONITORING_ALERT_TO || 'info@designerwallcoverings.com';
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: { user, pass },
});
try {
await transporter.sendMail({ from: user, to, subject, text: body });
return { sent: true };
} catch (err) {
return { sent: false, reason: err.message };
}
}
async function main() {
ensureDirs();
const baseline = readBaseline();
// Explicit conn config so the pool ignores stray env from secrets-manager .env.
// Local pg uses peer auth as the OS user — no password needed.
const client = new Client({
host: process.env.DW_PGHOST || '/tmp',
user: process.env.DW_PGUSER || process.env.USER,
database: 'dw_unified',
password: undefined,
});
await client.connect();
let count;
try {
const res = await client.query(QUERY);
count = res.rows[0].broken;
} finally {
await client.end();
}
const ts = new Date().toISOString();
const ratio = count / baseline.count;
const entry = { ts, count, baseline: baseline.count, ratio: +ratio.toFixed(4) };
appendHistory(entry);
const breach = count >= baseline.count * GROWTH_THRESHOLD;
if (breach) {
const alert = writeAlert({ ts, count, baseline, ratio });
const smtp = await trySmtpSend(
`[dw-monitoring] broken-image count up ${(ratio * 100 - 100).toFixed(2)}% (now ${count})`,
alert.body,
);
console.log(
`[ALERT] count=${count} baseline=${baseline.count} ratio=${ratio.toFixed(4)} ` +
`alert=${alert.file} smtp=${smtp.sent ? 'sent' : 'skipped: ' + smtp.reason}`,
);
} else {
console.log(
`[ok] count=${count} baseline=${baseline.count} ratio=${ratio.toFixed(4)} ` +
`(threshold ${GROWTH_THRESHOLD.toFixed(2)})`,
);
}
}
main().catch((err) => {
console.error('[ERROR]', err.message);
process.exit(1);
});