From 445f5c5ebd35e436582c780c371f98d93159e74e Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 12 Oct 2026 09:20:34 +0200 Subject: [PATCH] feat(ui): Prototyp Uebersicht und Schluesseldetail (#002) Klickbarer Prototyp mit Beispieldaten aus src/beispiel/, sichtbar als Beispiel gekennzeichnet. Er zeigt das verbindliche Design an echten Bildschirmen, bevor die Lesemodelle (#012, #021, #026) existieren; die Seiten tauschen spaeter nur die Datenquelle. Die bisherige Startseite entfaellt zugunsten der Route-Gruppe (app). Co-Authored-By: Claude Opus 5.5 --- src/app/(app)/page.tsx | 187 +++++++++++++++++++++ src/app/(app)/schluessel/beispiel/page.tsx | 181 ++++++++++++++++++++ src/app/page.tsx | 3 - src/beispiel/schluessel.ts | 75 +++++++++ src/beispiel/uebersicht.ts | 89 ++++++++++ 5 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 src/app/(app)/page.tsx create mode 100644 src/app/(app)/schluessel/beispiel/page.tsx delete mode 100644 src/app/page.tsx create mode 100644 src/beispiel/schluessel.ts create mode 100644 src/beispiel/uebersicht.ts diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx new file mode 100644 index 0000000..6978a9b --- /dev/null +++ b/src/app/(app)/page.tsx @@ -0,0 +1,187 @@ +import { ArrowRight, Plus, RefreshCw, Route } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { + abweichungen, + BEISPIEL_JETZT, + baldAblaufend, + kennzahlen, + letzteAktionen, + letzterAbgleich, + type AktionsArt, +} from "@/beispiel/uebersicht"; +import { Badge } from "@/components/ui/Badge"; +import { Button } from "@/components/ui/Button"; +import { Karte } from "@/components/ui/Karte"; +import { Kennzahl } from "@/components/ui/Kennzahl"; +import { Seitenkopf } from "@/components/ui/Seitenkopf"; +import { Statuspunkt } from "@/components/ui/Statuspunkt"; +import { Tabelle, TabellenZeile, TabellenZelle } from "@/components/ui/Tabelle"; +import { datum, datumZeit, relativ, tageBis, uhrzeit } from "@/lib/format"; + +export const metadata: Metadata = { title: "Übersicht" }; + +const AKTION: Record = { + ausgerollt: { text: "Ausgerollt", farbe: "text-ok" }, + widerrufen: { text: "Widerrufen", farbe: "text-danger" }, + erstellt: { text: "Erstellt", farbe: "text-text" }, + abgleich: { text: "Abgleich", farbe: "text-info" }, + rotiert: { text: "Rotiert", farbe: "text-accent" }, +}; + +/** + * Prototyp der Übersicht mit Beispieldaten (#002). Die echte Übersicht mit Lesemodellen + * folgt in #026; Aufbau und Komponenten bleiben. + */ +export default function Uebersicht() { + return ( +
+ Prototyp · Beispieldaten} + untertitel={ + <> + Letzter Abgleich {relativ(letzterAbgleich, BEISPIEL_JETZT)} ·{" "} + + + } + aktionen={ + <> + + + + + } + /> + +
+ + + + +
+ +
+ + + {abweichungen.map((a) => ( + + + {a.host} + + {a.benutzer} + + + + + + Prüfen – {a.host} + + + + ))} + + + + +
    + {baldAblaufend.map((s) => { + const tage = tageBis(s.ablauf, BEISPIEL_JETZT); + return ( +
  • + + + {s.zweck} + + {s.person} · {s.typ} + + + + + + +
  • + ); + })} +
+
+
+ + + Zum Audit-Log + + } + > +
    + {letzteAktionen.map((a) => ( +
  1. + + {AKTION[a.art].text} + {a.beschreibung} + + {a.benutzer} + +
  2. + ))} +
