Files
proxdeploy/docs/architektur.md
T
2026-10-12 15:08:42 +02:00

34 KiB
Raw Permalink Blame History

ProxDeploy – Architektur- und Umsetzungsplan

Stand: 2026-10-12 · Version 0.1.0-dev · freigegeben am 2026-10-12

1. Überblick

 Browser (Web-GUI)        Terminal (proxdeploy, Go)
        │  Cookie                 │  Bearer-Token
        └──────────┬──────────────┘
                   ▼  REST /api/v1 + SSE
┌────────────────────────────────────────────────────────────────────────┐
│ ProxDeploy (Docker Compose oder Debian-LXC), hinter Reverse-Proxy       │
│  ┌──────────────────────┐   pg-boss    ┌────────────────────────────┐   │
│  │ app (Next.js)        │──Auftrag────▶│ worker (Node)              │   │
│  │ UI · REST-API · SSE  │◀─LISTEN/─────│ Engine: Planer, Phasen,    │   │
│  │ Dry-Run (Planer)     │   NOTIFY     │ Proxmox-Client, Adapter    │   │
│  └──────────┬───────────┘              └──────┬───────────┬─────────┘   │
│             └────────── PostgreSQL ───────────┘           │             │
└───────────────────────────────────────────────────────────┼─────────────┘
                        HTTPS :8006 (Token, Fingerprint-Pin) │ optional SSH
                                                             ▼
                              Proxmox VE 8.x / 9.x (Einzelhost oder Cluster)
                                     │ Guest-Agent / SSH in den Gast
                                     ▼
                                VM oder LXC (Debian, Ubuntu, …)
  • Ein Backend, zwei Oberflächen. Die Engine (src/engine/) ist frameworkfrei und enthält die gesamte Deploy-Logik. Die Web-GUI und die CLI sprechen ausschließlich die REST-API; die GUI hat keinen Sonderweg an der API vorbei (Server Actions rufen dieselben Services wie die Route Handler).
  • app liefert UI, REST-API, OpenAPI-Spezifikation und SSE-Ströme. Sie führt nie ein Deployment aus, nur den Planer für den Dry-Run (reine Funktion, keine schreibenden Proxmox-Aufrufe).
  • worker ist derselbe Codestand als eigener Prozess und arbeitet Aufträge aus der pg-boss-Queue ab. Jede Phasenänderung wird in die DB geschrieben und per NOTIFY auftrag_<id> gemeldet; die app hängt sich mit LISTEN daran und streamt an alle offenen SSE-Verbindungen. Ein Neuladen der Seite oder ein Wiederverbinden der CLI liest zuerst den Stand aus der DB und setzt dann ab Last-Event-ID fort.
  • PostgreSQL ist die einzige Datenhaltung (inkl. Queue). Keine Dateien außer dem Katalog im Image.

2. Tech-Stack

Bereich Wahl Begründung
Web/API Next.js 16 (App Router), React 19, TypeScript strict Vorgabe; wie Laufzettel/ProxLedger
Styling Tailwind CSS 4, Tokens als CSS-Variablen unter @theme, eigene Komponenten Vorgabe, kein Admin-Template
Schriften Space Grotesk, JetBrains Mono über @fontsource-variable, selbst gehostet keine Google-Anfragen aus dem Intranet
Icons Lucide (Stroke) + die Statussymbole ✓ ◐ ✗ ○ als Text Status nie nur Farbe
DB PostgreSQL 17, Prisma 7 mit @prisma/adapter-pg Vorgabe
Jobs pg-boss auf derselben Datenbank Vorgabe; kein Redis nötig
Validierung zod (Request, Katalog-YAML, Profil-Editor) eine Schemaquelle für API, OpenAPI und Katalog
OpenAPI aus den zod-Schemas erzeugt (/api/v1/openapi.json) Go-Client wird daraus generiert
Auth jose (Sitzungs-Cookie), openid-client (OIDC, PKCE), argon2id wie ProxLedger; kein Auth.js-Beta
SSH ssh2 (Node) Snippets schreiben, pct exec, SSH in den Gast
YAML yaml (eemeli) erhält Kommentare im Profil-Editor
Logs pino (JSON auf stdout), zusätzlich pro Auftrag in der DB Betrieb + GUI
Tests Vitest (Engine, API mit echter Test-DB), Playwright (Assistent, Live-Ansicht, 390 px)
CLI Go, statisches Binary siehe Abschnitt 9

Exakte Versionen werden in #001 gegen npm/Go-Proxy geprüft und gepinnt (abgelagerte Fassungen, keine RCs) und in CLAUDE.md festgehalten.

3. Was geht per Proxmox-API, was braucht SSH

Geprüft am 2026-10-12 gegen das API-Schema von PVE 9.2 (apidoc.js), die Quellen der Zweige stable-8/stable-bookworm (pve-storage, qemu-server, pve-container) und die Release Notes 8.0–9.2. Belege stehen in den Issues #005 und #007.

