feat(zuweisungen): Matrix Schluessel x Host mit CSV-Export (#025)

Bisher liess sich nur je Schluessel oder je Host nachsehen, wo ein Key
verwaltet ist. Die Matrix zeigt das auf einen Blick, loest
Hostgruppen-Zuweisungen je Mitglied auf (P vs. P·G) und macht nicht
synchrone Zustaende ohne Farbe erkennbar.

Die Matrix plant nur: Zuweisen und Entfernen laufen ueber die
vorhandenen Aktionen, ausgerollt wird weiter per Dry-Run auf der
Schluesselseite. Der Aufbau ist eine reine Funktion in src/lib, damit
Seite und CSV-Export garantiert dasselbe zeigen und die
Gruppenaufloesung ohne Datenbank testbar ist. Der CSV-Export entschaerft
Formelbeginne, weil Schluesselnamen frei waehlbar sind und Excel sie
sonst ausfuehren wuerde.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:05:12 +02:00
co-authored by Claude Opus 5.5
parent 2dad8e5631
commit a47cae4082
16 changed files with 1608 additions and 16 deletions
+37
View File
@@ -0,0 +1,37 @@
/**
* CSV fuer Excel (#025). Framework-frei, damit spaetere Exporte (Protokoll, Phase 5) dieselben
* Regeln nutzen.
*
* - Semikolon als Trenner und UTF-8-BOM: ein deutsches Excel oeffnet die Datei sonst als eine
* Spalte mit kaputten Umlauten.
* - Jeder Wert in Anfuehrungszeichen, damit Kommas in `authorized_keys`-Optionen und
* Zeilenumbrueche nichts verschieben.
* - Formel-Injektion: Excel und LibreOffice werten Zellen, die mit `= + - @` (oder Tab/CR)
* beginnen, als Formel aus – ein Schluesselname wie `=HYPERLINK(…)` wuerde beim Oeffnen
* ausgefuehrt. Ein vorangestelltes `'` macht daraus Text (OWASP „CSV Injection“).
*/
export const CSV_BOM = "";
export const CSV_TRENNER = ";";
const FORMELBEGINN = /^[=+\-@\t\r]/;
export function csvEntschaerfen(wert: string): string {
return FORMELBEGINN.test(wert) ? `'${wert}` : wert;
}
export function csvWert(wert: string | number | null | undefined): string {
const text = csvEntschaerfen(wert == null ? "" : String(wert));
return `"${text.replace(/"/g, '""')}"`;
}
export function csvZeile(werte: readonly (string | number | null | undefined)[]): string {
return werte.map(csvWert).join(CSV_TRENNER);
}
/** Ganze Datei: BOM, Kopfzeile, Datenzeilen, CRLF wie Excel selbst. */
export function csvDatei(
kopf: readonly string[],
zeilen: readonly (readonly (string | number | null | undefined)[])[],
): string {
return CSV_BOM + [kopf, ...zeilen].map(csvZeile).join("\r\n") + "\r\n";
}