docs(readme): Banner, Screenshots, Installation und erste Schritte
Screenshots per scripts/screenshots.ts aus dem gebauten Stand. Dabei gefunden und gefüllt: die Übersicht zeigte nur einen Platzhalter statt der letzten Deployments. Closes #018 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -11,13 +11,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
- Betrieb per Docker Compose (postgres, migrate, app, worker) mit einem Image für alles, non-root (#019)
|
- Betrieb per Docker Compose (postgres, migrate, app, worker) mit einem Image für alles, non-root (#019)
|
||||||
- LXC-Installer `deploy/install-lxc.sh`: legt auf dem Proxmox-Host einen Debian-13-Container an, installiert Node.js und PostgreSQL, richtet systemd-Dienste ein; mit Update-Pfad (#019)
|
- LXC-Installer `deploy/install-lxc.sh`: legt auf dem Proxmox-Host einen Debian-13-Container an, installiert Node.js und PostgreSQL, richtet systemd-Dienste ein; mit Update-Pfad (#019)
|
||||||
- Migrationen im Betrieb ohne Prisma-CLI, kompatibel zu Prisma (#019)
|
- Migrationen im Betrieb ohne Prisma-CLI, kompatibel zu Prisma (#019)
|
||||||
- Übersicht zeigt Nodes, CPUs und freien RAM je Server sowie die letzten Deployments (#018)
|
- Übersicht zeigt Nodes, CPUs und freien RAM je Server (#018)
|
||||||
|
- README mit Banner, Screenshots, Installation, ersten Schritten und Katalog-Anleitungen (#018)
|
||||||
- Marken-Dateien nach Vorgabe unter `public/brand/`: Symbol (dunkel/hell), kleines Icon, Avatar, Logo (dunkel/hell), Banner 1280×640, Favicons, Apple-Touch- und Manifest-Icons; erzeugt mit `pnpm marke` (#017)
|
- Marken-Dateien nach Vorgabe unter `public/brand/`: Symbol (dunkel/hell), kleines Icon, Avatar, Logo (dunkel/hell), Banner 1280×640, Favicons, Apple-Touch- und Manifest-Icons; erzeugt mit `pnpm marke` (#017)
|
||||||
- Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017)
|
- Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017)
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
- Die Übersicht zeigte statt der letzten Deployments nur einen Platzhalter (#018)
|
||||||
|
|
||||||
### Entfernt
|
### Entfernt
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,80 @@
|
|||||||
# ProxDeploy
|
<p align="center"><img src="public/brand/banner.svg" alt="ProxDeploy – VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI." width="100%"></p>
|
||||||
|
|
||||||
**VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.**
|
**ProxDeploy** ist eine selbst gehostete Webanwendung mit einer CLI (`proxdeploy`). Damit wählt man
|
||||||
|
einen von mehreren Proxmox-VE-Servern (8.x/9.x, Einzelhost oder Cluster) und rollt darauf vollständig
|
||||||
|
unattended eine VM oder einen LXC-Container mit Debian oder Ubuntu aus – mit Profilen wie `base`,
|
||||||
|
`docker` oder `zabbix-agent`. Distributionen und Profile sind YAML-Dateien im Katalog, kein Code.
|
||||||
|
|
||||||
ProxDeploy ist eine selbst gehostete Webanwendung mit einer CLI (`proxdeploy`). Damit wählt man einen von
|
> ProxDeploy ist ein unabhängiges Werkzeug und stammt nicht von der Proxmox Server Solutions GmbH.
|
||||||
mehreren Proxmox-VE-Servern (8.x/9.x, Einzelhost oder Cluster) und rollt darauf vollständig unattended
|
|
||||||
eine VM oder einen LXC-Container mit Debian oder Ubuntu aus, inklusive Profilen wie `base`, `docker`
|
|
||||||
oder `zabbix-agent`. Weitere Distributionen und Profile sind YAML-Dateien im Katalog, kein Code.
|
|
||||||
|
|
||||||
> ProxDeploy ist ein unabhängiges Werkzeug und stammt nicht von Proxmox Server Solutions GmbH.
|
|
||||||
> „Proxmox“ ist eine Marke der Proxmox Server Solutions GmbH.
|
> „Proxmox“ ist eine Marke der Proxmox Server Solutions GmbH.
|
||||||
|
|
||||||
## Stand
|
<p align="center"><img src="docs/bilder/live-ansicht.png" alt="Live-Ansicht eines Deployments: Phasen mit Status, Abschlussbox mit IP und SSH-Befehl" width="100%"></p>
|
||||||
|
|
||||||
In Entwicklung. Version und Fortschritt stehen im [CHANGELOG](CHANGELOG.md), der Plan in
|
## Was es kann
|
||||||
[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md).
|
|
||||||
|
|
||||||
| Etappe | Inhalt | Stand |
|
- **Assistent in neun Schritten** – Server, Typ, Distribution, Name, Ressourcen, Netzwerk (DHCP/statisch,
|
||||||
|
VLAN), Zugang (SSH-Schlüssel), Profile mit Variablen – und am Ende ein **Dry-Run** mit allen geplanten
|
||||||
|
Proxmox-Aufrufen und dem passenden CLI-Befehl.
|
||||||
|
- **Live-Fortschritt** je Phase (Server-Sent Events), in Web und CLI mit denselben Texten; bei Fehlern
|
||||||
|
Ursache und Lösungshinweis, danach auf Wunsch **Aufräumen** des halb angelegten Gasts.
|
||||||
|
- **Templates** aus offiziellen Cloud-Images (Prüfsumme geprüft, UEFI, Guest-Agent eingebaut), auf
|
||||||
|
Wunsch mit eingebackenen Profilen – ein Docker-Host ist dann in Sekunden statt Minuten fertig.
|
||||||
|
- **Ohne SSH zum Node** per API und Guest-Agent; mit SSH zusätzlich LXC-Provisionierung und
|
||||||
|
cloud-init-Snippets. Was ein Server kann, zeigt der Verbindungstest.
|
||||||
|
- **Verlauf** mit Filtern und Wiederholen, **Vorlagen** („Docker-Host klein“), hinterlegte
|
||||||
|
**SSH-Schlüssel**, **Rollen** Admin/Operator, **Audit-Log**, Anmeldung lokal oder per **OIDC**.
|
||||||
|
- **CLI** als einzelnes Binary für Linux, macOS und Windows – mit Batch-Dateien für viele Gäste.
|
||||||
|
|
||||||
|
| Übersicht | Assistent mit Dry-Run | Am Handy |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 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) |
|
Weitere Ansichten: [Verlauf](docs/bilder/verlauf.png) · [Server mit Verbindungstest](docs/bilder/server.png) · [Katalog](docs/bilder/katalog.png).
|
||||||
| 4 · v0.4.0 | CLI `proxdeploy` (Go), API-Tokens | ✓ fertig, auf PVE 9.2 geprüft |
|
Erzeugt mit `scripts/screenshots.ts` aus dem gebauten Stand.
|
||||||
| 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, Installer | ◐ als Nächstes |
|
## Installation
|
||||||
|
|
||||||
|
ProxDeploy braucht PostgreSQL und läuft als zwei Prozesse (app und worker). Es gehört hinter einen
|
||||||
|
Reverse-Proxy mit TLS; dann `TRUST_PROXY=true` und `APP_URL` auf die öffentliche Adresse setzen.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.wilde-server.de/florian.wilde/proxdeploy.git && cd proxdeploy
|
||||||
|
cp .env.example .env
|
||||||
|
# POSTGRES_PASSWORT=$(openssl rand -hex 24)
|
||||||
|
# PROXDEPLOY_SCHLUESSEL=$(openssl rand -base64 32) ← sichern! ohne ihn sind Tokens und Schlüssel verloren
|
||||||
|
# SITZUNG_GEHEIMNIS=$(openssl rand -base64 48)
|
||||||
|
# APP_URL=https://proxdeploy.example
|
||||||
|
# ADMIN_PASSWORT=… ← erster Admin, nur beim allerersten Start
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose startet `postgres`, spielt mit `migrate` die Datenbank ein und startet dann `app` (Port 3000 auf
|
||||||
|
127.0.0.1) und `worker`. Aktualisieren: `git pull && docker compose up -d --build`.
|
||||||
|
|
||||||
|
### In einem Debian-LXC auf dem Proxmox-Host
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
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/`.
|
||||||
|
|
||||||
|
## Erste Schritte
|
||||||
|
|
||||||
|
1. **Token in Proxmox:** eigenen Benutzer und die Rolle `ProxDeploy` anlegen, Token ohne
|
||||||
|
Privilege Separation – die Befehle für 8.x und 9.x stehen in [docs/proxmox-rechte.md](docs/proxmox-rechte.md).
|
||||||
|
2. **Server anlegen:** URL, Token, Zertifikat bestätigen (Fingerprint-Pinning für selbst signierte
|
||||||
|
Zertifikate), „Verbindung testen“. Optional SSH-Zugang zum Node hinterlegen.
|
||||||
|
3. **SSH-Schlüssel** unter Einstellungen hinterlegen – sie kommen automatisch in jeden Gast.
|
||||||
|
4. **Ausrollen:** fehlt das Template, baut ProxDeploy es vorher (oder unter „Templates“).
|
||||||
|
|
||||||
## Was ProxDeploy auf einem Server kann
|
## Was ProxDeploy auf einem Server kann
|
||||||
|
|
||||||
@@ -129,6 +181,28 @@ OIDC_GRUPPE_OPERATOR=proxdeploy-operators
|
|||||||
- Gesperrte Konten bleiben gesperrt, auch per SSO. Getestet gegen einen Mock-IdP (`tests/oidc.test.ts`);
|
- Gesperrte Konten bleiben gesperrt, auch per SSO. Getestet gegen einen Mock-IdP (`tests/oidc.test.ts`);
|
||||||
ein echter IdP folgt beim Feldtest.
|
ein echter IdP folgt beim Feldtest.
|
||||||
|
|
||||||
|
## Katalog erweitern
|
||||||
|
|
||||||
|
- **Neue Distribution:** eine YAML-Datei unter `catalog/distros/` – Image-URL, Prüfsummen-Quelle,
|
||||||
|
Standardbenutzer, LXC-Template-Muster. Für eine neue Familie (z. B. `rhel` mit dnf) kommt ein
|
||||||
|
Adapter unter `src/engine/adapter/` dazu. → [docs/katalog.md](docs/katalog.md#neue-distribution-hinzufügen)
|
||||||
|
- **Eigenes Profil:** in der Web-GUI unter Katalog → „Profil anlegen“ (YAML mit Live-Prüfung und
|
||||||
|
Skriptvorschau) oder als Datei unter `catalog/profiles/`. → [docs/katalog.md](docs/katalog.md#eigenes-profil-anlegen)
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
| 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, Installer | ◐ als Nächstes |
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
Voraussetzungen: Node 24 (z. B. über nvm), pnpm 12 (corepack), Docker für PostgreSQL.
|
Voraussetzungen: Node 24 (z. B. über nvm), pnpm 12 (corepack), Docker für PostgreSQL.
|
||||||
@@ -154,12 +228,6 @@ Go-Client nutzt die aus `cli/api/openapi.json` erzeugten Typen.
|
|||||||
|
|
||||||
Konventionen (deutsche Bezeichner, Schichtung, Versionen) stehen in [CLAUDE.md](CLAUDE.md).
|
Konventionen (deutsche Bezeichner, Schichtung, Versionen) stehen in [CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
## Betrieb
|
|
||||||
|
|
||||||
Docker Compose (app, worker, postgres) und ein Installer für einen Debian-LXC auf Proxmox folgen mit
|
|
||||||
Etappe 6. ProxDeploy ist für den Betrieb hinter einem Reverse-Proxy mit TLS gedacht
|
|
||||||
(`TRUST_PROXY=true`, `APP_URL` auf die öffentliche Adresse).
|
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
Noch nicht festgelegt.
|
Noch nicht festgelegt.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 137 KiB |
@@ -1,5 +1,5 @@
|
|||||||
# #018 README und Dokumentation
|
# #018 README und Dokumentation
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Meilenstein:** v0.6.0
|
**Meilenstein:** v0.6.0
|
||||||
**Labels:** doku
|
**Labels:** doku
|
||||||
**Geschätzt:** 4 h **Tatsächlich:** –
|
**Geschätzt:** 4 h **Tatsächlich:** –
|
||||||
@@ -8,14 +8,16 @@
|
|||||||
README mit Banner, Screenshots, Installation, CLI, Proxmox-Rechten, neuer Distribution, eigenem Profil.
|
README mit Banner, Screenshots, Installation, CLI, Proxmox-Rechten, neuer Distribution, eigenem Profil.
|
||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
- [ ] Screenshots aus dem gebauten Stand
|
- [x] Screenshots aus dem gebauten Stand (`scripts/screenshots.ts`, dunkel, 1440 px und 390 px, echte Daten von pve-demo)
|
||||||
- [ ] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions
|
- [x] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions
|
||||||
|
- [x] Banner oben, Funktionen, Installation (Compose, LXC), Erste Schritte, CLI, OIDC, Katalog erweitern
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
- [ ] Anleitungen einmal Schritt für Schritt nachvollzogen
|
- [x] Anleitungen einmal Schritt für Schritt nachvollzogen: Compose (eigene Env-Datei, frische DB), LXC-Installer auf pve-demo, „Erste Schritte“ in der frisch installierten Instanz (Server anlegen, testen, Token, CLI, Dry-Run, echter LXC). „Neue Distribution“ ist der Weg, auf dem Ubuntu 26.04 dazukam (#010); „Eigenes Profil“ prüft der Playwright-Ablauf `profil-editor`.
|
||||||
|
|
||||||
## Abhängigkeiten
|
## Abhängigkeiten
|
||||||
#013
|
#013
|
||||||
|
|
||||||
## Notizen / Entscheidungen
|
## Notizen / Entscheidungen
|
||||||
–
|
- Beim Erstellen der Screenshots fiel auf: die Übersicht zeigte seit Etappe 1 nur einen Platzhalter
|
||||||
|
(„Noch keine Deployments“) – jetzt Server mit Nodes, CPUs und freiem RAM sowie die letzten Aufträge.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
|||||||
| [#015](015-rollen-audit.md) | Rollen, Benutzer, Tokens, Audit-Log | erledigt | v0.5.0 | 6 h | 0 h 10 min |
|
| [#015](015-rollen-audit.md) | Rollen, Benutzer, Tokens, Audit-Log | erledigt | v0.5.0 | 6 h | 0 h 10 min |
|
||||||
| [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min |
|
| [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min |
|
||||||
| [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | – |
|
| [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | – |
|
||||||
| [#018](018-readme.md) | README und Dokumentation | offen | v0.6.0 | 4 h | – |
|
| [#018](018-readme.md) | README und Dokumentation | erledigt | v0.6.0 | 4 h | – |
|
||||||
| [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | – |
|
| [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | – |
|
||||||
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – |
|
| [#020](020-feldtest.md) | Feldtest 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 |
|
| [#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 |
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Screenshots für das README aus dem gebauten Stand (`scripts/neu-starten.sh`, Port 3310), dunkles
|
||||||
|
* Theme, 1440 px und 390 px. Ergebnis: docs/bilder/*.png
|
||||||
|
*
|
||||||
|
* pnpm exec tsx scripts/screenshots.ts [auftrag-kurzid]
|
||||||
|
*/
|
||||||
|
import { mkdirSync } from "node:fs";
|
||||||
|
import { chromium, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
const BASIS = process.env["SCREENSHOT_URL"] ?? "http://127.0.0.1:3310";
|
||||||
|
const BENUTZER = process.env["E2E_BENUTZER"] ?? "admin";
|
||||||
|
const PASSWORT = process.env["E2E_PASSWORT"] ?? "proxdeploy-dev-admin";
|
||||||
|
const ZIEL = "docs/bilder";
|
||||||
|
|
||||||
|
async function anmelden(seite: Page) {
|
||||||
|
await seite.goto(`${BASIS}/anmelden`);
|
||||||
|
await seite.getByLabel("Benutzername").fill(BENUTZER);
|
||||||
|
await seite.getByLabel("Passwort").fill(PASSWORT);
|
||||||
|
await seite.getByRole("button", { name: "Anmelden" }).click();
|
||||||
|
await seite.waitForURL((url) => !url.pathname.startsWith("/anmelden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bild(seite: Page, name: string, ganz = false) {
|
||||||
|
await seite.waitForLoadState("networkidle");
|
||||||
|
// Blinkender Cursor und Spinner sollen nicht zufällig halb im Bild stehen.
|
||||||
|
await seite.addStyleTag({ content: "*{animation:none!important;transition:none!important}" });
|
||||||
|
await seite.screenshot({ path: `${ZIEL}/${name}.png`, fullPage: ganz });
|
||||||
|
console.log(`${ZIEL}/${name}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const auftrag = process.argv[2];
|
||||||
|
if (!auftrag) throw new Error("Kurz-ID eines erfolgreichen Auftrags angeben (für die Live-Ansicht).");
|
||||||
|
mkdirSync(ZIEL, { recursive: true });
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
try {
|
||||||
|
const kontext = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
});
|
||||||
|
await kontext.addCookies([{ name: "pd_theme", value: "dunkel", url: BASIS }]);
|
||||||
|
const seite = await kontext.newPage();
|
||||||
|
await anmelden(seite);
|
||||||
|
|
||||||
|
await seite.goto(`${BASIS}/`);
|
||||||
|
await bild(seite, "uebersicht");
|
||||||
|
|
||||||
|
await seite.goto(`${BASIS}/auftraege/${auftrag}`);
|
||||||
|
await bild(seite, "live-ansicht");
|
||||||
|
|
||||||
|
await seite.goto(`${BASIS}/auftraege`);
|
||||||
|
await bild(seite, "verlauf");
|
||||||
|
|
||||||
|
await seite.goto(`${BASIS}/server/demo`);
|
||||||
|
await bild(seite, "server", true);
|
||||||
|
|
||||||
|
await seite.goto(`${BASIS}/katalog`);
|
||||||
|
await bild(seite, "katalog");
|
||||||
|
|
||||||
|
// Assistent bis zur Zusammenfassung – mit echtem Dry-Run gegen den Server.
|
||||||
|
await seite.goto(`${BASIS}/ausrollen`);
|
||||||
|
await seite.waitForLoadState("networkidle");
|
||||||
|
const weiter = async (bis: number) => {
|
||||||
|
await seite.getByRole("button", { name: "Weiter" }).click();
|
||||||
|
await seite.getByText(`Schritt ${bis} von 9`).waitFor();
|
||||||
|
};
|
||||||
|
for (let s = 2; s <= 4; s++) await weiter(s);
|
||||||
|
await seite.getByLabel("Name (Hostname)").fill("web01");
|
||||||
|
for (let s = 5; s <= 7; s++) await weiter(s);
|
||||||
|
// Ohne hinterlegten Schlüssel einen angeben (öffentlicher Beispielschlüssel).
|
||||||
|
const feld = seite.locator("textarea#schluessel");
|
||||||
|
if ((await feld.inputValue()) === "")
|
||||||
|
await feld.fill(
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFTdkCuqQ6mF7I8QzmkNaQpKj7S/I2+yuxWeYasJjLd florian@laptop",
|
||||||
|
);
|
||||||
|
for (let s = 8; s <= 9; s++) await weiter(s);
|
||||||
|
await seite.getByText("Geplante Proxmox-Aufrufe").waitFor({ timeout: 60_000 });
|
||||||
|
await bild(seite, "assistent-zusammenfassung");
|
||||||
|
|
||||||
|
const handy = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
|
||||||
|
await handy.addCookies([{ name: "pd_theme", value: "dunkel", url: BASIS }]);
|
||||||
|
const klein = await handy.newPage();
|
||||||
|
await anmelden(klein);
|
||||||
|
await klein.goto(`${BASIS}/auftraege/${auftrag}`);
|
||||||
|
await bild(klein, "handy-live-ansicht");
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
import { Karte } from "@/components/karte";
|
import { Karte } from "@/components/karte";
|
||||||
import { Status } from "@/components/status";
|
import { Status } from "@/components/status";
|
||||||
import Link from "next/link";
|
import { ART_TEXT, STATUS_ANZEIGE } from "@/lib/auftrag-text";
|
||||||
|
import { bytes, zeitpunkt } from "@/lib/formate";
|
||||||
import { seiteSchuetzen } from "@/server/auth/zugang";
|
import { seiteSchuetzen } from "@/server/auth/zugang";
|
||||||
|
import { auftraegeSuchen } from "@/server/services/auftraege";
|
||||||
import { serverListe } from "@/server/services/server";
|
import { serverListe } from "@/server/services/server";
|
||||||
import { ServerStatus } from "./server/server-status";
|
import { ServerStatus } from "./server/server-status";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Übersicht" };
|
export const metadata: Metadata = { title: "Übersicht" };
|
||||||
|
|
||||||
|
/** Server mit Status und freien Ressourcen (aus dem letzten Verbindungstest), letzte Aufträge. */
|
||||||
export default async function Uebersicht() {
|
export default async function Uebersicht() {
|
||||||
const benutzer = await seiteSchuetzen("lesen", "/");
|
const benutzer = await seiteSchuetzen("lesen", "/");
|
||||||
const server = await serverListe();
|
const [server, { eintraege }] = await Promise.all([serverListe(), auftraegeSuchen({ seite: 1 })]);
|
||||||
|
const letzte = eintraege.slice(0, 8);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -32,23 +37,73 @@ export default async function Uebersicht() {
|
|||||||
<Status art="offen">Noch kein Proxmox-Server angelegt.</Status>
|
<Status art="offen">Noch kein Proxmox-Server angelegt.</Status>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-linie flex flex-col divide-y">
|
<ul className="divide-linie flex flex-col divide-y">
|
||||||
{server.map((eintrag) => (
|
{server.map((eintrag) => {
|
||||||
<li
|
const knoten = eintrag.letzterTest?.knoten ?? [];
|
||||||
key={eintrag.name}
|
const online = knoten.filter((k) => k.online);
|
||||||
className="flex items-center justify-between gap-3 py-2.5 first:pt-0 last:pb-0"
|
const frei = online.reduce(
|
||||||
>
|
(s, k) => s + ((k.speicherGesamt ?? 0) - (k.speicherBelegt ?? 0)),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const cpus = online.reduce((s, k) => s + (k.cpus ?? 0), 0);
|
||||||
|
return (
|
||||||
|
<li key={eintrag.name} className="flex flex-col gap-1 py-2.5 first:pt-0 last:pb-0">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Link href={`/server/${eintrag.name}`} className="min-w-0 hover:underline">
|
<Link href={`/server/${eintrag.name}`} className="min-w-0 hover:underline">
|
||||||
<span className="font-medium">{eintrag.titel}</span>{" "}
|
<span className="font-medium">{eintrag.titel}</span>{" "}
|
||||||
<span className="text-muted font-mono text-sm">{eintrag.name}</span>
|
<span className="text-muted font-mono text-sm">{eintrag.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<ServerStatus server={eintrag} />
|
<ServerStatus server={eintrag} />
|
||||||
|
</div>
|
||||||
|
{knoten.length > 0 ? (
|
||||||
|
<p className="text-muted text-xs">
|
||||||
|
{online.length} von {knoten.length} {knoten.length === 1 ? "Node" : "Nodes"} online
|
||||||
|
{online.length > 0 ? ` · ${cpus} CPUs · ${bytes(frei)} RAM frei` : ""}
|
||||||
|
{eintrag.letzterTest?.cluster ? ` · Cluster ${eintrag.letzterTest.cluster.name}` : ""}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</Karte>
|
</Karte>
|
||||||
<Karte titel="Letzte Deployments">
|
<Karte
|
||||||
|
titel="Letzte Deployments"
|
||||||
|
aktion={
|
||||||
|
<Link href="/auftraege" className="text-akzent text-sm">
|
||||||
|
Verlauf
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{letzte.length === 0 ? (
|
||||||
<Status art="offen">Noch keine Deployments.</Status>
|
<Status art="offen">Noch keine Deployments.</Status>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-linie flex flex-col divide-y">
|
||||||
|
{letzte.map((a) => {
|
||||||
|
const [art, text] = STATUS_ANZEIGE[a.status];
|
||||||
|
return (
|
||||||
|
<li key={a.id}>
|
||||||
|
<Link
|
||||||
|
href={`/auftraege/${a.kurzId}`}
|
||||||
|
className="hover:bg-panel-hoch/60 -mx-2 flex items-center justify-between gap-3 rounded-lg px-2 py-2"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
<span className="font-medium">{a.name ?? ART_TEXT[a.art]}</span>
|
||||||
|
<span className="text-muted text-sm">
|
||||||
|
{" "}
|
||||||
|
{a.art !== "AUSROLLEN" && a.name ? `· ${ART_TEXT[a.art]} ` : ""}· {a.server.name}
|
||||||
|
{a.vmid ? ` · ${a.vmid}` : ""} · {zeitpunkt(a.erstelltAm)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<Status art={art} className="shrink-0 text-sm">
|
||||||
|
{text}
|
||||||
|
</Status>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</Karte>
|
</Karte>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||