+
+
+ ); +} diff --git a/src/app/(app)/schluessel/beispiel/page.tsx b/src/app/(app)/schluessel/beispiel/page.tsx new file mode 100644 index 0000000..186f9f5 --- /dev/null +++ b/src/app/(app)/schluessel/beispiel/page.tsx @@ -0,0 +1,181 @@ +import { Ban, Check, Lock, PlugZap, Plus, RotateCw } from "lucide-react"; +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { + beispielSchluessel as s, + dryRunDatei, + dryRunDiff, + privatAusgerollt, + publicAusgerollt, + zusatzaktionen, + type Ausrollung, +} from "@/beispiel/schluessel"; +import { BEISPIEL_JETZT } from "@/beispiel/uebersicht"; +import { Badge } from "@/components/ui/Badge"; +import { Breadcrumb } from "@/components/ui/Breadcrumb"; +import { Button } from "@/components/ui/Button"; +import { DiffBox } from "@/components/ui/DiffBox"; +import { InnereBox, Karte } from "@/components/ui/Karte"; +import { MonoKopieren } from "@/components/ui/MonoKopieren"; +import { Seitenkopf } from "@/components/ui/Seitenkopf"; +import { Statuspunkt } from "@/components/ui/Statuspunkt"; +import { datum, datumZeit, tageBis } from "@/lib/format"; + +export const metadata: Metadata = { title: `${s.zweck} (Beispiel)` }; + +/** Prototyp des Schlüsseldetails mit Beispieldaten (#002); echte Daten ab #012/#016/#017. */ +export default function SchluesselBeispiel() { + const tage = tageBis(s.ablauf, BEISPIEL_JETZT); + return ( +
+ } + titel={s.zweck} + badges={ + <> + {s.typ} + {s.kategorie} + Vorlage: {s.vorlage} + + + läuft ab am · in {tage} Tagen + + + + Beispieldaten + + + } + aktionen={ + <> + + + + + } + /> + + +
+ + + + + + von {s.erstelltVon} + + + + + verschlüsselt gespeichert · + + + +
+
+ +
+ + +
+ + + Abgleich für backup@srv-pve02 – noch nichts geschrieben + + } + > + +
+ Zusätzlich ausführen +
    + {zusatzaktionen.map((z) => ( +
  • + +
  • + ))} +
