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
This commit is contained in:
Florian Wilde
2026-08-23 23:37:02 +02:00
parent 6a0db51e3b
commit 1ed19eb76e
24 changed files with 7448 additions and 7448 deletions
+187 -187
View File
@@ -1,187 +1,187 @@
# 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:
```sql
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 —
```jsonc
"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.
# 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:
```sql
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 —
```jsonc
"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.