Anmeldung, Einrichtung des zweiten Faktors fuer den Notfall-Admin, erneute Bestaetigung vor kritischen Aktionen und die Seite fuer fehlende Rechte. Die Huelle zeigt jetzt den echten Benutzer, und jede Seite prueft die Sitzung selbst; ein struktureller Test haelt das fest. Die Ablaeufe sind im gebauten Stand gegen den Mock-OIDC geprueft (pnpm test:e2e): Admin, Konto ohne Gruppe, Notfall-Admin mit TOTP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
7.1 KiB
TypeScript
191 lines
7.1 KiB
TypeScript
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";
|
||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||
|
||
export const metadata: Metadata = { title: "Übersicht" };
|
||
|
||
const AKTION: Record<AktionsArt, { text: string; farbe: string }> = {
|
||
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 async function Uebersicht() {
|
||
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
|
||
await sitzungErforderlich();
|
||
return (
|
||
<div className="flex flex-col gap-6 sm:gap-7">
|
||
<Seitenkopf
|
||
titel="Übersicht"
|
||
badges={<Badge variante="messing">Prototyp · Beispieldaten</Badge>}
|
||
untertitel={
|
||
<>
|
||
Letzter Abgleich {relativ(letzterAbgleich, BEISPIEL_JETZT)} ·{" "}
|
||
<time dateTime={letzterAbgleich} className="font-mono text-[13px]">
|
||
{datumZeit(letzterAbgleich)}
|
||
</time>
|
||
</>
|
||
}
|
||
aktionen={
|
||
<>
|
||
<Button icon={<RefreshCw className="size-4" />}>Jetzt abgleichen</Button>
|
||
<Button href="/zuweisungen" icon={<Route className="size-4" />}>
|
||
Host-zu-Host-Assistent
|
||
</Button>
|
||
<Button
|
||
href="/schluessel"
|
||
variante="primaer"
|
||
icon={<Plus className="size-4" strokeWidth={2.4} />}
|
||
>
|
||
Neuer Schlüssel
|
||
</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<div className="grid grid-cols-2 gap-3 sm:gap-5 xl:grid-cols-4">
|
||
<Kennzahl
|
||
titel="Hosts"
|
||
wert={kennzahlen.hosts.wert}
|
||
hinweis={kennzahlen.hosts.hinweis}
|
||
href="/hosts"
|
||
/>
|
||
<Kennzahl
|
||
titel="Schlüssel"
|
||
wert={kennzahlen.schluessel.wert}
|
||
hinweis={kennzahlen.schluessel.hinweis}
|
||
href="/schluessel"
|
||
/>
|
||
<Kennzahl
|
||
titel="Abweichungen"
|
||
variante="gefahr"
|
||
wert={kennzahlen.abweichungen.wert}
|
||
hinweis={kennzahlen.abweichungen.hinweis}
|
||
href="/hosts"
|
||
/>
|
||
<Kennzahl
|
||
titel="Läuft in 30 Tagen ab"
|
||
variante="messing"
|
||
wert={kennzahlen.ablaufend.wert}
|
||
hinweis={kennzahlen.ablaufend.hinweis}
|
||
href="/schluessel"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.55fr)_minmax(0,1fr)]">
|
||
<Karte titel="Hosts mit Abweichung" untertitel="Ergebnis des letzten Abgleichs">
|
||
<Tabelle
|
||
beschriftung="Hosts mit Abweichung"
|
||
spalten={[
|
||
{ titel: "Host" },
|
||
{ titel: "Benutzer" },
|
||
{ titel: "Befund" },
|
||
{ titel: "Aktion", rechts: true, versteckt: true },
|
||
]}
|
||
>
|
||
{abweichungen.map((a) => (
|
||
<TabellenZeile key={`${a.host}-${a.benutzer}`}>
|
||
<TabellenZelle mono className="whitespace-nowrap">
|
||
{a.host}
|
||
</TabellenZelle>
|
||
<TabellenZelle mono>{a.benutzer}</TabellenZelle>
|
||
<TabellenZelle className="whitespace-nowrap">
|
||
<Statuspunkt status={a.status} text={a.befund} />
|
||
</TabellenZelle>
|
||
<TabellenZelle rechts>
|
||
<Link
|
||
href="/hosts"
|
||
className="text-accent -my-2 inline-flex min-h-11 items-center px-1 font-medium hover:underline"
|
||
>
|
||
Prüfen<span className="sr-only"> – {a.host}</span>
|
||
</Link>
|
||
</TabellenZelle>
|
||
</TabellenZeile>
|
||
))}
|
||
</Tabelle>
|
||
</Karte>
|
||
|
||
<Karte titel="Bald ablaufend" untertitel="Schlüssel mit Ablaufdatum in den nächsten 30 Tagen">
|
||
<ul className="flex flex-col">
|
||
{baldAblaufend.map((s) => {
|
||
const tage = tageBis(s.ablauf, BEISPIEL_JETZT);
|
||
return (
|
||
<li key={s.zweck} className="border-border border-b last:border-b-0">
|
||
<Link
|
||
href="/schluessel/beispiel"
|
||
className="hover:bg-raised/50 rounded-box -mx-2 flex min-h-11 items-center justify-between gap-3 px-2 py-2.5 transition-colors"
|
||
>
|
||
<span className="min-w-0">
|
||
<span className="text-text block truncate text-[14.5px] font-medium">{s.zweck}</span>
|
||
<span className="text-muted block truncate text-[13px]">
|
||
{s.person} · <span className="font-mono">{s.typ}</span>
|
||
</span>
|
||
</span>
|
||
<Badge variante="messing" className="shrink-0">
|
||
<time dateTime={s.ablauf}>{tage === 1 ? "morgen" : `in ${tage} Tagen`}</time>
|
||
</Badge>
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</Karte>
|
||
</div>
|
||
|
||
<Karte
|
||
titel="Letzte Aktionen"
|
||
aktionen={
|
||
<Link
|
||
href="/protokoll"
|
||
className="text-accent inline-flex min-h-11 items-center gap-1.5 text-[14px] font-medium hover:underline"
|
||
>
|
||
Zum Audit-Log <ArrowRight className="size-4" aria-hidden />
|
||
</Link>
|
||
}
|
||
>
|
||
<ol className="flex flex-col">
|
||
{letzteAktionen.map((a) => (
|
||
<li
|
||
key={a.zeit}
|
||
className="border-border grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5 border-b py-3 text-[14px] last:border-b-0 md:grid-cols-[8.5rem_6.5rem_1fr_auto] md:items-baseline"
|
||
>
|
||
<time dateTime={a.zeit} className="text-muted font-mono text-[12.5px] whitespace-nowrap">
|
||
{datum(a.zeit)} {uhrzeit(a.zeit)}
|
||
</time>
|
||
<span className={`${AKTION[a.art].farbe} font-medium md:order-none`}>{AKTION[a.art].text}</span>
|
||
<span className="text-text col-span-2 min-w-0 md:col-span-1">{a.beschreibung}</span>
|
||
<span className="text-muted col-span-2 text-[13px] md:col-span-1 md:text-right">
|
||
{a.benutzer}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</Karte>
|
||
</div>
|
||
);
|
||
}
|