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>
This commit is contained in:
2026-10-12 17:37:26 +02:00
co-authored by Claude Opus 5.5
parent 533a821087
commit 5379f34a0c
13 changed files with 265 additions and 47 deletions
+91
View File
@@ -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();