11 Commits
Author SHA1 Message Date
florian.wildeandClaude Opus 5.5 e8bb4bd391 docs(issues): Abnahme des Releases v1.0.0 nachgetragen
Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:21:10 +02:00
florian.wildeandClaude Opus 5.5 f338061d0f chore(release): v1.0.0
Feldtest auf PVE 9.2 vollständig (VM/LXC × Debian 12/13, Ubuntu 22.04/24.04/26.04, mit und ohne SSH zum Node); 8.x nicht im Einsatz und nicht geprüft. Dazu Standardwerte je Server und die Fehler aus dem Feldtest.

Closes #020
Closes #023

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:13:04 +02:00
florian.wildeandClaude Opus 5.5 ca9d6de446 fix(ablauf): angelegte ID merken, bevor auf Proxmox gewartet wird
Brach das Warten auf Klonen oder Anlegen ab, legte Proxmox den Gast trotzdem fertig an – und „Aufräumen“ wusste nichts von ihm (CT 114 im Feldtest).

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:02:57 +02:00
florian.wildeandClaude Opus 5.5 bc13b94c7e fix(proxmox): Task-Status unter Last weiter abfragen, nesting an
Im Feldtest wurde ein LXC fertig angelegt, während ProxDeploy nach drei gescheiterten Statusabfragen aufgab (Verbindungsaufbau > 10 s bei 76 % I/O-Last). Statusabfragen werden jetzt bis zum Task-Zeitlimit wiederholt, der Verbindungsaufbau darf 30 s dauern. nesting ist für Container standardmäßig an wie in der Proxmox-Oberfläche – Proxmox warnt sonst bei systemd 249/252.

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:46:15 +02:00
florian.wildeandClaude Opus 5.5 db57818eb3 fix(adapter): Benutzer anlegen, auch wenn die Gruppe schon existiert
Ubuntu 22.04 bringt eine Gruppe „admin“ mit; heißt der Gast-Benutzer so, brach useradd mit Code 9 ab (gefunden im Feldtest auf pve-demo). Der Container-Test legt jetzt diese Lage nach und lief seit pnpm 12 nicht mehr (pnpm -s gibt es dort nicht).

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:08:05 +02:00
florian.wildeandClaude Opus 5.5 64859b3637 fix(proxmox): vorübergehende Störungen wiederholen statt abbrechen
Der Feldtest auf einem ausgelasteten pve-demo scheiterte an Verbindungswacklern, nicht an Proxmox: pveproxy meldete „Broken pipe“ (Backend weg), schloss Keep-Alive-Verbindungen, Storage-Abfragen dauerten 15 s. GET wird bis zu dreimal wiederholt, Schreibendes nur bei sicher nicht angekommener Anfrage; Schreibaufrufe dürfen 120 s dauern, Keep-Alive 2 s.

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:14:47 +02:00
florian.wildeandClaude Opus 5.5 420aca3994 fix(sicherheit): Next 16.4.2 und Prisma 7.10.1
Next.js bis 16.4.0 hat zwei kritische Lücken (RCE in der Bildoptimierung mit SVG und in use cache) und eine hohe (DoS über Server Actions). Prisma 7.10.1 hebt mysql2 und deepmerge-ts über bekannte Lücken. Volle Prüfung grün; dazu der Vorlagen-Ablauf mit mehr Zeit, weil sein Dry-Run wirklich gegen den Testhost plant.

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:06:57 +02:00
florian.wildeandClaude Opus 5.5 a9ea35cab5 fix(ui): Übersicht am Handy nicht breiter als der Bildschirm
Lange Zeilen unter „Letzte Deployments“ sprengten die Gitterspalte; der mobile Browser zoomte heraus, die Navigationsschublade verdeckte dann „Abmelden“. keinQuerscrollen misst jetzt gegen die eingestellte Breite – gegen innerWidth gemessen fiel das Herauszoomen nicht auf.

Refs #023

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:06:13 +02:00
florian.wildeandClaude Opus 5.5 2b6381de41 feat(server): Standardwerte für Node, Storage und Bridge
Der Server kannte die Standards schon für kurze CLI-Befehle, aber nichts setzte sie. Jetzt im Server-Formular (Auswahl aus dem Verbindungstest) und als Vorbelegung im Assistenten.

Closes #023

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:57:31 +02:00
florian.wildeandClaude Opus 5.5 8b37329c92 chore(release): v0.6.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:25:49 +02:00
florian.wildeandClaude Opus 5.5 7beeafd2f6 fix(templates): cloud-init-Laufwerk an scsi1 statt ide2
q35 hängt IDE an den SATA-Controller; der Cloud-Kernel von Debian 12 kennt ihn nicht, cloud-init fand keine Datenquelle. Gefunden bei der Abnahme von #021 auf pve-demo: Template-Bau über die Bau-Adresse und eine Debian-12-VM daraus, beides ohne SSH zum Node.

