Aufträge laufen als pg-boss-Jobs im worker. Der Phasen-Runner der Engine kennt weder Proxmox noch Datenbank; eine Senke schreibt Phasen und Log in die DB und meldet per NOTIFY, die app streamt per SSE an Web und CLI (Last-Event-ID, Herzschlag, Polling-Fallback im Browser). VMIDs vergibt eine Advisory-Sperre je Server zusammen mit einer Reservierungstabelle; zehn parallele Reservierungen im Test bekommen zehn verschiedene IDs. Der Proxmox-Client hat einen Trockenmodus für den Dry-Run: Lesendes echt, Schreibendes nur aufgezeichnet. Live-Ansicht im Aufbau der CLI-Ausgabe. Im echten Betrieb aufgefallen: ssh2 und pg sind CommonJS und brauchen im worker Default-Importe. Closes #006 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
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<void>;
|
||
phaseGestartet(schluessel: string, detail?: string): Promise<void>;
|
||
/** Zwischenstand, z. B. „Download 43 %“ oder „cloud-init läuft …“. */
|
||
phaseDetail(schluessel: string, detail: string): Promise<void>;
|
||
phaseBeendet(
|
||
schluessel: string,
|
||
status: Exclude<PhasenStatusWert, "OFFEN" | "LAEUFT">,
|
||
angaben: { detail?: string; fehler?: FehlerBeschreibung },
|
||
): Promise<void>;
|
||
log(stufe: LogStufe, nachricht: string, angaben?: { phase?: string; daten?: unknown }): Promise<void>;
|
||
}
|
||
|
||
export type PhasenKontext = {
|
||
/** Detailtext der laufenden Phase aktualisieren. */
|
||
detail(text: string): Promise<void>;
|
||
log(nachricht: string, stufe?: LogStufe, daten?: unknown): Promise<void>;
|
||
/** Wirft AbbruchFehler, wenn der Auftrag abgebrochen werden soll. */
|
||
pruefeAbbruch(): Promise<void>;
|
||
};
|
||
|
||
export type PhasenErgebnis = { detail?: string; uebersprungen?: boolean };
|
||
|
||
export type PhasenSchritt = {
|
||
schluessel: string;
|
||
titel: string;
|
||
ausfuehren: (kontext: PhasenKontext) => Promise<PhasenErgebnis | void>;
|
||
};
|
||
|
||
export type AblaufErgebnis =
|
||
{ ok: true } | { ok: false; abgebrochen: boolean; phase: string; fehler: FehlerBeschreibung };
|
||
|
||
export async function phasenAusfuehren(
|
||
schritte: PhasenSchritt[],
|
||
senke: EreignisSenke,
|
||
optionen: { abbruchGewuenscht?: () => Promise<boolean> } = {},
|
||
): Promise<AblaufErgebnis> {
|
||
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 };
|
||
}
|