Merge branch 'phase/009-host-key-pruefung'

This commit is contained in:
2026-10-12 09:51:59 +02:00
10 changed files with 800 additions and 3 deletions
+93
View File
@@ -0,0 +1,93 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState, useTransition, type ReactNode } from "react";
import { Button, type ButtonVariante } from "@/components/ui/Button";
/**
* Startet einen Vorgang im Worker und zeigt seinen Fortschritt (#009). Fragt den Status
* jede Sekunde ab und laedt nach dem Ende die Seite neu, damit Server Components die neuen
* Daten zeigen.
*/
type Antwort = { readonly vorgangId?: string; readonly fehler?: string };
type Status = { status: string; fehler: string | null };
const LAUFEND = new Set(["WARTEND", "LAEUFT"]);
export function VorgangsKnopf({
aktion,
beschriftung,
variante = "sekundaer",
icon,
klein,
}: {
aktion: () => Promise<Antwort>;
beschriftung: string;
variante?: ButtonVariante;
icon?: ReactNode;
klein?: boolean;
}) {
const router = useRouter();
const [vorgangId, setVorgangId] = useState<string>();
const [status, setStatus] = useState<Status>();
const [fehler, setFehler] = useState<string>();
const [startet, starten] = useTransition();
const abbrechen = useRef(false);
useEffect(() => {
if (!vorgangId) return;
abbrechen.current = false;
const abfragen = async () => {
while (!abbrechen.current) {
const antwort = await fetch(`/api/vorgaenge/${vorgangId}`, { cache: "no-store" });
if (!antwort.ok) {
setFehler("Status des Vorgangs nicht abrufbar.");
return;
}
const neu = (await antwort.json()) as Status;
setStatus(neu);
if (!LAUFEND.has(neu.status)) {
router.refresh();
return;
}
await new Promise((weiter) => setTimeout(weiter, 1000));
}
};
void abfragen();
return () => {
abbrechen.current = true;
};
}, [vorgangId, router]);
const laeuft =
startet || (status !== undefined && LAUFEND.has(status.status)) || (vorgangId !== undefined && !status);
return (
<div className="flex flex-col items-start gap-2">
<Button
variante={variante}
icon={icon}
klein={klein}
disabled={laeuft}
onClick={() =>
starten(async () => {
setFehler(undefined);
setStatus(undefined);
const antwort = await aktion();
if (antwort.fehler) setFehler(antwort.fehler);
else setVorgangId(antwort.vorgangId);
})
}
>
{laeuft ? "Läuft …" : beschriftung}
</Button>
<p role="status" className="text-[13px]">
{fehler && <span className="text-danger">{fehler}</span>}
{!fehler && status?.status === "FEHLGESCHLAGEN" && (
<span className="text-danger">{status.fehler}</span>
)}
{!fehler && status?.status === "ERLEDIGT" && <span className="text-ok">Erledigt.</span>}
</p>
</div>
);
}