Closes #021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:25:45 +02:00
41 changed files with 792 additions and 299 deletions
+30
View File
@@ -17,6 +17,36 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
### Sicherheit ### Sicherheit
## [1.0.0] – 2026-10-23
Erste stabile Fassung: alle Etappen der Vorgabe, Feldtest auf Proxmox VE 9.2 (#020). Proxmox VE 8.x ist im Code unterstützt, aber nicht im Feldtest geprüft; OIDC ist gegen einen nachgebauten Identity Provider getestet.
### Hinzugefügt
- Standardwerte je Server (Node, Storage, Bridge) im Server-Formular; der Assistent belegt sie vor, die CLI nutzt sie für kurze Befehle (#023)
### Geändert
- LXC: `nesting` ist für Container standardmäßig an, wie in der Proxmox-Oberfläche – Debian 12 und Ubuntu 22.04 (systemd 252/249) brauchen es; abwählbar im Assistenten und mit `--nesting=false` (#020)
### Behoben
- „Aufräumen“ kannte einen Gast nicht, wenn das Warten auf Klonen bzw. Anlegen abbrach, Proxmox ihn aber fertig anlegte – die ID wird jetzt gemerkt, sobald Proxmox den Auftrag angenommen hat (#020)
- Warten auf Proxmox-Tasks bricht bei vorübergehend fehlschlagenden Statusabfragen nicht mehr ab, sondern fragt bis zum Zeitlimit des Tasks weiter; Verbindungsaufbau darf 30 s statt 10 s dauern (#020)
- Gast-Benutzer, der wie eine vorhandene Gruppe heißt (z. B. „admin“ auf Ubuntu 22.04), ließ die Provisionierung mit Code 9 abbrechen; die Gruppe wird jetzt Primärgruppe (#020)
- `pnpm test:container` lief seit pnpm 12 nicht mehr (`pnpm -s` gibt es dort nicht) (#020)
- Auf ausgelasteten Nodes brachen Aufträge beim ersten Verbindungswackler ab („Broken pipe“, „other side closed“, Zeitüberschreitung). Lesende Proxmox-Aufrufe werden jetzt bis zu dreimal wiederholt, schreibende nur, wenn die Anfrage sicher nicht ankam; Schreibaufrufe dürfen 120 s dauern; Wiederholungen stehen im Detail-Log (#020)
- `proxdeploy batch --dry-run --quiet` gab Leerzeilen für erfolgreiche Einträge aus (#020)
- Übersicht am Handy breiter als der Bildschirm (lange Auftragszeilen); der Browser zoomte heraus und die Navigation verdeckte „Abmelden“ (#023)
- Seitenleiste scrollt, wenn ihr Inhalt höher als der Bildschirm ist (#023)
- Beim Wechsel des Servers im Assistenten wurde die erste Bridge statt vmbr0 vorbelegt (#023)
### Sicherheit
- Next.js 16.4.0 → 16.4.2: zwei kritische Lücken (Remote Code Execution in der Bildoptimierung mit SVG und in `use cache`) und eine hohe (DoS über Server Actions); ProxDeploy nutzte die betroffenen Funktionen nicht, der Bild-Endpunkt ist aber standardmäßig erreichbar
- Prisma 7.10.0 → 7.10.1: hebt `mysql2` und `deepmerge-ts` über bekannte Lücken (nur im Prisma-CLI, nicht zur Laufzeit)
## [0.6.1] – 2026-10-12
### Behoben
- Debian-12-Templates bekamen keine cloud-init-Konfiguration: das cloud-init-Laufwerk hing an ide2 (SATA bei q35), den der Debian-12-Cloud-Kernel nicht kennt. Neue Templates nutzen scsi1 (#021)
## [0.6.0] – 2026-10-12 ## [0.6.0] – 2026-10-12
### Hinzugefügt ### Hinzugefügt
+7 -3
View File
@@ -57,7 +57,7 @@ cli/ CLI in Go; nur REST-API. Typen aus cli/api/openapi.json
## Werkzeuge und Versionen ## Werkzeuge und Versionen
Node 24 über nvm (`export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"`), pnpm 12. Exakt gepinnt: Node 24 über nvm (`export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"`), pnpm 12. Exakt gepinnt:
`next@16.4.0`, `react@19.3.0`, `prisma`/`@prisma/client`/`@prisma/adapter-pg` **`7.10.0`** `next@16.4.2`, `react@19.3.0`, `prisma`/`@prisma/client`/`@prisma/adapter-pg` **`7.10.1`**
(der `latest`-Tag von prisma zeigt auf einen 8.0-RC), `vitest@5.0.1`, `@playwright/test@1.63.0`, (der `latest`-Tag von prisma zeigt auf einen 8.0-RC), `vitest@5.0.1`, `@playwright/test@1.63.0`,
`pino@10.4.0`. Obergrenzen: **eslint 9.x** (eslint-plugin-react aus eslint-config-next kann 10 nicht), `pino@10.4.0`. Obergrenzen: **eslint 9.x** (eslint-plugin-react aus eslint-config-next kann 10 nicht),
**typescript ~6.0.3** (typescript-eslint 8 erlaubt < 6.1). **typescript ~6.0.3** (typescript-eslint 8 erlaubt < 6.1).
@@ -67,6 +67,10 @@ CLI: **Go 1.27.2** ohne sudo unter `~/.local/go` (`export PATH="$HOME/.local/go/
`go.yaml.in/yaml/v3 3.0.5`, `oapi-codegen 2.8.0` (als `go tool`). Antwortschemas der API stehen in `go.yaml.in/yaml/v3 3.0.5`, `oapi-codegen 2.8.0` (als `go tool`). Antwortschemas der API stehen in
`src/lib/schemas/api.ts`; nach einer Änderung `pnpm openapi` und `go generate ./...` in `cli/`. `src/lib/schemas/api.ts`; nach einer Änderung `pnpm openapi` und `go generate ./...` in `cli/`.
**Sicherheitsupdates** gehen vor die Regel „mindestens zwei Wochen abgelagert“: mit bekannter
kritischer Lücke kein Release (`pnpm audit --prod` vor jedem Tag; Next 16.4.0 → 16.4.2 und Prisma
7.10.0 → 7.10.1 am 2026-10-23 aus diesem Grund).
Next 16: `proxy.ts` statt middleware; `cookies()`, `headers()`, `params`, `searchParams` sind async. Next 16: `proxy.ts` statt middleware; `cookies()`, `headers()`, `params`, `searchParams` sind async.
Prisma 7: Client aus `@/generated/prisma/client`, Enum-Werte aus `@/generated/prisma/enums`, Zugriff Prisma 7: Client aus `@/generated/prisma/client`, Enum-Werte aus `@/generated/prisma/enums`, Zugriff
über den Lazy-Proxy `src/lib/db.ts`. über den Lazy-Proxy `src/lib/db.ts`.
@@ -96,8 +100,8 @@ Abläufe prüfen Desktop (1440 px) und Handy (**390 px, kein horizontales Scroll
- Branch `main`; je Issue/Etappe `phase/NNN-kurzname`, nach Abschluss `--ff-only` nach `main`. - Branch `main`; je Issue/Etappe `phase/NNN-kurzname`, nach Abschluss `--ff-only` nach `main`.
- Conventional Commits, deutscher Betreff ≤ 72 Zeichen, Körper erklärt das Warum, Fußzeile - Conventional Commits, deutscher Betreff ≤ 72 Zeichen, Körper erklärt das Warum, Fußzeile
`Closes #NNN` bzw. `Refs #NNN`. Identität `Florian Wilde <wilde.florian@gmail.com>`. `Closes #NNN` bzw. `Refs #NNN`. Identität `Florian Wilde <wilde.florian@gmail.com>`.
- SemVer; jede Etappe ein annotierter Tag `vX.Y.Z`. **1.0.0 erst nach dem Feldtest** auf echtem - SemVer; jede Etappe ein annotierter Tag `vX.Y.Z`. 1.0.0 nach dem Feldtest auf PVE 9.x (#020) –
Proxmox 8.x und 9.x (#020). im Einsatz ist nur 9.x; 8.x bleibt im Code, gilt aber als „nicht im Feldtest geprüft“.
- `CHANGELOG.md` (Keep a Changelog, deutsch) und `DEVLOG.md` (Zeit je Issue/Meilenstein aus den - `CHANGELOG.md` (Keep a Changelog, deutsch) und `DEVLOG.md` (Zeit je Issue/Meilenstein aus den
Commit-Zeitstempeln, Sitzungen mit „Erledigt“ und „Entscheidungen“) werden mit jedem Issue gepflegt; Commit-Zeitstempeln, Sitzungen mit „Erledigt“ und „Entscheidungen“) werden mit jedem Issue gepflegt;
`docs/issues/*.md` tragen Status und tatsächliche Zeit. `docs/issues/*.md` tragen Status und tatsächliche Zeit.
+41 -3
View File
@@ -2,7 +2,7 @@
## Zeitübersicht ## Zeitübersicht
**Gesamt:** 7 h 39 min (Sitzung 1–3) **Gesamt:** 12 h 09 min (Sitzung 1–5)
Zeiten stammen aus den Git-Commit-Zeitstempeln je Issue; Wartezeit auf Rückmeldungen ist enthalten. Zeiten stammen aus den Git-Commit-Zeitstempeln je Issue; Wartezeit auf Rückmeldungen ist enthalten.
@@ -22,7 +22,9 @@ Zeiten stammen aus den Git-Commit-Zeitstempeln je Issue; Wartezeit auf Rückmeld
| #012 Vorlagen | 0 h 11 min | | #012 Vorlagen | 0 h 11 min |
| #022 Profile im Template | 0 h 22 min | | #022 Profile im Template | 0 h 22 min |
| #015 (Teil) SSH-Schlüssel | 0 h 10 min | | #015 (Teil) SSH-Schlüssel | 0 h 10 min |
| #021 Template-Bau ohne SSH | 0 h 19 min | | #021 Template-Bau ohne SSH | 0 h 19 min + 0 h 35 min Abnahme |
| #023 Standardwerte je Server | 1 h 40 min |
| #020 Feldtest 9.x, 1.0.0 | 2 h 15 min |
| #013 CLI und API-Tokens | 0 h 42 min | | #013 CLI und API-Tokens | 0 h 42 min |
| #014 Verlauf, Wiederholen, Aufräumen | 1 h 21 min | | #014 Verlauf, Wiederholen, Aufräumen | 1 h 21 min |
| #015 Benutzer, Passwort, Audit-Log | 0 h 10 min | | #015 Benutzer, Passwort, Audit-Log | 0 h 10 min |
@@ -35,10 +37,46 @@ Zeiten stammen aus den Git-Commit-Zeitstempeln je Issue; Wartezeit auf Rückmeld
|---|---| |---|---|
| v0.1.0 | 1 h 15 min | **abgeschlossen** | v0.1.0 | 1 h 15 min | **abgeschlossen**
| v0.2.0 | 1 h 32 min | **abgeschlossen** | v0.2.0 | 1 h 32 min | **abgeschlossen**
| v0.3.0 | 1 h 48 min | **abgeschlossen** (Abnahme #021 offen) | v0.3.0 | 1 h 48 min | **abgeschlossen**
| v0.4.0 | 0 h 42 min | **abgeschlossen** | v0.4.0 | 0 h 42 min | **abgeschlossen**
| v0.5.0 | 1 h 39 min | **abgeschlossen** | v0.5.0 | 1 h 39 min | **abgeschlossen**
| v0.6.0 | 0 h 40 min | **abgeschlossen** | v0.6.0 | 0 h 40 min | **abgeschlossen**
| v0.6.1 | 0 h 35 min | **abgeschlossen**
| v1.0.0 | 3 h 55 min | **abgeschlossen**
---
## 2026-10-23 · Sitzung 5 · 13:55–16:10 (dazu 2026-10-12 20:26–22:06 für #023)
**Issue:** #020, #023
**Version:** 1.0.0
### Erledigt
- #023: Standardwerte je Server (Node, Storage, Bridge) im Formular, im Assistenten und für die CLI; dabei die zu breite Übersicht am Handy gefunden – und dass `keinQuerscrollen` Herauszoomen nicht bemerkte
- Florians Entscheidungen: nur PVE 9.x, OIDC vorerst ohne echten IdP, Lizenz offen, Gitea-Release freigegeben
- Sicherheit: Next.js 16.4.0 → 16.4.2 (zwei kritische Lücken), Prisma 7.10.1
- Feldtest 9.x: Ubuntu 22.04/26.04 als VM und LXC, Debian 12 als LXC, Ubuntu 26.04 mit Docker im LXC – über die installierte Instanz (CT 109), Update dreimal durchgespielt
- Gefunden und behoben: Abbrüche bei ausgelastetem Node (Wiederholen, Task-Status bis Zeitlimit), angelegte ID zu spät gemerkt, useradd bei vorhandener Gruppe, nesting-Standard, `pnpm test:container` unter pnpm 12
### Entscheidungen
- **Wiederholen nur, wo es sicher ist:** lesend immer, schreibend nur bei sicher nicht angekommener Anfrage – ein doppeltes „VM anlegen“ wäre schlimmer als ein Abbruch.
- **Sicherheitsupdates vor der Zwei-Wochen-Regel.**
- **nesting standardmäßig an** wie die Proxmox-Oberfläche.
---
## 2026-10-12 · Sitzung 4 · 19:50–20:26
**Issue:** #021 (Abnahme), Vorüberlegung #020
**Version:** 0.6.1
### Erledigt
- Freie Bau-Adresse gesucht (Ping/ARP-Probe vom Testhost, .150–.199 stumm) → 10.0.10.180
- #021 abgenommen: Template 9003 (Debian 12) ohne Node-SSH über die Bau-Adresse gebaut, daraus VM 111 `pd-deb12` ohne Node-SSH ausgerollt
- Dabei gefunden und behoben: cloud-init-Laufwerk an `ide2` wird vom Debian-12-Cloud-Kernel nicht gesehen → `scsi1`
- Fehlgeschlagenen ersten Bau mit `proxdeploy cleanup` aufgeräumt (VM 9003 gestoppt und gelöscht)
### Entscheidungen
- **cloud-init-Laufwerk an virtio-SCSI** für neue Templates; vorhandene (9000–9002) bleiben – deren Kernel kennen SATA.
- **Feldtest 8.x (#020) nicht als verschachtelte VM auf pve-demo:** 7 GB RAM, VMs liefen dort auf dritter Virtualisierungsebene; braucht einen echten 8.x-Host.
--- ---
+25 -15
View File
@@ -55,17 +55,22 @@ Compose startet `postgres`, spielt mit `migrate` die Datenbank ein und startet d
### In einem Debian-LXC auf dem Proxmox-Host ### In einem Debian-LXC auf dem Proxmox-Host
Auf dem Proxmox-Host als root – Paket und Skripte kommen aus dem
[Release](https://git.wilde-server.de/florian.wilde/proxdeploy/releases):
```bash ```bash
scripts/paket-bauen.sh 0.6.0 --tar # im Repository: proxdeploy-0.6.0.tar.gz v=1.0.0; basis=https://git.wilde-server.de/florian.wilde/proxdeploy/releases/download/v$v
scp proxdeploy-0.6.0.tar.gz deploy/install-lxc.sh deploy/im-container.sh root@pve01:/root/ for f in install-lxc.sh im-container.sh proxdeploy-$v.tar.gz SHA256SUMS; do curl -fsSLO "$basis/$f"; done
ssh root@pve01 ./install-lxc.sh --paket proxdeploy-0.6.0.tar.gz --url https://proxdeploy.example sha256sum -c --ignore-missing SHA256SUMS && chmod +x install-lxc.sh
./install-lxc.sh --paket proxdeploy-$v.tar.gz --url https://proxdeploy.example
``` ```
Der Installer legt einen unprivilegierten Debian-13-Container an (DHCP auf `vmbr0`; `--ip`, `--gw`, Der Installer legt einen unprivilegierten Debian-13-Container an (DHCP auf `vmbr0`; `--ip`, `--gw`,
`--vlan`, `--storage`, `--ctid` u. a. siehe `--help`), installiert Node.js 24 (Prüfsumme geprüft) und `--vlan`, `--storage`, `--ctid` u. a. siehe `--help`), installiert Node.js 24 (Prüfsumme geprüft) und
PostgreSQL, richtet systemd-Dienste ein und zeigt am Ende Adresse und das Passwort des ersten Admins. PostgreSQL, richtet systemd-Dienste ein und zeigt am Ende Adresse und das Passwort des ersten Admins.
Aktualisieren mit `./install-lxc.sh --aktualisieren <ctid> --paket proxdeploy-0.6.1.tar.gz` – die Aktualisieren mit `./install-lxc.sh --aktualisieren <ctid> --paket proxdeploy-<neu>.tar.gz` – die
letzten drei Fassungen bleiben unter `/opt/proxdeploy/versionen/`. letzten drei Fassungen bleiben unter `/opt/proxdeploy/versionen/`. Ein eigenes Paket aus dem
Repository baut `scripts/paket-bauen.sh <version> --tar`.
## Erste Schritte ## Erste Schritte
@@ -94,6 +99,13 @@ Das API-Token braucht eine eigene Rolle; die Befehle stehen in
`proxdeploy` ist ein einzelnes Binary (Linux, macOS, Windows; amd64 und arm64) und spricht nur die `proxdeploy` ist ein einzelnes Binary (Linux, macOS, Windows; amd64 und arm64) und spricht nur die
REST-API – dieselbe wie die Web-GUI, mit denselben Phasentexten. REST-API – dieselbe wie die Web-GUI, mit denselben Phasentexten.
```bash
# Linux amd64 – andere Plattformen: siehe Release (darwin, windows, arm64)
curl -fsSL -o /usr/local/bin/proxdeploy \
https://git.wilde-server.de/florian.wilde/proxdeploy/releases/download/v1.0.0/proxdeploy-1.0.0-linux-amd64
chmod +x /usr/local/bin/proxdeploy
```
```bash ```bash
proxdeploy login https://proxdeploy.example # fragt Benutzer und Passwort, speichert ein Token proxdeploy login https://proxdeploy.example # fragt Benutzer und Passwort, speichert ein Token
proxdeploy servers proxdeploy servers
@@ -191,17 +203,15 @@ OIDC_GRUPPE_OPERATOR=proxdeploy-operators
## Stand ## Stand
Version und Änderungen stehen im [CHANGELOG](CHANGELOG.md), der Plan in **1.0.0** – alle Etappen der Vorgabe umgesetzt. Änderungen stehen im [CHANGELOG](CHANGELOG.md), der
[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md). Plan in [docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md).
| Etappe | Inhalt | Stand | | Geprüft | |
|---|---|---| |---|---|
| 1 · v0.1.0 | Server-Verwaltung, Fingerprint-Pinning, Verbindungstest mit Fähigkeitserkennung | ✓ fertig, auf PVE 9.2 geprüft | | Proxmox VE 9.x | **Feldtest auf PVE 9.2**: VMs und LXC mit Debian 12/13 und Ubuntu 22.04/24.04/26.04, mit und ohne SSH zum Node ([#020](docs/issues/020-feldtest.md)) |
| 2 · v0.2.0 | Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt, Assistent | ✓ fertig, auf PVE 9.2 geprüft | | Proxmox VE 8.x | im Code unterstützt (Rechte, API-Wege je Version gegen nachgebaute Server getestet), **nicht im Feldtest** |
| 3 · v0.3.0 | LXC, Ubuntu, Profile, Vorlagen, Profile im Template, SSH-Schlüssel | ✓ fertig, auf PVE 9.2 geprüft (Bau-Adresse #021: Abnahme offen) | | Anmeldung | lokale Benutzer im Betrieb geprüft; OIDC gegen einen nachgebauten Identity Provider getestet, noch nicht gegen einen echten |
| 4 · v0.4.0 | CLI `proxdeploy` (Go), API-Tokens | ✓ fertig, auf PVE 9.2 geprüft | | Betrieb | Docker Compose lokal, LXC-Installer samt Update auf PVE 9.2 |
| 5 · v0.5.0 | Verlauf, Aufräumen, Audit-Log, Benutzer, OIDC | ✓ fertig (Aufräumen auf PVE 9.2 geprüft, OIDC gegen Mock-IdP) |
| 6 · v0.6.0 | Branding, README mit Screenshots, Docker Compose, LXC-Installer | ✓ fertig, Installer auf PVE 9.2 geprüft |
## Entwicklung ## Entwicklung
+3 -3
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "ProxDeploy API", "title": "ProxDeploy API",
"version": "0.6.0", "version": "1.0.0",
"description": "REST-API von ProxDeploy – dieselbe für Web-GUI und CLI. Fehler: `{ fehler: { code, nachricht, hinweis?, felder? } }`." "description": "REST-API von ProxDeploy – dieselbe für Web-GUI und CLI. Fehler: `{ fehler: { code, nachricht, hinweis?, felder? } }`."
}, },
"servers": [ "servers": [
@@ -503,13 +503,13 @@
}, },
"features": { "features": {
"default": { "default": {
"nesting": false, "nesting": true,
"keyctl": false "keyctl": false
}, },
"type": "object", "type": "object",
"properties": { "properties": {
"nesting": { "nesting": {
"default": false, "default": true,
"type": "boolean" "type": "boolean"
}, },
"keyctl": { "keyctl": {
+1 -1
View File
@@ -49,7 +49,7 @@ func (g *gastFlags) registrieren(f *pflag.FlagSet, typ string) {
if typ == "lxc" { if typ == "lxc" {
f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)") f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)")
f.BoolVar(&g.privilegiert, "privileged", false, "privilegierter Container (Standard: unprivilegiert)") f.BoolVar(&g.privilegiert, "privileged", false, "privilegierter Container (Standard: unprivilegiert)")
f.BoolVar(&g.nesting, "nesting", false, "Feature nesting (das Profil docker setzt es selbst)") f.BoolVar(&g.nesting, "nesting", true, "Feature nesting (Standard an wie in Proxmox; --nesting=false schaltet es ab)")
} else { } else {
f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen") f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen")
} }
+16 -4
View File
@@ -1,6 +1,7 @@
package befehle package befehle
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -189,11 +190,22 @@ func (u *umgebung) batchBefehl() *cobra.Command {
fehlgeschlagen := 0 fehlgeschlagen := 0
if trocken { if trocken {
for nr, e := range eintraege { ausgegeben := false
if nr > 0 && !u.opt.json { for _, e := range eintraege {
fmt.Fprintln(u.aus) // Leerzeile nur zwischen Blöcken, die auch etwas ausgeben (mit --quiet bleiben gute stumm).
puffer := &bytes.Buffer{}
ziel := u.aus
u.aus = puffer
err := u.trockenlauf(ctx, c, e.anfrage, e.name)
u.aus = ziel
if puffer.Len() > 0 {
if ausgegeben && !u.opt.json {
fmt.Fprintln(u.aus)
}
_, _ = u.aus.Write(puffer.Bytes())
ausgegeben = true
} }
if err := u.trockenlauf(ctx, c, e.anfrage, e.name); err != nil { if err != nil {
var ef *exitFehler var ef *exitFehler
if !errors.As(err, &ef) || !ef.gemeldet { if !errors.As(err, &ef) || !ef.gemeldet {
u.fehlerMelden(err) u.fehlerMelden(err)
+1 -1
View File
@@ -595,7 +595,7 @@ proxdeploy/
| v0.4.0 | 4 – CLI | #013 | alle Befehle gegen die laufende Instanz | | v0.4.0 | 4 – CLI | #013 | alle Befehle gegen die laufende Instanz |
| v0.5.0 | 5 – Verlauf, Audit, Rollen, OIDC | #014–#016 | – | | v0.5.0 | 5 – Verlauf, Audit, Rollen, OIDC | #014–#016 | – |
| v0.6.0 | 6 – Branding, README, Installer | #017–#019 | Installer in echtem LXC | | 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 | | v1.0.0 | Feldtest 9.x (8.x nicht im Einsatz), Feinschliff | #020, #023 | vollständig |
Bis die Testumgebung steht, entwickle ich gegen einen **Proxmox-Mock** (`tests/proxmox-mock/`, ein 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 kleiner HTTP-Server mit Zustand, der die benutzten Endpunkte inkl. UPID-Tasks nachbildet). Die
+48 -11
View File
@@ -1,21 +1,58 @@
# #020 Feldtest 8.x und 9.x, Version 1.0.0 # #020 Feldtest und Version 1.0.0
**Status:** offen **Status:** erledigt
**Meilenstein:** v1.0.0 **Meilenstein:** v1.0.0
**Labels:** test **Labels:** test
**Geschätzt:** 6 h **Tatsächlich:** – **Geschätzt:** 6 h **Tatsächlich:** 2 h 15 min (Sitzung 5)
## Ziel ## Ziel
Vollständiger Durchlauf gegen echte Proxmox-Hosts beider Hauptversionen. Vollständiger Durchlauf gegen echte Proxmox-Hosts.
**Umfang angepasst (Florian, 2026-10-23):** im Einsatz ist nur Proxmox VE 9.x – kein Feldtest auf 8.x,
kein zweiter 9.x-Host; OIDC vorerst nicht gegen einen echten Identity Provider (lokale Benutzer reichen).
## Aufgaben ## Aufgaben
- [ ] Matrix: VM/LXC × Debian/Ubuntu × mit/ohne SSH × 8.x/9.x - [x] Matrix auf pve-demo (PVE 9.2.2): VM/LXC × Debian/Ubuntu × mit/ohne SSH zum Node
- [ ] Gefundene Fehler als eigene Issues - [x] Gefundene Fehler behoben (siehe unten), jeweils mit Test
- [x] Abhängigkeiten auf bekannte Lücken geprüft (`pnpm audit --prod`)
- [x] Gitea-Release v1.0.0 mit CLI für 6 Plattformen, Paket, Installer und SHA256SUMS; danach auf pve-demo genau nach README aus dem Release installiert (CT 117 `pd-release`, Prüfsummen OK, Version 1.0.0 läuft)
## Akzeptanzkriterien ## Matrix (PVE 9.2.2)
- [ ] Alle Kombinationen grün oder mit Begründung ausgeschlossen
## Abhängigkeiten | Gast | mit SSH zum Node | ohne SSH zum Node |
alle |---|---|---|
| VM Debian 13 | ✓ pd-test01…04, pd-cli01 | – (Weg wie Debian 12) |
| VM Debian 12 | – | ✓ pd-deb12 (Template über Bau-Adresse, #021) |
| VM Ubuntu 22.04 | – | ✓ pd-ft-u2204 |
| VM Ubuntu 24.04 | ✓ pd-ubu01 | – |
| VM Ubuntu 26.04 | – | ✓ pd-ft-u2604 |
| LXC Debian 13 | ✓ pd-ct01 (pct exec) | ✓ pd-abnahme (SSH in den Container) |
| LXC Debian 12 | – | ✓ pd-ft-ct-d12 |
| LXC Ubuntu 22.04 | – | ✓ pd-ft-ct-u2204 |
| LXC Ubuntu 24.04 | ✓ pd-cli02 | ✓ pd-ct02 |
| LXC Ubuntu 26.04 + docker | – | ✓ pd-ft-ct-u2604 (Docker 29.9 im unprivilegierten CT) |
Templates: Debian 12/13, Ubuntu 22.04/24.04/26.04, Debian 13 mit eingebackenem docker. „Ohne SSH“
heißt hier: über die installierte Instanz (LXC-Installer, CT 109), die keinen SSH-Zugang zum Node hat.
Die Wege „mit/ohne SSH“ unterscheiden sich nur beim Template-Bau (Snippet vs. Bau-Adresse) und bei der
LXC-Provisionierung (pct exec vs. SSH in den Container); beide sind je Familie mindestens einmal gelaufen.
## Gefunden und behoben
- **Next.js 16.4.0** hatte zwei kritische und eine hohe Lücke → 16.4.2; Prisma 7.10.1 hebt `mysql2`/`deepmerge-ts`.
- **Ausgelasteter Node** (76 % I/O-Druck, Storage-Abfragen 15 s): Aufträge brachen beim ersten
Verbindungswackler ab. Jetzt: GET bis zu dreimal wiederholt, Statusabfragen laufender Tasks bis zum
Task-Zeitlimit, Schreibendes nur bei sicher nicht angekommener Anfrage, Verbindungsaufbau 30 s,
Schreibaufrufe 120 s.
- **Angelegte ID zu spät gemerkt:** brach das Warten ab, legte Proxmox den Gast trotzdem fertig an und
„Aufräumen“ kannte ihn nicht (CT 114). Jetzt gemerkt, sobald Proxmox den Auftrag angenommen hat.
- **Benutzer wie eine vorhandene Gruppe** („admin“ auf Ubuntu 22.04) → useradd Code 9. Gruppe wird Primärgruppe.
- **nesting** jetzt standardmäßig an wie in der Proxmox-Oberfläche (Warnung bei systemd 249/252).
- `pnpm test:container` lief seit pnpm 12 nicht (`pnpm -s`); deckt jetzt auch den Gruppenfall ab.
## Notizen / Entscheidungen ## Notizen / Entscheidungen
– - Paralleles Ausrollen wurde auf dem Testhost bewusst nur mit zwei Gästen geprüft (Batch in Etappe 4):
fünf Gäste plus zwei Template-Builds überfordern dessen Storage.
- Randfall: eine VMID-Reservierung wird bei Auftragsende frei, auch wenn Proxmox den Gast noch anlegt;
ein zweiter Auftrag kann dieselbe ID bekommen. Mit dem längeren Schreib-Zeitlimit selten; „Aufräumen“
löscht wegen der Prüfung der Beschreibung trotzdem nie das Falsche (im Feldtest so beobachtet).
- Die Entwicklungs-App wurde zweimal von außen beendet (Neustart eines anderen Projekts per
`pkill -f "next start"`); der Feldtest lief deshalb über die installierte Instanz.
+9 -3
View File
@@ -1,8 +1,8 @@
# #021 Template-Bau ohne SSH (Bau-Adresse) # #021 Template-Bau ohne SSH (Bau-Adresse)
**Status:** umgesetzt, Abnahme auf pve-demo ausstehend **Status:** erledigt
**Meilenstein:** v0.3.0 **Meilenstein:** v0.3.0
**Labels:** feature, proxmox **Labels:** feature, proxmox
**Geschätzt:** 3 h **Tatsächlich:** 0 h 19 min **Geschätzt:** 3 h **Tatsächlich:** 0 h 19 min + 0 h 35 min Abnahme
## Ziel ## Ziel
Templates auch auf Servern ohne SSH zum Node bauen. Der Guest-Agent wird dann nicht per Snippet, Templates auch auf Servern ohne SSH zum Node bauen. Der Guest-Agent wird dann nicht per Snippet,
@@ -15,7 +15,7 @@ sondern über SSH **in die Vorbereitungs-VM** eingebaut.
- [x] Fähigkeitserkennung: „Template-Bau per API ohne SSH möglich, wenn Bau-Adresse gesetzt“ - [x] Fähigkeitserkennung: „Template-Bau per API ohne SSH möglich, wenn Bau-Adresse gesetzt“
## Akzeptanzkriterien ## Akzeptanzkriterien
- [ ] Template-Bau auf pve-demo mit abgeschaltetem SSH - [x] Template-Bau auf pve-demo mit abgeschaltetem SSH (2026-10-12, Bau-Adresse 10.0.10.180): Template 9003 tpl-debian-12; daraus VM 111 pd-deb12 ebenfalls ohne Node-SSH ausgerollt, Agent aus dem Template, eigene Maschinen-ID
## Abhängigkeiten ## Abhängigkeiten
#007 #007
@@ -25,3 +25,9 @@ sondern über SSH **in die Vorbereitungs-VM** eingebaut.
- Der Bereitstellungsschlüssel kommt per cloud-init an den Standardbenutzer des Images; danach werden alle authorized_keys, ciuser, sshkeys und die feste Adresse entfernt. - Der Bereitstellungsschlüssel kommt per cloud-init an den Standardbenutzer des Images; danach werden alle authorized_keys, ciuser, sshkeys und die feste Adresse entfernt.
- Host-Key der frischen VM ist vorher unbekannt (TOFU, nur für diese eine Verbindung). - Host-Key der frischen VM ist vorher unbekannt (TOFU, nur für diese eine Verbindung).
- Aus #007 ausgegliedert, damit Etappe 2 nicht wartet: der Testhost hat SSH, und VM-Deployments aus vorhandenen Templates laufen ohnehin ohne SSH. - Aus #007 ausgegliedert, damit Etappe 2 nicht wartet: der Testhost hat SSH, und VM-Deployments aus vorhandenen Templates laufen ohnehin ohne SSH.
- **Abnahme fand einen echten Fehler:** Das cloud-init-Laufwerk hing als `ide2` an der q35-Maschine
(also am SATA-Controller). Der „cloud“-Kernel von Debian 12 hat dafür keinen Treiber – cloud-init
fand keine Datenquelle, die VM bootete als „localhost“ ohne Adresse. Debian 13 und Ubuntu kennen
den Controller, deshalb fiel es bisher nicht auf. Neue Templates hängen das Laufwerk an `scsi1`
(virtio-SCSI wie die System-Disk). Vorhandene Templates bleiben, wie sie sind.
- Bau-Adresse per Ping- und ARP-Probe vom Testhost gewählt (10.0.10.150–199 stumm, Florians Freigabe „such dir eine IP“).
+23
View File
@@ -0,0 +1,23 @@
# #023 Standardwerte je Server
**Status:** erledigt
**Meilenstein:** v1.0.0
**Labels:** feature
**Geschätzt:** 1 h **Tatsächlich:** 1 h 40 min (inkl. Fehlersuche Handy-Übersicht)
## Ziel
Die Vorgabe nennt unter „Einstellungen“ Standardwerte. Der Server kannte sie schon (die CLI füllt
damit kurze Befehle), aber es gab keine Oberfläche dafür.
## Aufgaben
- [x] `standards` (Node, Storage, Bridge) im Server-Schema und in der API, leere Felder fallen weg
- [x] Server-Formular: Auswahl aus dem letzten Verbindungstest, „automatisch“ möglich; ein nicht mehr bekannter Wert bleibt sichtbar
- [x] Assistent belegt Node, Storage und Bridge daraus vor – auch beim Wechsel von Server und Node (vorher nahm der Server-Wechsel die erste statt vmbr0)
## Akzeptanzkriterien
- [x] Speichern und Leeren getestet (`tests/server.test.ts`); im Browser bei 390 px geprüft
## Abhängigkeiten
#003, #013
## Notizen / Entscheidungen
- Persönliche Standardwerte (`Benutzer.standards`) bleiben vorerst ungenutzt – die Werte hängen am Server, nicht an der Person.
+3 -2
View File
@@ -23,6 +23,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
| [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | 0 h 17 min | | [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | 0 h 17 min |
| [#018](018-readme.md) | README und Dokumentation | erledigt | v0.6.0 | 4 h | 0 h 02 min | | [#018](018-readme.md) | README und Dokumentation | erledigt | v0.6.0 | 4 h | 0 h 02 min |
| [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | 0 h 20 min | | [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | 0 h 20 min |
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – | | [#020](020-feldtest.md) | Feldtest 9.x, Version 1.0.0 | erledigt | v1.0.0 | 6 h | 2 h 15 min |
| [#021](021-template-bau-ohne-ssh.md) | Template-Bau ohne SSH (Bau-Adresse) | umgesetzt, Abnahme offen | v0.3.0 | 3 h | 0 h 19 min | | [#021](021-template-bau-ohne-ssh.md) | Template-Bau ohne SSH (Bau-Adresse) | erledigt | v0.3.0 | 3 h | 0 h 54 min |
| [#022](022-template-profile.md) | Templates mit eingebackenen Profilen | erledigt | v0.3.0 | 4 h | 0 h 22 min | | [#022](022-template-profile.md) | Templates mit eingebackenen Profilen | erledigt | v0.3.0 | 4 h | 0 h 22 min |
| [#023](023-standardwerte.md) | Standardwerte je Server | erledigt | v1.0.0 | 1 h | 1 h 40 min |
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxdeploy", "name": "proxdeploy",
"version": "0.6.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI", "description": "VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI",
@@ -35,12 +35,12 @@
"@fontsource-variable/jetbrains-mono": "^5.3.0", "@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource-variable/space-grotesk": "^5.3.0", "@fontsource-variable/space-grotesk": "^5.3.0",
"@node-rs/argon2": "^2.2.1", "@node-rs/argon2": "^2.2.1",
"@prisma/adapter-pg": "7.10.0", "@prisma/adapter-pg": "7.10.1",
"@prisma/client": "7.10.0", "@prisma/client": "7.10.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.2.12", "jose": "^6.2.12",
"lucide-react": "1.47.0", "lucide-react": "1.47.0",
"next": "16.4.0", "next": "16.4.2",
"openid-client": "6.8.8", "openid-client": "6.8.8",
"pg": "^8.23.0", "pg": "^8.23.0",
"pg-boss": "12.37.1", "pg-boss": "12.37.1",
@@ -67,13 +67,13 @@
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"eslint": "^9.39.5", "eslint": "^9.39.5",
"eslint-config-next": "16.4.0", "eslint-config-next": "16.4.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^17.5.1", "lint-staged": "^17.5.1",
"opentype.js": "1.3.5", "opentype.js": "1.3.5",
"prettier": "^3.9.9", "prettier": "^3.9.9",
"prisma": "7.10.0", "prisma": "7.10.1",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"tsx": "^4.23.13", "tsx": "^4.23.13",
"typescript": "~6.0.3", "typescript": "~6.0.3",
+121 -130
View File
@@ -176,11 +176,11 @@ importers:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.2 version: 2.2.2
'@prisma/adapter-pg': '@prisma/adapter-pg':
specifier: 7.10.0 specifier: 7.10.1
version: 7.10.0 version: 7.10.1
'@prisma/client': '@prisma/client':
specifier: 7.10.0 specifier: 7.10.1
version: 7.10.0(prisma@7.10.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3) version: 7.10.1(prisma@7.10.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@@ -191,8 +191,8 @@ importers:
specifier: 1.47.0 specifier: 1.47.0
version: 1.47.0(react@19.3.0) version: 1.47.0(react@19.3.0)
next: next:
specifier: 16.4.0 specifier: 16.4.2
version: 16.4.0(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(@types/node@24.19.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) version: 16.4.2(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(@types/node@24.19.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
openid-client: openid-client:
specifier: 6.8.8 specifier: 6.8.8
version: 6.8.8 version: 6.8.8
@@ -267,8 +267,8 @@ importers:
specifier: ^9.39.5 specifier: ^9.39.5
version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
eslint-config-next: eslint-config-next:
specifier: 16.4.0 specifier: 16.4.2
version: 16.4.0(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 16.4.2(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint-config-prettier: eslint-config-prettier:
specifier: ^10.1.8 specifier: ^10.1.8
version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
@@ -285,8 +285,8 @@ importers:
specifier: ^3.9.9 specifier: ^3.9.9
version: 3.9.10 version: 3.9.10
prisma: prisma:
specifier: 7.10.0 specifier: 7.10.1
version: 7.10.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3) version: 7.10.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
tailwindcss: tailwindcss:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3 version: 4.3.3
@@ -1054,60 +1054,60 @@ packages:
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
'@next/env@16.4.0': '@next/env@16.4.2':
resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==} resolution: {integrity: sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==}
'@next/eslint-plugin-next@16.4.0': '@next/eslint-plugin-next@16.4.2':
resolution: {integrity: sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==} resolution: {integrity: sha512-gCTWlw0e+Y6kajpFyKKgA195jUJCRhlzJE0lV7ufe8a+ghIsANWZ0/FiJwg6DuOG1+OvRbcZDcFBoRh3mNJx0g==}
'@next/swc-darwin-arm64@16.4.0': '@next/swc-darwin-arm64@16.4.2':
resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==} resolution: {integrity: sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@16.4.0': '@next/swc-darwin-x64@16.4.2':
resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==} resolution: {integrity: sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@16.4.0': '@next/swc-linux-arm64-gnu@16.4.2':
resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==} resolution: {integrity: sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/swc-linux-arm64-musl@16.4.0': '@next/swc-linux-arm64-musl@16.4.2':
resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==} resolution: {integrity: sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/swc-linux-x64-gnu@16.4.0': '@next/swc-linux-x64-gnu@16.4.2':
resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==} resolution: {integrity: sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@next/swc-linux-x64-musl@16.4.0': '@next/swc-linux-x64-musl@16.4.2':
resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==} resolution: {integrity: sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@next/swc-win32-arm64-msvc@16.4.0': '@next/swc-win32-arm64-msvc@16.4.2':
resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==} resolution: {integrity: sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@16.4.0': '@next/swc-win32-x64-msvc@16.4.2':
resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==} resolution: {integrity: sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -1229,14 +1229,14 @@ packages:
engines: {node: '>=20'} engines: {node: '>=20'}
hasBin: true hasBin: true
'@prisma/adapter-pg@7.10.0': '@prisma/adapter-pg@7.10.1':
resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==} resolution: {integrity: sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==}
'@prisma/client-runtime-utils@7.10.0': '@prisma/client-runtime-utils@7.10.1':
resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==} resolution: {integrity: sha512-AzoATVIgmO3lcZfM443+9SI7+cyLMW2+Psw4op8WQHpHvHCX//CKxv8fwhiL0yGWF8DjdQrFewxrZo5McBdB8g==}
'@prisma/client@7.10.0': '@prisma/client@7.10.1':
resolution: {integrity: sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==} resolution: {integrity: sha512-5tgAbFGh//zVOC6WPNVgziOErJHYkUE+wmqCwbIyq6IrTXUYtv9y2iBFGh/Owo99Mx0+cr0j1FOrxkYBMuyhaQ==}
engines: {node: ^20.19 || ^22.12 || >=24.0} engines: {node: ^20.19 || ^22.12 || >=24.0}
peerDependencies: peerDependencies:
prisma: '*' prisma: '*'
@@ -1247,11 +1247,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@prisma/config@7.10.0': '@prisma/config@7.10.1':
resolution: {integrity: sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==} resolution: {integrity: sha512-+0swF5ELUa3P/9cIR26+gpYjjCbGE0qvEBZhV4zMDkYsrtHjZ884MQbOnWFWXj600N0sSJNnHbtBqe12yDngWg==}
'@prisma/debug@7.10.0': '@prisma/debug@7.10.1':
resolution: {integrity: sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==} resolution: {integrity: sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==}
'@prisma/debug@7.2.0': '@prisma/debug@7.2.0':
resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==}
@@ -1259,20 +1259,20 @@ packages:
'@prisma/dev@0.24.17': '@prisma/dev@0.24.17':
resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==} resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==}
'@prisma/driver-adapter-utils@7.10.0': '@prisma/driver-adapter-utils@7.10.1':
resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==} resolution: {integrity: sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==}
'@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': '@prisma/engines-version@8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8':
resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==} resolution: {integrity: sha512-XjXmwjN2NE340RpXoXv8ZF8aNAvr0FE+7gNfkKeW6vyjXs7F8uxvEbWQoVrhioXDQoIsnNmv7r+wxvzfdEZaWA==}
'@prisma/engines@7.10.0': '@prisma/engines@7.10.1':
resolution: {integrity: sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==} resolution: {integrity: sha512-R6/ghv1Bu36FhCdQLFof1pNtyoWoeDzJLYeTKjgqb4eGtOAJx7ZqJtcm4pzkahLfZ9rYz8AkmsMDwkyiYbXPCQ==}
'@prisma/fetch-engine@7.10.0': '@prisma/fetch-engine@7.10.1':
resolution: {integrity: sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==} resolution: {integrity: sha512-qZezeaExLWG2ptbHOrMxdiWluKHgqEclTdJbvY6pk6+Wg7EiVkeq5U3cXz9ti8nd0bngNvsDgledLHVRg5LF1w==}
'@prisma/get-platform@7.10.0': '@prisma/get-platform@7.10.1':
resolution: {integrity: sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==} resolution: {integrity: sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==}
'@prisma/get-platform@7.2.0': '@prisma/get-platform@7.2.0':
resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==}
@@ -2305,9 +2305,9 @@ packages:
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge-ts@7.1.5: deepmerge-ts@8.0.2:
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.9.0'}
define-data-property@1.1.4: define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
@@ -2323,10 +2323,6 @@ packages:
delaunator@5.1.0: delaunator@5.1.0:
resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
destr@2.0.5: destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
@@ -2440,8 +2436,8 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'} engines: {node: '>=10'}
eslint-config-next@16.4.0: eslint-config-next@16.4.2:
resolution: {integrity: sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==} resolution: {integrity: sha512-P33vjcETMXc+qBjpF77TeJHZLzk6cPcTy8KjpxLucYB3mCIFIORMcZ+cBXcAiIYjE5RM7jyknEEOwMNdnx/ADQ==}
peerDependencies: peerDependencies:
eslint: '>=9.0.0' eslint: '>=9.0.0'
typescript: '>=3.3.1' typescript: '>=3.3.1'
@@ -3221,9 +3217,11 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mysql2@3.15.3: mysql2@3.24.4:
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} resolution: {integrity: sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==}
engines: {node: '>= 8.0'} engines: {node: '>= 8.0'}
peerDependencies:
'@types/node': '>= 8'
named-placeholders@1.1.6: named-placeholders@1.1.6:
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
@@ -3245,8 +3243,8 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@16.4.0: next@16.4.2:
resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==} resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==}
engines: {node: '>=20.9.0'} engines: {node: '>=20.9.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3493,8 +3491,8 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
prisma@7.10.0: prisma@7.10.1:
resolution: {integrity: sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==} resolution: {integrity: sha512-GoVeErHGGtsSjsflQ1r2BA3RKTXtxNraQJz8OLkLhoLr7OgCscdx4qw4WMM4HnERYc+a1cq2VnMNbep+vOjNcQ==}
engines: {node: ^20.19 || ^22.12 || >=24.0} engines: {node: ^20.19 || ^22.12 || >=24.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -3642,9 +3640,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
serialize-error@13.0.2: serialize-error@13.0.2:
resolution: {integrity: sha512-G+YKwp9I6IvWMe5RlwqxdmTPIcq/G8HK/sU51gdV1XAJznYkRSTB0ph+zB9AfSxG0fx6xrqYA6i9HeIcHFTXow==} resolution: {integrity: sha512-G+YKwp9I6IvWMe5RlwqxdmTPIcq/G8HK/sU51gdV1XAJznYkRSTB0ph+zB9AfSxG0fx6xrqYA6i9HeIcHFTXow==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -3715,9 +3710,9 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
sqlstring@2.3.3: sql-escaper@1.5.2:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==}
engines: {node: '>= 0.6'} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'}
ssh2@1.17.0: ssh2@1.17.0:
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
@@ -4711,37 +4706,37 @@ snapshots:
'@tybys/wasm-util': 0.10.4 '@tybys/wasm-util': 0.10.4
optional: true optional: true
'@next/env@16.4.0': {} '@next/env@16.4.2': {}
'@next/eslint-plugin-next@16.4.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': '@next/eslint-plugin-next@16.4.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
fast-glob: 3.3.1 fast-glob: 3.3.1
transitivePeerDependencies: transitivePeerDependencies:
- eslint - eslint
'@next/swc-darwin-arm64@16.4.0': '@next/swc-darwin-arm64@16.4.2':
optional: true optional: true
'@next/swc-darwin-x64@16.4.0': '@next/swc-darwin-x64@16.4.2':
optional: true optional: true
'@next/swc-linux-arm64-gnu@16.4.0': '@next/swc-linux-arm64-gnu@16.4.2':
optional: true optional: true
'@next/swc-linux-arm64-musl@16.4.0': '@next/swc-linux-arm64-musl@16.4.2':
optional: true optional: true
'@next/swc-linux-x64-gnu@16.4.0': '@next/swc-linux-x64-gnu@16.4.2':
optional: true optional: true
'@next/swc-linux-x64-musl@16.4.0': '@next/swc-linux-x64-musl@16.4.2':
optional: true optional: true
'@next/swc-win32-arm64-msvc@16.4.0': '@next/swc-win32-arm64-msvc@16.4.2':
optional: true optional: true
'@next/swc-win32-x64-msvc@16.4.0': '@next/swc-win32-x64-msvc@16.4.2':
optional: true optional: true
'@node-rs/argon2-android-arm-eabi@2.2.2': '@node-rs/argon2-android-arm-eabi@2.2.2':
@@ -4823,34 +4818,34 @@ snapshots:
dependencies: dependencies:
playwright: 1.63.0 playwright: 1.63.0
'@prisma/adapter-pg@7.10.0': '@prisma/adapter-pg@7.10.1':
dependencies: dependencies:
'@prisma/driver-adapter-utils': 7.10.0 '@prisma/driver-adapter-utils': 7.10.1
'@types/pg': 8.23.1 '@types/pg': 8.23.1
pg: 8.23.1 pg: 8.23.1
postgres-array: 3.0.4 postgres-array: 3.0.4
transitivePeerDependencies: transitivePeerDependencies:
- pg-native - pg-native
'@prisma/client-runtime-utils@7.10.0': {} '@prisma/client-runtime-utils@7.10.1': {}
'@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)': '@prisma/client@7.10.1(prisma@7.10.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)':
dependencies: dependencies:
'@prisma/client-runtime-utils': 7.10.0 '@prisma/client-runtime-utils': 7.10.1
optionalDependencies: optionalDependencies:
prisma: 7.10.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3) prisma: 7.10.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
'@prisma/config@7.10.0': '@prisma/config@7.10.1':
dependencies: dependencies:
c12: 3.3.4 c12: 3.3.4
deepmerge-ts: 7.1.5 deepmerge-ts: 8.0.2
effect: 3.20.0 effect: 3.20.0
empathic: 2.0.0 empathic: 2.0.0
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@prisma/debug@7.10.0': {} '@prisma/debug@7.10.1': {}
'@prisma/debug@7.2.0': {} '@prisma/debug@7.2.0': {}
@@ -4874,28 +4869,28 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@prisma/driver-adapter-utils@7.10.0': '@prisma/driver-adapter-utils@7.10.1':
dependencies: dependencies:
'@prisma/debug': 7.10.0 '@prisma/debug': 7.10.1
'@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': {} '@prisma/engines-version@8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8': {}
'@prisma/engines@7.10.0': '@prisma/engines@7.10.1':
dependencies: dependencies:
'@prisma/debug': 7.10.0 '@prisma/debug': 7.10.1
'@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 '@prisma/engines-version': 8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8
'@prisma/fetch-engine': 7.10.0 '@prisma/fetch-engine': 7.10.1
'@prisma/get-platform': 7.10.0 '@prisma/get-platform': 7.10.1
'@prisma/fetch-engine@7.10.0': '@prisma/fetch-engine@7.10.1':
dependencies: dependencies:
'@prisma/debug': 7.10.0 '@prisma/debug': 7.10.1
'@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3 '@prisma/engines-version': 8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8
'@prisma/get-platform': 7.10.0 '@prisma/get-platform': 7.10.1
'@prisma/get-platform@7.10.0': '@prisma/get-platform@7.10.1':
dependencies: dependencies:
'@prisma/debug': 7.10.0 '@prisma/debug': 7.10.1
'@prisma/get-platform@7.2.0': '@prisma/get-platform@7.2.0':
dependencies: dependencies:
@@ -5854,7 +5849,7 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
deepmerge-ts@7.1.5: {} deepmerge-ts@8.0.2: {}
define-data-property@1.1.4: define-data-property@1.1.4:
dependencies: dependencies:
@@ -5874,8 +5869,6 @@ snapshots:
dependencies: dependencies:
robust-predicates: 3.0.3 robust-predicates: 3.0.3
denque@2.1.0: {}
destr@2.0.5: {} destr@2.0.5: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -6097,9 +6090,9 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-config-next@16.4.0(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): eslint-config-next@16.4.2(@typescript-eslint/parser@8.71.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies: dependencies:
'@next/eslint-plugin-next': 16.4.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) '@next/eslint-plugin-next': 16.4.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
@@ -6880,17 +6873,16 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
mysql2@3.15.3: mysql2@3.24.4(@types/node@24.19.2):
dependencies: dependencies:
'@types/node': 24.19.2
aws-ssl-profiles: 1.1.2 aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1 generate-function: 2.3.1
iconv-lite: 0.7.3 iconv-lite: 0.7.3
long: 5.3.2 long: 5.3.2
lru.min: 1.1.5 lru.min: 1.1.5
named-placeholders: 1.1.6 named-placeholders: 1.1.6
seq-queue: 0.0.5 sql-escaper: 1.5.2
sqlstring: 2.3.3
named-placeholders@1.1.6: named-placeholders@1.1.6:
dependencies: dependencies:
@@ -6905,9 +6897,9 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next@16.4.0(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(@types/node@24.19.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): next@16.4.2(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(@types/node@24.19.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies: dependencies:
'@next/env': 16.4.0 '@next/env': 16.4.2
'@swc/helpers': 0.5.23 '@swc/helpers': 0.5.23
baseline-browser-mapping: 2.11.28 baseline-browser-mapping: 2.11.28
caniuse-lite: 1.0.30001815 caniuse-lite: 1.0.30001815
@@ -6916,14 +6908,14 @@ snapshots:
react-dom: 19.3.0(react@19.3.0) react-dom: 19.3.0(react@19.3.0)
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 16.4.0 '@next/swc-darwin-arm64': 16.4.2
'@next/swc-darwin-x64': 16.4.0 '@next/swc-darwin-x64': 16.4.2
'@next/swc-linux-arm64-gnu': 16.4.0 '@next/swc-linux-arm64-gnu': 16.4.2
'@next/swc-linux-arm64-musl': 16.4.0 '@next/swc-linux-arm64-musl': 16.4.2
'@next/swc-linux-x64-gnu': 16.4.0 '@next/swc-linux-x64-gnu': 16.4.2
'@next/swc-linux-x64-musl': 16.4.0 '@next/swc-linux-x64-musl': 16.4.2
'@next/swc-win32-arm64-msvc': 16.4.0 '@next/swc-win32-arm64-msvc': 16.4.2
'@next/swc-win32-x64-msvc': 16.4.0 '@next/swc-win32-x64-msvc': 16.4.2
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
'@playwright/test': 1.63.0 '@playwright/test': 1.63.0
sharp: 0.35.5(@types/node@24.19.2) sharp: 0.35.5(@types/node@24.19.2)
@@ -7160,17 +7152,18 @@ snapshots:
prettier@3.9.10: {} prettier@3.9.10: {}
prisma@7.10.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3): prisma@7.10.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3):
dependencies: dependencies:
'@prisma/config': 7.10.0 '@prisma/config': 7.10.1
'@prisma/dev': 0.24.17(typescript@6.0.3) '@prisma/dev': 0.24.17(typescript@6.0.3)
'@prisma/engines': 7.10.0 '@prisma/engines': 7.10.1
'@prisma/studio-core': 0.33.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@prisma/studio-core': 0.33.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
mysql2: 3.15.3 mysql2: 3.24.4(@types/node@24.19.2)
postgres: 3.4.7 postgres: 3.4.7
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node'
- '@types/react' - '@types/react'
- '@types/react-dom' - '@types/react-dom'
- magicast - magicast
@@ -7326,8 +7319,6 @@ snapshots:
semver@7.8.5: {} semver@7.8.5: {}
seq-queue@0.0.5: {}
serialize-error@13.0.2: serialize-error@13.0.2:
dependencies: dependencies:
non-error: 0.1.0 non-error: 0.1.0
@@ -7437,7 +7428,7 @@ snapshots:
split2@4.2.0: {} split2@4.2.0: {}
sqlstring@2.3.3: {} sql-escaper@1.5.2: {}
ssh2@1.17.0: ssh2@1.17.0:
dependencies: dependencies:
+6 -3
View File
@@ -7,6 +7,9 @@ import { katalogLaden } from "@/engine/katalog/laden";
import { profileAufloesen } from "@/engine/profile/aufloesen"; import { profileAufloesen } from "@/engine/profile/aufloesen";
import { profileRendern } from "@/engine/profile/rendern"; import { profileRendern } from "@/engine/profile/rendern";
/** Gast-Benutzer; „admin“ prüft den Fall „Gruppe gibt es schon“ (Ubuntu 22.04). */
const BENUTZER = process.env["PD_BENUTZER"] ?? "florian";
const [distroId = "debian-13", auswahl = "base", ...zuweisungen] = process.argv.slice(2); const [distroId = "debian-13", auswahl = "base", ...zuweisungen] = process.argv.slice(2);
const katalog = await katalogLaden("catalog"); const katalog = await katalogLaden("catalog");
const distro = katalog.distributionen.find((d) => d.eintrag.id === distroId)?.eintrag; const distro = katalog.distributionen.find((d) => d.eintrag.id === distroId)?.eintrag;
@@ -29,12 +32,12 @@ const { skript } = profileRendern(
adapter, adapter,
{ {
os: distro.os, os: distro.os,
gast: { name: "testgast", benutzer: "florian" }, gast: { name: "testgast", benutzer: BENUTZER },
}, },
[ [
adapter.benutzerAnlegen({ adapter.benutzerAnlegen({
name: "florian", name: BENUTZER,
sshSchluessel: ["ssh-ed25519 AAAAtest florian@test"], sshSchluessel: [`ssh-ed25519 AAAAtest ${BENUTZER}@test`],
sudo: true, sudo: true,
}), }),
], ],
+22 -19
View File
@@ -36,6 +36,8 @@ export type AssistentDaten = {
bridges: Array<{ name: string; vlan: boolean }>; bridges: Array<{ name: string; vlan: boolean }>;
}>; }>;
templates: Array<{ distribution: string; knoten: string; vmid: number }>; templates: Array<{ distribution: string; knoten: string; vmid: number }>;
/** Standardwerte aus den Server-Einstellungen. */
standards: { knoten?: string; storage?: string; bridge?: string } | null;
}>; }>;
distributionen: Array<{ id: string; name: string; familie: string; vm: boolean; lxc: boolean }>; distributionen: Array<{ id: string; name: string; familie: string; vm: boolean; lxc: boolean }>;
profile: Array<{ profile: Array<{
@@ -74,6 +76,17 @@ type Plan = {
ok: boolean; ok: boolean;
}; };
/** Node, Storage und Bridge vorbelegen – Standards des Servers zuerst, sonst das Erste, was passt. */
function vorbelegung(s: AssistentDaten["server"][number], knotenName?: string) {
const std = s.standards ?? {};
const k = s.knoten.find((x) => x.name === (knotenName || std.knoten)) ?? s.knoten[0];
const vmStorages = k?.storages.filter((x) => x.vm) ?? [];
const storage = (vmStorages.find((x) => x.name === std.storage) ?? vmStorages[0])?.name ?? "";
const bridge =
[std.bridge, "vmbr0"].find((b) => b && k?.bridges.some((x) => x.name === b)) ?? k?.bridges[0]?.name ?? "";
return { knoten: k?.name ?? "", storage, bridge };
}
// ── Zustand ───────────────────────────────────────────────────────────────────────────────────── // ── Zustand ─────────────────────────────────────────────────────────────────────────────────────
type Werte = { type Werte = {
@@ -295,10 +308,10 @@ export function Assistent({
const anfang = useMemo<Werte>(() => { const anfang = useMemo<Werte>(() => {
const server = daten.server.find((s) => s.name === vorauswahl) ?? daten.server[0]!; const server = daten.server.find((s) => s.name === vorauswahl) ?? daten.server[0]!;
const knoten = server.knoten[0]; const vor = vorbelegung(server);
return { return {
server: server.name, server: server.name,
knoten: knoten?.name ?? "", knoten: vor.knoten,
typ: "vm", typ: "vm",
distribution: distribution:
daten.distributionen.find((d) => d.id === "debian-13")?.id ?? daten.distributionen[0]?.id ?? "", daten.distributionen.find((d) => d.id === "debian-13")?.id ?? daten.distributionen[0]?.id ?? "",
@@ -308,11 +321,11 @@ export function Assistent({
cores: "2", cores: "2",
speicherMb: "2048", speicherMb: "2048",
diskGb: "20", diskGb: "20",
storage: knoten?.storages.find((s) => s.vm)?.name ?? "", storage: vor.storage,
swapMb: "512", swapMb: "512",
unprivilegiert: true, unprivilegiert: true,
nesting: false, nesting: true,
bridge: knoten?.bridges.find((b) => b.name === "vmbr0")?.name ?? knoten?.bridges[0]?.name ?? "", bridge: vor.bridge,
vlan: "", vlan: "",
modus: "dhcp", modus: "dhcp",
ip: "", ip: "",
@@ -554,13 +567,7 @@ export function Assistent({
value={werte.server} value={werte.server}
onChange={(e) => { onChange={(e) => {
const s = daten.server.find((x) => x.name === e.target.value)!; const s = daten.server.find((x) => x.name === e.target.value)!;
const k = s.knoten[0]; setze({ server: s.name, ...vorbelegung(s) });
setze({
server: s.name,
knoten: k?.name ?? "",
storage: k?.storages.find((x) => x.vm)?.name ?? "",
bridge: k?.bridges[0]?.name ?? "",
});
}} }}
> >
{daten.server.map((s) => ( {daten.server.map((s) => (
@@ -580,12 +587,8 @@ export function Assistent({
className={EINGABE_KLASSE} className={EINGABE_KLASSE}
value={werte.knoten} value={werte.knoten}
onChange={(e) => { onChange={(e) => {
const k = server.knoten.find((x) => x.name === e.target.value); const vor = vorbelegung(server, e.target.value);
setze({ setze({ knoten: e.target.value, storage: vor.storage, bridge: vor.bridge });
knoten: e.target.value,
storage: k?.storages.find((x) => x.vm)?.name ?? werte.storage,
bridge: k?.bridges[0]?.name ?? werte.bridge,
});
}} }}
> >
<option value="">automatisch</option> <option value="">automatisch</option>
@@ -814,7 +817,7 @@ export function Assistent({
hilfe={ hilfe={
werte.profile.includes("docker") werte.profile.includes("docker")
? "Das Profil docker schaltet es ohnehin ein." ? "Das Profil docker schaltet es ohnehin ein."
: "Für Docker oder systemd-Dienste mit eigenem Namensraum." : "Standard an, wie in Proxmox: neuere systemd-Versionen brauchen es im unprivilegierten Container."
} }
checked={werte.nesting || werte.profile.includes("docker")} checked={werte.nesting || werte.profile.includes("docker")}
disabled={werte.profile.includes("docker")} disabled={werte.profile.includes("docker")}
+1
View File
@@ -58,6 +58,7 @@ export default async function Ausrollen({
})), })),
bridges: k.bridges.map((b) => ({ name: b.name, vlan: b.vlanBewusst })), bridges: k.bridges.map((b) => ({ name: b.name, vlan: b.vlanBewusst })),
})), })),
standards: (s.standards as { knoten?: string; storage?: string; bridge?: string } | null) ?? null,
templates: templates templates: templates
.filter((t) => t.serverId === s.id) .filter((t) => t.serverId === s.id)
.map((t) => ({ distribution: t.distributionId, knoten: t.knoten, vmid: t.vmid })), .map((t) => ({ distribution: t.distributionId, knoten: t.knoten, vmid: t.vmid })),
+2 -1
View File
@@ -24,7 +24,8 @@ export default async function Uebersicht() {
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Übersicht</h1> <h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Übersicht</h1>
</header> </header>
<div className="grid gap-6 lg:grid-cols-2"> {/* min-w-0: Gitterspalten schrumpfen sonst nicht unter lange Zeilen – am Handy zoomte der Browser dann heraus. */}
<div className="grid gap-6 lg:grid-cols-2 [&>*]:min-w-0">
<Karte <Karte
titel="Server" titel="Server"
aktion={ aktion={
+92 -1
View File
@@ -46,6 +46,9 @@ type Werte = {
bauIp: string; bauIp: string;
bauGateway: string; bauGateway: string;
bauDns: string; bauDns: string;
stdKnoten: string;
stdStorage: string;
stdBridge: string;
}; };
function anfangswerte(server?: ServerSicht): Werte { function anfangswerte(server?: ServerSicht): Werte {
@@ -66,6 +69,9 @@ function anfangswerte(server?: ServerSicht): Werte {
bauIp: server?.bauNetz?.ip ?? "", bauIp: server?.bauNetz?.ip ?? "",
bauGateway: server?.bauNetz?.gateway ?? "", bauGateway: server?.bauNetz?.gateway ?? "",
bauDns: server?.bauNetz?.dns.join(", ") ?? "", bauDns: server?.bauNetz?.dns.join(", ") ?? "",
stdKnoten: server?.standards?.knoten ?? "",
stdStorage: server?.standards?.storage ?? "",
stdBridge: server?.standards?.bridge ?? "",
}; };
} }
@@ -123,7 +129,7 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
setSpeichert(true); setSpeichert(true);
setFelder({}); setFelder({});
setMeldung(null); setMeldung(null);
const { bauIp, bauGateway, bauDns, ...ohneBau } = werte; const { bauIp, bauGateway, bauDns, stdKnoten, stdStorage, stdBridge, ...ohneBau } = werte;
const rumpf = { const rumpf = {
...ohneBau, ...ohneBau,
sshPort: Number(werte.sshPort), sshPort: Number(werte.sshPort),
@@ -133,6 +139,7 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
bauNetz: bauIp.trim() bauNetz: bauIp.trim()
? { ip: bauIp.trim(), gateway: bauGateway.trim(), dns: bauDns.split(/[\s,;]+/).filter(Boolean) } ? { ip: bauIp.trim(), gateway: bauGateway.trim(), dns: bauDns.split(/[\s,;]+/).filter(Boolean) }
: null, : null,
standards: { knoten: stdKnoten, storage: stdStorage, bridge: stdBridge },
}; };
try { try {
const gespeichert = bearbeiten const gespeichert = bearbeiten
@@ -409,6 +416,8 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
</div> </div>
</fieldset> </fieldset>
{server?.letzterTest ? <StandardwerteFelder server={server} werte={werte} setze={setze} /> : null}
<Schalter <Schalter
id="sshAktiv" id="sshAktiv"
label="SSH zu den Nodes nutzen" label="SSH zu den Nodes nutzen"
@@ -469,3 +478,85 @@ function fehlerText(fehler: unknown): { text: string; hinweis?: string } {
if (fehler instanceof ApiFehler) return { text: fehler.message, hinweis: fehler.hinweis }; if (fehler instanceof ApiFehler) return { text: fehler.message, hinweis: fehler.hinweis };
return { text: "Unerwarteter Fehler." }; return { text: "Unerwarteter Fehler." };
} }
/**
* Standardwerte für Assistent und CLI – Auswahl aus dem letzten Verbindungstest. „Automatisch“:
* ProxDeploy nimmt den ersten passenden Node, Storage bzw. vmbr0.
*/
function StandardwerteFelder({
server,
werte,
setze,
}: {
server: ServerSicht;
werte: Werte;
setze: <K extends keyof Werte>(feld: K, wert: Werte[K]) => void;
}) {
const knotenListe = (server.letzterTest?.knoten ?? []).filter((k) => k.online);
const knoten = knotenListe.find((k) => k.name === werte.stdKnoten) ?? knotenListe[0];
const storages = (knoten?.storages ?? []).filter(
(st) => st.aktiv && (st.inhalte.includes("images") || st.inhalte.includes("rootdir")),
);
const bridges = knoten?.bridges ?? [];
// Ein gespeicherter Wert, den der letzte Test nicht mehr kennt, bleibt sichtbar statt still zu verschwinden.
const mitAltem = (liste: string[], wert: string) =>
wert && !liste.includes(wert) ? [...liste, wert] : liste;
return (
<fieldset className="flex flex-col gap-3">
<legend className="text-sm font-medium">Standardwerte</legend>
<p className="text-muted text-sm">
Vorbelegung im Assistenten; die CLI nimmt sie, wenn --node, --storage oder --bridge fehlen.
</p>
<div className="grid gap-4 sm:grid-cols-3">
<Feld id="stdKnoten" label="Node">
<select
id="stdKnoten"
className={EINGABE_KLASSE}
value={werte.stdKnoten}
onChange={(e) => setze("stdKnoten", e.target.value)}
>
<option value="">automatisch</option>
{mitAltem(
knotenListe.map((k) => k.name),
werte.stdKnoten,
).map((n) => (
<option key={n}>{n}</option>
))}
</select>
</Feld>
<Feld id="stdStorage" label="Storage">
<select
id="stdStorage"
className={EINGABE_KLASSE}
value={werte.stdStorage}
onChange={(e) => setze("stdStorage", e.target.value)}
>
<option value="">automatisch</option>
{mitAltem(
storages.map((st) => st.storage),
werte.stdStorage,
).map((n) => (
<option key={n}>{n}</option>
))}
</select>
</Feld>
<Feld id="stdBridge" label="Bridge">
<select
id="stdBridge"
className={EINGABE_KLASSE}
value={werte.stdBridge}
onChange={(e) => setze("stdBridge", e.target.value)}
>
<option value="">automatisch (vmbr0)</option>
{mitAltem(
bridges.map((b) => b.name),
werte.stdBridge,
).map((n) => (
<option key={n}>{n}</option>
))}
</select>
</Feld>
</div>
</fieldset>
);
}
+4 -2
View File
@@ -58,7 +58,9 @@ export function Seitenleiste({ eintraege, benutzer, version, theme, serverUmscha
} }
const inhalt = ( const inhalt = (
<div className="flex h-full flex-col gap-6 p-4"> // min-h-full statt h-full: ist der Inhalt höher als der Bildschirm (Handy quer, viele Einträge),
// wächst er und die Leiste scrollt, statt dass die Navigation den Fuß mit „Abmelden“ überdeckt.
<div className="flex min-h-full flex-col gap-6 p-4">
<Link href="/" className="flex items-center gap-2.5 px-2 pt-1" onClick={() => setOffen(false)}> <Link href="/" className="flex items-center gap-2.5 px-2 pt-1" onClick={() => setOffen(false)}>
<Symbol className="h-8 w-8 shrink-0" /> <Symbol className="h-8 w-8 shrink-0" />
<Wortmarke className="text-lg" /> <Wortmarke className="text-lg" />
@@ -148,7 +150,7 @@ export function Seitenleiste({ eintraege, benutzer, version, theme, serverUmscha
<aside <aside
id="seitenleiste" id="seitenleiste"
className={clsx( className={clsx(
"border-linie bg-panel fixed inset-y-0 left-0 z-50 w-72 border-r transition-transform lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0", "border-linie bg-panel fixed inset-y-0 left-0 z-50 w-72 overflow-y-auto border-r transition-transform lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0",
offen ? "translate-x-0" : "-translate-x-full", offen ? "translate-x-0" : "-translate-x-full",
)} )}
> >
+23 -24
View File
@@ -245,31 +245,30 @@ export function lxcAusrollSchritte(e: LxcAusrollEingabe, stand: LxcAusrollStand
ausfuehren: async () => { ausfuehren: async () => {
if (stand.weg === "gast") bereitstellung = await e.bereitstellung(); if (stand.weg === "gast") bereitstellung = await e.bereitstellung();
const featureListe = [features.nesting ? "nesting=1" : null].filter(Boolean).join(","); const featureListe = [features.nesting ? "nesting=1" : null].filter(Boolean).join(",");
await aufgabeAbwarten( const upid = await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, {
client, vmid: stand.vmid!,
await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, { hostname: a.name,
vmid: stand.vmid!, ostemplate: stand.ostemplate!,
hostname: a.name, ostype: lxc!.ostype,
ostemplate: stand.ostemplate!, rootfs: `${a.storage}:${a.diskGb}`,
ostype: lxc!.ostype, cores: a.cores,
rootfs: `${a.storage}:${a.diskGb}`, memory: a.speicherMb,
cores: a.cores, swap: a.swapMb,
memory: a.speicherMb, unprivileged: a.unprivilegiert,
swap: a.swapMb, ...(featureListe ? { features: featureListe } : {}),
unprivileged: a.unprivilegiert, net0: lxcNetz(a.netz),
...(featureListe ? { features: featureListe } : {}), ...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}),
net0: lxcNetz(a.netz), ...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}),
...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}), // Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel.
...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}), ...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}),
// Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel. onboot: a.startBeiBoot,
...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}), tags: ["proxdeploy", d.id, ...a.tags].join(";"),
onboot: a.startBeiBoot, description: `Ausgerollt mit ProxDeploy (Auftrag ${e.auftragKurzId})\nDistribution: ${d.name}\nProfile: ${e.profile.map((p) => p.profil.id).join(", ") || "–"}`,
tags: ["proxdeploy", d.id, ...a.tags].join(";"), });
description: `Ausgerollt mit ProxDeploy (Auftrag ${e.auftragKurzId})\nDistribution: ${d.name}\nProfile: ${e.profile.map((p) => p.profil.id).join(", ") || "–"}`, // Sofort merken, nicht erst nach dem Warten: bricht das Warten ab, legt Proxmox den
}), // Container trotzdem fertig an – „Aufräumen“ muss ihn dann kennen.
{ zeitlimitMs: 15 * 60 * 1000 },
);
await e.angelegt({ vmid: stand.vmid }); await e.angelegt({ vmid: stand.vmid });
await aufgabeAbwarten(client, upid, { zeitlimitMs: 15 * 60 * 1000 });
if (features.keyctl) { if (features.keyctl) {
const ssh = e.sshFuer(stand.knoten!)!; const ssh = e.sshFuer(stand.knoten!)!;
if (!client.trocken) { if (!client.trocken) {
+4 -1
View File
@@ -470,7 +470,10 @@ export function templateBauSchritte(e: TemplateBauEingabe, stand: TemplateBauSta
serial0: "socket", serial0: "socket",
vga: "serial0", vga: "serial0",
agent: "enabled=1,fstrim_cloned_disks=1", agent: "enabled=1,fstrim_cloned_disks=1",
ide2: `${e.storage}:cloudinit`, // cloud-init-Laufwerk am virtio-SCSI-Controller, nicht an ide2: q35 hängt IDE an SATA (AHCI),
// und der „cloud“-Kernel von Debian 12 hat keinen Treiber dafür – cloud-init fände keine
// Datenquelle und bliebe stumm (gefunden bei der Abnahme von #021).
scsi1: `${e.storage}:cloudinit`,
ipconfig0: "ip=dhcp", ipconfig0: "ip=dhcp",
ciupgrade: false, ciupgrade: false,
tags: ["proxdeploy-template", d.id, ...profilIds.map((p) => `profil-${p}`)].join(";"), tags: ["proxdeploy-template", d.id, ...profilIds.map((p) => `profil-${p}`)].join(";"),
+25 -26
View File
@@ -234,32 +234,32 @@ export function vmAusrollSchritte(e: VmAusrollEingabe, stand: VmAusrollStand = {
titel: "Klonen", titel: "Klonen",
ausfuehren: async (k) => { ausfuehren: async (k) => {
const tpl = stand.template!; const tpl = stand.template!;
const klonen = async (vmid: number) => const klonen = async (vmid: number) => {
await aufgabeAbwarten( const upid = await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, {
client, newid: vmid,
await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, { name: a.name,
newid: vmid, // Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört.
name: a.name, description: beschreibung(),
// Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört. full: true,
description: beschreibung(), target: stand.knoten!,
full: true, storage: a.storage,
target: stand.knoten!, });
storage: a.storage, // Sofort merken, nicht erst nach dem Warten: der Klon läuft auf dem Node weiter, auch
}), // wenn das Warten abbricht – „Aufräumen“ muss die VM dann kennen.
{ await e.angelegt({ vmid });
zeitlimitMs: 60 * 60 * 1000, await aufgabeAbwarten(client, upid, {
intervallMs: 2000, zeitlimitMs: 60 * 60 * 1000,
// „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon. intervallMs: 2000,
beiLog: (zeilen) => { // „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon.
const letzte = zeilen beiLog: (zeilen) => {
.map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z)) const letzte = zeilen
.filter(Boolean) .map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z))
.at(-1); .filter(Boolean)
if (letzte) .at(-1);
void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`); if (letzte) void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`);
},
}, },
); });
};
try { try {
await klonen(stand.vmid!); await klonen(stand.vmid!);
} catch (fehler) { } catch (fehler) {
@@ -273,7 +273,6 @@ export function vmAusrollSchritte(e: VmAusrollEingabe, stand: VmAusrollStand = {
await k.log(`VMID war inzwischen belegt – neuer Versuch mit ${stand.vmid}`, "warn"); await k.log(`VMID war inzwischen belegt – neuer Versuch mit ${stand.vmid}`, "warn");
await klonen(stand.vmid); await klonen(stand.vmid);
} }
await e.angelegt({ vmid: stand.vmid });
return { detail: `full clone ${tpl.vmid} → ${stand.vmid}` }; return { detail: `full clone ${tpl.vmid} → ${stand.vmid}` };
}, },
}, },
+3 -1
View File
@@ -119,7 +119,9 @@ setupcon --save-only >/dev/null 2>&1 || true
const name = q(b.name); const name = q(b.name);
const schluessel = b.sshSchluessel.join("\n") + "\n"; const schluessel = b.sshSchluessel.join("\n") + "\n";
const zeilen = [ const zeilen = [
`id -u ${name} >/dev/null 2>&1 || useradd -m -s /bin/bash ${name}`, // Gibt es schon eine gleichnamige Gruppe (Ubuntu 22.04 bringt „admin“ mit), wird sie die
// Primärgruppe – sonst bricht useradd mit Code 9 ab.
`id -u ${name} >/dev/null 2>&1 || { if getent group ${name} >/dev/null; then useradd -m -s /bin/bash -g ${name} ${name}; else useradd -m -s /bin/bash ${name}; fi; }`,
`pd_home="$(getent passwd ${name} | cut -d: -f6)"`, `pd_home="$(getent passwd ${name} | cut -d: -f6)"`,
`install -d -m 0700 -o ${name} -g ${name} "$pd_home/.ssh"`, `install -d -m 0700 -o ${name} -g ${name} "$pd_home/.ssh"`,
// Schlüssel ergänzen statt ersetzen: cloud-init hat sie bei VMs vielleicht schon eingetragen. // Schlüssel ergänzen statt ersetzen: cloud-init hat sie bei VMs vielleicht schon eingetragen.
+14 -5
View File
@@ -1,4 +1,4 @@
import { istTrockenUpid, pfad, type ProxmoxClient } from "./client"; import { istTrockenUpid, pfad, stoerung, type ProxmoxClient } from "./client";
import { ProxmoxFehler } from "./fehler"; import { ProxmoxFehler } from "./fehler";
/** /**
@@ -53,10 +53,19 @@ export async function aufgabeAbwarten(
}; };
for (;;) { for (;;) {
const status = await client.get<TaskStatus>(pfad`/nodes/${node}/tasks/${upid}/status`); // Nachsehen ist harmlos: scheitert eine Abfrage vorübergehend (ausgelasteter Node), wird bis
if (optionen.beiLog) { // zum Zeitlimit des Tasks weiter gefragt – der Task läuft auf dem Node ja weiter.
const neu = await logLesen(); let status: TaskStatus;
if (neu.length > 0) optionen.beiLog(neu); try {
status = await client.get<TaskStatus>(pfad`/nodes/${node}/tasks/${upid}/status`);
if (optionen.beiLog) {
const neu = await logLesen();
if (neu.length > 0) optionen.beiLog(neu);
}
} catch (fehler) {
if (stoerung(fehler) === null || Date.now() - beginn > zeitlimit) throw fehler;
await warten(Math.max(intervall, 5000));
continue;
} }
if (status.status === "stopped") { if (status.status === "stopped") {
if (status.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return; if (status.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return;
+70 -12
View File
@@ -23,7 +23,7 @@ export type Aufruf = { methode: Methode; pfad: string; parameter?: Parameter };
export type AufrufBeobachter = ( export type AufrufBeobachter = (
aufruf: Aufruf, aufruf: Aufruf,
ergebnis: { dauerMs: number; fehler?: ProxmoxFehler }, ergebnis: { dauerMs: number; fehler?: ProxmoxFehler; wiederholung?: number },
) => void; ) => void;
/** /**
@@ -38,6 +38,44 @@ export function istTrockenUpid(upid: unknown): boolean {
} }
const STANDARD_ZEITLIMIT_MS = 30_000; const STANDARD_ZEITLIMIT_MS = 30_000;
/**
* Schreibende Aufrufe dürfen länger dauern: ein zu früh abgebrochenes „VM anlegen“ läuft auf dem
* Node womöglich trotzdem – ein zweiter Versuch wäre dann doppelt.
*/
const SCHREIB_ZEITLIMIT_MS = 120_000;
/** Versuche je Aufruf bei vorübergehenden Störungen (nur, wo ein Wiederholen sicher ist). */
const VERSUCHE = 3;
/**
* Ob eine Störung vorübergehend ist – und ob die Anfrage den Node womöglich erreicht hat.
* Ausgelastete Nodes liefern davon reichlich: pveproxy verliert die Verbindung zu pvedaemon
* („Broken pipe“, 595/596), schließt Keep-Alive-Verbindungen, Abfragen laufen ins Zeitlimit.
*/
export function stoerung(fehler: unknown): "nicht_angekommen" | "unklar" | null {
if (fehler instanceof ProxmoxFehler) {
const status = fehler.details.status;
// Schon übersetzte Netzwerkfehler (ohne HTTP-Status); TLS und Fingerprint sind nie vorübergehend.
if (status === undefined && (fehler.art === "zeitueberschreitung" || fehler.art === "verbindung"))
return "unklar";
if (status === 595 || status === 596) return "unklar";
if (
(status === 500 || status === 502 || status === 503) &&
/broken pipe|connection (refused|reset|closed)|got timeout|timed out/i.test(fehler.ursache)
)
return "unklar";
return null;
}
let ursache: unknown = fehler;
for (let i = 0; i < 3 && ursache instanceof Error && ursache.cause; i += 1) ursache = ursache.cause;
const code = (ursache as { code?: string } | undefined)?.code ?? "";
if (code === "ECONNREFUSED") return "nicht_angekommen";
if (
["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT", "ETIMEDOUT"].includes(code) ||
(ursache instanceof Error && (ursache.name === "TimeoutError" || ursache.name === "AbortError"))
)
return "unklar";
return null;
}
/** Formular-Kodierung wie pvesh: Wahrheitswerte als 1/0, Listen als wiederholte Schlüssel. */ /** Formular-Kodierung wie pvesh: Wahrheitswerte als 1/0, Listen als wiederholte Schlüssel. */
export function parameterKodieren(parameter: Parameter | undefined): URLSearchParams { export function parameterKodieren(parameter: Parameter | undefined): URLSearchParams {
@@ -75,6 +113,8 @@ export class ProxmoxClient {
zeitlimitMs?: number; zeitlimitMs?: number;
beobachter?: AufrufBeobachter; beobachter?: AufrufBeobachter;
trocken?: boolean; trocken?: boolean;
/** Pause vor dem n-ten Wiederholen (Tests setzen 0). */
pauseMs?: (versuch: number) => number;
} = {}, } = {},
) { ) {
if (!TOKEN_ID_MUSTER.test(verbindung.tokenId)) { if (!TOKEN_ID_MUSTER.test(verbindung.tokenId)) {
@@ -85,7 +125,7 @@ export class ProxmoxClient {
); );
} }
this.basis = verbindung.url.replace(/\/+$/, "") + "/api2/json"; this.basis = verbindung.url.replace(/\/+$/, "") + "/api2/json";
this.agent = new Agent({ connect: tlsConnector(verbindung), keepAliveTimeout: 10_000 }); this.agent = new Agent({ connect: tlsConnector(verbindung), keepAliveTimeout: 4_000 });
this.kopf = `PVEAPIToken=${verbindung.tokenId}=${verbindung.tokenGeheimnis}`; this.kopf = `PVEAPIToken=${verbindung.tokenId}=${verbindung.tokenGeheimnis}`;
} }
@@ -120,15 +160,30 @@ export class ProxmoxClient {
return `${TROCKEN_UPID_PRAEFIX}${knoten}:${this.geplant.length}` as T; return `${TROCKEN_UPID_PRAEFIX}${knoten}:${this.geplant.length}` as T;
} }
const beginn = Date.now(); const beginn = Date.now();
try { for (let versuch = 1; ; versuch += 1) {
const ergebnis = await this.senden<T>(aufruf); try {
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn }); const ergebnis = await this.senden<T>(aufruf);
return ergebnis; this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn });
} catch (fehler) { return ergebnis;
const proxmoxFehler = } catch (fehler) {
fehler instanceof ProxmoxFehler ? fehler : verbindungsFehler(fehler, this.verbindung.url); // GET darf immer noch einmal; Schreibendes nur, wenn die Anfrage sicher nie ankam.
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn, fehler: proxmoxFehler }); const art = stoerung(fehler);
throw proxmoxFehler; const sicher = art === "nicht_angekommen" || (art === "unklar" && aufruf.methode === "GET");
if (sicher && versuch < VERSUCHE) {
const pause = this.optionen.pauseMs?.(versuch) ?? (versuch === 1 ? 1000 : 3000);
this.optionen.beobachter?.(aufruf, {
dauerMs: Date.now() - beginn,
fehler: fehler instanceof ProxmoxFehler ? fehler : verbindungsFehler(fehler, this.verbindung.url),
wiederholung: versuch,
});
await new Promise((fertig) => setTimeout(fertig, pause));
continue;
}
const proxmoxFehler =
fehler instanceof ProxmoxFehler ? fehler : verbindungsFehler(fehler, this.verbindung.url);
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn, fehler: proxmoxFehler });
throw proxmoxFehler;
}
} }
} }
@@ -146,7 +201,10 @@ export class ProxmoxClient {
...(mitRumpf ? { "content-type": "application/x-www-form-urlencoded" } : {}), ...(mitRumpf ? { "content-type": "application/x-www-form-urlencoded" } : {}),
}, },
body: mitRumpf ? kodiert.toString() : undefined, body: mitRumpf ? kodiert.toString() : undefined,
signal: AbortSignal.timeout(this.optionen.zeitlimitMs ?? STANDARD_ZEITLIMIT_MS), signal: AbortSignal.timeout(
this.optionen.zeitlimitMs ??
(aufruf.methode === "GET" ? STANDARD_ZEITLIMIT_MS : SCHREIB_ZEITLIMIT_MS),
),
}); });
const text = await antwort.text(); const text = await antwort.text();
+6 -1
View File
@@ -104,7 +104,12 @@ export function tlsConnector(einstellung: TlsEinstellung): buildConnector.connec
} }
// Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit, // Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit,
// und der Pin ließe sich dann nicht prüfen. // und der Pin ließe sich dann nicht prüfen.
const basis = buildConnector({ rejectUnauthorized: einstellung.tlsPruefen, maxCachedSessions: 0 }); // 30 s statt der 10 s von undici: ein ausgelasteter Node nimmt neue Verbindungen nur zögerlich an.
const basis = buildConnector({
rejectUnauthorized: einstellung.tlsPruefen,
maxCachedSessions: 0,
timeout: 30_000,
});
if (!pin) return basis; if (!pin) return basis;
return (optionen, rueckruf) => { return (optionen, rueckruf) => {
+4 -2
View File
@@ -98,8 +98,10 @@ export const AusrollAnfrage = z
swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512), swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512),
unprivilegiert: z.boolean().default(true), unprivilegiert: z.boolean().default(true),
features: z features: z
.object({ nesting: z.boolean().default(false), keyctl: z.boolean().default(false) }) // nesting an wie in der Proxmox-Oberfläche: neuere systemd-Versionen brauchen es im
.default({ nesting: false, keyctl: false }), // unprivilegierten Container („Systemd 252 detected. You may need to enable nesting.“).
.object({ nesting: z.boolean().default(true), keyctl: z.boolean().default(false) })
.default({ nesting: true, keyctl: false }),
netz: Netz, netz: Netz,
zugang: Zugang, zugang: Zugang,
profile: z.array(z.string()).default(["base"]), profile: z.array(z.string()).default(["base"]),
+18
View File
@@ -59,6 +59,22 @@ export const BauNetz = z
export type BauNetz = NonNullable<z.infer<typeof BauNetz>>; export type BauNetz = NonNullable<z.infer<typeof BauNetz>>;
const Auswahl = z
.string()
.trim()
.max(64)
.optional()
.transform((wert) => wert || undefined);
/** Standardwerte des Servers für Assistent und CLI (leer = ProxDeploy wählt). */
export const Standards = z
.object({ knoten: Auswahl, storage: Auswahl, bridge: Auswahl })
.nullable()
.default(null)
.transform((s) => (s && (s.knoten || s.storage || s.bridge) ? s : null));
export type Standards = NonNullable<z.infer<typeof Standards>>;
const Vmid = z.coerce.number().int("Ganze Zahl erwartet.").min(100, "Mindestens 100.").max(999_999_999); const Vmid = z.coerce.number().int("Ganze Zahl erwartet.").min(100, "Mindestens 100.").max(999_999_999);
const Basis = z.object({ const Basis = z.object({
@@ -82,6 +98,7 @@ const Basis = z.object({
templateVmidBis: Vmid.default(9999), templateVmidBis: Vmid.default(9999),
gastVmidVon: Vmid.default(100), gastVmidVon: Vmid.default(100),
bauNetz: BauNetz, bauNetz: BauNetz,
standards: Standards,
}); });
function bereicheGueltig<T extends { templateVmidVon: number; templateVmidBis: number }>(wert: T) { function bereicheGueltig<T extends { templateVmidVon: number; templateVmidBis: number }>(wert: T) {
@@ -143,6 +160,7 @@ export type ServerSicht = {
templateVmidBis: number; templateVmidBis: number;
gastVmidVon: number; gastVmidVon: number;
bauNetz: BauNetz | null; bauNetz: BauNetz | null;
standards: Standards | null;
erreichbar: boolean | null; erreichbar: boolean | null;
pveVersion: string | null; pveVersion: string | null;
geprueftAm: string | null; geprueftAm: string | null;
+1 -1
View File
@@ -2,7 +2,7 @@
* Version und Git-Stand für Seitenleiste, `/api/v1/ich` und die Kopfzeile der CLI. * Version und Git-Stand für Seitenleiste, `/api/v1/ich` und die Kopfzeile der CLI.
* Beide Werte werden beim Bauen gesetzt (Docker-Build-Arg bzw. Installer). * Beide Werte werden beim Bauen gesetzt (Docker-Build-Arg bzw. Installer).
*/ */
export const VERSION = process.env["NEXT_PUBLIC_APP_VERSION"] ?? "0.6.0"; export const VERSION = process.env["NEXT_PUBLIC_APP_VERSION"] ?? "1.0.0";
export const GIT_SHA = (process.env["NEXT_PUBLIC_GIT_SHA"] ?? "").slice(0, 7); export const GIT_SHA = (process.env["NEXT_PUBLIC_GIT_SHA"] ?? "").slice(0, 7);
export function versionText(): string { export function versionText(): string {
+4
View File
@@ -12,6 +12,7 @@ import {
ServerAnlegen, ServerAnlegen,
ServerPruefen, ServerPruefen,
type BauNetz, type BauNetz,
type Standards,
type ServerSicht, type ServerSicht,
} from "@/lib/schemas/server"; } from "@/lib/schemas/server";
import { AnwendungsFehler, nichtGefunden } from "@/server/fehler"; import { AnwendungsFehler, nichtGefunden } from "@/server/fehler";
@@ -34,6 +35,7 @@ export function serverSicht(server: Server): ServerSicht {
templateVmidBis: server.templateVmidBis, templateVmidBis: server.templateVmidBis,
gastVmidVon: server.gastVmidVon, gastVmidVon: server.gastVmidVon,
bauNetz: (server.bauNetz as BauNetz | null) ?? null, bauNetz: (server.bauNetz as BauNetz | null) ?? null,
standards: (server.standards as Standards | null) ?? null,
erreichbar: server.erreichbar, erreichbar: server.erreichbar,
pveVersion: server.pveVersion, pveVersion: server.pveVersion,
geprueftAm: server.geprueftAm?.toISOString() ?? null, geprueftAm: server.geprueftAm?.toISOString() ?? null,
@@ -86,6 +88,7 @@ export async function serverAnlegen(
data: { data: {
...eingabe, ...eingabe,
bauNetz: eingabe.bauNetz ?? Prisma.JsonNull, bauNetz: eingabe.bauNetz ?? Prisma.JsonNull,
standards: eingabe.standards ?? Prisma.JsonNull,
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck), fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
tokenGeheimnis: verschluesseln(eingabe.tokenGeheimnis), tokenGeheimnis: verschluesseln(eingabe.tokenGeheimnis),
}, },
@@ -121,6 +124,7 @@ export async function serverAendern(
const daten = { const daten = {
...rest, ...rest,
bauNetz: rest.bauNetz ?? Prisma.JsonNull, bauNetz: rest.bauNetz ?? Prisma.JsonNull,
standards: rest.standards ?? Prisma.JsonNull,
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck), fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
...(tokenGeheimnis ? { tokenGeheimnis: verschluesseln(tokenGeheimnis) } : {}), ...(tokenGeheimnis ? { tokenGeheimnis: verschluesseln(tokenGeheimnis) } : {}),
}; };
+3 -1
View File
@@ -14,7 +14,9 @@ export function aufrufLogger(senke: EreignisSenke): AufrufBeobachter {
if (!ergebnis.fehler && aufruf.methode === "GET" && SCHLEIFE.test(pfad)) return; if (!ergebnis.fehler && aufruf.methode === "GET" && SCHLEIFE.test(pfad)) return;
void senke.log( void senke.log(
ergebnis.fehler ? "warn" : "debug", ergebnis.fehler ? "warn" : "debug",
`${aufruf.methode} ${pfad} · ${ergebnis.dauerMs} ms${ergebnis.fehler ? ` · ${ergebnis.fehler.ursache}` : ""}`, `${aufruf.methode} ${pfad} · ${ergebnis.dauerMs} ms${ergebnis.fehler ? ` · ${ergebnis.fehler.ursache}` : ""}${
ergebnis.wiederholung ? ` · vorübergehend, Versuch ${ergebnis.wiederholung + 1} folgt` : ""
}`,
); );
}; };
} }
+4 -2
View File
@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
# Läuft im Container: systemctl-Stub, Skript ausführen, Ergebnis prüfen. # Läuft im Container: systemctl-Stub, Skript ausführen, Ergebnis prüfen.
printf '#!/bin/sh\necho "[systemctl] $*" >> /tmp/systemctl.log\n' > /usr/local/bin/systemctl && chmod +x /usr/local/bin/systemctl printf '#!/bin/sh\necho "[systemctl] $*" >> /tmp/systemctl.log\n' > /usr/local/bin/systemctl && chmod +x /usr/local/bin/systemctl
# Wie im Ubuntu-22.04-Cloud-Image: eine Gruppe „admin“ gibt es schon, der Benutzer heißt genauso.
getent group admin >/dev/null || groupadd admin
bash "$1" > /tmp/lauf.log 2>&1; code=$? bash "$1" > /tmp/lauf.log 2>&1; code=$?
grep '::proxdeploy::' /tmp/lauf.log grep '::proxdeploy::' /tmp/lauf.log
echo "exit=$code" echo "exit=$code"
@@ -10,7 +12,7 @@ docker --version; docker compose version
zabbix_agent2 -V | head -1; cat /etc/zabbix/zabbix_agent2.d/proxdeploy.conf zabbix_agent2 -V | head -1; cat /etc/zabbix/zabbix_agent2.d/proxdeploy.conf
cat /etc/timezone; readlink /etc/localtime; grep LANG /etc/default/locale; locale -a | grep -i de_de cat /etc/timezone; readlink /etc/localtime; grep LANG /etc/default/locale; locale -a | grep -i de_de
grep XKBLAYOUT /etc/default/keyboard grep XKBLAYOUT /etc/default/keyboard
id florian; cat /etc/sudoers.d/90-proxdeploy-florian; cat ~florian/.ssh/authorized_keys id admin; cat /etc/sudoers.d/90-proxdeploy-admin; cat ~admin/.ssh/authorized_keys
cat /etc/apt/apt.conf.d/20auto-upgrades; dpkg -l fail2ban chrony qemu-guest-agent | grep ^ii | awk '{print $2}' cat /etc/apt/apt.conf.d/20auto-upgrades; dpkg -l fail2ban chrony qemu-guest-agent | grep ^ii | awk '{print $2}'
cat /tmp/systemctl.log | sort -u | tr '\n' ' '; echo cat /tmp/systemctl.log | sort -u | tr '\n' ' '; echo
echo "--- Zweiter Lauf (Idempotenz)"; bash "$1" > /tmp/lauf2.log 2>&1; echo "exit2=$?"; grep -c florian ~florian/.ssh/authorized_keys echo "--- Zweiter Lauf (Idempotenz)"; bash "$1" > /tmp/lauf2.log 2>&1; echo "exit2=$?"; grep -c admin ~admin/.ssh/authorized_keys
+1 -1
View File
@@ -16,7 +16,7 @@ arbeit="$(mktemp -d)"; trap 'rm -rf "$arbeit"' EXIT
cp tests/container/im-gast-pruefen.sh "$arbeit/" cp tests/container/im-gast-pruefen.sh "$arbeit/"
fehler=0 fehler=0
for distro in ${ziele[*]}; do for distro in ${ziele[*]}; do
pnpm -s exec tsx scripts/profil-rendern.ts "$distro" docker,zabbix-agent server=zbx.example fail2ban=true > "$arbeit/$distro.sh" PD_BENUTZER=admin pnpm exec tsx scripts/profil-rendern.ts "$distro" docker,zabbix-agent server=zbx.example fail2ban=true > "$arbeit/$distro.sh"
echo "=== $distro (${IMAGES[$distro]})" echo "=== $distro (${IMAGES[$distro]})"
if docker run --rm -v "$arbeit:/s:ro" "${IMAGES[$distro]}" bash /s/im-gast-pruefen.sh "/s/$distro.sh" > "$arbeit/$distro.log" 2>&1 \ if docker run --rm -v "$arbeit:/s:ro" "${IMAGES[$distro]}" bash /s/im-gast-pruefen.sh "/s/$distro.sh" > "$arbeit/$distro.log" 2>&1 \
&& grep -q '^exit=0$' "$arbeit/$distro.log" && grep -q '^exit2=0$' "$arbeit/$distro.log"; then && grep -q '^exit=0$' "$arbeit/$distro.log" && grep -q '^exit2=0$' "$arbeit/$distro.log"; then
+9 -3
View File
@@ -12,12 +12,18 @@ export async function anmelden(page: Page, ziel = "/") {
await expect(page).not.toHaveURL(/\/anmelden/); await expect(page).not.toHaveURL(/\/anmelden/);
} }
/**
* Gegen die eingestellte Bildschirmbreite messen, nicht gegen innerWidth: bei mobiler Emulation
* zoomt der Browser bei zu breitem Inhalt heraus, und innerWidth wächst einfach mit.
*/
export async function keinQuerscrollen(page: Page) { export async function keinQuerscrollen(page: Page) {
const breiten = await page.evaluate(() => ({ const breite = page.viewportSize()!.width;
inhalt: document.documentElement.scrollWidth, const inhalt = await page.evaluate(() => ({
scroll: document.documentElement.scrollWidth,
fenster: window.innerWidth, fenster: window.innerWidth,
})); }));
expect(breiten.inhalt).toBeLessThanOrEqual(breiten.fenster); expect(inhalt.fenster, "Der Browser hat herausgezoomt – etwas ist breiter als der Bildschirm").toBe(breite);
expect(inhalt.scroll).toBeLessThanOrEqual(breite);
} }
/** Mit dem Präfix zz-e2e lassen sich Testdaten erkennen und aufräumen. */ /** Mit dem Präfix zz-e2e lassen sich Testdaten erkennen und aufräumen. */
+3 -1
View File
@@ -6,6 +6,8 @@ import { anmelden, E2E_PRAEFIX, keinQuerscrollen } from "./helfer";
* keinen, wird der Ablauf übersprungen und sagt das. * keinen, wird der Ablauf übersprungen und sagt das.
*/ */
test("Vorlage im Assistenten anwenden und als neue Vorlage speichern", async ({ page }) => { test("Vorlage im Assistenten anwenden und als neue Vorlage speichern", async ({ page }) => {
// Der Dry-Run plant wirklich gegen den Proxmox-Host – auf dem langsamen Testhost dauert das.
test.setTimeout(120_000);
const name = `${E2E_PRAEFIX}-${test.info().project.name}`; const name = `${E2E_PRAEFIX}-${test.info().project.name}`;
await anmelden(page, "/vorlagen"); await anmelden(page, "/vorlagen");
await expect(page.getByText("Docker-Host klein")).toBeVisible(); await expect(page.getByText("Docker-Host klein")).toBeVisible();
@@ -30,7 +32,7 @@ test("Vorlage im Assistenten anwenden und als neue Vorlage speichern", async ({
await page.getByRole("button", { name: "Weiter" }).click(); await page.getByRole("button", { name: "Weiter" }).click();
await expect(page.getByLabel("Docker CE")).toBeChecked(); await expect(page.getByLabel("Docker CE")).toBeChecked();
await page.getByRole("button", { name: "Weiter" }).click(); await page.getByRole("button", { name: "Weiter" }).click();
await expect(page.getByText(/--preset docker-host-klein/)).toBeVisible({ timeout: 30_000 }); await expect(page.getByText(/--preset docker-host-klein/)).toBeVisible({ timeout: 90_000 });
await page.getByRole("button", { name: "Als Vorlage speichern" }).click(); await page.getByRole("button", { name: "Als Vorlage speichern" }).click();
await page.getByLabel("Kurzname (für --preset)").fill(name); await page.getByLabel("Kurzname (für --preset)").fill(name);
+15
View File
@@ -127,6 +127,21 @@ async function eingabe(
} }
describe("LXC ausrollen", () => { describe("LXC ausrollen", () => {
it("merkt sich die CTID schon nach dem Absenden – auch wenn die Anlage danach scheitert", async () => {
const { mock, client } = await aufbauen({ templateLokal: ["debian-13-standard_13.6-1_amd64.tar.zst"] });
mock.route("GET", /\/tasks\/[^/]+vzcreate[^/]*\/status$/, () => ({
data: { status: "stopped", exitstatus: "unable to create CT 130 - storage full" },
}));
const { e } = await eingabe(client);
const gemerkt: Array<Record<string, unknown>> = [];
const ergebnis = await phasenAusfuehren(
lxcAusrollSchritte({ ...e, angelegt: async (x) => void gemerkt.push(x) }),
stumm,
);
expect(ergebnis.ok).toBe(false);
expect(gemerkt).toContainEqual({ vmid: 130 });
});
it("mit Node-SSH: neuestes Template laden, anlegen mit nesting (docker), DHCP-IP, pct exec mit Skript auf stdin", async () => { it("mit Node-SSH: neuestes Template laden, anlegen mit nesting (docker), DHCP-IP, pct exec mit Skript auf stdin", async () => {
const { mock, client } = await aufbauen({ dhcpNachAbfragen: 2 }); const { mock, client } = await aufbauen({ dhcpNachAbfragen: 2 });
const befehle: Array<{ befehl: string; eingabe?: string }> = []; const befehle: Array<{ befehl: string; eingabe?: string }> = [];
+96 -9
View File
@@ -8,15 +8,22 @@ import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-moc
const mock = new ProxmoxMock(); const mock = new ProxmoxMock();
function client(ueberschreiben: Partial<ConstructorParameters<typeof ProxmoxClient>[0]> = {}) { function client(
return new ProxmoxClient({ ueberschreiben: Partial<ConstructorParameters<typeof ProxmoxClient>[0]> = {},
url: mock.url, optionen: ConstructorParameters<typeof ProxmoxClient>[1] = {},
tokenId: MOCK_TOKEN_ID, ) {
tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS, return new ProxmoxClient(
tlsPruefen: false, {
fingerabdruck: mock.fingerabdruck, url: mock.url,
...ueberschreiben, tokenId: MOCK_TOKEN_ID,
}); tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS,
tlsPruefen: false,
fingerabdruck: mock.fingerabdruck,
...ueberschreiben,
},
// Ohne Pausen: Wiederholungen sollen Tests nicht ausbremsen.
{ pauseMs: () => 0, ...optionen },
);
} }
async function fehlerVon(versprechen: Promise<unknown>): Promise<ProxmoxFehler> { async function fehlerVon(versprechen: Promise<unknown>): Promise<ProxmoxFehler> {
@@ -174,6 +181,58 @@ describe("ProxmoxClient", () => {
}); });
}); });
describe("Vorübergehende Störungen", () => {
it("wiederholt GET nach „Broken pipe“ (596) und getrennter Verbindung und meldet es dem Beobachter", async () => {
let n = 0;
mock.route("GET", "/nodes/pve01/storage", () => {
n += 1;
if (n === 1) return { status: 596, statusText: "Broken pipe" };
if (n === 2) return { trennen: true };
return { data: [{ storage: "local" }] };
});
const gesehen: Array<{ fehler?: string; wiederholung?: number }> = [];
const c = client(
{},
{ beobachter: (_a, e) => gesehen.push({ fehler: e.fehler?.ursache, wiederholung: e.wiederholung }) },
);
expect(await c.get("/nodes/pve01/storage")).toEqual([{ storage: "local" }]);
expect(n).toBe(3);
expect(gesehen.map((g) => g.wiederholung)).toEqual([1, 2, undefined]);
expect(gesehen[0]!.fehler).toBe("Broken pipe");
});
it("gibt nach drei Versuchen auf", async () => {
let n = 0;
mock.route("GET", "/nodes/pve02/storage", () => {
n += 1;
return { status: 596, statusText: "Broken pipe" };
});
const fehler = await fehlerVon(client().get("/nodes/pve02/storage"));
expect(fehler.ursache).toBe("Broken pipe");
expect(n).toBe(3);
});
it("wiederholt Schreibendes nicht, wenn die Anfrage den Node erreicht haben kann", async () => {
let n = 0;
mock.route("POST", "/nodes/pve01/qemu", () => {
n += 1;
return n === 1 ? { trennen: true } : { data: "UPID:pve01:1:2:3:qmcreate:100:x:" };
});
await expect(client().post("/nodes/pve01/qemu", { vmid: 100 })).rejects.toThrow();
expect(n).toBe(1);
});
it("wiederholt keine fachlichen Fehler (500 mit Proxmox-Meldung, 403, 501)", async () => {
let n = 0;
mock.route("GET", "/nodes/pve03/storage", () => {
n += 1;
return { status: 500, statusText: "storage 'x' does not exist" };
});
await fehlerVon(client().get("/nodes/pve03/storage"));
expect(n).toBe(1);
});
});
describe("Proxmox-Tasks (UPID)", () => { describe("Proxmox-Tasks (UPID)", () => {
const UPID = "UPID:pve01:0000A1B2:00C3D4E5:6710F00D:qmclone:9013:proxdeploy@pve!test:"; const UPID = "UPID:pve01:0000A1B2:00C3D4E5:6710F00D:qmclone:9013:proxdeploy@pve!test:";
@@ -216,3 +275,31 @@ describe("Proxmox-Tasks (UPID)", () => {
expect(fehler.hinweis).toContain("TASK ERROR"); expect(fehler.hinweis).toContain("TASK ERROR");
}); });
}); });
describe("Task abwarten unter Last", () => {
it("fragt nach vorübergehenden Fehlern der Statusabfrage weiter, bis der Task fertig ist", async () => {
let n = 0;
mock.route("GET", /\/nodes\/pve09\/tasks\/[^/]+\/status$/, () => {
n += 1;
if (n <= 4) return { status: 596, statusText: "Broken pipe" };
return { data: { status: "stopped", exitstatus: "WARNINGS: 1" } };
});
await aufgabeAbwarten(client(), "UPID:pve09:00000001:00000002:00000003:vzcreate:114:root@pam:", {
warten: async () => {},
});
expect(n).toBe(5);
});
it("bricht bei fachlichen Fehlern sofort ab", async () => {
mock.route("GET", /\/nodes\/pve08\/tasks\/[^/]+\/status$/, () => ({
status: 403,
statusText: "Permission check failed (/nodes/pve08, Sys.Audit)",
}));
const fehler = await fehlerVon(
aufgabeAbwarten(client(), "UPID:pve08:00000001:00000002:00000003:vzcreate:114:root@pam:", {
warten: async () => {},
}),
);
expect(fehler.art).toBe("recht");
});
});
+1 -1
View File
@@ -147,7 +147,7 @@ describe("Template-Bau", () => {
cpu: "host", cpu: "host",
serial0: "socket", serial0: "socket",
agent: "enabled=1,fstrim_cloned_disks=1", agent: "enabled=1,fstrim_cloned_disks=1",
ide2: "local-zfs:cloudinit", scsi1: "local-zfs:cloudinit",
tags: "proxdeploy-template;debian-13", tags: "proxdeploy-template;debian-13",
}); });
const configs = mock.anfragen.filter((a) => a.methode === "PUT").map((a) => Object.fromEntries(a.rumpf)); const configs = mock.anfragen.filter((a) => a.methode === "PUT").map((a) => Object.fromEntries(a.rumpf));
+6
View File
@@ -26,6 +26,8 @@ export type MockAntwort = {
statusText?: string; statusText?: string;
data?: unknown; data?: unknown;
errors?: Record<string, string>; errors?: Record<string, string>;
/** Verbindung ohne Antwort hart trennen – wie ein pveproxy, der eine Keep-Alive-Verbindung schließt. */
trennen?: boolean;
}; };
export type MockRoute = ( export type MockRoute = (
anfrage: MockAnfrage, anfrage: MockAnfrage,
@@ -124,6 +126,10 @@ export class ProxmoxMock {
if (!treffer) continue; if (!treffer) continue;
try { try {
const antwort = await route(anfrage, treffer); const antwort = await route(anfrage, treffer);
if (antwort.trennen) {
res.socket?.destroy();
return;
}
senden(antwort.status ?? 200, antwort.statusText ?? "OK", { senden(antwort.status ?? 200, antwort.statusText ?? "OK", {
data: antwort.data ?? null, data: antwort.data ?? null,
...(antwort.errors ? { errors: antwort.errors } : {}), ...(antwort.errors ? { errors: antwort.errors } : {}),
+21
View File
@@ -130,6 +130,27 @@ describe.runIf(datenbankVerfuegbar)("Server-Verwaltung", () => {
expect(alles).not.toContain(MOCK_TOKEN_GEHEIMNIS); expect(alles).not.toContain(MOCK_TOKEN_GEHEIMNIS);
}); });
it("Standardwerte: gespeichert, leere Felder fallen weg, ganz leer ist null", async () => {
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
const setzen = async (standards: unknown) =>
await (
await einzeln.PATCH(
anfrage("PATCH", "/api/v1/server/rz1", admin, eingabe({ tokenGeheimnis: "", standards })),
kontext({ name: "rz1" }),
)
).json();
expect((await setzen({ knoten: "pve01", storage: " local-zfs ", bridge: "" })).standards).toEqual({
knoten: "pve01",
storage: "local-zfs",
});
expect((await testPrisma.server.findFirstOrThrow()).standards).toEqual({
knoten: "pve01",
storage: "local-zfs",
});
expect((await setzen({ knoten: "", storage: "", bridge: "" })).standards).toBeNull();
expect((await testPrisma.server.findFirstOrThrow()).standards).toBeNull();
});
it("löscht und protokolliert", async () => { it("löscht und protokolliert", async () => {
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe())); await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
expect( expect(