Files
claudetools/projects/dataforth-dos/database/notify.js
Mike Swanson e75ddfbc53 sync: auto-sync from DESKTOP-0O8A1RL at 2026-05-12 07:04:17
Author: Mike Swanson
Machine: DESKTOP-0O8A1RL
Timestamp: 2026-05-12 07:04:17
2026-05-12 07:04:18 -07:00

206 lines
7.0 KiB
JavaScript

'use strict';
const os = require('os');
const fs = require('fs');
const https = require('https');
const qs = require('querystring');
const CREDS_PATH = 'C:\\ProgramData\\dataforth-uploader\\credentials.json';
const TO = ['mike@azcomputerguru.com']; // add jlehman@dataforth.com once confirmed working
const FROM = 'sysadmin@dataforth.com';
const HOST = os.hostname();
const PREFIX = '[NOTIFY]';
const SEP_EQ = '='.repeat(60);
const SEP_DAS = '-'.repeat(60);
function loadGraphCreds() {
try {
const raw = fs.readFileSync(CREDS_PATH, 'utf8');
const c = JSON.parse(raw);
if (!c.GRAPH_TENANT_ID || !c.GRAPH_CLIENT_ID || !c.GRAPH_CLIENT_SECRET) {
process.stderr.write(`${PREFIX} GRAPH_TENANT_ID/CLIENT_ID/CLIENT_SECRET not in credentials.json — skipping email\n`);
return null;
}
return { tenantId: c.GRAPH_TENANT_ID, clientId: c.GRAPH_CLIENT_ID, clientSecret: c.GRAPH_CLIENT_SECRET };
} catch (e) {
process.stderr.write(`${PREFIX} Could not load credentials.json: ${e.message} — skipping email\n`);
return null;
}
}
function httpsPost(hostname, path, headers, body) {
return new Promise((resolve, reject) => {
const data = Buffer.from(body);
const req = https.request({ hostname, path, method: 'POST', headers: { ...headers, 'Content-Length': data.length } }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function getToken(creds) {
const body = qs.stringify({
grant_type: 'client_credentials',
client_id: creds.clientId,
client_secret: creds.clientSecret,
scope: 'https://graph.microsoft.com/.default',
});
const res = await httpsPost(
'login.microsoftonline.com',
`/${creds.tenantId}/oauth2/v2.0/token`,
{ 'Content-Type': 'application/x-www-form-urlencoded' },
body
);
const parsed = JSON.parse(res.body);
if (!parsed.access_token) throw new Error(`Token error: ${parsed.error}${parsed.error_description}`);
return parsed.access_token;
}
async function sendMail(subject, text) {
const creds = loadGraphCreds();
if (!creds) return;
try {
const token = await getToken(creds);
const payload = JSON.stringify({
message: {
subject,
body: { contentType: 'Text', content: text },
toRecipients: TO.map((a) => ({ emailAddress: { address: a } })),
from: { emailAddress: { address: FROM } },
},
});
const res = await httpsPost(
'graph.microsoft.com',
`/v1.0/users/${FROM}/sendMail`,
{ 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
payload
);
if (res.status !== 202) {
process.stderr.write(`${PREFIX} sendMail HTTP ${res.status}: ${res.body.slice(0, 200)}\n`);
}
} catch (e) {
process.stderr.write(`${PREFIX} sendMail failed: ${e.message}\n`);
}
}
function buildAlertText(subject, context) {
const lines = [];
const ts = new Date().toISOString();
lines.push(`Host: ${HOST}`);
lines.push(`Time: ${ts}`);
lines.push(`Subject: [TestDataDB] ALERT: ${subject}`);
lines.push(SEP_DAS);
if (context.stage !== undefined) lines.push(`Stage: ${context.stage}`);
if (context.error !== undefined) {
lines.push('');
lines.push(`Error: ${context.error}`);
}
if (Array.isArray(context.details) && context.details.length > 0) {
lines.push('');
for (const line of context.details) lines.push(` ${line}`);
}
if (context.stats !== undefined) {
lines.push('');
lines.push(`Stats: ${JSON.stringify(context.stats, null, 2)}`);
}
return lines.join('\n');
}
/**
* Send an alert email for pipeline errors.
* Never throws — callers do not guard this.
*
* @param {string} subject - Short description; prefixed with [TestDataDB] ALERT:
* @param {object} [context={}]
* @param {string} [context.stage] - Pipeline stage where the alert fired
* @param {string} [context.error] - Error message
* @param {string[]} [context.details] - Additional detail lines
* @param {object} [context.stats] - Stats object
*/
function alert(subject, context) {
context = context || {};
try {
const preview = [
`${PREFIX} ${SEP_EQ}`,
`${PREFIX} ALERT: [TestDataDB] ${subject}`,
`${PREFIX} Host: ${HOST} | Time: ${new Date().toISOString()}`,
];
if (context.stage) preview.push(`${PREFIX} Stage: ${context.stage}`);
if (context.error) preview.push(`${PREFIX} Error: ${context.error}`);
if (context.details) for (const d of context.details) preview.push(`${PREFIX} ${d}`);
if (context.stats) preview.push(`${PREFIX} Stats: ${JSON.stringify(context.stats)}`);
preview.push(`${PREFIX} ${SEP_EQ}`);
for (const line of preview) process.stderr.write(line + '\n');
} catch (_) {}
const text = buildAlertText(subject, context);
sendMail(`[TestDataDB] ALERT: ${subject}`, text).catch(() => {});
}
/**
* Send a daily pipeline summary email.
*
* @param {object} stats
* @param {number} stats.received
* @param {number} stats.created
* @param {number} stats.updated
* @param {number} stats.unchanged
* @param {number} stats.errors
* @returns {Promise<void>}
*/
async function summary(stats) {
const date = new Date().toISOString().slice(0, 10);
const status = (stats.errors > 0) ? 'FAIL' : 'OK';
const subject = `[TestDataDB] Daily pipeline ${status}${date}`;
const lines = [
`Host: ${HOST}`,
`Time: ${new Date().toISOString()}`,
`Date: ${date}`,
`Status: ${status}`,
SEP_DAS,
`Received: ${stats.received}`,
`Created: ${stats.created}`,
`Updated: ${stats.updated}`,
`Unchanged: ${stats.unchanged}`,
`Errors: ${stats.errors}`,
];
process.stderr.write(`${PREFIX} Sending daily summary (${status}) for ${date}\n`);
await sendMail(subject, lines.join('\n'));
}
if (require.main === module) {
const cmd = process.argv[2];
if (cmd === 'summary') {
let stats;
try {
stats = JSON.parse(process.argv[3] || '{}');
} catch (e) {
process.stderr.write(`${PREFIX} Could not parse stats JSON: ${e.message}\n`);
process.exit(1);
}
summary(stats).then(() => process.exit(0)).catch((e) => {
process.stderr.write(`${PREFIX} summary failed: ${e.message}\n`);
process.exit(1);
});
} else {
process.stderr.write(`${PREFIX} Unknown command: ${cmd}\n`);
process.exit(1);
}
}
module.exports = { alert, summary };