Die Regel „eine Nachricht gehört zu genau einem Ort" wird jetzt stärker
geprüft als vorher: zwei Orte, kein Ort und genau einer. Vorher hing sie
allein an der Anwendung, jetzt an einem CHECK.
Der Worker-Test prüft ausdrücklich, dass die Meldung den Namen der Person
nicht nennt.
Der bisherige Suchvektor enthielt nur, was jeder sehen darf. Das war
leck-sicher und unbrauchbar zugleich: ab Stufe 1 fand die Suche nichts von
dem, was einen Fall ausmacht.
SearchIndex hält jetzt sechs Zeilen je Ziel, eine pro Freigabestufe. Die Zeile
zur Stufe N enthält alles, was ein Leser dieser Stufe sehen darf. Eine Suche
nimmt genau diese eine Zeile und kann deshalb nichts finden, was sie nicht
lesen darf - auch keinen höher eingestuften Eintrag in einer sichtbaren Akte.
Schwärzungen werden dabei stufenweise aufgelöst: strip_redactions_above
entfernt nur, was über der Stufe liegt. Sonst fände ein Leser der Stufe 4
einen Namen nicht, den er auf der Seite im Klartext sieht, und ein Leser der
Stufe 1 einen, den er dort nur als Balken sieht.
Die Suchbedingung ist nicht die Zugriffsprüfung: der Volltext liefert
Kandidaten, die Sichtbarkeit kommt aus visibleCaseWhere - derselben Funktion
wie in der Fallliste, und Treffer, Trefferzahl und Facettenzähler entstehen
gemeinsam in einer Abfrage. Eine in SQL nachgebaute Sichtbarkeitsregel wäre
die Sorte Duplikat, die auseinanderläuft.
Filter stehen in der Adresse. Damit ist jeder Zustand teilbar, der
Zurück-Knopf tut das Richtige, und eine gespeicherte Ansicht ist ein
Lesezeichen. Die mitgelieferten Ansichten stehen im Programm, weil sie von der
Gegenwart abhängen.
Strg+K öffnet die Kommandopalette. Im Grundgerüst steckt nur das
Tastaturereignis, die Palette selbst wird beim Öffnen geholt - das kostet die
anderen Seiten rund hundert Byte.
Offen geblieben: "Zuletzt geöffnet" als Ansicht. Dafür müsste jeder Aufruf
jeder Akte mitgeschrieben werden; das wäre eine Überwachungsfunktion, die
niemand bestellt hat. In TASKS vermerkt.
165 Tests im Web-Paket, 17 in der Datenbank, 7 im Worker, 34 e2e.
Grösste Route bei 119,7 kB gegen 120 erlaubte.
Die Dokumentebene aus docs/13 ist gebaut, additiv: Document, DocumentSection
und DocumentSectionVersion neben Case, CaseSection und CaseEntry. Ein Dokument
hängt an einer Fallakte, an einer Personalakte oder an nichts, höchstens an
einem - gesichert durch eine CHECK-Bedingung. Es gibt kein DELETE; eine
Zurücknahme lässt einen Grabstein stehen, der auch für den Admin zu ist.
Eine neue Dokumentart ist damit ein Datensatz, kein Deployment. Im Seed stehen
vier: vermerk, vorfallsbericht, sonderarbeitsvertrag (mehrseitiger Vertragstext
mit Platzhaltern und Unterschriftenzeilen) und bussgeld.
Zwei Umbenennungen, beide von Hand migriert, damit die vorhandenen Zeilen und
Zählerstände stehen bleiben:
- CaseTemplate -> DocumentTemplate, plus uses[] und layout
- CaseCounter -> NumberSeries, ein Nummernkreis je Präfix für Aktenzeichen
und Dokumentnummern
Die Regeln für Abschnitte stehen jetzt einmal in lib/sections/render.ts und
gelten für Fallakten und Dokumente gleichermaßen. Die Frage aus docs/13, ob
CaseSection langfristig in die Dokumentebene wandert, ist damit verneint und
begründet.
Rückverweise auf der Personalakte laufen durch visibleCaseWhere, die Zahl
daneben entsteht aus dem gefilterten Ergebnis. Auch die Rollengruppe
verschwindet mit, sonst verriete "Betroffener" ohne Eintrag die Ermittlung.
Die eigene Personalakte ist keine Ausnahme von der Freigabestufe.
Vier neue Rechte: document.create, .edit, .finalize, .retract. Das letzte
steht bei den zurückgehaltenen, es greift in Geschriebenes ein.
91 Tests im Web-Paket, 15 in der Datenbank, 28 e2e. Größte Route 118 kB
gegen das Budget von 120 kB - die Luft wird knapp.
pnpm-Workspace mit web/, worker/, pdf/ und packages/db. Das Datenbankpaket ist
eigenständig, weil Web-App und Worker denselben Prisma-Client brauchen.
Schema nach docs/04-datenmodell.md, mit drei Abweichungen, die dort begründet
sind: CaseParticipant bekommt einen eigenen Schlüssel statt eines
zusammengesetzten mit NULL-Spalten, die Aktenzeichen laufen über CaseCounter
statt über eine Sequenz pro Jahr, und ServiceHeartbeat kommt dazu, damit
/api/health den Gateway-Zustand kennt, ohne Discord zu fragen.
Die zweite Migration enthält, was Prisma nicht ausdrücken kann:
- kein DELETE auf CaseEntry, CaseEntryVersion und AuditLog
- authorId, createdAt, caseId und type eines Eintrags sind unveränderlich
- an einer Eintragsversion darf nur minClearance angehoben werden
- Volltextsuche mit deutscher Konfiguration; geschwärzte Bereiche werden vor
dem Indizieren entfernt, sonst verrät ein Treffer den Inhalt
- pg_notify bei Rollen- und Mapping-Änderungen
Acht Tests prüfen diese Regeln gegen eine echte Postgres-Instanz, nicht gegen
die Anwendungslogik.