+
+
+ + +
+
+
+ ); +} + +function InfoFeld({ titel, children }: { titel: string; children: ReactNode }) { + return ( +
+
{titel}
+
{children}
+
+ ); +} + +function AusrollKarte({ + titel, + untertitel, + eintraege, +}: { + titel: string; + untertitel: string; + eintraege: Ausrollung[]; +}) { + return ( + }> + Ausrollen + + } + > +
    + {eintraege.map((e) => ( +
  • + +
    +

    {e.ziel}

    +

    {e.pfad}

    + {e.zusatz &&

    {e.zusatz}

    } +
    + +
    +
  • + ))} +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index 9b089d7..0000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Startseite() { - return
SSHKeyVault
; -} diff --git a/src/beispiel/schluessel.ts b/src/beispiel/schluessel.ts new file mode 100644 index 0000000..679edf0 --- /dev/null +++ b/src/beispiel/schluessel.ts @@ -0,0 +1,75 @@ +/** + * BEISPIELDATEN für den Prototyp des Schlüsseldetails (#002) – kein echter Schlüssel. + * Ersetzt durch das Lesemodell aus #012/#016/#017. + */ +import type { HostStatus } from "@/components/ui/Statuspunkt"; +import type { DiffZeile } from "@/components/ui/DiffBox"; + +export const beispielSchluessel = { + zweck: "Backup srv-backup01 → srv-pve01", + typ: "ed25519", + kategorie: "Dienst", + vorlage: "Backup-Zugang", + ablauf: "2026-10-30T10:00:00Z", + fingerprint: "SHA256:q3vN8xP0b4Tz7KfL2mWcR9yHdE1sJ6uAoGi5VnBQe0k", + erstellt: "2026-04-30T08:12:00Z", + erstelltVon: "Florian Wilde", +}; + +export type Ausrollung = { + ziel: string; + pfad: string; + zusatz?: string; + status: HostStatus; + statusText?: string; +}; + +export const privatAusgerollt: Ausrollung[] = [ + { + ziel: "backup@srv-backup01", + pfad: "~/.ssh/id_ed25519_pve01", + zusatz: "config: Host srv-pve01", + status: "synchron", + }, +]; + +export const publicAusgerollt: Ausrollung[] = [ + { + ziel: "backup@srv-pve01", + pfad: "~/.ssh/authorized_keys", + zusatz: 'from="10.20.0.15",no-pty,no-agent-forwarding', + status: "synchron", + }, + { + ziel: "backup@srv-pve02", + pfad: "~/.ssh/authorized_keys", + zusatz: 'from="10.20.0.15",no-pty', + status: "drift", + statusText: "Optionen geändert", + }, +]; + +export const dryRunDatei = "backup@srv-pve02:~/.ssh/authorized_keys"; + +export const dryRunDiff: DiffZeile[] = [ + { art: "kontext", text: "# --- sshkv verwaltet: Anfang ---" }, + { + art: "kontext", + text: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMvq2Lr7… monitoring@zabbix", + }, + { + art: "entfernt", + text: 'from="10.20.0.15",no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFt0b8c1… backup@srv-backup01', + }, + { + art: "hinzu", + text: 'from="10.20.0.15",no-pty,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFt0b8c1… backup@srv-backup01', + }, + { art: "kontext", text: "# --- sshkv verwaltet: Ende ---" }, +]; + +export const zusatzaktionen = [ + { id: "backup", text: "Backup der Datei anlegen (authorized_keys.sshkv-bak)", an: true }, + { id: "config", text: "~/.ssh/config-Eintrag auf srv-backup01 anlegen", an: true }, + { id: "knownhosts", text: "known_hosts auf srv-backup01 aktualisieren", an: false }, +]; diff --git a/src/beispiel/uebersicht.ts b/src/beispiel/uebersicht.ts new file mode 100644 index 0000000..d0f9bc0 --- /dev/null +++ b/src/beispiel/uebersicht.ts @@ -0,0 +1,89 @@ +/** + * BEISPIELDATEN für den klickbaren Prototyp (#002) – keine echten Hosts oder Schlüssel. + * Die echten Lesemodelle kommen in #021 (Abgleich) und #026 (Übersicht); dann entfällt + * diese Datei. Alle Zeiten beziehen sich auf BEISPIEL_JETZT, damit der statisch gebaute + * Prototyp nicht mit jedem Build andere Relativangaben zeigt. + */ +import type { HostStatus } from "@/components/ui/Statuspunkt"; + +export const BEISPIEL_JETZT = "2026-10-12T07:00:00Z"; // 09:00 Berliner Zeit + +export const letzterAbgleich = "2026-10-12T06:42:00Z"; + +export const kennzahlen = { + hosts: { wert: 42, hinweis: "38 synchron · 1 nicht erreichbar" }, + schluessel: { wert: 117, hinweis: "davon 9 mit Private Key" }, + abweichungen: { wert: 3, hinweis: "auf 3 Hosts" }, + ablaufend: { wert: 5, hinweis: "in den nächsten 30 Tagen" }, +}; + +export type Abweichung = { + host: string; + benutzer: string; + status: HostStatus; + befund: string; +}; + +export const abweichungen: Abweichung[] = [ + { host: "srv-web03.intern", benutzer: "root", status: "drift", befund: "1 fremder Schlüssel" }, + { host: "srv-db01.intern", benutzer: "deploy", status: "drift", befund: "Schlüssel fehlt" }, + { host: "fw-edge01.intern", benutzer: "sshkv", status: "drift", befund: "Optionen geändert" }, + { host: "srv-legacy02.intern", benutzer: "root", status: "unerreichbar", befund: "nicht erreichbar" }, +]; + +export type AblaufEintrag = { zweck: string; person: string; typ: string; ablauf: string }; + +export const baldAblaufend: AblaufEintrag[] = [ + { zweck: "Laptop Florian", person: "Florian Wilde", typ: "ed25519", ablauf: "2026-10-16T10:00:00Z" }, + { zweck: "CI-Deployment Shop", person: "Dienst", typ: "ed25519", ablauf: "2026-10-21T10:00:00Z" }, + { + zweck: "Backup srv-backup01 → srv-pve01", + person: "Dienst", + typ: "ed25519", + ablauf: "2026-10-30T10:00:00Z", + }, + { zweck: "Notebook Sandra", person: "Sandra Keller", typ: "rsa-4096", ablauf: "2026-11-04T10:00:00Z" }, + { zweck: "Monitoring Zabbix", person: "Dienst", typ: "ed25519", ablauf: "2026-11-09T10:00:00Z" }, +]; + +export type AktionsArt = "ausgerollt" | "widerrufen" | "erstellt" | "abgleich" | "rotiert"; + +export type LetzteAktion = { + zeit: string; + art: AktionsArt; + beschreibung: string; + benutzer: string; +}; + +export const letzteAktionen: LetzteAktion[] = [ + { + zeit: "2026-10-12T06:42:00Z", + art: "abgleich", + beschreibung: "Abgleich aller Hosts: 3 Abweichungen", + benutzer: "System", + }, + { + zeit: "2026-10-12T06:15:00Z", + art: "ausgerollt", + beschreibung: "Public Key „Backup srv-backup01 → srv-pve01“ auf backup@srv-pve01", + benutzer: "Florian Wilde", + }, + { + zeit: "2026-10-11T15:30:00Z", + art: "widerrufen", + beschreibung: "Schlüssel „Laptop alt Thomas“ von 6 Hosts entfernt", + benutzer: "Florian Wilde", + }, + { + zeit: "2026-10-11T12:04:00Z", + art: "rotiert", + beschreibung: "CI-Deployment Shop durch neuen ed25519-Schlüssel ersetzt", + benutzer: "Sandra Keller", + }, + { + zeit: "2026-10-11T09:20:00Z", + art: "erstellt", + beschreibung: "Schlüssel „Monitoring Zabbix“ aus Vorlage Deployment-Zugang", + benutzer: "Sandra Keller", + }, +];