Durchgesehen, was mit Text passiert, der nicht als Text ankommt. Die Grundlage
war schon in Ordnung: genau ein dangerouslySetInnerHTML mit einer Konstanten,
kein eval, kein $queryRawUnsafe, keine Weiterleitung aus einem Adressparameter,
Dateien mit nosniff und sandbox-CSP als Anhang statt inline.
Drei Ränder waren offen.
**Vorlagen.** Das zod-Schema hatte einen festen Satz Feldarten und speicherte
das Geprüfte, verwarf unbekannte Schlüssel aber stillschweigend. Mit
strictObject fällt ein `typ` statt `type` auf, statt eine Vorlage zu
hinterlassen, die nicht tut, was dasteht.
**Platzhalter lasen über die Prototypkette.** Beim Durchprobieren gefunden:
`constructor` ist ein gültiger Feldschlüssel, die Schlüsselregel erlaubt
Kleinbuchstaben. Ein Baustein mit {{constructor}} bekam den Quelltext der
geerbten Funktion in den Text geschrieben — kein ausgeführter Code, aber
Innenleben in einem Dokument, das weitergereicht wird. Jetzt Object.hasOwn.
**Farbmarken aus der Datenbank** landen als Name einer CSS-Variablen in einem
style-Attribut. Heute setzt das nur der Seed, und eine zweite Deklaration liesse
sich darüber ohnehin nicht einschleusen — aber der Kommentar sagt „im
Adminbereich pflegbar", und was aus der Datenbank in ein Stilattribut geht,
wird geprüft.
Dreizehn Prüfungen, die es mit echten Versuchen probieren. Bösartiger Text
bleibt dabei ausdrücklich erlaubt: eine Feldbeschriftung darf
<script>alert(1)</script> heissen und sieht dann genau so aus. Verboten ist,
dass daraus ein Element wird.
Nebenbei: eine Vorlagenänderung stand im Zugriffsprotokoll als roles.update.
Das Formular der Personalabteilung ist übernommen, nicht durch eine
Bildschirmansicht ersetzt: schwarzer Rahmen, Kopf mit Kennung und
Klassifizierungsbalken, Raster aus fünf Spalten, Unterschriftenzeilen,
Fußzeile. Es steht bewusst außerhalb des Design-Systems — es wird gedruckt und
muss auf Papier aussehen wie das Blatt daneben.
Datengetrieben bleibt es trotzdem. Kein Feld steht im Programm: sheetTitle und
sheetSubtitle an der Vorlage, span für die Zellbreite, rows für das Lichtbild,
dateKey für das Datum unter einer Unterschrift, slot für den
Klassifizierungsbalken. Eine weitere Dokumentart mit layout = DOSSIER braucht
keine Zeile Code.
Der Balken zeigt die Foundation-Stufe aus der neuen Tabelle
FoundationClearance. Sie ist Inhalt, kein Zugriff — was jemand sehen darf,
entscheidet weiterhin allein die interne Stufe. Ohne Auswahl bleibt der Balken
neutral, statt eine Einstufung zu behaupten.
Die Dienstnummer ist entfallen, es gibt sie nicht. Der Status ist zu mehreren
Etiketten geworden: "Gekündigt" und "Verhaftet" schließen einander nicht aus.
Unbekannte Schlüssel werden verworfen und die Reihenfolge kommt aus der
Konfiguration, nicht aus der Eingabe.
PersonnelAssignment hängt AfiS-Mitglieder an eine Person — Aktenführung,
Ermittler, Ansprechpartner — und ist zugleich der ausnahmsweise Zugang, wie
CaseParticipant bei der Fallakte. Betroffen zu sein gibt weiterhin keinen
Zugang: das ist eine Eigenschaft der Person, die Zuordnung eine Entscheidung
der Abteilung.
Fall und Person lassen sich von beiden Seiten verknüpfen und schon beim
Anlegen einer Fallakte. Alle drei Wege laufen in addParticipant zusammen.
Noch offen: das Lichtbild. Feldtyp und Platz stehen, hochladen kommt mit der
Prüfkette in Phase 5 — ein Bild als Data-URL im Abschnittswert verstiesse
gegen harte Regel 4.
102 Tests im Web-Paket, davon sechs auf dem gerenderten Blatt selbst.
Eine Vorlage ist eine geordnete Liste von Bausteinen: Eingabefelder und feste
Textbausteine mit Platzhaltern. Damit trägt dieselbe Struktur den
Vorfallsbericht und den Sonderarbeitsvertrag. Die Startvorlagen bilden jetzt
den echten Bericht der Abteilung ab, samt Berichtsart, Gefahreneinschätzung
und Foundation-Freigabe — letztere als gewöhnliches Feld ohne Wirkung auf den
Zugriff.
Abschnitte werden gerendert und bearbeitet, Entwürfe alle zwei Sekunden lokal
gesichert. Gespeichert wird auf Knopfdruck: ein Abschnitt ist der Sachstand
der Akte, kein Chatbeitrag. Die geladene Version geht mit; weicht sie ab, gibt
es kein stilles Überschreiben, sondern beide Fassungen nebeneinander mit
"Meine übernehmen" und "Fremde übernehmen".
Felder über der Stufe des Lesers werden serverseitig durch einen Platzhalter
ersetzt, Schwärzungen im Fliesstext kommen als Segmente. Beides so, dass der
echte Wert den Server nicht verlässt — nicht als Ausblenden im Client.
Der Wortdiff ist selbst geschrieben statt eingekauft: ein Dutzend Zeilen gegen
30 kB im Bündel für etwas, das nur serverseitig läuft.
Beim Bauen des Vorlagen-Editors riss das Bündel-Budget auf 127,9 kB, weil zod
über das Vorlagenmodul in den Client geriet. Typen und Validierung sind jetzt
getrennt: fields.ts ohne zod für Client und Server, schema.ts nur
serverseitig. Wieder bei 111,9 kB von 120.
85 Tests, 22 E2E.
Entschieden und festgehalten: jede Akte trägt Kurzvermerke und Dokumente, die
Fallakte zusätzlich ihre Abschnitte. Die Trennung aus docs/05 bleibt damit
bestehen.
Eine Vorlage ist jetzt eine geordnete Liste von Bausteinen. Manche nehmen eine
Eingabe entgegen, andere liefern festen Text mit Platzhaltern. Damit tragen
dieselbe Struktur ein Vorfallsbericht, der fast nur aus Eingaben besteht, und
ein Sonderarbeitsvertrag, der fast nur aus Paragraphen und Unterschriftenzeilen
besteht. Eine neue Dokumentart bleibt ein Datensatz, kein Deployment.
Der Schwärzungs-Parser gibt Segmente zurück, nicht den vollen Text mit einem
Sichtbarkeits-Flag: was der Client ausblenden müsste, hat er dann schon. Die
Balkenlänge wird auf ein Raster von acht Zeichen gerundet, sonst verrät sie,
wie lang der geschwärzte Name war.
Ein Test vergleicht den TypeScript-Parser gegen die SQL-Funktion
strip_redactions aus der Migration. Laufen die beiden auseinander, findet die
Suche einen Begriff, den die Ansicht verbirgt — und der Treffer allein verrät
ihn dann.