feat(uebersicht): echte Zahlen statt Beispieldaten (#026)
Kennzahlen, Hosts mit Abweichung, bald ablaufende Schluessel und letzte Aktionen kommen aus der Datenbank; die Beispieldaten des Prototyps sind entfernt. Befunde werden je Host und Benutzer in fester Reihenfolge zusammengefasst, abgelaufene und noch ausgerollte Schluessel stehen in Koralle. Anmeldungen bleiben dem Audit-Log vorbehalten, sonst verdraengten sie auf der Uebersicht alles andere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+92
-63
@@ -1,15 +1,6 @@
|
||||
import { ArrowRight, Plus, 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 { AlleAbgleichenKnopf } from "@/components/abgleich/AlleAbgleichenKnopf";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
@@ -19,7 +10,10 @@ 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 { Leerzustand } from "@/components/ui/Leerzustand";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { uebersicht, type AktionsArt } from "@/server/queries/uebersicht";
|
||||
|
||||
export const metadata: Metadata = { title: "Übersicht" };
|
||||
|
||||
@@ -29,36 +23,45 @@ const AKTION: Record<AktionsArt, { text: string; farbe: string }> = {
|
||||
erstellt: { text: "Erstellt", farbe: "text-text" },
|
||||
abgleich: { text: "Abgleich", farbe: "text-info" },
|
||||
rotiert: { text: "Rotiert", farbe: "text-accent" },
|
||||
anmeldung: { text: "Anmeldung", farbe: "text-muted" },
|
||||
verweigert: { text: "Verweigert", farbe: "text-danger" },
|
||||
sonstiges: { text: "Aktion", farbe: "text-text" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Prototyp der Übersicht mit Beispieldaten (#002). Die echte Übersicht mit Lesemodellen
|
||||
* folgt in #026; Aufbau und Komponenten bleiben.
|
||||
*/
|
||||
/** Übersicht (#026): Aufbau aus dem Prototyp (#002), Zahlen aus der Datenbank. */
|
||||
export default async function Uebersicht() {
|
||||
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
|
||||
await sitzungErforderlich();
|
||||
const { benutzer } = await sitzungErforderlich();
|
||||
const jetzt = new Date();
|
||||
const { letzterAbgleich, kennzahlen, mitAbweichung, weitereAbweichungen, baldAblaufend, letzteAktionen } =
|
||||
await uebersicht(jetzt);
|
||||
const verwalten = darf(benutzer.rolle, "hostsVerwalten");
|
||||
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>
|
||||
</>
|
||||
letzterAbgleich ? (
|
||||
<>
|
||||
Letzter Abgleich {relativ(letzterAbgleich, jetzt)} ·{" "}
|
||||
<time dateTime={letzterAbgleich.toISOString()} className="font-mono text-[13px]">
|
||||
{datumZeit(letzterAbgleich)}
|
||||
</time>
|
||||
</>
|
||||
) : (
|
||||
"Noch kein Abgleich gelaufen."
|
||||
)
|
||||
}
|
||||
aktionen={
|
||||
<>
|
||||
<AlleAbgleichenKnopf />
|
||||
<Button href="/assistent" icon={<Route className="size-4" />}>
|
||||
Host-zu-Host-Assistent
|
||||
</Button>
|
||||
{verwalten && <AlleAbgleichenKnopf />}
|
||||
{darf(benutzer.rolle, "privateKeyAusrollen") && (
|
||||
<Button href="/assistent" icon={<Route className="size-4" />}>
|
||||
Host-zu-Host-Assistent
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
href="/schluessel"
|
||||
href="/schluessel/neu"
|
||||
variante="primaer"
|
||||
icon={<Plus className="size-4" strokeWidth={2.4} />}
|
||||
>
|
||||
@@ -98,46 +101,60 @@ export default async function Uebersicht() {
|
||||
</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
|
||||
titel="Hosts mit Abweichung"
|
||||
untertitel={
|
||||
weitereAbweichungen
|
||||
? `Ergebnis des letzten Abgleichs · ${weitereAbweichungen} weitere`
|
||||
: "Ergebnis des letzten Abgleichs"
|
||||
}
|
||||
>
|
||||
{mitAbweichung.length === 0 ? (
|
||||
<p className="text-ok text-[14px]">● Alle Hosts synchron – keine offenen Abweichungen.</p>
|
||||
) : (
|
||||
<Tabelle
|
||||
beschriftung="Hosts mit Abweichung"
|
||||
spalten={[
|
||||
{ titel: "Host" },
|
||||
{ titel: "Benutzer" },
|
||||
{ titel: "Befund" },
|
||||
{ titel: "Aktion", rechts: true, versteckt: true },
|
||||
]}
|
||||
>
|
||||
{mitAbweichung.map((a) => (
|
||||
<TabellenZeile key={`${a.hostId}-${a.benutzer}-${a.status}`}>
|
||||
<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/${a.hostId}`}
|
||||
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">
|
||||
{baldAblaufend.length === 0 && (
|
||||
<p className="text-muted text-[14px]">Kein Schlüssel läuft in den nächsten 30 Tagen ab.</p>
|
||||
)}
|
||||
<ul className="flex flex-col">
|
||||
{baldAblaufend.map((s) => {
|
||||
const tage = tageBis(s.ablauf, BEISPIEL_JETZT);
|
||||
const tage = tageBis(s.ablauf, jetzt);
|
||||
return (
|
||||
<li key={s.zweck} className="border-border border-b last:border-b-0">
|
||||
<li key={s.id} className="border-border border-b last:border-b-0">
|
||||
<Link
|
||||
href="/schluessel"
|
||||
href={`/schluessel/${s.id}`}
|
||||
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">
|
||||
@@ -146,8 +163,16 @@ export default async function Uebersicht() {
|
||||
{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 variante={tage < 0 ? "gefahr" : "messing"} className="shrink-0">
|
||||
<time dateTime={s.ablauf.toISOString()}>
|
||||
{tage < 0
|
||||
? `abgelaufen${s.ausgerollt ? " · noch ausgerollt" : ""}`
|
||||
: tage === 0
|
||||
? "heute"
|
||||
: tage === 1
|
||||
? "morgen"
|
||||
: `in ${tage} Tagen`}
|
||||
</time>
|
||||
</Badge>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -168,13 +193,17 @@ export default async function Uebersicht() {
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{letzteAktionen.length === 0 && <Leerzustand titel="Noch keine Aktionen" />}
|
||||
<ol className="flex flex-col">
|
||||
{letzteAktionen.map((a) => (
|
||||
<li
|
||||
key={a.zeit}
|
||||
key={a.id}
|
||||
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">
|
||||
<time
|
||||
dateTime={a.zeit.toISOString()}
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user