← back to Commercialrealestate
scripts/crcp-drift-canary.js
108 lines
#!/usr/bin/env node
/**
* crcp-drift-canary.js — READ-ONLY origin-vs-live drift monitor for CRCP.
*
* WHY: crcp.agentabrams.com is a Kamatera-local copy synced by MANUAL rsync.
* It silently drifted 9 days stale (Jul 7->16 2026) because nobody NOTICED —
* the manual deploy wasn't the failure, the INVISIBILITY was. DTD verdict D
* (2026-07-16): keep deploys manual (push-frontend-kamatera.sh) + this canary.
*
* Compares sha256 of each front-end asset (public/*.html,*.js,*.css) on the
* origin (this box) vs live (Kamatera), counts drifted files, and fires a
* WORSENING-ONLY alert (drift count grew vs the stored baseline) to CNCP +
* George email. Steady-state / recovery / first-run are silent. Touches
* nothing on prod — one ssh sha256sum read. Always writes data/latest.json
* so dw-canary-meta-watchdog sees it ran.
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ROOT = path.join(__dirname, '..');
const PUB = path.join(ROOT, 'public');
const HOST = 'root@45.61.58.125';
const REMOTE = '/root/public-projects/commercialrealestate/public';
const STATE = path.join(ROOT, 'data', 'crcp-drift-state.json');
const LATEST = path.join(ROOT, 'data', 'latest.json');
const EXT = /\.(html|js|css)$/;
function sha(buf) { return crypto.createHash('sha256').update(buf).digest('hex'); }
function localHashes() {
const out = {};
for (const f of fs.readdirSync(PUB)) {
if (!EXT.test(f)) continue;
out[f] = sha(fs.readFileSync(path.join(PUB, f)));
}
return out;
}
function remoteHashes() {
// single read-only ssh call; sha256sum every html/js/css in the remote public dir
const cmd = `ssh -o ConnectTimeout=10 -o BatchMode=yes ${HOST} ` +
`"cd ${REMOTE} 2>/dev/null && for f in *.html *.js *.css; do [ -f \\"\\$f\\" ] && sha256sum \\"\\$f\\"; done"`;
const raw = execSync(cmd, { encoding: 'utf8', timeout: 30000 });
const out = {};
for (const line of raw.trim().split('\n')) {
const m = line.match(/^([0-9a-f]{64})\s+(.+)$/);
if (m) out[m[2]] = m[1];
}
return out;
}
function main() {
let live, err = null;
const local = localHashes();
try { live = remoteHashes(); } catch (e) { err = String(e.message).split('\n')[0]; }
const now = new Date().toISOString();
if (err) {
// unreachable = UNKNOWN, not a drift alert (transient); still stamp latest.json
writeLatest({ status: 'UNKNOWN', ts: now, error: err });
console.log(`[crcp-drift-canary] UNKNOWN — ${err}`);
return;
}
const drifted = [];
const names = new Set([...Object.keys(local), ...Object.keys(live)]);
for (const n of names) {
if (local[n] !== live[n]) {
drifted.push({ file: n, state: !live[n] ? 'missing-on-live' : !local[n] ? 'extra-on-live' : 'differs' });
}
}
const count = drifted.length;
const prev = fs.existsSync(STATE) ? JSON.parse(fs.readFileSync(STATE, 'utf8')) : null;
const baseline = prev ? prev.count : null;
const worsening = baseline !== null && count > baseline;
const status = count === 0 ? 'IN_SYNC' : 'DRIFTED';
writeLatest({ status, ts: now, driftCount: count, drifted, worsening });
fs.writeFileSync(STATE, JSON.stringify({ count, ts: now, drifted }, null, 2));
console.log(`[crcp-drift-canary] ${status} — ${count} file(s) drifted` +
(baseline !== null ? ` (was ${baseline})` : ' (first run — baseline set, silent)'));
// WORSENING-ONLY alert; first-run / steady / recovery are silent
if (worsening) {
const msg = `CRCP live is ${count} front-end file(s) behind origin (was ${baseline}). ` +
`Files: ${drifted.map(d => d.file + ':' + d.state).join(', ')}. ` +
`Fix: bash deploy/push-frontend-kamatera.sh`;
console.log('[crcp-drift-canary] WORSENING — alerting: ' + msg);
try {
execSync(`curl -s -m 8 -X POST http://127.0.0.1:3333/api/parking-lot ` +
`-H 'Content-Type: application/json' ` +
`-d ${JSON.stringify(JSON.stringify({ title: 'CRCP deploy drift', note: msg, source: 'crcp-drift-canary' }))}`,
{ stdio: 'ignore' });
} catch (_) { /* CNCP best-effort */ }
}
}
function writeLatest(o) {
try { fs.mkdirSync(path.dirname(LATEST), { recursive: true }); } catch (_) {}
fs.writeFileSync(LATEST, JSON.stringify({ canary: 'crcp-drift', ...o }, null, 2));
}
main();