Files
stack/packages/cli/src/boot-status.mjs
T
jason.woltjeandClaude Opus 5.5 91b963e256 bus start records why it failed closed; Console banner and bus status show it (vikunja:19/302, row 144)
Jason-approved merge, landed unchanged: git diff 07c12b51b..abda248a8
from work/vikunja-19-302. Business reviewer verdict 4998de97 approve on that
commit; Jason resolved merge decision 29ff6589 via the CLI. No commit on
refactor since 07c12b51b touches these paths.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 17:20:09 -05:00

59 lines
3.0 KiB
JavaScript

// Why the bus isn't up (vikunja:19/302, REQ-EVT-1, REQ-ROLE-3). `mosaic bus start` fails closed on a
// missing or invalid organization file: no broker, no sessions, no verbs. Before it throws it writes
// this status, where the Console and `mosaic bus status` can read it with the bus down:
// <dataRoot>/bus-host/boot-status.json the latest outcome (0600), replaced each start
// <dataRoot>/bus-host/boot-events.jsonl one event per start (0600), appended
// An event is { event: "bus.boot.refused" | "bus.boot.ok", at, business, file, reason }. The
// reason is the validator's own message, which names the key or line; it holds references and
// paths, never a token value. Nothing here retries: a fixed file comes back on the next start.
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const REASON_MAX = 1000;
const dir = (dataRoot) => join(dataRoot, "bus-host");
export const bootStatusFile = (dataRoot) => join(dir(dataRoot), "boot-status.json");
export const bootEventsFile = (dataRoot) => join(dir(dataRoot), "boot-events.jsonl");
// The status file, or null when no start has been recorded. An unreadable file is reported as
// null, not thrown: a status read never takes the Console or the CLI down.
export function readBootStatus(dataRoot) {
const file = bootStatusFile(dataRoot);
if (!existsSync(file)) return null;
try {
const s = JSON.parse(readFileSync(file, "utf8"));
if (!s || typeof s !== "object" || typeof s.at !== "string" || typeof s.ok !== "boolean") return null;
return s;
} catch {
return null;
}
}
function write(dataRoot, status, event) {
mkdirSync(dir(dataRoot), { recursive: true, mode: 0o700 });
const file = bootStatusFile(dataRoot);
const tmp = `${file}.${process.pid}.tmp`;
writeFileSync(tmp, `${JSON.stringify(status, null, 2)}\n`, { mode: 0o600 });
renameSync(tmp, file);
appendFileSync(bootEventsFile(dataRoot), `${JSON.stringify(event)}\n`, { mode: 0o600 });
}
// A start refused: keeps the time of the last good load from the status it replaces.
export function recordBootRefused(dataRoot, { business, file, reason, at = new Date().toISOString() }) {
const prior = readBootStatus(dataRoot);
const lastGood = prior?.ok ? prior.at : prior?.lastGood ?? null;
const text = String(reason).slice(0, REASON_MAX);
write(dataRoot, { ok: false, at, business, file, reason: text, lastGood }, { event: "bus.boot.refused", at, business, file, reason: text });
}
// A start that loaded the file: clears any refusal.
export function recordBootOk(dataRoot, { business, file, at = new Date().toISOString() }) {
write(dataRoot, { ok: true, at, business, file, reason: null, lastGood: at }, { event: "bus.boot.ok", at, business, file });
}
// One line for the CLI.
export function bootLine(s) {
if (!s) return "boot: no start recorded";
if (s.ok) return `boot: organization file loaded at ${s.at}`;
return `boot: REFUSED at ${s.at}: bus down, organization file invalid: ${s.reason} (file ${s.file}; last good load ${s.lastGood ?? "none"})`;
}