# 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_` 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: :0,import-from=:import/`) | 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=` (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/` (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/`, `/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//latest/debian--genericcloud-amd64.qcow2`, `SHA512SUMS` (Zeilenformat `␣␣`), Benutzer `debian` - Ubuntu: `cloud-images.ubuntu.com//current/-server-cloudimg-amd64.img` (qcow2), `SHA256SUMS` (Zeilenformat `␣*`), 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): ```ts 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). ```yaml 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.` 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) | ```prisma 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::: 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 `[profiles.]` für mehrere Instanzen), Rechte 0600; `PROXDEPLOY_URL`/`PROXDEPLOY_TOKEN` überschreiben. - `login ` fragt Benutzername/Passwort ab, legt ein Token an und speichert es (oder `--token`). - 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 `` 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`.