Fähigkeit PVE 8.0–8.3 PVE 8.4+ / 9.x Weg
Cloud-Image herunterladen + Checksumme prüfen (download-url, content=import, qcow2/raw/vmdk) nein (nur iso/vztmpl, OVA ab 8.2) ja API
Disk aus heruntergeladenem Image anlegen (scsi0: <stor>:0,import-from=<stor>:import/<datei>) nur per absolutem Pfad → nur root@pam ja, per Volume-ID API
Klonen, Konfiguration, cloud-init (ciuser, sshkeys, ipconfigN, nameserver, searchdomain, ciupgrade), Start/Stopp, Template ja ja API
Guest-Agent exec, exec-status, file-write (max. 60 KiB je Aufruf), network-get-interfaces ja (VM.Monitor) ja (VM.GuestAgent.* ab 9.0) API
LXC anlegen (ssh-public-keys, net0 mit tag, unprivileged, features: nesting) ja ja API
LXC-Template-Liste und -Download (aplinfo) ja ja API
LXC-IP auslesen (lxc/{id}/interfaces) ja ja API
LXC-Features außer nesting (keyctl, fuse, …) nur root@pam per Ticket, kein Token dito SSH (pct set)
Befehle im LXC ausführen keine API keine API SSH (pct exec auf dem Node oder SSH in den Container)
cloud-init-Snippets schreiben (cicustom) keine API (Upload nur iso/vztmpl/import) dito SSH
Zertifikats-Fingerprint nodes/{n}/certificates/info dito API (nach erstem Pin)

Folgerungen für die Engine:

  1. Fähigkeitserkennung pro Server (src/engine/proxmox/faehigkeiten.ts), beim Verbindungstest und vor jedem Auftrag aufgefrischt: PVE-Version je Node, Storages mit Inhaltstyp import, snippets, vztmpl, images/rootdir, Rechte des Tokens (GET /access/permissions), SSH konfiguriert und erreichbar. Daraus ergibt sich pro Server und Vorgang die Strategie, die in der GUI sichtbar ist („Template-Bau: per API“ / „braucht SSH, weil PVE 8.3“).
  2. VM-Provisionierung läuft immer über den Guest-Agent (Skript per file-write in 60-KiB-Stücken, dann exec + exec-status-Polling). Damit funktioniert sie ohne SSH und liefert Live-Fortschritt. Standard-cloud-init von Proxmox liefert nur Benutzer, Keys und Netz; ciupgrade=0, Updates macht das Profil base (sichtbar als eigene Phase statt stummem Boot-Upgrade).
  3. Der Guest-Agent ist in keinem der vier Cloud-Images vorinstalliert (geprüft in den Paketlisten). Deshalb wird er beim Template-Bau eingebaut, damit jeder Klon sofort einen Agent hat:
    • mit SSH zum Node: einmaliger Vorbereitungsstart mit cicustom: vendor=<snippet> (installiert qemu-guest-agent, cloud-init clean, poweroff) – funktioniert auch mit DHCP;
    • ohne SSH: Vorbereitungsstart mit einer Bau-Adresse (statische IP aus den Server-Einstellungen) und dem ProxDeploy-Bereitstellungsschlüssel per sshkeys, dann SSH in die VM, dieselben Schritte.
    • Danach cloud-init clean, Maschinen-ID leeren, als Template markieren.
  4. LXC-Provisionierung braucht immer SSH – bevorzugt pct exec über SSH zum Node (kein Netzweg zum Container nötig), sonst SSH direkt in den Container (Bereitstellungsschlüssel per ssh-public-keys, IP aus Konfiguration bzw. lxc/{id}/interfaces). Das debian-Standard-Template bringt openssh-server mit; der Bereitstellungsschlüssel wird am Ende wieder entfernt.
  5. Ohne jedes SSH sind möglich: VM aus vorhandenem oder per API gebautem Template (PVE ≥ 8.4) mit allen Profilen; LXC ohne Profile (nur Anlegen + Start). Die GUI graut nicht Mögliches mit Begründung aus.

SSH-Zugang pro Server: ProxDeploy erzeugt ein ed25519-Schlüsselpaar, zeigt den öffentlichen Schlüssel samt Einzeiler zum Hinterlegen, pinnt den Host-Key beim ersten Test (Bestätigung durch den Admin). Benutzer root auf dem Node (für pct und /var/lib/vz/snippets nötig).

4. Proxmox-Rechte (Rolle ProxDeploy)

