"use client"; import { Play, ScanSearch } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; import { Button } from "@/components/ui/Button"; import { DiffBox } from "@/components/ui/DiffBox"; import { laeuftNoch, useVorgang } from "@/components/vorgang/useVorgang"; import { ausrollenAktion, trockenlaufAktion, type AusrollAuswahl } from "@/server/actions/verwendungen"; import type { DiffZeile } from "@/lib/ssh/zeilen-diff"; /** * Dry-Run-Vorschau und Ausfuehren (#016). Was angezeigt wird, ist genau das, was der Worker * schreibt: der Plan traegt die Hashes, und das Ausrollen bricht ab, wenn sich seitdem auf * dem Host oder an den Zuweisungen etwas geaendert hat. */ type DateiPlan = { art: "privateKey" | "publicKey" | "config" | "knownHosts"; pfad: string; aktion: "anlegen" | "aendern" | "keine" | "entfernen"; diff?: DiffZeile[]; hinweis?: string; }; type PlanZiel = { art?: "public" | "privat"; dateien?: DateiPlan[]; hostName: string; zielbenutzer: string; pfad?: string; geaendert: boolean; diff: DiffZeile[]; hinweise: string[]; fehler?: string; }; export function AusrollenPanel({ auswahl, offen }: { auswahl: AusrollAuswahl; offen: number }) { const router = useRouter(); const [trockenlaufId, setTrockenlaufId] = useState(); const [ausrollenId, setAusrollenId] = useState(); const [fehler, setFehler] = useState(); const [startet, starten] = useTransition(); const trockenlauf = useVorgang(trockenlaufId); const ausrollen = useVorgang(ausrollenId); useEffect(() => { if (ausrollen.stand && !laeuftNoch(ausrollen.stand)) router.refresh(); }, [ausrollen.stand, router]); const plan = (trockenlauf.stand?.plan as { ziele?: PlanZiel[] } | null)?.ziele; const aenderungen = plan?.filter((z) => z.geaendert && !z.fehler).length ?? 0; const laeuft = startet || laeuftNoch(trockenlauf.stand) || laeuftNoch(ausrollen.stand); const fertig = ausrollen.stand?.status === "ERLEDIGT"; return (
{offen > 0 && !plan && ( {offen} Zuweisung(en) noch nicht ausgerollt. )}
{(fehler || trockenlauf.fehler || trockenlauf.stand?.status === "FEHLGESCHLAGEN") && (

{fehler ?? trockenlauf.fehler ?? trockenlauf.stand?.fehler}

)} {plan?.map((ziel) => (
{ziel.fehler ? (

{ziel.zielbenutzer}@{ziel.hostName}: {ziel.fehler}

) : ziel.art === "privat" ? ( ) : ( )} {ziel.hinweise.map((h) => (

▲ {h}

))}
))} {plan && aenderungen > 0 && !fertig && (
)} {plan && aenderungen === 0 && (

Alles ist bereits auf dem Soll-Stand.

)} {ausrollen.stand?.status === "FEHLGESCHLAGEN" && (

{ausrollen.stand.fehler}

)} {fertig &&

Ausgerollt.

}
); } const AKTION_TEXT: Record = { anlegen: "wird angelegt", aendern: "wird geändert", entfernen: "wird entfernt", keine: "unverändert", }; /** * Private-Key-Ziel (#017): die Dateien unter ~/.ssh einzeln. Vom Private Key steht nur die * Aktion da – sein Inhalt erreicht den Browser nie. */ function PrivatPlanAnzeige({ ziel }: { ziel: PlanZiel }) { const dateien = ziel.dateien ?? []; return (

{ziel.zielbenutzer}@{ziel.hostName} · Private Key, ~/.ssh/config, known_hosts

    {dateien .filter((d) => d.art === "privateKey" || d.art === "publicKey") .map((d) => (
  • {d.aktion === "keine" ? " " : d.aktion === "entfernen" ? "−" : "+"} {d.pfad} –{" "} {AKTION_TEXT[d.aktion]} {d.art === "privateKey" && d.aktion !== "keine" && d.aktion !== "entfernen" ? " (Inhalt wird nicht angezeigt)" : ""} {d.hinweis ? ` · ${d.hinweis}` : ""}
  • ))}
{dateien .filter((d) => (d.art === "config" || d.art === "knownHosts") && d.aktion !== "keine" && d.diff) .map((d) => ( ))}
); }