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
188 lines
7.8 KiB
Markdown
188 lines
7.8 KiB
Markdown
# 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.
|