Files
sshkeyvault/scripts/screenshots.ts
T
florian.wildeandClaude Opus 5.5 263143134f feat(schluessel): erzeugen, importieren, exportieren (#012)
Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste
mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem
Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub.

Warum so:
- Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil
  nur er den Master-Key hat. Passphrase und importierter Private Key
  reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt
  im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach.
- Ein reiner Public Key ist kein Geheimnis und wird direkt im Web
  angelegt, mit verstaendlicher Meldung bei einem Duplikat.
- Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist,
  verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein
  struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf
  Schluessel ausserhalb des Workers ein festes select.
- Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das
  Loeschen des Private Keys; CHECKs fuer den Slug (landet in
  Dateinamen) und "geloescht nur ohne Chiffrat".
- Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:29:52 +02:00

120 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Screenshots der Oberfläche für Anleitung und Design-Abnahme (#002).
*
* pnpm build && pnpm start # erstes Terminal
* pnpm exec tsx scripts/screenshots.ts # zweites Terminal
*
* Umgebung:
* SSHKV_URL Basis-URL (Standard http://localhost:3400)
* SSHKV_BILDER Zielordner (Standard docs/anleitung/bilder)
* SSHKV_SEITEN Kommagetrennte Auswahl der Seitennamen (Standard: alle)
*
* Prüft nebenbei, dass keine Seite horizontal scrollt (Design-Vorgabe ab 390 px), und
* endet dann mit Fehlercode – so taugt das Skript auch als schneller Layout-Test.
*/
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { chromium } from "@playwright/test";
const BASIS = (process.env["SSHKV_URL"] ?? "http://localhost:3400").replace(/\/$/, "");
const ZIEL = process.env["SSHKV_BILDER"] ?? join(process.cwd(), "docs/anleitung/bilder");
/**
* `oeffentlich`: ohne Anmeldung aufnehmen. Alle anderen nach Anmeldung ueber den Mock-OIDC (#004).
* `folgen`: Seiten mit wechselnder ID (#008) – erst `pfad` laden, dann dem ersten Link folgen,
* der zum Selektor passt (z. B. der erste Host der Liste, bei SSHKV_SEED_DEMO=1 ein Demo-Host).
*/
const SEITEN: { name: string; pfad: string; oeffentlich?: boolean; folgen?: string }[] = [
{ name: "anmelden", pfad: "/anmelden", oeffentlich: true },
{ name: "uebersicht", pfad: "/" },
{ name: "schluessel", pfad: "/schluessel" },
{ name: "schluessel-neu", pfad: "/schluessel/neu" },
// Der Demo-Key mit nahem Ablaufdatum zeigt die meisten Badges (SSHKV_SEED_DEMO=1, #012).
{
name: "schluessel-detail",
pfad: "/schluessel?q=wartung",
folgen: 'main table a[href^="/schluessel/"]',
},
{ name: "hosts", pfad: "/hosts" },
{ name: "host-detail", pfad: "/hosts", folgen: 'main table a[href^="/hosts/"]' },
];
const ANSICHTEN = [
{ name: "desktop", width: 1440, height: 900 },
{ name: "mobil", width: 390, height: 844 },
];
async function main() {
const auswahl = process.env["SSHKV_SEITEN"]?.split(",").map((s) => s.trim());
const seiten = auswahl ? SEITEN.filter((s) => auswahl.includes(s.name)) : SEITEN;
await mkdir(ZIEL, { recursive: true });
const browser = await chromium.launch();
const fehler: string[] = [];
try {
for (const ansicht of ANSICHTEN) {
const kontext = await browser.newContext({
viewport: { width: ansicht.width, height: ansicht.height },
deviceScaleFactor: 1,
locale: "de-DE",
timezoneId: "Europe/Berlin",
colorScheme: "dark",
});
const seite = await kontext.newPage();
let angemeldet = false;
for (const s of seiten) {
if (!s.oeffentlich && !angemeldet) {
// Anmeldung ueber den Mock-OIDC aus docker-compose.test.yml, Rolle Admin.
await seite.goto(`${BASIS}/anmelden`);
await seite
.getByRole("link", { name: /anmelden/i })
.first()
.click();
await seite.locator('button[data-rolle="admin"]').click();
await seite.getByRole("button", { name: "Anmelden", exact: true }).click();
await seite.waitForURL(`${BASIS}/`);
angemeldet = true;
}
let antwort = await seite.goto(`${BASIS}${s.pfad}`, { waitUntil: "networkidle" });
if (s.folgen && antwort?.ok()) {
const links = seite.locator(s.folgen);
const ziel = (await links.count()) > 0 ? await links.first().getAttribute("href") : null;
if (!ziel) {
fehler.push(`${s.pfad}: kein Link für „${s.name}“ (${s.folgen}) – Demo-Daten geseedet?`);
continue;
}
antwort = await seite.goto(`${BASIS}${ziel}`, { waitUntil: "networkidle" });
}
if (!antwort?.ok()) {
fehler.push(`${s.pfad}: HTTP ${antwort?.status() ?? "keine Antwort"}`);
continue;
}
// Erst nach dem Laden der selbst gehosteten Schriften fotografieren, sonst
// stehen Fallback-Schriften im Bild.
await seite.evaluate(() => document.fonts.ready);
const ueberlauf = await seite.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
if (ueberlauf > 0) {
fehler.push(`${s.pfad} @ ${ansicht.width}px: ${ueberlauf}px horizontaler Überlauf`);
}
const datei = join(ZIEL, `${s.name}-${ansicht.name}.png`);
await seite.screenshot({ path: datei, fullPage: true });
console.log(`✓ ${datei}`);
}
await kontext.close();
}
} finally {
await browser.close();
}
if (fehler.length > 0) {
console.error(`\n${fehler.length} Problem(e):\n- ${fehler.join("\n- ")}`);
process.exitCode = 1;
}
}
await main();