Compare commits
10
Commits
9c4dd6da3d
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f338061d0f | ||
|
|
ca9d6de446 | ||
|
|
bc13b94c7e | ||
|
|
db57818eb3 | ||
|
|
64859b3637 | ||
|
|
420aca3994 | ||
|
|
a9ea35cab5 | ||
|
|
2b6381de41 | ||
|
|
8b37329c92 | ||
|
|
7beeafd2f6 |
@@ -17,6 +17,36 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
### 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
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
@@ -57,7 +57,7 @@ cli/ CLI in Go; nur REST-API. Typen aus cli/api/openapi.json
|
||||
## Werkzeuge und Versionen
|
||||
|
||||
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`,
|
||||
`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).
|
||||
@@ -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
|
||||
`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.
|
||||
Prisma 7: Client aus `@/generated/prisma/client`, Enum-Werte aus `@/generated/prisma/enums`, Zugriff
|
||||
ü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`.
|
||||
- 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>`.
|
||||
- SemVer; jede Etappe ein annotierter Tag `vX.Y.Z`. **1.0.0 erst nach dem Feldtest** auf echtem
|
||||
Proxmox 8.x und 9.x (#020).
|
||||
- SemVer; jede Etappe ein annotierter Tag `vX.Y.Z`. 1.0.0 nach dem Feldtest auf PVE 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
|
||||
Commit-Zeitstempeln, Sitzungen mit „Erledigt“ und „Entscheidungen“) werden mit jedem Issue gepflegt;
|
||||
`docs/issues/*.md` tragen Status und tatsächliche Zeit.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -22,7 +22,9 @@ Zeiten stammen aus den Git-Commit-Zeitstempeln je Issue; Wartezeit auf Rückmeld
|
||||
| #012 Vorlagen | 0 h 11 min |
|
||||
| #022 Profile im Template | 0 h 22 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 |
|
||||
| #014 Verlauf, Wiederholen, Aufräumen | 1 h 21 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.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.5.0 | 1 h 39 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -55,17 +55,22 @@ Compose startet `postgres`, spielt mit `migrate` die Datenbank ein und startet d
|
||||
|
||||
### 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
|
||||
scripts/paket-bauen.sh 0.6.0 --tar # im Repository: proxdeploy-0.6.0.tar.gz
|
||||
scp proxdeploy-0.6.0.tar.gz deploy/install-lxc.sh deploy/im-container.sh root@pve01:/root/
|
||||
ssh root@pve01 ./install-lxc.sh --paket proxdeploy-0.6.0.tar.gz --url https://proxdeploy.example
|
||||
v=1.0.0; basis=https://git.wilde-server.de/florian.wilde/proxdeploy/releases/download/v$v
|
||||
for f in install-lxc.sh im-container.sh proxdeploy-$v.tar.gz SHA256SUMS; do curl -fsSLO "$basis/$f"; done
|
||||
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`,
|
||||
`--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.
|
||||
Aktualisieren mit `./install-lxc.sh --aktualisieren <ctid> --paket proxdeploy-0.6.1.tar.gz` – die
|
||||
letzten drei Fassungen bleiben unter `/opt/proxdeploy/versionen/`.
|
||||
Aktualisieren mit `./install-lxc.sh --aktualisieren <ctid> --paket proxdeploy-<neu>.tar.gz` – die
|
||||
letzten drei Fassungen bleiben unter `/opt/proxdeploy/versionen/`. Ein eigenes Paket aus dem
|
||||
Repository baut `scripts/paket-bauen.sh <version> --tar`.
|
||||
|
||||
## 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
|
||||
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
|
||||
proxdeploy login https://proxdeploy.example # fragt Benutzer und Passwort, speichert ein Token
|
||||
proxdeploy servers
|
||||
@@ -191,17 +203,15 @@ OIDC_GRUPPE_OPERATOR=proxdeploy-operators
|
||||
|
||||
## Stand
|
||||
|
||||
Version und Änderungen stehen im [CHANGELOG](CHANGELOG.md), der Plan in
|
||||
[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md).
|
||||
**1.0.0** – alle Etappen der Vorgabe umgesetzt. Änderungen stehen im [CHANGELOG](CHANGELOG.md), der
|
||||
Plan in [docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md).
|
||||
|
||||
| Etappe | Inhalt | Stand |
|
||||
|---|---|---|
|
||||
| 1 · v0.1.0 | Server-Verwaltung, Fingerprint-Pinning, Verbindungstest mit Fähigkeitserkennung | ✓ fertig, auf PVE 9.2 geprüft |
|
||||
| 2 · v0.2.0 | Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt, Assistent | ✓ fertig, auf PVE 9.2 geprüft |
|
||||
| 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) |
|
||||
| 4 · v0.4.0 | CLI `proxdeploy` (Go), API-Tokens | ✓ fertig, auf PVE 9.2 geprüft |
|
||||
| 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 |
|
||||
| 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)) |
|
||||
| Proxmox VE 8.x | im Code unterstützt (Rechte, API-Wege je Version gegen nachgebaute Server getestet), **nicht im Feldtest** |
|
||||
| Anmeldung | lokale Benutzer im Betrieb geprüft; OIDC gegen einen nachgebauten Identity Provider getestet, noch nicht gegen einen echten |
|
||||
| Betrieb | Docker Compose lokal, LXC-Installer samt Update auf PVE 9.2 |
|
||||
|
||||
## Entwicklung
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"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? } }`."
|
||||
},
|
||||
"servers": [
|
||||
@@ -503,13 +503,13 @@
|
||||
},
|
||||
"features": {
|
||||
"default": {
|
||||
"nesting": false,
|
||||
"nesting": true,
|
||||
"keyctl": false
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nesting": {
|
||||
"default": false,
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"keyctl": {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (g *gastFlags) registrieren(f *pflag.FlagSet, typ string) {
|
||||
if typ == "lxc" {
|
||||
f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)")
|
||||
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 {
|
||||
f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package befehle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -189,11 +190,22 @@ func (u *umgebung) batchBefehl() *cobra.Command {
|
||||
fehlgeschlagen := 0
|
||||
|
||||
if trocken {
|
||||
for nr, e := range eintraege {
|
||||
if nr > 0 && !u.opt.json {
|
||||
fmt.Fprintln(u.aus)
|
||||
ausgegeben := false
|
||||
for _, e := range eintraege {
|
||||
// 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
|
||||
if !errors.As(err, &ef) || !ef.gemeldet {
|
||||
u.fehlerMelden(err)
|
||||
|
||||
+1
-1
@@ -595,7 +595,7 @@ proxdeploy/
|
||||
| v0.4.0 | 4 – CLI | #013 | alle Befehle gegen die laufende Instanz |
|
||||
| v0.5.0 | 5 – Verlauf, Audit, Rollen, OIDC | #014–#016 | – |
|
||||
| v0.6.0 | 6 – Branding, README, Installer | #017–#019 | Installer in echtem LXC |
|
||||
| v1.0.0 | Feldtest 8.x + 9.x, Feinschliff | #020 | vollständig |
|
||||
| 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
|
||||
kleiner HTTP-Server mit Zustand, der die benutzten Endpunkte inkl. UPID-Tasks nachbildet). Die
|
||||
|
||||
+47
-11
@@ -1,21 +1,57 @@
|
||||
# #020 Feldtest 8.x und 9.x, Version 1.0.0
|
||||
**Status:** offen
|
||||
# #020 Feldtest und Version 1.0.0
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v1.0.0
|
||||
**Labels:** test
|
||||
**Geschätzt:** 6 h **Tatsächlich:** –
|
||||
**Geschätzt:** 6 h **Tatsächlich:** 2 h 15 min (Sitzung 5)
|
||||
|
||||
## 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
|
||||
- [ ] Matrix: VM/LXC × Debian/Ubuntu × mit/ohne SSH × 8.x/9.x
|
||||
- [ ] Gefundene Fehler als eigene Issues
|
||||
- [x] Matrix auf pve-demo (PVE 9.2.2): VM/LXC × Debian/Ubuntu × mit/ohne SSH zum Node
|
||||
- [x] Gefundene Fehler behoben (siehe unten), jeweils mit Test
|
||||
- [x] Abhängigkeiten auf bekannte Lücken geprüft (`pnpm audit --prod`)
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] Alle Kombinationen grün oder mit Begründung ausgeschlossen
|
||||
## Matrix (PVE 9.2.2)
|
||||
|
||||
## Abhängigkeiten
|
||||
alle
|
||||
| Gast | mit SSH zum Node | ohne SSH zum Node |
|
||||
|---|---|---|
|
||||
| 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
|
||||
–
|
||||
- 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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# #021 Template-Bau ohne SSH (Bau-Adresse)
|
||||
**Status:** umgesetzt, Abnahme auf pve-demo ausstehend
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.3.0
|
||||
**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
|
||||
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“
|
||||
|
||||
## 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
|
||||
#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.
|
||||
- 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.
|
||||
- **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“).
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
| [#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 |
|
||||
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – |
|
||||
| [#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 |
|
||||
| [#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) | 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 |
|
||||
| [#023](023-standardwerte.md) | Standardwerte je Server | erledigt | v1.0.0 | 1 h | 1 h 40 min |
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxdeploy",
|
||||
"version": "0.6.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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/space-grotesk": "^5.3.0",
|
||||
"@node-rs/argon2": "^2.2.1",
|
||||
"@prisma/adapter-pg": "7.10.0",
|
||||
"@prisma/client": "7.10.0",
|
||||
"@prisma/adapter-pg": "7.10.1",
|
||||
"@prisma/client": "7.10.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "^6.2.12",
|
||||
"lucide-react": "1.47.0",
|
||||
"next": "16.4.0",
|
||||
"next": "16.4.2",
|
||||
"openid-client": "6.8.8",
|
||||
"pg": "^8.23.0",
|
||||
"pg-boss": "12.37.1",
|
||||
@@ -67,13 +67,13 @@
|
||||
"dotenv": "^17.4.2",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "16.4.0",
|
||||
"eslint-config-next": "16.4.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^17.5.1",
|
||||
"opentype.js": "1.3.5",
|
||||
"prettier": "^3.9.9",
|
||||
"prisma": "7.10.0",
|
||||
"prisma": "7.10.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "~6.0.3",
|
||||
|
||||
Generated
+121
-130
@@ -176,11 +176,11 @@ importers:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.2
|
||||
'@prisma/adapter-pg':
|
||||
specifier: 7.10.0
|
||||
version: 7.10.0
|
||||
specifier: 7.10.1
|
||||
version: 7.10.1
|
||||
'@prisma/client':
|
||||
specifier: 7.10.0
|
||||
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)
|
||||
specifier: 7.10.1
|
||||
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:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -191,8 +191,8 @@ importers:
|
||||
specifier: 1.47.0
|
||||
version: 1.47.0(react@19.3.0)
|
||||
next:
|
||||
specifier: 16.4.0
|
||||
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)
|
||||
specifier: 16.4.2
|
||||
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:
|
||||
specifier: 6.8.8
|
||||
version: 6.8.8
|
||||
@@ -267,8 +267,8 @@ importers:
|
||||
specifier: ^9.39.5
|
||||
version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
|
||||
eslint-config-next:
|
||||
specifier: 16.4.0
|
||||
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)
|
||||
specifier: 16.4.2
|
||||
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:
|
||||
specifier: ^10.1.8
|
||||
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
|
||||
version: 3.9.10
|
||||
prisma:
|
||||
specifier: 7.10.0
|
||||
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)
|
||||
specifier: 7.10.1
|
||||
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:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
@@ -1054,60 +1054,60 @@ packages:
|
||||
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
|
||||
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
|
||||
|
||||
'@next/env@16.4.0':
|
||||
resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==}
|
||||
'@next/env@16.4.2':
|
||||
resolution: {integrity: sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==}
|
||||
|
||||
'@next/eslint-plugin-next@16.4.0':
|
||||
resolution: {integrity: sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==}
|
||||
'@next/eslint-plugin-next@16.4.2':
|
||||
resolution: {integrity: sha512-gCTWlw0e+Y6kajpFyKKgA195jUJCRhlzJE0lV7ufe8a+ghIsANWZ0/FiJwg6DuOG1+OvRbcZDcFBoRh3mNJx0g==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.4.0':
|
||||
resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==}
|
||||
'@next/swc-darwin-arm64@16.4.2':
|
||||
resolution: {integrity: sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.4.0':
|
||||
resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==}
|
||||
'@next/swc-darwin-x64@16.4.2':
|
||||
resolution: {integrity: sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.4.0':
|
||||
resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==}
|
||||
'@next/swc-linux-arm64-gnu@16.4.2':
|
||||
resolution: {integrity: sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.4.0':
|
||||
resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==}
|
||||
'@next/swc-linux-arm64-musl@16.4.2':
|
||||
resolution: {integrity: sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.4.0':
|
||||
resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==}
|
||||
'@next/swc-linux-x64-gnu@16.4.2':
|
||||
resolution: {integrity: sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.4.0':
|
||||
resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==}
|
||||
'@next/swc-linux-x64-musl@16.4.2':
|
||||
resolution: {integrity: sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.4.0':
|
||||
resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==}
|
||||
'@next/swc-win32-arm64-msvc@16.4.2':
|
||||
resolution: {integrity: sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.4.0':
|
||||
resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==}
|
||||
'@next/swc-win32-x64-msvc@16.4.2':
|
||||
resolution: {integrity: sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1229,14 +1229,14 @@ packages:
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
'@prisma/adapter-pg@7.10.0':
|
||||
resolution: {integrity: sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==}
|
||||
'@prisma/adapter-pg@7.10.1':
|
||||
resolution: {integrity: sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==}
|
||||
|
||||
'@prisma/client-runtime-utils@7.10.0':
|
||||
resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==}
|
||||
'@prisma/client-runtime-utils@7.10.1':
|
||||
resolution: {integrity: sha512-AzoATVIgmO3lcZfM443+9SI7+cyLMW2+Psw4op8WQHpHvHCX//CKxv8fwhiL0yGWF8DjdQrFewxrZo5McBdB8g==}
|
||||
|
||||
'@prisma/client@7.10.0':
|
||||
resolution: {integrity: sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==}
|
||||
'@prisma/client@7.10.1':
|
||||
resolution: {integrity: sha512-5tgAbFGh//zVOC6WPNVgziOErJHYkUE+wmqCwbIyq6IrTXUYtv9y2iBFGh/Owo99Mx0+cr0j1FOrxkYBMuyhaQ==}
|
||||
engines: {node: ^20.19 || ^22.12 || >=24.0}
|
||||
peerDependencies:
|
||||
prisma: '*'
|
||||
@@ -1247,11 +1247,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@prisma/config@7.10.0':
|
||||
resolution: {integrity: sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==}
|
||||
'@prisma/config@7.10.1':
|
||||
resolution: {integrity: sha512-+0swF5ELUa3P/9cIR26+gpYjjCbGE0qvEBZhV4zMDkYsrtHjZ884MQbOnWFWXj600N0sSJNnHbtBqe12yDngWg==}
|
||||
|
||||
'@prisma/debug@7.10.0':
|
||||
resolution: {integrity: sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==}
|
||||
'@prisma/debug@7.10.1':
|
||||
resolution: {integrity: sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==}
|
||||
|
||||
'@prisma/debug@7.2.0':
|
||||
resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==}
|
||||
@@ -1259,20 +1259,20 @@ packages:
|
||||
'@prisma/dev@0.24.17':
|
||||
resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==}
|
||||
|
||||
'@prisma/driver-adapter-utils@7.10.0':
|
||||
resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==}
|
||||
'@prisma/driver-adapter-utils@7.10.1':
|
||||
resolution: {integrity: sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==}
|
||||
|
||||
'@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3':
|
||||
resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==}
|
||||
'@prisma/engines-version@8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8':
|
||||
resolution: {integrity: sha512-XjXmwjN2NE340RpXoXv8ZF8aNAvr0FE+7gNfkKeW6vyjXs7F8uxvEbWQoVrhioXDQoIsnNmv7r+wxvzfdEZaWA==}
|
||||
|
||||
'@prisma/engines@7.10.0':
|
||||
resolution: {integrity: sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==}
|
||||
'@prisma/engines@7.10.1':
|
||||
resolution: {integrity: sha512-R6/ghv1Bu36FhCdQLFof1pNtyoWoeDzJLYeTKjgqb4eGtOAJx7ZqJtcm4pzkahLfZ9rYz8AkmsMDwkyiYbXPCQ==}
|
||||
|
||||
'@prisma/fetch-engine@7.10.0':
|
||||
resolution: {integrity: sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==}
|
||||
'@prisma/fetch-engine@7.10.1':
|
||||
resolution: {integrity: sha512-qZezeaExLWG2ptbHOrMxdiWluKHgqEclTdJbvY6pk6+Wg7EiVkeq5U3cXz9ti8nd0bngNvsDgledLHVRg5LF1w==}
|
||||
|
||||
'@prisma/get-platform@7.10.0':
|
||||
resolution: {integrity: sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==}
|
||||
'@prisma/get-platform@7.10.1':
|
||||
resolution: {integrity: sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==}
|
||||
|
||||
'@prisma/get-platform@7.2.0':
|
||||
resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==}
|
||||
@@ -2305,9 +2305,9 @@ packages:
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
deepmerge-ts@7.1.5:
|
||||
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
deepmerge-ts@8.0.2:
|
||||
resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
@@ -2323,10 +2323,6 @@ packages:
|
||||
delaunator@5.1.0:
|
||||
resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
destr@2.0.5:
|
||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||
|
||||
@@ -2440,8 +2436,8 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-next@16.4.0:
|
||||
resolution: {integrity: sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==}
|
||||
eslint-config-next@16.4.2:
|
||||
resolution: {integrity: sha512-P33vjcETMXc+qBjpF77TeJHZLzk6cPcTy8KjpxLucYB3mCIFIORMcZ+cBXcAiIYjE5RM7jyknEEOwMNdnx/ADQ==}
|
||||
peerDependencies:
|
||||
eslint: '>=9.0.0'
|
||||
typescript: '>=3.3.1'
|
||||
@@ -3221,9 +3217,11 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
mysql2@3.15.3:
|
||||
resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==}
|
||||
mysql2@3.24.4:
|
||||
resolution: {integrity: sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==}
|
||||
engines: {node: '>= 8.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>= 8'
|
||||
|
||||
named-placeholders@1.1.6:
|
||||
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
|
||||
@@ -3245,8 +3243,8 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
next@16.4.0:
|
||||
resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==}
|
||||
next@16.4.2:
|
||||
resolution: {integrity: sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -3493,8 +3491,8 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
prisma@7.10.0:
|
||||
resolution: {integrity: sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==}
|
||||
prisma@7.10.1:
|
||||
resolution: {integrity: sha512-GoVeErHGGtsSjsflQ1r2BA3RKTXtxNraQJz8OLkLhoLr7OgCscdx4qw4WMM4HnERYc+a1cq2VnMNbep+vOjNcQ==}
|
||||
engines: {node: ^20.19 || ^22.12 || >=24.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -3642,9 +3640,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
seq-queue@0.0.5:
|
||||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
|
||||
serialize-error@13.0.2:
|
||||
resolution: {integrity: sha512-G+YKwp9I6IvWMe5RlwqxdmTPIcq/G8HK/sU51gdV1XAJznYkRSTB0ph+zB9AfSxG0fx6xrqYA6i9HeIcHFTXow==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -3715,9 +3710,9 @@ packages:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
sqlstring@2.3.3:
|
||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
sql-escaper@1.5.2:
|
||||
resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==}
|
||||
engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'}
|
||||
|
||||
ssh2@1.17.0:
|
||||
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
|
||||
@@ -4711,37 +4706,37 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.4
|
||||
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:
|
||||
'@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
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
|
||||
'@next/swc-darwin-arm64@16.4.0':
|
||||
'@next/swc-darwin-arm64@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.4.0':
|
||||
'@next/swc-darwin-x64@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.4.0':
|
||||
'@next/swc-linux-arm64-gnu@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.4.0':
|
||||
'@next/swc-linux-arm64-musl@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.4.0':
|
||||
'@next/swc-linux-x64-gnu@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.4.0':
|
||||
'@next/swc-linux-x64-musl@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.4.0':
|
||||
'@next/swc-win32-arm64-msvc@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.4.0':
|
||||
'@next/swc-win32-x64-msvc@16.4.2':
|
||||
optional: true
|
||||
|
||||
'@node-rs/argon2-android-arm-eabi@2.2.2':
|
||||
@@ -4823,34 +4818,34 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.63.0
|
||||
|
||||
'@prisma/adapter-pg@7.10.0':
|
||||
'@prisma/adapter-pg@7.10.1':
|
||||
dependencies:
|
||||
'@prisma/driver-adapter-utils': 7.10.0
|
||||
'@prisma/driver-adapter-utils': 7.10.1
|
||||
'@types/pg': 8.23.1
|
||||
pg: 8.23.1
|
||||
postgres-array: 3.0.4
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@prisma/client-runtime-utils': 7.10.0
|
||||
'@prisma/client-runtime-utils': 7.10.1
|
||||
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
|
||||
|
||||
'@prisma/config@7.10.0':
|
||||
'@prisma/config@7.10.1':
|
||||
dependencies:
|
||||
c12: 3.3.4
|
||||
deepmerge-ts: 7.1.5
|
||||
deepmerge-ts: 8.0.2
|
||||
effect: 3.20.0
|
||||
empathic: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@prisma/debug@7.10.0': {}
|
||||
'@prisma/debug@7.10.1': {}
|
||||
|
||||
'@prisma/debug@7.2.0': {}
|
||||
|
||||
@@ -4874,28 +4869,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@prisma/driver-adapter-utils@7.10.0':
|
||||
'@prisma/driver-adapter-utils@7.10.1':
|
||||
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:
|
||||
'@prisma/debug': 7.10.0
|
||||
'@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3
|
||||
'@prisma/fetch-engine': 7.10.0
|
||||
'@prisma/get-platform': 7.10.0
|
||||
'@prisma/debug': 7.10.1
|
||||
'@prisma/engines-version': 8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8
|
||||
'@prisma/fetch-engine': 7.10.1
|
||||
'@prisma/get-platform': 7.10.1
|
||||
|
||||
'@prisma/fetch-engine@7.10.0':
|
||||
'@prisma/fetch-engine@7.10.1':
|
||||
dependencies:
|
||||
'@prisma/debug': 7.10.0
|
||||
'@prisma/engines-version': 7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3
|
||||
'@prisma/get-platform': 7.10.0
|
||||
'@prisma/debug': 7.10.1
|
||||
'@prisma/engines-version': 8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8
|
||||
'@prisma/get-platform': 7.10.1
|
||||
|
||||
'@prisma/get-platform@7.10.0':
|
||||
'@prisma/get-platform@7.10.1':
|
||||
dependencies:
|
||||
'@prisma/debug': 7.10.0
|
||||
'@prisma/debug': 7.10.1
|
||||
|
||||
'@prisma/get-platform@7.2.0':
|
||||
dependencies:
|
||||
@@ -5854,7 +5849,7 @@ snapshots:
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge-ts@7.1.5: {}
|
||||
deepmerge-ts@8.0.2: {}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
dependencies:
|
||||
@@ -5874,8 +5869,6 @@ snapshots:
|
||||
dependencies:
|
||||
robust-predicates: 3.0.3
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
destr@2.0.5: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
@@ -6097,9 +6090,9 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@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-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)
|
||||
@@ -6880,17 +6873,16 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mysql2@3.15.3:
|
||||
mysql2@3.24.4(@types/node@24.19.2):
|
||||
dependencies:
|
||||
'@types/node': 24.19.2
|
||||
aws-ssl-profiles: 1.1.2
|
||||
denque: 2.1.0
|
||||
generate-function: 2.3.1
|
||||
iconv-lite: 0.7.3
|
||||
long: 5.3.2
|
||||
lru.min: 1.1.5
|
||||
named-placeholders: 1.1.6
|
||||
seq-queue: 0.0.5
|
||||
sqlstring: 2.3.3
|
||||
sql-escaper: 1.5.2
|
||||
|
||||
named-placeholders@1.1.6:
|
||||
dependencies:
|
||||
@@ -6905,9 +6897,9 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@next/env': 16.4.0
|
||||
'@next/env': 16.4.2
|
||||
'@swc/helpers': 0.5.23
|
||||
baseline-browser-mapping: 2.11.28
|
||||
caniuse-lite: 1.0.30001815
|
||||
@@ -6916,14 +6908,14 @@ snapshots:
|
||||
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)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.4.0
|
||||
'@next/swc-darwin-x64': 16.4.0
|
||||
'@next/swc-linux-arm64-gnu': 16.4.0
|
||||
'@next/swc-linux-arm64-musl': 16.4.0
|
||||
'@next/swc-linux-x64-gnu': 16.4.0
|
||||
'@next/swc-linux-x64-musl': 16.4.0
|
||||
'@next/swc-win32-arm64-msvc': 16.4.0
|
||||
'@next/swc-win32-x64-msvc': 16.4.0
|
||||
'@next/swc-darwin-arm64': 16.4.2
|
||||
'@next/swc-darwin-x64': 16.4.2
|
||||
'@next/swc-linux-arm64-gnu': 16.4.2
|
||||
'@next/swc-linux-arm64-musl': 16.4.2
|
||||
'@next/swc-linux-x64-gnu': 16.4.2
|
||||
'@next/swc-linux-x64-musl': 16.4.2
|
||||
'@next/swc-win32-arm64-msvc': 16.4.2
|
||||
'@next/swc-win32-x64-msvc': 16.4.2
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@playwright/test': 1.63.0
|
||||
sharp: 0.35.5(@types/node@24.19.2)
|
||||
@@ -7160,17 +7152,18 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@prisma/config': 7.10.0
|
||||
'@prisma/config': 7.10.1
|
||||
'@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)
|
||||
mysql2: 3.15.3
|
||||
mysql2: 3.24.4(@types/node@24.19.2)
|
||||
postgres: 3.4.7
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
- magicast
|
||||
@@ -7326,8 +7319,6 @@ snapshots:
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
seq-queue@0.0.5: {}
|
||||
|
||||
serialize-error@13.0.2:
|
||||
dependencies:
|
||||
non-error: 0.1.0
|
||||
@@ -7437,7 +7428,7 @@ snapshots:
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
sqlstring@2.3.3: {}
|
||||
sql-escaper@1.5.2: {}
|
||||
|
||||
ssh2@1.17.0:
|
||||
dependencies:
|
||||
|
||||
@@ -7,6 +7,9 @@ import { katalogLaden } from "@/engine/katalog/laden";
|
||||
import { profileAufloesen } from "@/engine/profile/aufloesen";
|
||||
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 katalog = await katalogLaden("catalog");
|
||||
const distro = katalog.distributionen.find((d) => d.eintrag.id === distroId)?.eintrag;
|
||||
@@ -29,12 +32,12 @@ const { skript } = profileRendern(
|
||||
adapter,
|
||||
{
|
||||
os: distro.os,
|
||||
gast: { name: "testgast", benutzer: "florian" },
|
||||
gast: { name: "testgast", benutzer: BENUTZER },
|
||||
},
|
||||
[
|
||||
adapter.benutzerAnlegen({
|
||||
name: "florian",
|
||||
sshSchluessel: ["ssh-ed25519 AAAAtest florian@test"],
|
||||
name: BENUTZER,
|
||||
sshSchluessel: [`ssh-ed25519 AAAAtest ${BENUTZER}@test`],
|
||||
sudo: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -36,6 +36,8 @@ export type AssistentDaten = {
|
||||
bridges: Array<{ name: string; vlan: boolean }>;
|
||||
}>;
|
||||
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 }>;
|
||||
profile: Array<{
|
||||
@@ -74,6 +76,17 @@ type Plan = {
|
||||
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 ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type Werte = {
|
||||
@@ -295,10 +308,10 @@ export function Assistent({
|
||||
|
||||
const anfang = useMemo<Werte>(() => {
|
||||
const server = daten.server.find((s) => s.name === vorauswahl) ?? daten.server[0]!;
|
||||
const knoten = server.knoten[0];
|
||||
const vor = vorbelegung(server);
|
||||
return {
|
||||
server: server.name,
|
||||
knoten: knoten?.name ?? "",
|
||||
knoten: vor.knoten,
|
||||
typ: "vm",
|
||||
distribution:
|
||||
daten.distributionen.find((d) => d.id === "debian-13")?.id ?? daten.distributionen[0]?.id ?? "",
|
||||
@@ -308,11 +321,11 @@ export function Assistent({
|
||||
cores: "2",
|
||||
speicherMb: "2048",
|
||||
diskGb: "20",
|
||||
storage: knoten?.storages.find((s) => s.vm)?.name ?? "",
|
||||
storage: vor.storage,
|
||||
swapMb: "512",
|
||||
unprivilegiert: true,
|
||||
nesting: false,
|
||||
bridge: knoten?.bridges.find((b) => b.name === "vmbr0")?.name ?? knoten?.bridges[0]?.name ?? "",
|
||||
nesting: true,
|
||||
bridge: vor.bridge,
|
||||
vlan: "",
|
||||
modus: "dhcp",
|
||||
ip: "",
|
||||
@@ -554,13 +567,7 @@ export function Assistent({
|
||||
value={werte.server}
|
||||
onChange={(e) => {
|
||||
const s = daten.server.find((x) => x.name === e.target.value)!;
|
||||
const k = s.knoten[0];
|
||||
setze({
|
||||
server: s.name,
|
||||
knoten: k?.name ?? "",
|
||||
storage: k?.storages.find((x) => x.vm)?.name ?? "",
|
||||
bridge: k?.bridges[0]?.name ?? "",
|
||||
});
|
||||
setze({ server: s.name, ...vorbelegung(s) });
|
||||
}}
|
||||
>
|
||||
{daten.server.map((s) => (
|
||||
@@ -580,12 +587,8 @@ export function Assistent({
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.knoten}
|
||||
onChange={(e) => {
|
||||
const k = server.knoten.find((x) => x.name === e.target.value);
|
||||
setze({
|
||||
knoten: e.target.value,
|
||||
storage: k?.storages.find((x) => x.vm)?.name ?? werte.storage,
|
||||
bridge: k?.bridges[0]?.name ?? werte.bridge,
|
||||
});
|
||||
const vor = vorbelegung(server, e.target.value);
|
||||
setze({ knoten: e.target.value, storage: vor.storage, bridge: vor.bridge });
|
||||
}}
|
||||
>
|
||||
<option value="">automatisch</option>
|
||||
@@ -814,7 +817,7 @@ export function Assistent({
|
||||
hilfe={
|
||||
werte.profile.includes("docker")
|
||||
? "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")}
|
||||
disabled={werte.profile.includes("docker")}
|
||||
|
||||
@@ -58,6 +58,7 @@ export default async function Ausrollen({
|
||||
})),
|
||||
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
|
||||
.filter((t) => t.serverId === s.id)
|
||||
.map((t) => ({ distribution: t.distributionId, knoten: t.knoten, vmid: t.vmid })),
|
||||
|
||||
@@ -24,7 +24,8 @@ export default async function Uebersicht() {
|
||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">Übersicht</h1>
|
||||
</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
|
||||
titel="Server"
|
||||
aktion={
|
||||
|
||||
@@ -46,6 +46,9 @@ type Werte = {
|
||||
bauIp: string;
|
||||
bauGateway: string;
|
||||
bauDns: string;
|
||||
stdKnoten: string;
|
||||
stdStorage: string;
|
||||
stdBridge: string;
|
||||
};
|
||||
|
||||
function anfangswerte(server?: ServerSicht): Werte {
|
||||
@@ -66,6 +69,9 @@ function anfangswerte(server?: ServerSicht): Werte {
|
||||
bauIp: server?.bauNetz?.ip ?? "",
|
||||
bauGateway: server?.bauNetz?.gateway ?? "",
|
||||
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);
|
||||
setFelder({});
|
||||
setMeldung(null);
|
||||
const { bauIp, bauGateway, bauDns, ...ohneBau } = werte;
|
||||
const { bauIp, bauGateway, bauDns, stdKnoten, stdStorage, stdBridge, ...ohneBau } = werte;
|
||||
const rumpf = {
|
||||
...ohneBau,
|
||||
sshPort: Number(werte.sshPort),
|
||||
@@ -133,6 +139,7 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
|
||||
bauNetz: bauIp.trim()
|
||||
? { ip: bauIp.trim(), gateway: bauGateway.trim(), dns: bauDns.split(/[\s,;]+/).filter(Boolean) }
|
||||
: null,
|
||||
standards: { knoten: stdKnoten, storage: stdStorage, bridge: stdBridge },
|
||||
};
|
||||
try {
|
||||
const gespeichert = bearbeiten
|
||||
@@ -409,6 +416,8 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{server?.letzterTest ? <StandardwerteFelder server={server} werte={werte} setze={setze} /> : null}
|
||||
|
||||
<Schalter
|
||||
id="sshAktiv"
|
||||
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 };
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ export function Seitenleiste({ eintraege, benutzer, version, theme, serverUmscha
|
||||
}
|
||||
|
||||
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)}>
|
||||
<Symbol className="h-8 w-8 shrink-0" />
|
||||
<Wortmarke className="text-lg" />
|
||||
@@ -148,7 +150,7 @@ export function Seitenleiste({ eintraege, benutzer, version, theme, serverUmscha
|
||||
<aside
|
||||
id="seitenleiste"
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -245,31 +245,30 @@ export function lxcAusrollSchritte(e: LxcAusrollEingabe, stand: LxcAusrollStand
|
||||
ausfuehren: async () => {
|
||||
if (stand.weg === "gast") bereitstellung = await e.bereitstellung();
|
||||
const featureListe = [features.nesting ? "nesting=1" : null].filter(Boolean).join(",");
|
||||
await aufgabeAbwarten(
|
||||
client,
|
||||
await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, {
|
||||
vmid: stand.vmid!,
|
||||
hostname: a.name,
|
||||
ostemplate: stand.ostemplate!,
|
||||
ostype: lxc!.ostype,
|
||||
rootfs: `${a.storage}:${a.diskGb}`,
|
||||
cores: a.cores,
|
||||
memory: a.speicherMb,
|
||||
swap: a.swapMb,
|
||||
unprivileged: a.unprivilegiert,
|
||||
...(featureListe ? { features: featureListe } : {}),
|
||||
net0: lxcNetz(a.netz),
|
||||
...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}),
|
||||
...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}),
|
||||
// Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel.
|
||||
...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}),
|
||||
onboot: a.startBeiBoot,
|
||||
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(", ") || "–"}`,
|
||||
}),
|
||||
{ zeitlimitMs: 15 * 60 * 1000 },
|
||||
);
|
||||
const upid = await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, {
|
||||
vmid: stand.vmid!,
|
||||
hostname: a.name,
|
||||
ostemplate: stand.ostemplate!,
|
||||
ostype: lxc!.ostype,
|
||||
rootfs: `${a.storage}:${a.diskGb}`,
|
||||
cores: a.cores,
|
||||
memory: a.speicherMb,
|
||||
swap: a.swapMb,
|
||||
unprivileged: a.unprivilegiert,
|
||||
...(featureListe ? { features: featureListe } : {}),
|
||||
net0: lxcNetz(a.netz),
|
||||
...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}),
|
||||
...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}),
|
||||
// Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel.
|
||||
...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}),
|
||||
onboot: a.startBeiBoot,
|
||||
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.
|
||||
await e.angelegt({ vmid: stand.vmid });
|
||||
await aufgabeAbwarten(client, upid, { zeitlimitMs: 15 * 60 * 1000 });
|
||||
if (features.keyctl) {
|
||||
const ssh = e.sshFuer(stand.knoten!)!;
|
||||
if (!client.trocken) {
|
||||
|
||||
@@ -470,7 +470,10 @@ export function templateBauSchritte(e: TemplateBauEingabe, stand: TemplateBauSta
|
||||
serial0: "socket",
|
||||
vga: "serial0",
|
||||
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",
|
||||
ciupgrade: false,
|
||||
tags: ["proxdeploy-template", d.id, ...profilIds.map((p) => `profil-${p}`)].join(";"),
|
||||
|
||||
@@ -234,32 +234,32 @@ export function vmAusrollSchritte(e: VmAusrollEingabe, stand: VmAusrollStand = {
|
||||
titel: "Klonen",
|
||||
ausfuehren: async (k) => {
|
||||
const tpl = stand.template!;
|
||||
const klonen = async (vmid: number) =>
|
||||
await aufgabeAbwarten(
|
||||
client,
|
||||
await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, {
|
||||
newid: vmid,
|
||||
name: a.name,
|
||||
// Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört.
|
||||
description: beschreibung(),
|
||||
full: true,
|
||||
target: stand.knoten!,
|
||||
storage: a.storage,
|
||||
}),
|
||||
{
|
||||
zeitlimitMs: 60 * 60 * 1000,
|
||||
intervallMs: 2000,
|
||||
// „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon.
|
||||
beiLog: (zeilen) => {
|
||||
const letzte = zeilen
|
||||
.map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z))
|
||||
.filter(Boolean)
|
||||
.at(-1);
|
||||
if (letzte)
|
||||
void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`);
|
||||
},
|
||||
const klonen = async (vmid: number) => {
|
||||
const upid = await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, {
|
||||
newid: vmid,
|
||||
name: a.name,
|
||||
// Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört.
|
||||
description: beschreibung(),
|
||||
full: true,
|
||||
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 });
|
||||
await aufgabeAbwarten(client, upid, {
|
||||
zeitlimitMs: 60 * 60 * 1000,
|
||||
intervallMs: 2000,
|
||||
// „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon.
|
||||
beiLog: (zeilen) => {
|
||||
const letzte = zeilen
|
||||
.map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z))
|
||||
.filter(Boolean)
|
||||
.at(-1);
|
||||
if (letzte) void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
try {
|
||||
await klonen(stand.vmid!);
|
||||
} 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 klonen(stand.vmid);
|
||||
}
|
||||
await e.angelegt({ vmid: stand.vmid });
|
||||
return { detail: `full clone ${tpl.vmid} → ${stand.vmid}` };
|
||||
},
|
||||
},
|
||||
|
||||
@@ -119,7 +119,9 @@ setupcon --save-only >/dev/null 2>&1 || true
|
||||
const name = q(b.name);
|
||||
const schluessel = b.sshSchluessel.join("\n") + "\n";
|
||||
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)"`,
|
||||
`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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { istTrockenUpid, pfad, type ProxmoxClient } from "./client";
|
||||
import { istTrockenUpid, pfad, stoerung, type ProxmoxClient } from "./client";
|
||||
import { ProxmoxFehler } from "./fehler";
|
||||
|
||||
/**
|
||||
@@ -53,10 +53,19 @@ export async function aufgabeAbwarten(
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
const 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);
|
||||
// Nachsehen ist harmlos: scheitert eine Abfrage vorübergehend (ausgelasteter Node), wird bis
|
||||
// zum Zeitlimit des Tasks weiter gefragt – der Task läuft auf dem Node ja weiter.
|
||||
let status: TaskStatus;
|
||||
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.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return;
|
||||
|
||||
@@ -23,7 +23,7 @@ export type Aufruf = { methode: Methode; pfad: string; parameter?: Parameter };
|
||||
|
||||
export type AufrufBeobachter = (
|
||||
aufruf: Aufruf,
|
||||
ergebnis: { dauerMs: number; fehler?: ProxmoxFehler },
|
||||
ergebnis: { dauerMs: number; fehler?: ProxmoxFehler; wiederholung?: number },
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@@ -38,6 +38,44 @@ export function istTrockenUpid(upid: unknown): boolean {
|
||||
}
|
||||
|
||||
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. */
|
||||
export function parameterKodieren(parameter: Parameter | undefined): URLSearchParams {
|
||||
@@ -75,6 +113,8 @@ export class ProxmoxClient {
|
||||
zeitlimitMs?: number;
|
||||
beobachter?: AufrufBeobachter;
|
||||
trocken?: boolean;
|
||||
/** Pause vor dem n-ten Wiederholen (Tests setzen 0). */
|
||||
pauseMs?: (versuch: number) => number;
|
||||
} = {},
|
||||
) {
|
||||
if (!TOKEN_ID_MUSTER.test(verbindung.tokenId)) {
|
||||
@@ -85,7 +125,7 @@ export class ProxmoxClient {
|
||||
);
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -120,15 +160,30 @@ export class ProxmoxClient {
|
||||
return `${TROCKEN_UPID_PRAEFIX}${knoten}:${this.geplant.length}` as T;
|
||||
}
|
||||
const beginn = Date.now();
|
||||
try {
|
||||
const ergebnis = await this.senden<T>(aufruf);
|
||||
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn });
|
||||
return ergebnis;
|
||||
} catch (fehler) {
|
||||
const proxmoxFehler =
|
||||
fehler instanceof ProxmoxFehler ? fehler : verbindungsFehler(fehler, this.verbindung.url);
|
||||
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn, fehler: proxmoxFehler });
|
||||
throw proxmoxFehler;
|
||||
for (let versuch = 1; ; versuch += 1) {
|
||||
try {
|
||||
const ergebnis = await this.senden<T>(aufruf);
|
||||
this.optionen.beobachter?.(aufruf, { dauerMs: Date.now() - beginn });
|
||||
return ergebnis;
|
||||
} catch (fehler) {
|
||||
// GET darf immer noch einmal; Schreibendes nur, wenn die Anfrage sicher nie ankam.
|
||||
const art = stoerung(fehler);
|
||||
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" } : {}),
|
||||
},
|
||||
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();
|
||||
|
||||
@@ -104,7 +104,12 @@ export function tlsConnector(einstellung: TlsEinstellung): buildConnector.connec
|
||||
}
|
||||
// Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit,
|
||||
// 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;
|
||||
|
||||
return (optionen, rueckruf) => {
|
||||
|
||||
@@ -98,8 +98,10 @@ export const AusrollAnfrage = z
|
||||
swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512),
|
||||
unprivilegiert: z.boolean().default(true),
|
||||
features: z
|
||||
.object({ nesting: z.boolean().default(false), keyctl: z.boolean().default(false) })
|
||||
.default({ nesting: false, keyctl: false }),
|
||||
// nesting an wie in der Proxmox-Oberfläche: neuere systemd-Versionen brauchen es im
|
||||
// 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,
|
||||
zugang: Zugang,
|
||||
profile: z.array(z.string()).default(["base"]),
|
||||
|
||||
@@ -59,6 +59,22 @@ export const BauNetz = z
|
||||
|
||||
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 Basis = z.object({
|
||||
@@ -82,6 +98,7 @@ const Basis = z.object({
|
||||
templateVmidBis: Vmid.default(9999),
|
||||
gastVmidVon: Vmid.default(100),
|
||||
bauNetz: BauNetz,
|
||||
standards: Standards,
|
||||
});
|
||||
|
||||
function bereicheGueltig<T extends { templateVmidVon: number; templateVmidBis: number }>(wert: T) {
|
||||
@@ -143,6 +160,7 @@ export type ServerSicht = {
|
||||
templateVmidBis: number;
|
||||
gastVmidVon: number;
|
||||
bauNetz: BauNetz | null;
|
||||
standards: Standards | null;
|
||||
erreichbar: boolean | null;
|
||||
pveVersion: string | null;
|
||||
geprueftAm: string | null;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
* 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).
|
||||
*/
|
||||
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 function versionText(): string {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ServerAnlegen,
|
||||
ServerPruefen,
|
||||
type BauNetz,
|
||||
type Standards,
|
||||
type ServerSicht,
|
||||
} from "@/lib/schemas/server";
|
||||
import { AnwendungsFehler, nichtGefunden } from "@/server/fehler";
|
||||
@@ -34,6 +35,7 @@ export function serverSicht(server: Server): ServerSicht {
|
||||
templateVmidBis: server.templateVmidBis,
|
||||
gastVmidVon: server.gastVmidVon,
|
||||
bauNetz: (server.bauNetz as BauNetz | null) ?? null,
|
||||
standards: (server.standards as Standards | null) ?? null,
|
||||
erreichbar: server.erreichbar,
|
||||
pveVersion: server.pveVersion,
|
||||
geprueftAm: server.geprueftAm?.toISOString() ?? null,
|
||||
@@ -86,6 +88,7 @@ export async function serverAnlegen(
|
||||
data: {
|
||||
...eingabe,
|
||||
bauNetz: eingabe.bauNetz ?? Prisma.JsonNull,
|
||||
standards: eingabe.standards ?? Prisma.JsonNull,
|
||||
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
|
||||
tokenGeheimnis: verschluesseln(eingabe.tokenGeheimnis),
|
||||
},
|
||||
@@ -121,6 +124,7 @@ export async function serverAendern(
|
||||
const daten = {
|
||||
...rest,
|
||||
bauNetz: rest.bauNetz ?? Prisma.JsonNull,
|
||||
standards: rest.standards ?? Prisma.JsonNull,
|
||||
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
|
||||
...(tokenGeheimnis ? { tokenGeheimnis: verschluesseln(tokenGeheimnis) } : {}),
|
||||
};
|
||||
|
||||
@@ -14,7 +14,9 @@ export function aufrufLogger(senke: EreignisSenke): AufrufBeobachter {
|
||||
if (!ergebnis.fehler && aufruf.methode === "GET" && SCHLEIFE.test(pfad)) return;
|
||||
void senke.log(
|
||||
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` : ""
|
||||
}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/bin/bash
|
||||
# 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
|
||||
# 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=$?
|
||||
grep '::proxdeploy::' /tmp/lauf.log
|
||||
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
|
||||
cat /etc/timezone; readlink /etc/localtime; grep LANG /etc/default/locale; locale -a | grep -i de_de
|
||||
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 /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
|
||||
|
||||
@@ -16,7 +16,7 @@ arbeit="$(mktemp -d)"; trap 'rm -rf "$arbeit"' EXIT
|
||||
cp tests/container/im-gast-pruefen.sh "$arbeit/"
|
||||
fehler=0
|
||||
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]})"
|
||||
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
|
||||
|
||||
+9
-3
@@ -12,12 +12,18 @@ export async function anmelden(page: Page, ziel = "/") {
|
||||
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) {
|
||||
const breiten = await page.evaluate(() => ({
|
||||
inhalt: document.documentElement.scrollWidth,
|
||||
const breite = page.viewportSize()!.width;
|
||||
const inhalt = await page.evaluate(() => ({
|
||||
scroll: document.documentElement.scrollWidth,
|
||||
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. */
|
||||
|
||||
@@ -6,6 +6,8 @@ import { anmelden, E2E_PRAEFIX, keinQuerscrollen } from "./helfer";
|
||||
* keinen, wird der Ablauf übersprungen und sagt das.
|
||||
*/
|
||||
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}`;
|
||||
await anmelden(page, "/vorlagen");
|
||||
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 expect(page.getByLabel("Docker CE")).toBeChecked();
|
||||
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.getByLabel("Kurzname (für --preset)").fill(name);
|
||||
|
||||
@@ -127,6 +127,21 @@ async function eingabe(
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const { mock, client } = await aufbauen({ dhcpNachAbfragen: 2 });
|
||||
const befehle: Array<{ befehl: string; eingabe?: string }> = [];
|
||||
|
||||
@@ -8,15 +8,22 @@ import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-moc
|
||||
|
||||
const mock = new ProxmoxMock();
|
||||
|
||||
function client(ueberschreiben: Partial<ConstructorParameters<typeof ProxmoxClient>[0]> = {}) {
|
||||
return new ProxmoxClient({
|
||||
url: mock.url,
|
||||
tokenId: MOCK_TOKEN_ID,
|
||||
tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS,
|
||||
tlsPruefen: false,
|
||||
fingerabdruck: mock.fingerabdruck,
|
||||
...ueberschreiben,
|
||||
});
|
||||
function client(
|
||||
ueberschreiben: Partial<ConstructorParameters<typeof ProxmoxClient>[0]> = {},
|
||||
optionen: ConstructorParameters<typeof ProxmoxClient>[1] = {},
|
||||
) {
|
||||
return new ProxmoxClient(
|
||||
{
|
||||
url: mock.url,
|
||||
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> {
|
||||
@@ -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)", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,7 +147,7 @@ describe("Template-Bau", () => {
|
||||
cpu: "host",
|
||||
serial0: "socket",
|
||||
agent: "enabled=1,fstrim_cloned_disks=1",
|
||||
ide2: "local-zfs:cloudinit",
|
||||
scsi1: "local-zfs:cloudinit",
|
||||
tags: "proxdeploy-template;debian-13",
|
||||
});
|
||||
const configs = mock.anfragen.filter((a) => a.methode === "PUT").map((a) => Object.fromEntries(a.rumpf));
|
||||
|
||||
@@ -26,6 +26,8 @@ export type MockAntwort = {
|
||||
statusText?: string;
|
||||
data?: unknown;
|
||||
errors?: Record<string, string>;
|
||||
/** Verbindung ohne Antwort hart trennen – wie ein pveproxy, der eine Keep-Alive-Verbindung schließt. */
|
||||
trennen?: boolean;
|
||||
};
|
||||
export type MockRoute = (
|
||||
anfrage: MockAnfrage,
|
||||
@@ -124,6 +126,10 @@ export class ProxmoxMock {
|
||||
if (!treffer) continue;
|
||||
try {
|
||||
const antwort = await route(anfrage, treffer);
|
||||
if (antwort.trennen) {
|
||||
res.socket?.destroy();
|
||||
return;
|
||||
}
|
||||
senden(antwort.status ?? 200, antwort.statusText ?? "OK", {
|
||||
data: antwort.data ?? null,
|
||||
...(antwort.errors ? { errors: antwort.errors } : {}),
|
||||
|
||||
@@ -130,6 +130,27 @@ describe.runIf(datenbankVerfuegbar)("Server-Verwaltung", () => {
|
||||
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 () => {
|
||||
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user