import { AbbruchFehler, fehlerBeschreiben, type FehlerBeschreibung } from "./fehler"; /** * Der Phasen-Runner. Er kennt weder Proxmox noch die Datenbank: Phasen sind Funktionen, Ereignisse * gehen an eine Senke. Der worker hängt eine Senke an, die in die Datenbank schreibt und per * NOTIFY meldet; Tests hängen eine an, die mitschreibt. * * Die Texte (Titel, Detail, Ursache, Hinweis) entstehen nur hier und in den Phasen – Web und CLI * zeigen sie unverändert an. */ export type PhasenStatusWert = "OFFEN" | "LAEUFT" | "ERLEDIGT" | "FEHLER" | "UEBERSPRUNGEN"; export type LogStufe = "debug" | "info" | "warn" | "error"; export interface EreignisSenke { phasenAnlegen(phasen: Array<{ schluessel: string; titel: string }>): Promise; phaseGestartet(schluessel: string, detail?: string): Promise; /** Zwischenstand, z. B. „Download 43 %“ oder „cloud-init läuft …“. */ phaseDetail(schluessel: string, detail: string): Promise; phaseBeendet( schluessel: string, status: Exclude, angaben: { detail?: string; fehler?: FehlerBeschreibung }, ): Promise; log(stufe: LogStufe, nachricht: string, angaben?: { phase?: string; daten?: unknown }): Promise; } export type PhasenKontext = { /** Detailtext der laufenden Phase aktualisieren. */ detail(text: string): Promise; log(nachricht: string, stufe?: LogStufe, daten?: unknown): Promise; /** Wirft AbbruchFehler, wenn der Auftrag abgebrochen werden soll. */ pruefeAbbruch(): Promise; }; export type PhasenErgebnis = { detail?: string; uebersprungen?: boolean }; export type PhasenSchritt = { schluessel: string; titel: string; ausfuehren: (kontext: PhasenKontext) => Promise; }; export type AblaufErgebnis = { ok: true } | { ok: false; abgebrochen: boolean; phase: string; fehler: FehlerBeschreibung }; export async function phasenAusfuehren( schritte: PhasenSchritt[], senke: EreignisSenke, optionen: { abbruchGewuenscht?: () => Promise } = {}, ): Promise { await senke.phasenAnlegen(schritte.map(({ schluessel, titel }) => ({ schluessel, titel }))); for (const schritt of schritte) { const kontext: PhasenKontext = { detail: (text) => senke.phaseDetail(schritt.schluessel, text), log: (nachricht, stufe = "info", daten) => senke.log(stufe, nachricht, { phase: schritt.schluessel, daten }), pruefeAbbruch: async () => { if (await optionen.abbruchGewuenscht?.()) throw new AbbruchFehler(); }, }; try { await kontext.pruefeAbbruch(); await senke.phaseGestartet(schritt.schluessel); const ergebnis = (await schritt.ausfuehren(kontext)) ?? {}; await senke.phaseBeendet(schritt.schluessel, ergebnis.uebersprungen ? "UEBERSPRUNGEN" : "ERLEDIGT", { detail: ergebnis.detail, }); } catch (fehler) { const beschreibung = fehlerBeschreiben(fehler); await senke.phaseBeendet(schritt.schluessel, "FEHLER", { fehler: beschreibung }); await senke.log("error", `${schritt.titel}: ${beschreibung.ursache}`, { phase: schritt.schluessel, daten: fehler instanceof Error ? { name: fehler.name, stack: fehler.stack } : undefined, }); return { ok: false, abgebrochen: fehler instanceof AbbruchFehler, phase: schritt.schluessel, fehler: beschreibung, }; } } return { ok: true }; }