2026-09-24 14:16:50 -04:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
'use strict';
|
|
|
|
|
// alert.js — send one alert email, reusing the server's own SMTP settings.
|
|
|
|
|
//
|
|
|
|
|
// Exists because backup-to-drive.sh used to report failure only into
|
|
|
|
|
// ~/Library/Logs/checklist-drive-backup.log, where nobody was ever going to look. That is
|
|
|
|
|
// how the Drive copies went ten weeks stale in July 2026, and then — despite the script
|
|
|
|
|
// written to prevent exactly that — another nine days in September 2026.
|
|
|
|
|
//
|
|
|
|
|
// Usage: node scripts/alert.js "<subject>" "<body>"
|
|
|
|
|
// Exit 0 if the mail went out, 1 otherwise. Callers should NOT fail on a failed alert:
|
|
|
|
|
// the thing being reported matters more than the reporting of it.
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
2026-09-24 16:11:22 -04:00
|
|
|
// Overridable because the deploy job runs inside a container that has the repo checked out
|
|
|
|
|
// but no server/.env and no node_modules — those live on the NAS at the mounted deploy
|
|
|
|
|
// directory instead. Without this the deploy could not report its own failures.
|
|
|
|
|
const ENV_FILE = process.env.CHECKLIST_ENV_FILE || path.join(ROOT, 'server', '.env');
|
|
|
|
|
const NODE_MODULES = process.env.CHECKLIST_NODE_MODULES || path.join(ROOT, 'server', 'node_modules');
|
2026-09-24 14:16:50 -04:00
|
|
|
|
|
|
|
|
// Minimal .env reader — the server uses dotenv, but this script has to run from a plain
|
|
|
|
|
// LaunchAgent with no cwd guarantees and no node_modules of its own.
|
|
|
|
|
function readEnv(file) {
|
|
|
|
|
const out = {};
|
|
|
|
|
let text;
|
|
|
|
|
try { text = fs.readFileSync(file, 'utf8'); }
|
|
|
|
|
catch { return out; }
|
|
|
|
|
for (const raw of text.split('\n')) {
|
|
|
|
|
const line = raw.trim();
|
|
|
|
|
if (!line || line.startsWith('#')) continue;
|
|
|
|
|
const eq = line.indexOf('=');
|
|
|
|
|
if (eq < 0) continue;
|
|
|
|
|
let v = line.slice(eq + 1).trim();
|
|
|
|
|
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
|
|
|
out[line.slice(0, eq).trim()] = v;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
const subject = process.argv[2];
|
|
|
|
|
const body = process.argv[3] || '';
|
|
|
|
|
if (!subject) { console.error('alert.js: no subject given'); process.exit(1); }
|
|
|
|
|
|
|
|
|
|
const env = readEnv(ENV_FILE);
|
2026-09-24 14:18:47 -04:00
|
|
|
// ALERT_EMAIL is for things that are broken; REPORT_EMAIL is for the Sunday digest. They
|
|
|
|
|
// are separate because an alert that only reaches a shared inbox is easy to scroll past,
|
|
|
|
|
// and because the digest should not become a channel people learn to ignore. Comma
|
|
|
|
|
// separated, and it falls back to REPORT_EMAIL so a machine whose .env predates this
|
|
|
|
|
// still alerts somewhere rather than silently not at all.
|
|
|
|
|
const to = (env.ALERT_EMAIL || env.REPORT_EMAIL || '')
|
|
|
|
|
.split(',').map(a => a.trim()).filter(Boolean).join(', ');
|
|
|
|
|
if (!to) { console.error(`alert.js: no ALERT_EMAIL or REPORT_EMAIL in ${ENV_FILE}`); process.exit(1); }
|
2026-09-24 14:16:50 -04:00
|
|
|
if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) {
|
|
|
|
|
console.error(`alert.js: SMTP is not configured in ${ENV_FILE}`); process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nodemailer lives in server/node_modules; this script has none of its own.
|
|
|
|
|
let nodemailer;
|
2026-09-24 16:11:22 -04:00
|
|
|
try { nodemailer = require(path.join(NODE_MODULES, 'nodemailer')); }
|
|
|
|
|
catch { console.error(`alert.js: nodemailer not found under ${NODE_MODULES} — run \`npm install\` in server/`); process.exit(1); }
|
2026-09-24 14:16:50 -04:00
|
|
|
|
|
|
|
|
const transport = nodemailer.createTransport({
|
|
|
|
|
host: env.SMTP_HOST,
|
|
|
|
|
port: Number(env.SMTP_PORT || 587),
|
|
|
|
|
secure: String(env.SMTP_SECURE || 'false') === 'true',
|
|
|
|
|
auth: { user: env.SMTP_USER, pass: env.SMTP_PASS },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await transport.sendMail({
|
|
|
|
|
from: env.SMTP_FROM || env.SMTP_USER,
|
|
|
|
|
to,
|
|
|
|
|
subject,
|
|
|
|
|
text: `${body}\n\n-- \nSent by scripts/alert.js on ${require('os').hostname()} at ${new Date().toISOString()}\n`,
|
|
|
|
|
});
|
|
|
|
|
console.log(`alert.js: sent "${subject}" to ${to}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch(err => { console.error('alert.js:', err && err.message ? err.message : err); process.exit(1); });
|