// 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: // /bus-host/boot-status.json the latest outcome (0600), replaced each start // /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. `prior` is the // status from before this start, for a refusal that comes after the start recorded its load (org-defscan: // the broker's held-secret check on the definitions). export function recordBootRefused(dataRoot, { business, file, reason, at = new Date().toISOString(), 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"})`; }