feat(hosts): Services, Lesemodelle und Aktionen fuer Hosts (#008)

Hosts und Hostgruppen brauchen eine Schreibschicht, bevor Host-Key-Pruefung
(#009) und Bootstrap (#010) darauf aufsetzen. Die Regeln (Name, Adresse wie
der DB-CHECK, Port, Tags, Anmeldeart) liegen framework-frei in src/lib/hosts.ts,
damit Action (Feldmeldungen) und Service (Schutz fuer Seed und Assistenten)
dieselben pruefen.

Archivieren statt Loeschen haelt Protokollbezuege lesbar; dabei fallen die
Gruppenmitgliedschaften weg, damit Zuweisungen an eine Gruppe nie einen
archivierten Host treffen. Jede Aenderung landet mit den geaenderten Feldern
im Protokoll, Eindeutigkeitsverletzungen kommen als deutsche HostFehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:42:56 +02:00
co-authored by Claude Opus 5.5
parent b194054d30
commit 4f9d5d9680
8 changed files with 1237 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { Benutzer } from "@/generated/prisma/client";
import { BestaetigungFehler, RechteFehler } from "@/server/auth/sitzungspflicht";
import { HostFehler, type Akteur } from "@/server/services/hosts";
/**
* Gemeinsames fuer Formular-Aktionen (#008). Kein "use server": diese Datei exportiert
* Hilfen, keine Aktionen – sonst waeren sie vom Browser aus aufrufbar.
*/
export type FormularZustand = {
readonly fehler?: string;
readonly feldfehler?: Record<string, string>;
};
/** Fachliche Fehler werden zur Meldung im Formular; alles andere bleibt ein echter Fehler. */
export function fehlerZustand(fehler: unknown): FormularZustand {
if (fehler instanceof HostFehler) {
return { fehler: fehler.message, feldfehler: fehler.feldfehler };
}
if (fehler instanceof RechteFehler || fehler instanceof BestaetigungFehler) {
return { fehler: fehler.message };
}
throw fehler;
}
export function akteurVon(benutzer: Pick<Benutzer, "id" | "name">): Akteur {
return { id: benutzer.id, name: benutzer.name };
}
/** Einzelnes Textfeld; fehlend bleibt `undefined`, damit Standardwerte des Schemas greifen. */
export function textfeld(formular: FormData, name: string): string | undefined {
const wert = formular.get(name);
return typeof wert === "string" && wert !== "" ? wert : undefined;
}
export function listenfeld(formular: FormData, name: string): string[] {
return formular.getAll(name).filter((w): w is string => typeof w === "string" && w !== "");
}