Files
Florian Wilde 1ed19eb76e Zeilenenden der 24 verbliebenen Dateien normalisieren
Ausschliesslich Zeilenenden. Nachgewiesen mit
"git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich
kein Zeichen Inhalt geaendert.

Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit
echten Aenderungen zu verstecken, macht die Historie an dieser Stelle
unlesbar. So laesst er sich beim Durchsehen ueberspringen.

Refs #26
2026-08-23 23:37:02 +02:00

7.8 KiB

Schema-Wünsche aus Etappe 3 „Papier“

Gesammelt beim Bau der Dokumentenakte. prisma/schema.prisma wurde absprachegemäß nicht angefasst; alles hier ist umgangen worden und funktioniert, wäre aber sauberer, wenn es im Modell stünde.

Reihenfolge: was am meisten weh tut, steht oben.


1. DokumentVerknuepfung: genau eine Zielspalte — nur in der Anwendung erzwungen

Stand: Zwölf nullbare Fremdschlüsselspalten. Der Kommentar im Schema sagt „Die Anwendung stellt sicher, dass genau eine Ziel-Spalte gesetzt ist“ — genau so ist es umgesetzt (ZIEL_SPALTE in src/lib/validierung/dokumente.ts, Prüfung in dokumentVerknuepfen).

Wunsch: Ein CHECK-Constraint in der Datenbank, damit auch ein Skript, ein Seed oder ein späterer Import das nicht verletzen kann:

ALTER TABLE dokument_verknuepfung ADD CONSTRAINT genau_ein_ziel CHECK (
  (("vorgangId" IS NOT NULL)::int + ("fristId" IS NOT NULL)::int
   + ("buchungId" IS NOT NULL)::int + ("vermoegenspostenId" IS NOT NULL)::int
   + ("immobilieId" IS NOT NULL)::int + ("vertragId" IS NOT NULL)::int
   + ("kontaktprotokollId" IS NOT NULL)::int + ("gesundheitsereignisId" IS NOT NULL)::int
   + ("genehmigungId" IS NOT NULL)::int + ("nachforderungId" IS NOT NULL)::int
   + ("bescheidId" IS NOT NULL)::int + ("kontaktId" IS NOT NULL)::int) = 1
);

Ließe sich als eigenes SQL-Skript neben volltextsuche.sql legen, ohne das Prisma-Schema zu ändern. Warum wichtig: Eine Akte, in der eine Verknüpfung auf zwei Dinge gleichzeitig zeigt, ist gegenüber dem Gericht nicht mehr erklärbar.

Zusätzlich: Es fehlt eine Eindeutigkeit je Paar. Doppelte Verknüpfungen werden derzeit vor dem Anlegen abgefragt (findFirst) — ein Race zwischen zwei Klicks kann sie trotzdem erzeugen. Ein partieller Unique-Index je Zielspalte, z. B. CREATE UNIQUE INDEX ... ON dokument_verknuepfung ("dokumentId", "vorgangId") WHERE "vorgangId" IS NOT NULL;, würde das schließen.


2. Dokument.ocrStatus ist ein freier String

Stand: ocrStatus String? mit dem Kommentar "ausstehend" | "laeuft" | "fertig" | "fehlgeschlagen" | "nicht_verfuegbar".

Wunsch: Ein Enum OcrStatus mit genau diesen fünf Werten. Der Vertrag in docs/SCHNITTSTELLEN.md Abschnitt 3 gibt sie ohnehin vor, und die Oberfläche (OcrAbzeichen) schaltet darauf. Ein Tippfehler beim Zurückschreiben fällt heute erst auf, wenn das Abzeichen leer bleibt.


3. Kein Index auf Dokument.objektSchluessel

Stand: Die Ausliefer-Route /api/dokumente/datei/[signatur] sucht das Dokument über objektSchluessel, um Anzeigename und Protokolleintrag zu bekommen. Ohne Index ist das ein Seq-Scan bei jedem Dateiabruf.

Wunsch: @@unique([objektSchluessel]) (der Schlüssel ist eine UUID, also ohnehin eindeutig) oder mindestens @@index([objektSchluessel]).


4. Keine Dublettenerkennung über die Prüfsumme

Stand: pruefsumme String? (SHA-256, wird beim Ablegen gefüllt). Wer denselben Bescheid zweimal fotografiert, bekommt zwei Datensätze.

Wunsch: @@index([betreuteId, pruefsumme]). Dann ließe sich beim Upload sagen „dieses Blatt liegt schon in der Akte, angelegt am 12.03.2026“ — ohne den zweiten Upload zu verbieten (manchmal ist die zweite Kopie gewollt).


5. Nachforderung und Bescheid tragen keine betreuteId

Stand: Beide hängen nur am SozialhilfeAntrag. Die Zugriffsprüfung beim Verknüpfen muss deshalb über die Beziehung gehen (where: { id, antrag: { betreuteId } }, siehe zielGehoertZurAkte).

