← back to Fix Live Board

watcher.mjs

25 lines

#!/usr/bin/env node
// Always-on auto-spinner. For every registered job with "watch" != false: run its probe, and if it
// has items still to follow AND no board is live, spin one up. Boards that reach 100% are left
// running so the finished state stays visible. Writes a heartbeat for later fleet-health use.
// Installed as launchd com.steve.fix-live-board-watcher (StartInterval; reversible: bootout + rm).
import fs from 'fs';
import path from 'path';
import { spawnBoard, boardStatus, brokenCount, ROOT } from './boardctl.mjs';

const jobs = fs.readdirSync(path.join(ROOT, 'jobs')).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''));
const summary = { ts: new Date().toISOString(), jobs: [] };
let spun = 0;
for (const id of jobs) {
  let job; try { job = JSON.parse(fs.readFileSync(path.join(ROOT, 'jobs', id + '.json'), 'utf8')); } catch (e) { continue; }
  if (job.watch === false) { summary.jobs.push({ id, watch: false }); continue; }
  const bc = brokenCount(id);
  let s = boardStatus(id);
  if (bc.ok && bc.broken > 0 && !s.up) { const r = spawnBoard(id); s = { pid: r.pid, port: r.port, up: r.up }; if (r.started) spun++; }
  summary.jobs.push({ id, total: bc.total, broken: bc.broken, probeOk: bc.ok, err: bc.err || null, up: s.up, port: s.port || null, url: s.port ? 'http://127.0.0.1:' + s.port + '/' : null });
}
summary.spunThisRun = spun;
summary.status = summary.jobs.some(j => j.probeOk === false) ? 'WARN' : 'PASS';
fs.writeFileSync(path.join(ROOT, '.runtime', 'watcher-status.json'), JSON.stringify(summary, null, 2));
console.log('[fix-live-board-watcher] ' + summary.ts + ' spun=' + spun + ' ' + summary.jobs.filter(j => j.watch !== false).map(j => j.id + ':' + (j.up ? j.port : 'off') + '(' + (j.broken ?? '?') + ' to follow)').join(' '));