← back to Cronjobs Viewer
server.js
317 lines
#!/usr/bin/env node
/**
* cronjobs-viewer — a standalone, live launchd cron-calendar for macstudio3.
*
* Introspects EVERY com.steve.* LaunchAgent live (launchctl list + the plist
* files in ~/Library/LaunchAgents) and serves a single full-page viewer that
* shows, per job: label + inferred purpose, schedule, last run, next run, last
* exit code, state (active/running/alert/error), program, and model.
*
* The launchd-introspection core (readCronPlistMeta / collectCrons / sh) is
* lifted verbatim from the AbramsEgo command center (:9773) /api/crons handler
* so there is ONE parser, not two divergent ones.
*
* Local surface behind Basic Auth (admin / DW2024!). Public exposure is via a
* Cloudflare tunnel + Cloudflare Access Zero-Trust gate — that step is GATED
* (see the go-live runbook in ~/.claude/yolo-queue/pending-approval/); this
* server itself only binds localhost-friendly and never opens a public port.
*/
'use strict';
const express = require('express');
const basicAuth = require('basic-auth');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFile } = require('child_process');
const HOME = os.homedir();
const UID = process.getuid();
const PORT = process.env.PORT || 9796; // free port picked at build time
const AUTH_USER = process.env.CRONJOBS_USER || 'admin';
const AUTH_PASS = process.env.CRONJOBS_PASS || 'DW2024!';
const SNAPSHOT_PATH = path.join(__dirname, 'data', 'liveness-snapshot.json');
const RUNS_REFRESH_MS = 5 * 60 * 1000;
const RUNS_CONCURRENCY = 8;
// These jobs intentionally encode a detected condition as a non-zero exit.
// Explicit membership avoids hiding genuine crashes behind a name heuristic.
const ALERT_EXIT_LABELS = new Set([
'com.steve.dw-backup-canary',
'com.steve.gmc-feed-guard',
'com.steve.pm2-fracture-canary',
'com.steve.chargeandexplore-release-canary',
'com.steve.enrichment-health',
]);
// ---- launchd introspection (lifted from AbramsEgo server.js) ---------------
function sh(cmd, args, timeoutMs = 8000) {
return new Promise((resolve) => {
execFile(cmd, args, { timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 * 16 }, (err, stdout) => {
resolve({ ok: !err, out: (stdout || '').toString(), err: err && err.message });
});
});
}
// next occurrence of HH:MM local time from now (today, or tomorrow if passed)
function nextDailyRun(hour, minute) {
const now = new Date();
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);
return next.toISOString();
}
function readCronPlistMeta(filePath) {
const meta = { schedule: null, program: null, created: null, modified: null, lastRun: null, nextRun: null, model: null };
try {
const st = fs.statSync(filePath);
meta.created = new Date(st.birthtimeMs || st.ctimeMs).toISOString();
meta.modified = new Date(st.mtimeMs).toISOString();
} catch (e) {}
let xml = '';
try { xml = fs.readFileSync(filePath, 'utf8'); } catch (e) { return meta; }
const iv = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/i);
if (iv) {
const s = parseInt(iv[1], 10);
meta.schedule = s % 3600 === 0 ? `every ${s / 3600}h` : s % 60 === 0 ? `every ${s / 60}m` : `every ${s}s`;
} else if (/<key>StartCalendarInterval<\/key>/i.test(xml)) {
const h = xml.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/i);
const m = xml.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/i);
meta.schedule = h ? `daily ${String(h[1]).padStart(2, '0')}:${m ? String(m[1]).padStart(2, '0') : '00'}` : 'calendar';
if (h) meta.nextRun = nextDailyRun(parseInt(h[1], 10), m ? parseInt(m[1], 10) : 0);
} else if (/<key>RunAtLoad<\/key>\s*<true/i.test(xml)) {
meta.schedule = 'at load';
}
const prog = xml.match(/<key>Program<\/key>\s*<string>([^<]+)<\/string>/i)
|| xml.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/i);
if (prog) meta.program = prog[1].split('/').pop();
let lastMs = 0;
for (const re of [/<key>StandardOutPath<\/key>\s*<string>([^<]+)<\/string>/i, /<key>StandardErrorPath<\/key>\s*<string>([^<]+)<\/string>/i]) {
const p = xml.match(re);
if (!p) continue;
try { const st = fs.statSync(p[1]); if (st.mtimeMs > lastMs) lastMs = st.mtimeMs; } catch (e) {}
}
if (lastMs) meta.lastRun = new Date(lastMs).toISOString();
const mArg = xml.match(/--model\s*<\/string>\s*<string>([^<]+)</i);
const mTok = xml.match(/\bclaude-(?:opus|sonnet|haiku|fable)[\w.-]*/i);
if (mArg) meta.model = mArg[1].trim();
else if (mTok) meta.model = mTok[0];
if (!meta.model) {
try {
const argsBlock = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i);
const args = argsBlock ? [...argsBlock[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]) : [];
let script = args.find((a) => a.startsWith('/') && /\.(sh|js|cjs|mjs|py)$/.test(a));
if (!script) {
const wd = (xml.match(/<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/i) || [])[1];
const rel = args.find((a) => !a.startsWith('-') && !a.startsWith('/') && /\.(sh|js|cjs|mjs|py)$/.test(a));
if (wd && rel) script = path.join(wd, rel);
}
if (!script && args.length) {
const last = args[args.length - 1];
if (last.startsWith('/') && fs.existsSync(last) && fs.statSync(last).isFile()) script = last;
}
if (script && fs.existsSync(script)) {
const st = fs.statSync(script);
if (st.isFile() && st.size < 512 * 1024) {
const src = fs.readFileSync(script, 'utf8');
const sArg = src.match(/--model[= ]["']?([\w.-]+)/);
const sTok = src.match(/\bclaude-(?:opus|sonnet|haiku|fable)[\w.-]*/i);
if (sArg) meta.model = sArg[1];
else if (sTok) meta.model = sTok[0];
}
}
} catch (e) {}
}
// inferred purpose: first non-boilerplate XML comment in the plist, else the
// program name. Cheap heuristic — good enough for a "what does it do" column.
let purpose = null;
const cmt = xml.match(/<!--\s*([\s\S]*?)\s*-->/);
if (cmt) purpose = cmt[1].replace(/\s+/g, ' ').trim().slice(0, 220);
meta.purpose = purpose;
return meta;
}
async function collectCrons() {
const laDir = path.join(HOME, 'Library', 'LaunchAgents');
let files = [];
try { files = fs.readdirSync(laDir).filter((f) => /^com\.steve\..*\.plist$/.test(f)); } catch (e) {}
const ll = await sh('launchctl', ['list'], 5000);
const loaded = {};
if (ll.ok) {
ll.out.trim().split('\n').forEach((line) => {
const cols = line.split(/\s+/);
const label = cols[cols.length - 1];
if (label && label.startsWith('com.steve.')) loaded[label] = { pid: cols[0], exit: cols[1] };
});
}
const jobs = files.map((f) => {
const label = f.replace(/\.plist$/, '');
const l = loaded[label];
const pid = l && l.pid !== '-' ? l.pid : null;
const lastExit = l ? l.exit : null;
const meta = readCronPlistMeta(path.join(laDir, f));
// Some canaries deliberately use a non-zero exit to mean "condition found".
// Keep that operational alert distinct from a job that crashed. This list is
// explicit: broad canary/guard name matching would hide genuine script errors.
const nonzeroExit = lastExit && lastExit !== '0' && lastExit !== '-';
// State from launchd TRUTH, not log mtime: running if a pid is present,
// alert for a known signal-style nonzero exit, error for other nonzero exits.
const state = pid ? 'running' : nonzeroExit
? (ALERT_EXIT_LABELS.has(label) ? 'alert' : 'error')
: 'active';
const live = livenessFor(RUNS_SNAPSHOT[label]);
// Purpose: prefer the plist's own comment; else de-slug the label into a
// readable phrase (com.steve.dw-shopify-storage-canary -> "dw shopify
// storage canary") so EVERY card answers "what does it do", not just the
// ~17% whose author left a comment.
const purpose = meta.purpose
|| label.replace(/^com\.steve\./, '').replace(/[-_.]/g, ' ').trim();
return {
label, loaded: !!l, pid, lastExit, state,
schedule: meta.schedule, program: meta.program, purpose,
created: meta.created, modified: meta.modified,
// lastLogWrite (was `lastRun`): mtime of the job's stdout/stderr log — a
// FROZEN value for silently-running jobs, so it is NOT a real-run signal.
// Kept because it's still informative (last time the job wrote output).
lastLogWrite: meta.lastRun, nextRun: meta.nextRun, model: meta.model,
// true liveness from launchd run accounting (see livenessFor)
runs: live.runs, runsDelta: live.runsDelta,
lastExitCode: live.lastExitCode, liveness: live.liveness,
};
});
jobs.sort((a, b) => a.label.localeCompare(b.label));
return { count: jobs.length, loadedCount: jobs.filter((j) => j.loaded).length, jobs, generatedAt: new Date().toISOString() };
}
// ---- true liveness from launchd run accounting -----------------------------
// The log-mtime lastRun above is a LIE for silently-running jobs (they write
// nothing to stdout/stderr, so their log mtime freezes while they fire fine).
// The authoritative signal is `runs = N` from `launchctl print`, which counts
// every dispatch and does NOT reset on reload. We snapshot it on a timed
// interval (never per-request — printing ~281 jobs is too heavy for the API
// hit), persist it, and compute runsDelta across snapshots.
// parse the single top-level `runs =` / `last exit code =` / `state =` out of
// one `launchctl print` block (the indented `state = active` lines are service
// subtrees, so we take the FIRST match of each at the job's own indent level).
function parsePrint(out) {
const runs = out.match(/^\s*runs = (\d+)/m);
const exit = out.match(/^\s*last exit code = (\S.*?)\s*$/m);
const st = out.match(/^\tstate = (\S.*?)\s*$/m);
return {
runs: runs ? parseInt(runs[1], 10) : null,
exit: exit ? exit[1] : null,
running: st ? /running/.test(st[1]) && !/not running/.test(st[1]) : null,
};
}
let RUNS_SNAPSHOT = {}; // label -> { runs, exit, ts, runsDelta }
function loadSnapshot() {
try { RUNS_SNAPSHOT = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf8')); } catch (e) { RUNS_SNAPSHOT = {}; }
}
function saveSnapshot() {
try {
fs.mkdirSync(path.dirname(SNAPSHOT_PATH), { recursive: true });
fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(RUNS_SNAPSHOT, null, 2));
} catch (e) {}
}
let refreshing = false;
async function refreshRunsSnapshot() {
if (refreshing) return;
refreshing = true;
try {
const laDir = path.join(HOME, 'Library', 'LaunchAgents');
let labels = [];
try {
labels = fs.readdirSync(laDir)
.filter((f) => /^com\.steve\..*\.plist$/.test(f))
.map((f) => f.replace(/\.plist$/, ''));
} catch (e) {}
const prev = RUNS_SNAPSHOT;
const next = {};
const now = new Date().toISOString();
let i = 0;
async function worker() {
while (i < labels.length) {
const label = labels[i++];
const r = await sh('launchctl', ['print', `gui/${UID}/${label}`], 6000);
if (!r.ok) continue; // not loaded / print failed -> liveness "unknown"
const p = parsePrint(r.out);
if (p.runs === null) continue;
const was = prev[label];
const delta = was && typeof was.runs === 'number' ? p.runs - was.runs : (p.runs > 0 ? p.runs : 0);
next[label] = { runs: p.runs, exit: p.exit, running: p.running, runsDelta: delta, ts: now };
}
}
await Promise.all(Array.from({ length: RUNS_CONCURRENCY }, worker));
RUNS_SNAPSHOT = next;
saveSnapshot();
} finally {
refreshing = false;
}
}
// classify true liveness from the cached snapshot. firing = the runs counter
// advanced since the last snapshot (or first-seen with runs>0) → the job IS
// running regardless of log mtime. stale = counter did NOT advance (or the job
// has literally never run) → genuinely-not-firing. unknown = no snapshot yet /
// launchctl print failed (job not loaded).
function livenessFor(snap) {
if (!snap || typeof snap.runs !== 'number') return { runs: null, runsDelta: null, lastExitCode: null, liveness: 'unknown' };
const firstSeen = snap.runsDelta === snap.runs; // no prior snapshot for this label
const advancing = snap.runsDelta > 0 || (firstSeen && snap.runs > 0);
return {
runs: snap.runs,
runsDelta: snap.runsDelta,
lastExitCode: snap.exit,
liveness: advancing ? 'firing' : 'stale',
};
}
// ---- express app -----------------------------------------------------------
const app = express();
app.use((req, res, next) => {
const cred = basicAuth(req);
if (!cred || cred.name !== AUTH_USER || cred.pass !== AUTH_PASS) {
res.set('WWW-Authenticate', 'Basic realm="cronjobs-viewer"');
return res.status(401).send('Auth required');
}
next();
});
app.get('/api/crons', async (req, res) => {
try { res.json(await collectCrons()); }
catch (e) { res.status(500).json({ error: e.message }); }
});
// Lazy per-job liveness: prints ONE job live (bypassing the 5-min cache) so a
// card can refresh its own runs counter on demand without a full sweep.
app.get('/api/liveness/:label', async (req, res) => {
const label = req.params.label;
if (!/^com\.steve\.[\w.-]+$/.test(label)) return res.status(400).json({ error: 'bad label' });
const r = await sh('launchctl', ['print', `gui/${UID}/${label}`], 6000);
if (!r.ok) return res.json({ label, liveness: 'unknown' });
const p = parsePrint(r.out);
if (p.runs === null) return res.json({ label, liveness: 'unknown' });
const was = RUNS_SNAPSHOT[label];
const delta = was && typeof was.runs === 'number' ? p.runs - was.runs : (p.runs > 0 ? p.runs : 0);
res.json({ label, runs: p.runs, runsDelta: delta, lastExitCode: p.exit, running: p.running });
});
app.get('/healthz', (req, res) => res.json({ ok: true }));
// Favicon: browsers auto-request /favicon.ico with no HTML reference; without this
// route express.static 404s it → a (cached, intermittent) console error. Serve a tiny
// inline SVG clock so any client — including a direct bookmark hit — gets a 200.
app.get('/favicon.ico', (_req, res) => {
res.type('image/svg+xml').set('Cache-Control', 'public, max-age=86400').send(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='13' font-size='13'>🕓</text></svg>"
);
});
app.use(express.static(path.join(__dirname, 'public')));
loadSnapshot();
refreshRunsSnapshot(); // prime on boot (async; API serves cached/prior values meanwhile)
setInterval(refreshRunsSnapshot, RUNS_REFRESH_MS).unref();
app.listen(PORT, () => console.log(`cronjobs-viewer on http://127.0.0.1:${PORT} (Basic ${AUTH_USER}/****)`));