Wird im README ausführlich dokumentiert, inkl. pveum-Befehlen.

  • Gemeinsam: VM.Allocate VM.Clone VM.Audit VM.Config.Disk VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Network VM.Config.HWType VM.Config.Options VM.Config.Cloudinit VM.PowerMgmt VM.Console Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit SDN.Use SDN.Audit Sys.Audit Pool.Audit
  • nur 8.x: VM.Monitor (Guest-Agent); vor 8.2 statt Sys.AccessNetwork → Sys.Modify
  • 8.2+ und 9.x: Sys.AccessNetwork auf /nodes/<node> (für download-url)
  • nur 9.x: VM.GuestAgent.Audit VM.GuestAgent.Unrestricted (ersetzt VM.Monitor)
  • optional: Datastore.Allocate (heruntergeladene Images/Templates wieder löschen), Sys.Modify (privilegierte Container ab 9.0)
  • ACL: /vms oder ein Pool, /storage/<id>, /sdn/zones/localnetwork (bzw. konkrete Zonen), / nur für Sys.Audit. Der Verbindungstest gleicht die effektiven Rechte gegen diese Liste ab und nennt fehlende Rechte einzeln.

5. Distributionen, Adapter, Profile

Katalog als Daten

catalog/distros/*.yaml und catalog/profiles/*.yaml liegen im Repo und im Image. Der worker synchronisiert sie beim Start in die DB (Upsert über id, Hash der Datei; Aktiv-Schalter und eigene Profile bleiben unangetastet). Die YAML-Schlüssel bleiben englisch wie in der Vorgabe – sie sind ein Austauschformat, kein Code-Bezeichner. Validiert mit zod; ein fehlerhafter Eintrag wird mit Dateiname und Pfad gemeldet und übersprungen, nicht der Start abgebrochen.

Mitgeliefert (URLs am 2026-10-12 per Abruf geprüft): Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04. Dazu Ubuntu 26.04 LTS (resolute, Cloud-Image und LXC-Template verfügbar).

  • Debian: cloud.debian.org/images/cloud/<codename>/latest/debian-<n>-genericcloud-amd64.qcow2, SHA512SUMS (Zeilenformat <hash>␣␣<datei>), Benutzer debian
  • Ubuntu: cloud-images.ubuntu.com/<codename>/current/<codename>-server-cloudimg-amd64.img (qcow2), SHA256SUMS (Zeilenformat <hash>␣*<datei>), Benutzer ubuntu
  • LXC: alle *-standard_*_amd64.tar.zst; bei mehreren Treffern (Debian 13: 13.1-2 und 13.6-1) gewinnt die höchste Version (Versionssortierung).

Ergänzende Felder gegenüber der Vorgabe: codename, arch (vorerst amd64), vm.bios (ovmf Standard, seabios möglich), vm.min_disk_gb.

Provisionierungs-Adapter

src/engine/adapter/schnittstelle.ts definiert FamilienAdapter – jede Methode liefert ein idempotentes Shell-Fragment (kein Ausführen im Adapter, damit testbar und im Dry-Run anzeigbar):

interface FamilienAdapter {
  familie: string;                        // "debian"
  aktualisieren(): Skript;
  paketeInstallieren(pakete: string[]): Skript;
  dienstAktivieren(name: string, starten?: boolean): Skript;
  repoHinzufuegen(repo: PaketQuelle): Skript;     // Schlüssel-URL, URIs, Suites, Komponenten
  zeitzoneSetzen(tz: string): Skript;
  localeSetzen(locale: string): Skript;
  tastaturSetzen(layout: string): Skript;
  benutzerAnlegen(b: Benutzerangaben): Skript;    // inkl. sudo, authorized_keys
  sshHaerten(o: SshHaertung): Skript;             // PasswordAuthentication no, PermitRootLogin no
  gastAgentInstallieren(): Skript;
  automatischeUpdates(an: boolean): Skript;
  fakten(): Skript;                                // gibt OS-Fakten (codename, ID) als JSON aus
}

Registriert in src/engine/adapter/index.ts. Ein neuer Adapter (rhel, alpine, suse) ist eine neue Datei + ein Eintrag in der Registry + Tests; Katalog und Profile verweisen per family darauf.

Profile

Ein Profil ist eine Liste deklarativer Schritte je Familie, die der Adapter in Shell übersetzt. Für Sonderfälle gibt es run (rohes Shell, ausdrücklich nicht portabel). Variablen werden als Formularfelder angezeigt und mit {{ var.name }} eingesetzt (eigene kleine Template-Funktion, nur Ersetzung, keine Logik; Werte werden für Shell gequotet).

id: zabbix-agent
name: Zabbix Agent 2
description: Zabbix Agent 2 (7.0 LTS) aus dem offiziellen Repo
requires: [base]
variables:
  - { name: server, label: Zabbix-Server, type: string, required: true }
  - { name: server_active, label: ServerActive, type: string, default: "{{ var.server }}" }
lxc: {}                       # z. B. docker: { features: { nesting: true } }
families:
  debian:
    - repo_deb: "https://repo.zabbix.com/zabbix/7.0/{{ os.id }}/pool/main/z/zabbix-release/zabbix-release_latest_7.0+{{ os.id }}{{ os.version_id }}_all.deb"
    - packages: [zabbix-agent2]
    - file: { path: /etc/zabbix/zabbix_agent2.d/proxdeploy.conf, content: "Server={{ var.server }}\nServerActive={{ var.server_active }}\nHostname={{ guest.name }}\n" }
    - service: { name: zabbix-agent2, enable: true, restart: true }

Fehlt families.<familie> für die gewählte Distribution, ist das Profil im Assistenten ausgegraut („keine Umsetzung für Familie rhel“). Eigene Profile werden im YAML-Editor angelegt, gegen dasselbe zod-Schema geprüft und mit origin: custom gespeichert; Katalog-Profile sind schreibgeschützt, lassen sich aber als Kopie bearbeiten.

Mitgeliefert: base, docker (Docker CE, deb822 docker.sources, Key /etc/apt/keyrings/docker.asc, Pakete docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; LXC: nesting=1), zabbix-agent (7.0 LTS; 8.0 hat noch keine Agent-Pakete).

6. Engine: Aufträge und Phasen

src/engine/
  proxmox/       client.ts (fetch + undici, Fingerprint-Pinning), endpunkte/*.ts, faehigkeiten.ts, aufgaben.ts (UPID warten)
  ssh/           verbindung.ts, node.ts (pct exec, Snippet schreiben), gast.ts
  katalog/       schema.ts (zod), laden.ts, pruefsumme.ts (beide Zeilenformate)
  adapter/       schnittstelle.ts, debian.ts, index.ts
  profile/       aufloesen.ts (Reihenfolge, requires, Variablen), rendern.ts (→ Skript)
  planer/        vm.ts, lxc.ts, template.ts  → Plan { phasen[], aufrufe[] }  (rein, keine I/O außer Lesen)
  ablauf/        ausfuehren.ts (Phasen-Runner), ereignisse.ts (Senke-Interface), fehler.ts (Ursache + Hinweis)
  transport/     gastagent.ts, ssh-node.ts, ssh-gast.ts  → gemeinsames Interface `Ausfuehrer`
  sperren.ts     VMID-Reservierung

Planer und Ausführung trennen. Der Planer bekommt eine validierte Anfrage + einen lesenden Proxmox-Zugriff + Fähigkeiten und liefert den Plan: die Phasen und je Phase die geplanten API-Aufrufe (POST /nodes/pve01/qemu/9013/clone newid=120 full=1 …). Der Dry-Run zeigt genau diesen Plan; die Ausführung arbeitet ihn ab. So können Dry-Run und Wirklichkeit nicht auseinanderlaufen.

Phasen (VM): Prüfungen → Template (übersprungen, wenn vorhanden) → Klonen → Hardware & Disk → cloud-init → Start → Guest-Agent → Provisionierung (je Profil eine Unterzeile im Log) → Abschluss. LXC: Prüfungen → LXC-Template → Anlegen → Start → Netzwerk → Provisionierung → Abschluss. Jede Phase schreibt status, detail (der Muted-Text der CLI), dauer, bei Fehler ursache und hinweis. Web und CLI rendern nur diese Felder – kein Text wird in einem Client erfunden.

VMID-Vergabe ohne Doppelungen: pg_advisory_xact_lock(hash(serverId)), dann nächste freie ID aus /cluster/resources ∪ Tabelle VmidReservierung, Reservierung schreiben, Sperre fällt mit der Transaktion. Die Reservierung gilt bis Auftragsende (bzw. Ablauf nach 2 h als Sicherung). Beim create/clone wird ein Konflikt (already exists) einmal mit neuer ID wiederholt.

Fehler und Aufräumen: Ein Fehler beendet den Auftrag mit Status FEHLGESCHLAGEN, die Phase trägt Ursache und Lösungshinweis (z. B. „Token fehlt VM.GuestAgent.Unrestricted auf /vms/120 → Rolle ergänzen“). Was angelegt wurde, steht in Auftrag.angelegt (VMID, heruntergeladene Volumes). „Aufräumen“ ist ein eigener Auftrag (AUFRAEUMEN), nur nach Bestätigung, und löscht nur, was ProxDeploy selbst angelegt hat (geprüft über das Tag proxdeploy + Auftrags-ID in der Beschreibung).

Parallelität: pg-boss mit konfigurierbarer Parallelität im worker (Standard 4); Template-Bau pro Server+Distribution singleton (zweiter Deploy wartet auf denselben Bau statt doppelt zu bauen).

7. Datenmodell (Prisma, Entwurf)

Namensabbildung (verbindlich, kommt nach CLAUDE.md):

Vorgabe Modell / Begriff Vorgabe Modell / Begriff
User Benutzer Deployment / Job Auftrag (Art AUSROLLEN, TEMPLATE_BAUEN, AUFRAEUMEN)
API token (CLI) ApiToken Phase Phase
SSH key SshSchluessel Job log line Logeintrag
Server Server VMID lock VmidReservierung
Node Knoten (nur Feldname, kein Modell) Template (Proxmox) Template
Distribution Distribution Preset Vorlage
Profile Profil Audit log Protokoll
Setting Einstellung Key provider SchluesselQuelle (Interface)
enum Rolle            { ADMIN OPERATOR }
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 }
enum Herkunft         { KATALOG EIGEN }
enum Quelle           { WEB CLI API }

model Benutzer {
  id String @id @default(cuid())
  benutzername String @unique
  anzeigename String
  email String?
  passwortHash String?             // null bei reinen OIDC-Benutzern
  oidcSubjekt String? @unique
  rolle Rolle @default(OPERATOR)
  aktiv Boolean @default(true)
  standards Json?                  // persönliche Standardwerte im Assistenten
  erstelltAm DateTime @default(now())
  apiTokens ApiToken[]  sshSchluessel SshSchluessel[]  auftraege Auftrag[]  protokoll Protokoll[]
}

model ApiToken {
  id String @id @default(cuid())
  benutzerId String
  name String
  praefix String @unique           // "pd_ab12cd34" – erkennbar, Rest nur gehasht
  hash String                      // SHA-256 des Geheimnisses
  zuletztGenutztAm DateTime?
  ablaufAm DateTime?
  widerrufenAm DateTime?
  erstelltAm DateTime @default(now())
}

model SshSchluessel {
  id String @id @default(cuid())
  benutzerId String?               // null = für alle sichtbar (Team-Schlüssel)
  name String
  oeffentlich String
  fingerabdruck String             // SHA256:…
  quelle String @default("lokal")  // Kennung der SchluesselQuelle (später z. B. sshkeyvault)
  @@unique([benutzerId, fingerabdruck])
}

model Server {
  id String @id @default(cuid())
  name String @unique              // "rz1" – auch CLI-Name
  url String                       // https://host:8006
  tokenId String                   // user@realm!tokenid
  tokenGeheimnis String            // AES-256-GCM, v1:<iv>:<tag>:<daten>
  tlsPruefen Boolean @default(true)
  fingerabdruck String?            // SHA-256, gepinnt bei selbst signiertem Zertifikat
  sshAktiv Boolean @default(false)
  sshBenutzer String @default("root")
  sshPort Int @default(22)
  sshPrivat String?                // verschlüsselt
  sshOeffentlich String?
  sshHostSchluessel Json?          // je Node gepinnt
  templateVmidVon Int @default(9000)
  templateVmidBis Int @default(9999)
  gastVmidVon Int @default(100)
  bauNetz Json?                    // Bridge, VLAN, IP/CIDR, Gateway für Template-Vorbereitung ohne SSH
  standards Json?                  // Standard-Node, -Storage, -Bridge
  faehigkeiten Json?               // letzte Erkennung (Version, Storages, Rechte, Strategien)
  erreichbar Boolean?
  geprueftAm DateTime?
  erstelltAm DateTime @default(now())
}

model Distribution {
  id String @id                    // "debian-13"
  name String
  familie String
  definition Json                  // validiertes YAML
  dateiHash String
  aktiv Boolean @default(true)
  synchronisiertAm DateTime
}

model Profil {
  id String @id                    // "docker"
  name String
  beschreibung String?
  herkunft Herkunft
  quelltext String                 // YAML wie eingegeben (Kommentare bleiben)
  definition Json
  aktiv Boolean @default(true)
  geaendertVonId String?
  geaendertAm DateTime @updatedAt
}

model Template {
  id String @id @default(cuid())
  serverId String
  knoten String
  vmid Int
  distributionId String
  storage String
  imageQuelle String               // URL + Prüfsumme des Images
  gebautVonAuftragId String?
  gebautAm DateTime
  @@unique([serverId, vmid])
}

model Vorlage {                    // Preset
  id String @id @default(cuid())
  name String @unique              // "docker-host-klein"
  titel String
  beschreibung String?
  gastTyp GastTyp
  parameter Json                   // Teilmenge der Auftrags-Anfrage
  erstelltVonId String?
  geaendertAm DateTime @updatedAt
}

model Auftrag {
  id String @id @default(cuid())
  kurzId String @unique            // "d-7f3k2" für CLI und URLs
  art AuftragsArt
  status AuftragsStatus @default(WARTEND)
  serverId String
  knoten String?
  gastTyp GastTyp?
  vmid Int?
  name String?
  distributionId String?
  profile String[]
  anfrage Json                     // validierte Anfrage (Grundlage für „Wiederholen“)
  plan Json                        // Plan zum Zeitpunkt des Starts
  ergebnis Json?                   // IP, SSH-Befehl, Dauer
  angelegt Json?                   // was zum Aufräumen gehört
  fehler Json?                     // { phase, ursache, hinweis }
  quelle Quelle
  erstelltVonId String
  wiederholtVonId String?
  vorlageId String?
  erstelltAm DateTime @default(now())
  gestartetAm DateTime?
  beendetAm DateTime?
  phasen Phase[]  log Logeintrag[]
  @@index([serverId, erstelltAm])  @@index([status])
}

model Phase {
  id String @id @default(cuid())
  auftragId String
  reihenfolge Int
  schluessel String                // "klonen"
  titel String                     // "Klonen"
  status PhasenStatus @default(OFFEN)
  detail String?
  ursache String?
  hinweis String?
  gestartetAm DateTime?
  beendetAm DateTime?
  @@unique([auftragId, schluessel])
}

model Logeintrag {
  id BigInt @id @default(autoincrement())   // auch SSE-Event-ID
  auftragId String
  zeit DateTime @default(now())
  stufe String                     // debug|info|warn|error
  phase String?
  nachricht String
  daten Json?
  @@index([auftragId, id])
}

model VmidReservierung {
  serverId String
  vmid Int
  auftragId String
  ablaufAm DateTime
  @@id([serverId, vmid])
}

model Protokoll {                  // Audit-Log, nur anhängen
  id BigInt @id @default(autoincrement())
  zeit DateTime @default(now())
  benutzerId String?
  quelle Quelle
  aktion String                    // "Server angelegt", "Ausrollung gestartet"
  ziel String?                     // "server:rz1", "auftrag:d-7f3k2"
  details Json?
  ip String?
}

model Einstellung { schluessel String @id  wert Json }

Datenbankregeln (CHECK/Trigger, wie in Laufzettel): Protokoll nur INSERT, VMID-Bereiche von < bis, Auftrag.vmid innerhalb 100–999999999, ein Template je Server/VMID.

8. REST-API (/api/v1)

Authentifizierung: Sitzungs-Cookie (Web) oder Authorization: Bearer pd_… (CLI). Rolle und Aktiv-Status werden pro Anfrage aus der DB gelesen. Jede schreibende Anfrage → Protokoll. Fehler einheitlich { fehler: { code, nachricht, hinweis?, felder? } }.

Methode + Pfad Rolle Zweck
POST /auth/anmelden, POST /auth/abmelden, GET /auth/oidc/start, GET /auth/oidc/rueckruf – Anmeldung
GET /ich alle aktueller Benutzer, Rolle, Version
GET /server · POST /server · GET/PATCH/DELETE /server/:name Admin schreibt Server
POST /server/pruefen Admin Verbindungstest ohne Speichern (liefert Fingerprint zum Bestätigen)
POST /server/:name/test alle Version, Cluster/Nodes, Storages, Bridges, Rechte, Fähigkeiten
GET /server/:name/ressourcen?knoten= alle Nodes, freie Ressourcen, Storages, Bridges/VLANs/VNets, nächste freie ID
POST /server/:name/ssh Admin Schlüsselpaar erzeugen/erneuern, Host-Key pinnen
GET /distributionen?typ=vm|lxc · PATCH /distributionen/:id Admin schreibt Katalog
GET /profile · POST /profile · PUT/DELETE /profile/:id · POST /profile/pruefen Admin schreibt Profile + Validierung
GET /server/:name/templates alle Templates je Node
POST /server/:name/templates Admin, Operator Template bauen → Auftrag
POST /auftraege/plan alle Dry-Run: Plan + CLI-Befehl, nichts wird angelegt
POST /auftraege Operator+ Ausrollen (VM/LXC) → 202 { id, kurzId }
POST /auftraege/stapel Operator+ Batch (YAML/JSON-Liste)
GET /auftraege?server=&typ=&distribution=&status=&seite= alle Verlauf
GET /auftraege/:id alle Stand inkl. Phasen + Ergebnis
GET /auftraege/:id/ereignisse alle SSE: phase, log, ende; Last-Event-ID
GET /auftraege/:id/log?ab= alle Detail-Log
POST /auftraege/:id/wiederholen · /abbrechen · /aufraeumen Operator+
GET/POST/PATCH/DELETE /vorlagen… Operator+ Presets
GET/POST/DELETE /schluessel… eigene SSH-Public-Keys (über SchluesselQuelle)
GET/POST/DELETE /tokens… eigene CLI-Tokens (Geheimnis nur einmal angezeigt)
GET/POST/PATCH /benutzer… Admin Benutzer, Rollen
GET /protokoll?… Admin Audit-Log
GET/PUT /einstellungen Admin Standardwerte, OIDC, Bereitstellungsschlüssel
GET /openapi.json, GET /gesundheit – Spezifikation, Healthcheck

9. CLI (cli/, Go)

Begründung Go: ein statisches Binary pro Plattform (linux/darwin/windows × amd64/arm64, ~8 MB, keine Laufzeit nötig – passt auf jeden Admin-Rechner und Jump-Host), sehr gute Unterstützung für TTY-Erkennung, 24-bit-Farben und SSE. Der Nachteil (keine geteilten TS-Typen) wird durch den aus openapi.json generierten Client (oapi-codegen) aufgefangen; ein CI-Schritt prüft, dass der generierte Code aktuell ist. Bibliotheken: cobra (Befehle), BurntSushi/toml, golang.org/x/term.

  • Konfig ~/.config/proxdeploy/config.toml (url, token, optional server als Standard und [instances.<name>] für mehrere Instanzen – nicht profiles, das hieße wie --profile), Rechte 0600; PROXDEPLOY_URL/PROXDEPLOY_TOKEN/PROXDEPLOY_INSTANCE überschreiben.
  • Kurze Anfragen: Die CLI schickt nur gesetzte Flags. Der Server füllt nach der Vorlage Storage, Bridge, Node (Server-Standards bzw. Verbindungstest), Benutzer (eigener Name) und SSH-Schlüssel (hinterlegte) – anfrageErgaenzen in src/server/services/ausrollen.ts.
  • Abschlussbox (abschluss im Auftrag) berechnet der Server; Web und CLI zeigen dieselben Zeilen.
  • Generierter Code: Antwortschemas als zod in src/lib/schemas/api.ts, daraus openapi.json (pnpm openapi) und daraus die Go-Typen (oapi-codegen, nur Modelle). Anfragen gehen als Map hinaus (sonst schickte Go leere Pflichtfelder statt sie wegzulassen). pnpm cli:pruefen schlägt fehl, wenn etwas veraltet ist; tests/api-schemas.test.ts prüft echte Antworten gegen die Schemas.
  • login <url> fragt Benutzername/Passwort ab, tauscht sie über POST /api/v1/auth/token gegen ein Token und speichert es (oder --token). Tokens: pd_ + 256 Bit, gespeichert nur als SHA-256.
  • Ausgabe exakt nach Vorgabe; Renderer in cli/internal/ausgabe/ mit Golden-File-Tests für Farbe, --no-color, --ascii, --quiet, --json, --dry-run. Breite der Phasenspalte aus dem längsten Phasennamen, Box-Breite aus dem Inhalt (Unicode-Breite beachtet).
  • Exit-Codes: 0 Erfolg, 1 Auftrag fehlgeschlagen, 2 Bedienfehler, 3 Verbindung/Anmeldung.
  • Die Web-GUI zeigt im letzten Assistentenschritt den passenden CLI-Befehl; erzeugt wird er vom Server (POST /auftraege/plan liefert cliBefehl), damit beide Seiten dieselben Flags kennen.

10. Web-GUI

Seitenleiste mit Wortmarke, Server-Umschalter, Navigation (Übersicht, Ausrollen, Verlauf, Templates, Vorlagen, Katalog, Server*, Einstellungen; * nur Admin schreibend). Panel-Karten mit 1-px-Linie, Radius 14 px, Tokens aus der Farbtabelle der Vorgabe, dunkel zuerst, helles Theme umschaltbar (data-theme), Kontrast ≥ 4,5:1 in beiden (wird in Tests geprüft). Statusanzeige als Komponente <Status> mit Symbol + Farbe + Text.

  • Assistent /ausrollen: Schritte als Route-Segmente mit dem Zustand in der URL (?s=…), damit Zurückspringen, Neuladen und Teilen funktionieren; Vorbelegen per ?vorlage= oder ?wiederholen=. Am Handy eine Schrittleiste oben, Formular einspaltig.
  • Live-Ansicht /auftraege/[kurzId]: Terminal-Karte (JetBrains Mono) mit genau dem Aufbau der CLI, darunter ausklappbares Detail-Log, am Ende die Ergebnis-Karte mit Kopieren-Knopf. Fallback auf Polling, falls SSE durch den Proxy geblockt wird.

11. Sicherheit

  • Secrets (Proxmox-Token, SSH-Private-Keys, OIDC-Client-Secret) mit AES-256-GCM, Schlüssel aus PROXDEPLOY_SCHLUESSEL (32 Byte, Base64); Versionspräfix im Chiffrat für spätere Rotation. Geheimnisse verlassen die API nie (nur „gesetzt“/Fingerprint).
  • CLI-Tokens nur gehasht gespeichert; Passwörter argon2id; Anmeldung mit Rate-Limit.
  • TLS zu Proxmox: Standard Systemvertrauen; bei selbst signiertem Zertifikat Pinning des SHA-256-Fingerprints (TOFU mit Bestätigung im Formular). Kein globales NODE_TLS_REJECT_UNAUTHORIZED.
  • Hinter Reverse-Proxy: TRUST_PROXY=true wertet X-Forwarded-For/Proto aus; Cookies Secure abhängig vom tatsächlichen Schema (Lehre aus ProxLedger 0.11.1); SSE mit X-Accel-Buffering: no.
  • Profil-Variablen werden für Shell gequotet; run-Schritte nur in Profilen, die ein Admin anlegt.
  • Bereitstellungsschlüssel (für SSH in Gäste) wird nach der Provisionierung aus dem Gast entfernt.

12. Betrieb

  • docker-compose.yml: app, worker, postgres, migrate (einmalig prisma migrate deploy). Ein gemeinsames Image, Next output: "standalone", Non-root.
  • deploy/install-lxc.sh: auf dem Proxmox-Host ausführen → legt Debian-13-LXC an (per pct), installiert Node, PostgreSQL, systemd-Units für app und worker, erzeugt .env mit Zufallsschlüssel.
  • .env.example mit allen Variablen (DATABASE_URL, PROXDEPLOY_SCHLUESSEL, SITZUNG_GEHEIMNIS, APP_URL, TRUST_PROXY, OIDC_*, WORKER_PARALLEL, LOG_LEVEL, ADMIN_BENUTZER/PASSWORT für den ersten Start).

13. Dateistruktur

proxdeploy/
├─ catalog/{distros,profiles}/*.yaml
├─ cli/                         Go-Modul: cmd/proxdeploy, internal/{api,ausgabe,konfig,befehle}
├─ src/
│  ├─ app/(app)/…               Seiten (Übersicht, ausrollen, auftraege, server, templates, vorlagen, katalog, einstellungen)
│  ├─ app/(auth)/anmelden
│  ├─ app/api/v1/…              Route Handler (dünn: Recht prüfen, zod, Service)
│  ├─ components/               UI-Bausteine (Karte, Status, Terminal, Assistent, Formulare)
│  ├─ engine/                   siehe Abschnitt 6 – frameworkfrei, ohne Prisma-Import
│  ├─ server/services/          Regeln + Schreiboperationen, rufen Engine und Prisma
│  ├─ server/queries/           Lesemodelle
│  ├─ server/auth/              Sitzung, Token, OIDC, Rechte
│  ├─ worker/                   main.ts, Senke (Ereignisse → DB + NOTIFY), Katalog-Sync
│  └─ lib/                      kryptografie.ts, formate.ts, logger.ts
├─ prisma/schema.prisma, migrations/
├─ public/brand/                Logo-Assets, Favicons, Manifest-Icons
├─ assets/                      Banner, Avatar (Repo)
├─ deploy/install-lxc.sh
├─ tests/                       engine/*, api/*, e2e/*
├─ docs/architektur.md, docs/issues/
├─ CHANGELOG.md, DEVLOG.md, CLAUDE.md, README.md, .env.example
└─ docker-compose.yml, Dockerfile

14. Etappen und Meilensteine

Version Etappe Issues Test gegen echten Proxmox
v0.1.0 1 – Server-Verwaltung + Verbindungstest #001–#003 Verbindungstest, Fähigkeiten, Rechte
v0.2.0 2 – Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt #004–#008 Template bauen, VM ausrollen, Fehler + Aufräumen
v0.3.0 3 – LXC, Ubuntu, Profile, Presets #009–#012 LXC mit/ohne SSH, docker/zabbix
v0.4.0 4 – CLI #013 alle Befehle gegen die laufende Instanz
v0.5.0 5 – Verlauf, Audit, Rollen, OIDC #014–#016 –
v0.6.0 6 – Branding, README, Installer #017–#019 Installer in echtem LXC
v1.0.0 Feldtest 8.x + 9.x, Feinschliff #020 vollständig

Bis die Testumgebung steht, entwickle ich gegen einen Proxmox-Mock (tests/proxmox-mock/, ein kleiner HTTP-Server mit Zustand, der die benutzten Endpunkte inkl. UPID-Tasks nachbildet). Die Abnahme jeder Etappe gegen echten Proxmox wird nachgeholt und im DEVLOG vermerkt; ohne sie wird die Version nicht als abgeschlossen markiert.

15. Entscheidungen zur Freigabe (2026-10-12)

  1. Ubuntu 26.04 LTS wird mitgeliefert (Debian 12/13, Ubuntu 22.04/24.04/26.04).
  2. Templates booten UEFI (OVMF, q35, EFI-Disk mit pre-enrolled-keys=1); vm.bios pro Distribution überschreibbar.
  3. Template-VMID: keine feste Zuordnung. ProxDeploy nimmt die nächste freie ID im Template-Bereich des Servers (Standard 9000–9999), ermittelt aus /cluster/resources plus eigenen Reservierungen – dieselbe Sperre wie bei Gast-IDs. Welche VMID welches Template ist, steht in der Tabelle Template und als Tag (proxdeploy-template, distro-debian-13) in Proxmox.
  4. Namensabbildung aus Abschnitt 7 und deutsche API-Pfade bestätigt; Katalog-YAML bleibt englisch.
  5. Domain für APP_URL und OIDC-Redirect folgt später über .env.