// The machine's backups: the node-backup seat's three verbs — what is backed up, take one now, // restore beside the live data — and the night that runs without anyone asking (novox/hq ADR 0214, // to-be 43). What is backed up is composed by the mesh from the modules the machine runs; this code // only runs it. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { Backups, missedANight, nextNight, whereFromEnv } from "../client.js"; export function getSeatVerbs(backups: Backups): ToolDefinition[] { return [ { name: "backed-up", description: "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept.", input: { module: { type: "string", description: "one module (optional)" } }, run: async (args) => backups.backedUp(args.module ? String(args.module) : undefined), }, { name: "now", description: "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong. Answers when it has started; `backed-up` says how it went.", input: { module: { type: "string", description: "one module (optional)" } }, run: async (args) => { const only = args.module ? String(args.module) : undefined; // A night of a large store outlasts any call; it is started, and its outcome recorded. backups.backUp(only).catch((err) => console.error(`[restic] a backup asked for now failed: ${(err as Error).message}`)); return { started: only ?? "every module on this machine", follow: "backed-up" }; }, }, { name: "restore", description: "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as .restored-. Swapping it in is a person's act.", input: { module: { type: "string", description: "the module" }, snapshot: { type: "string", description: "the restore point (the newest when omitted)" }, path: { type: "string", description: "one of the module's directories (all of them when omitted)" }, }, run: async (args) => backups.restore(String(args.module ?? ""), args.snapshot ? String(args.snapshot) : undefined, args.path ? String(args.path) : undefined), }, ]; } const backups = new Backups(whereFromEnv()); registerModuleTools("node-backup", () => getSeatVerbs(backups)); // The night. At the hour, every module; and at start, if a night was missed — the machine was off // or this module was not running at the hour — one now rather than a day later. const HOUR = Number(process.env.MESH_BACKUP_HOUR ?? "3"); async function night(): Promise { try { const outcome = await backups.backUp(); const failed = Object.entries(outcome).filter(([, n]) => !n.ok).map(([m]) => m); if (failed.length > 0) console.error(`[restic] the night left ${failed.join(", ")} without a backup`); // Sundays, the repository's own integrity with a sample of the data read back. if (new Date().getDay() === 0) { await backups.check().then( () => console.error("[restic] the repository checks out"), (err) => console.error(`[restic] the repository does NOT check out: ${(err as Error).message}`), ); } } catch (err) { console.error(`[restic] the night did not run: ${(err as Error).message}`); } } function schedule(): void { const at = nextNight(new Date(), HOUR); setTimeout(() => void night().finally(schedule), at.getTime() - Date.now()); } if (missedANight(backups.nights(), new Date())) { // Not at once: a machine just started has its stores still coming up. setTimeout(() => void night(), 10 * 60 * 1000); } schedule();