Das Protokoll war bisher nur in der Datenbank lesbar. Die Ansicht zeigt es jeder Rolle mit lesbaren Aktionstexten, aufklappbarem Diff, Details und Hashes; ein Test haelt fest, dass jede Aktion im Code einen Text hat. Geblaettert wird ueber die ID statt OFFSET, weil das Protokoll waehrend des Lesens waechst. Die Kettenpruefung liest jede Zeile und laeuft deshalb nur auf Knopfdruck, mit 5 Minuten Zwischenspeicher; ein Bruch wird selbst protokolliert. Der CSV-Export streamt in Stapeln, entschaerft Formeln und steht selbst im Protokoll. protokollieren() reicht jeden Eintrag nach dem Schreiben an Syslog weiter, ohne Details und Diff. Kein Weg aendert oder loescht Eintraege; ein struktureller Test sichert das ab, zusaetzlich zum Trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { ShieldAlert, ShieldCheck, ShieldQuestion } from "lucide-react";
|
||
import { useState, useTransition } from "react";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { uhrzeit } from "@/lib/format";
|
||
import { kettePruefenAktion, type KettenAntwort } from "@/server/actions/protokoll";
|
||
|
||
/**
|
||
* Integritaet der Hash-Kette (#027). Geprueft wird nur auf Knopfdruck – die Pruefung liest
|
||
* jede Zeile; ein Ergebnis der letzten 5 Minuten bringt die Seite schon mit.
|
||
*/
|
||
export function KettenPruefung({ anfang }: { anfang: KettenAntwort | null }) {
|
||
const [ergebnis, setErgebnis] = useState<KettenAntwort | null>(anfang);
|
||
const [laeuft, starten] = useTransition();
|
||
|
||
const pruefen = () =>
|
||
starten(async () => {
|
||
setErgebnis(await kettePruefenAktion());
|
||
});
|
||
|
||
return (
|
||
<div
|
||
className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||
aria-live="polite"
|
||
>
|
||
<Status ergebnis={ergebnis} />
|
||
<Button onClick={pruefen} disabled={laeuft} klein className="self-start sm:self-auto">
|
||
{laeuft ? "Prüft …" : "Kette prüfen"}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Status({ ergebnis }: { ergebnis: KettenAntwort | null }) {
|
||
if (!ergebnis) {
|
||
return (
|
||
<p className="text-muted flex items-center gap-2 text-[14px]">
|
||
<ShieldQuestion aria-hidden className="size-4 shrink-0" />
|
||
Hash-Kette noch nicht geprüft
|
||
</p>
|
||
);
|
||
}
|
||
if (ergebnis.fehler) {
|
||
return <p className="text-danger text-[14px]">{ergebnis.fehler}</p>;
|
||
}
|
||
const wann = ergebnis.geprueftAm ? ` um ${uhrzeit(ergebnis.geprueftAm)} Uhr` : "";
|
||
if (!ergebnis.fehlerhafteId) {
|
||
return (
|
||
<p className="text-ok flex items-center gap-2 text-[14px] font-medium">
|
||
<ShieldCheck aria-hidden className="size-4 shrink-0" />
|
||
<span>
|
||
Hash-Kette unversehrt
|
||
<span className="text-muted font-normal">
|
||
{" "}
|
||
· {ergebnis.eintraege?.toLocaleString("de-DE")} Einträge geprüft{wann}
|
||
</span>
|
||
</span>
|
||
</p>
|
||
);
|
||
}
|
||
const id = ergebnis.fehlerhafteId;
|
||
// Die Seite beginnt beim fehlerhaften Eintrag, der Anker hebt ihn hervor.
|
||
const ziel = `/protokoll?vor=${(BigInt(id) + 1n).toString()}#eintrag-${id}`;
|
||
return (
|
||
<div
|
||
role="alert"
|
||
className="border-danger-border bg-danger-soft text-danger rounded-box flex min-w-0 items-start gap-2.5 border px-3 py-2.5 text-[14px]"
|
||
>
|
||
<ShieldAlert aria-hidden className="mt-0.5 size-4 shrink-0" />
|
||
<p className="min-w-0">
|
||
<strong className="font-semibold">Hash-Kette beschädigt.</strong> Der Eintrag{" "}
|
||
<a href={ziel} className="font-mono underline underline-offset-2">
|
||
#{id}
|
||
</a>{" "}
|
||
stimmt nicht mit seinem Hash oder Vorgänger überein – ab hier ist das Protokoll nicht mehr belegbar.
|
||
Vergleichen Sie mit der Syslog-Kopie und sichern Sie die Datenbank.{wann && ` Geprüft${wann}.`}
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|