feat(auftraege): worker, Phasen und Live-Fortschritt per SSE

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>
This commit is contained in:
2026-10-12 10:54:41 +02:00
co-authored by Claude Opus 5.5
parent 01d22aac3a
commit 76bc0e4269
38 changed files with 2274 additions and 22 deletions
+138
View File
@@ -38,6 +38,7 @@ model Benutzer {
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
protokoll Protokoll[]
auftraege Auftrag[]
}
/// Audit-Log. Nur anhängen – ein Trigger verweigert UPDATE und DELETE.
@@ -99,6 +100,8 @@ model Server {
geprueftAm DateTime?
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
auftraege Auftrag[]
templates Template[]
}
enum Herkunft {
@@ -139,3 +142,138 @@ model Profil {
geaendertVonId String?
geaendertAm DateTime @updatedAt
}
enum GastTyp {
VM
LXC
}
enum AuftragsArt {
AUSROLLEN
TEMPLATE_BAUEN
AUFRAEUMEN
}
enum AuftragsStatus {
WARTEND
LAEUFT
ERFOLGREICH
FEHLGESCHLAGEN
ABGEBROCHEN
AUFGERAEUMT
}
enum PhasenStatus {
OFFEN
LAEUFT
ERLEDIGT
FEHLER
UEBERSPRUNGEN
}
/// Ein Hintergrundjob: Ausrollen, Template bauen oder Aufräumen. Läuft im worker.
model Auftrag {
id String @id @default(cuid())
/// Kurz und tippbar für CLI und URLs: „d-7f3k2“.
kurzId String @unique
art AuftragsArt
status AuftragsStatus @default(WARTEND)
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Restrict)
knoten String?
gastTyp GastTyp?
vmid Int?
name String?
distributionId String?
profile String[]
/// Validierte Anfrage – Grundlage für „Wiederholen“.
anfrage Json
/// Plan zum Zeitpunkt des Starts (Phasen und geplante API-Aufrufe).
plan Json?
/// Kopfzeile wie in der CLI: „Server rz1 · Node pve01 · Debian 13 (Template 9013)“ – von der Engine gesetzt.
kopf String?
/// IP, SSH-Befehl, Dauer …
ergebnis Json?
/// Was ProxDeploy angelegt hat – Grundlage fürs Aufräumen.
angelegt Json?
/// { phase, ursache, hinweis }
fehler Json?
quelle Quelle
erstelltVonId String
erstelltVon Benutzer @relation(fields: [erstelltVonId], references: [id], onDelete: Restrict)
wiederholtVonId String?
/// Bei AUFRAEUMEN: der Auftrag, dessen Reste entfernt werden.
bezugId String?
abbrechen Boolean @default(false)
erstelltAm DateTime @default(now())
gestartetAm DateTime?
beendetAm DateTime?
phasen Phase[]
log Logeintrag[]
@@index([serverId, erstelltAm])
@@index([status])
@@index([erstelltAm])
}
model Phase {
id String @id @default(cuid())
auftragId String
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
reihenfolge Int
schluessel String
titel String
status PhasenStatus @default(OFFEN)
/// Der graue Detailtext hinter dem Phasennamen – in Web und CLI derselbe.
detail String?
ursache String?
hinweis String?
gestartetAm DateTime?
beendetAm DateTime?
@@unique([auftragId, schluessel])
@@index([auftragId, reihenfolge])
}
model Logeintrag {
/// Zugleich die Event-ID im SSE-Strom (Last-Event-ID).
id BigInt @id @default(autoincrement())
auftragId String
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
zeit DateTime @default(now())
stufe String
phase String?
nachricht String
daten Json?
@@index([auftragId, id])
}
/// Vergebene VMIDs, solange ein Auftrag läuft – verhindert Doppelvergabe zwischen parallelen Aufträgen.
model VmidReservierung {
serverId String
vmid Int
auftragId String
ablaufAm DateTime
@@id([serverId, vmid])
@@index([auftragId])
}
/// Von ProxDeploy gebaute Templates. Maßgeblich ist Proxmox (Tag proxdeploy-template); hier steht, woraus gebaut wurde.
model Template {
id String @id @default(cuid())
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
knoten String
vmid Int
distributionId String
storage String
imageDatei String
pruefsumme String
gebautVonAuftragId String?
gebautAm DateTime @default(now())
@@unique([serverId, vmid])
@@index([serverId, distributionId])
}