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)
|
||||
- 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)
|
||||
- Ü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)
|
||||
- Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017)
|
||||
|
||||
### Geändert
|
||||
|
||||
### Behoben
|
||||
- Die Übersicht zeigte statt der letzten Deployments nur einen Platzhalter (#018)
|
||||
|
||||
### 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
|
||||
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.
|
||||
> ProxDeploy ist ein unabhängiges Werkzeug und stammt nicht von 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
|
||||
[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md).
|
||||
## Was es kann
|
||||
|
||||
| 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) |
|
||||
| 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 |
|
||||
|  |  |  |
|
||||
|
||||
Weitere Ansichten: [Verlauf](docs/bilder/verlauf.png) · [Server mit Verbindungstest](docs/bilder/server.png) · [Katalog](docs/bilder/katalog.png).
|
||||
Erzeugt mit `scripts/screenshots.ts` aus dem gebauten Stand.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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`);
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.6.0
|
||||
**Labels:** doku
|
||||
**Geschätzt:** 4 h **Tatsächlich:** –
|
||||
@@ -8,14 +8,16 @@
|
||||
README mit Banner, Screenshots, Installation, CLI, Proxmox-Rechten, neuer Distribution, eigenem Profil.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Screenshots aus dem gebauten Stand
|
||||
- [ ] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions
|
||||
- [x] Screenshots aus dem gebauten Stand (`scripts/screenshots.ts`, dunkel, 1440 px und 390 px, echte Daten von pve-demo)
|
||||
- [x] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions
|
||||
- [x] Banner oben, Funktionen, Installation (Compose, LXC), Erste Schritte, CLI, OIDC, Katalog erweitern
|
||||
|
||||
## 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
|
||||
#013
|
||||
|
||||
## 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 |
|
||||
| [#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 | – |
|
||||
| [#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 | – |
|
||||
| [#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 |
|
||||
|
||||
@@ -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 Link from "next/link";
|
||||
import { Karte } from "@/components/karte";
|
||||
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 { auftraegeSuchen } from "@/server/services/auftraege";
|
||||
import { serverListe } from "@/server/services/server";
|
||||
import { ServerStatus } from "./server/server-status";
|
||||
|
||||
export const metadata: Metadata = { title: "Übersicht" };
|
||||
|
||||
/** Server mit Status und freien Ressourcen (aus dem letzten Verbindungstest), letzte Aufträge. */
|
||||
export default async function Uebersicht() {
|
||||
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 (
|
||||
<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>
|
||||
) : (
|
||||
<ul className="divide-linie flex flex-col divide-y">
|
||||
{server.map((eintrag) => (
|
||||
<li
|
||||
key={eintrag.name}
|
||||
className="flex items-center justify-between gap-3 py-2.5 first:pt-0 last:pb-0"
|
||||
>
|
||||
<Link href={`/server/${eintrag.name}`} className="min-w-0 hover:underline">
|
||||
<span className="font-medium">{eintrag.titel}</span>{" "}
|
||||
<span className="text-muted font-mono text-sm">{eintrag.name}</span>
|
||||
</Link>
|
||||
<ServerStatus server={eintrag} />
|
||||
</li>
|
||||
))}
|
||||
{server.map((eintrag) => {
|
||||
const knoten = eintrag.letzterTest?.knoten ?? [];
|
||||
const online = knoten.filter((k) => k.online);
|
||||
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">
|
||||
<span className="font-medium">{eintrag.titel}</span>{" "}
|
||||
<span className="text-muted font-mono text-sm">{eintrag.name}</span>
|
||||
</Link>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Karte>
|
||||
<Karte titel="Letzte Deployments">
|
||||
<Status art="offen">Noch keine Deployments.</Status>
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||