Wunsch: Eine denormalisierte betreuteId wie bei allen anderen Fachentitäten. Die Konvention „jede Abfrage ist auf betreuteId eingegrenzt“ ist sonst an genau diesen zwei Stellen nicht wörtlich einhaltbar — und Ausnahmen von einer Sicherheitsregel sind die Stellen, an denen später Fehler passieren.


6. Seitenzahl und OCR-Lauf ohne Zeitstempel

Stand: seitenzahl Int? wird von der Texterkennung nachgetragen. Wann ein OCR-Lauf zuletzt versucht wurde, steht nirgends.

Wunsch: ocrVersuchtAm DateTime?. ocrNachziehen() in src/lib/dokumente/ocr-nachlauf.ts holt heute alles nach, was auf ausstehend/fehlgeschlagen steht — mit einem Zeitstempel ließe sich ein dauerhaft kaputtes Dokument nach dem dritten Versuch in Ruhe lassen, statt es bei jedem Nachziehen erneut durch Tesseract zu schicken.


7. Anmerkung zur Verschlüsselung der abgelegten Dateien

Kein Schema-Wunsch, aber eine Abweichung vom Kommentar im Schema, die festgehalten gehört. Bei Dokument.ocrText steht:

„die Datei selbst liegt verschlüsselt im Objektspeicher“

Umgesetzt ist das nur beim Dateitreiber: src/lib/speicher/datei.ts verschlüsselt jede Datei anwendungsseitig mit AES-256-GCM (eigener Binär- Umschlag BCD1 | Schlüsselversion | IV | Tag | Geheimtext, Schlüssel aus ENC_KEY_V*). Beim S3-Treiber geht das nicht: Der Vertrag verlangt für signierteUrl() eine presigned URL, die der Objektspeicher selbst ausliefert — ein anwendungsseitig verschlüsselter Blob käme dort als Datenmüll an. Dort läuft der Schutz deshalb über ServerSideEncryption: AES256 plus den AV-Vertrag nach Art. 28 DSGVO mit dem Anbieter.

Die SHA-256-Prüfsumme wird in beiden Fällen über den Klartext gebildet — sonst wäre sie als Nachweis gegenüber dem Gericht wertlos.

Wer das vereinheitlichen will, müsste den Vertrag ändern: signierteUrl() würde auch beim S3-Treiber auf die eigene Route zeigen, die den Blob holt, entschlüsselt und ausliefert. Das kostet Durchsatz bei großen Scans, wäre aber konsequenter. Bewusst offengelassen, weil es Abschnitt 1 der Schnittstellen berührt.


Anhang: kein Schema-Wunsch, aber dringend

@react-pdf/renderer kann ohne Krücke keinen Buchstaben setzen

Beim Bau der Belegmappe aufgefallen und betrifft jedes PDF der Anwendung, also auch die Berichte aus Etappe 5.

Befund: Jeder Aufruf von renderToBuffer scheitert mit

TypeError: Cannot read properties of undefined (reading 'unitsPerEm')
  at @react-pdf/textkit/lib/textkit.js:3090

— auch bei einem Dokument ganz ohne Stilangaben. Nicht die Testumgebung ist schuld: in reinem Node reproduziert es genauso.

Ursache: In package.json ist nur @react-pdf/renderer auf 4.1.6 festgelegt. Seine Unterabhängigkeiten stehen als Caret-Bereiche darin und sind inzwischen weitergewandert:

Paket installiert von renderer 4.1.6 erwartet
@react-pdf/font 3.1.0 ^3.0.1
@react-pdf/layout 4.7.1 ^4.2.0
@react-pdf/textkit 6.4.1 (kommt über layout)

@react-pdf/font@3.1.0 gibt für die Standardschriften null zurück — früher löste die Schicht darunter den bloßen Namen selbst auf. @react-pdf/textkit@6.4.1 verlangt dagegen ein echtes Schriftobjekt mit layout() und wirft bei einem Namen ausdrücklich Invalid font. Zwischen beiden klafft eine Lücke.

Was ich getan habe: src/lib/pdf/standardschrift.ts baut das fehlende Schriftobjekt aus den Metriken, die @react-pdf/pdfkit ohnehin mitbringt, und hängt sich vor die Schriftauflösung — aber nur, wenn sie kaputt ist. Kein neues Paket, kein Nachladen aus dem Netz. pdfErzeugen() ruft das von selbst auf, alle PDF-Wege dieser Anwendung sind damit wieder benutzbar (Test: „Aus einem Bericht und einem Beleg wird eine Mappe" in tests/dokumente.test.ts).

Was noch zu tun ist (außerhalb meiner Pfade, deshalb hier): package.json um Festlegungen ergänzen, damit die Pakete zueinander passen —

"overrides": {
  "@react-pdf/layout": "4.2.x",
  "@react-pdf/textkit": "5.x"
}

Die genauen Stände bitte gegen @react-pdf/renderer@4.1.6 prüfen; alternativ @react-pdf/renderer auf einen aktuellen Stand heben, dessen Unterabhängigkeiten wieder zusammenpassen. Danach kann standardschrift.ts ersatzlos entfallen — standardschriftenSicherstellen() merkt von selbst, dass es nicht mehr gebraucht wird, und hält sich heraus.