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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
389 lines
22 KiB
Markdown
389 lines
22 KiB
Markdown
# Getroffene Entscheidungen
|
|
|
|
Stellen, an denen die Fachdokumente sich widersprechen, lückenhaft sind oder gegen eine
|
|
Eigenschaft von Postgres laufen. Jede Zeile nennt den Konflikt, die Entscheidung und den Grund.
|
|
Wo eine Entscheidung fachlich anders ausfallen kann, steht das ausdrücklich dabei.
|
|
|
|
## Struktur
|
|
|
|
| Konflikt | Entscheidung |
|
|
|---|---|
|
|
| `CLAUDE.md` und `README.md` verweisen auf `docs/`, die Dateien lagen im Wurzelverzeichnis | Nach `docs/` verschoben |
|
|
| `02-architektur.md` zeigt `app/` und `lib/` im Wurzelverzeichnis, `TASKS.md` Phase 0 verlangt einen pnpm-Workspace mit `web/` und `worker/` | Workspace mit `web/`, `worker/`, `pdf/` und `packages/db/`. Die Struktur aus `02-architektur.md` gilt innerhalb von `web/` |
|
|
| Der Worker braucht denselben Prisma-Client wie die Web-App | Eigenes Workspace-Paket `packages/db` (`@afis/db`), von `web` und `worker` benutzt. Migrationen und Seed liegen dort |
|
|
|
|
## Datenmodell
|
|
|
|
| Konflikt | Entscheidung |
|
|
|---|---|
|
|
| `CaseParticipant` hat den zusammengesetzten Primärschlüssel `[caseId, role, userId, personnelFileId]`, aber `userId` und `personnelFileId` sind beide optional. Postgres lässt NULL in einem Primärschlüssel nicht zu | Eigener Schlüssel `id`, Eindeutigkeit über zwei **partielle** Unique-Indizes plus eine CHECK-Bedingung, dass mindestens eines der beiden Felder gesetzt ist |
|
|
| `04-datenmodell.md` nennt zugleich `CREATE SEQUENCE case_number_2026` und die Tabelle `CaseCounter(year, lastNumber)` mit `SELECT … FOR UPDATE` | `CaseCounter`. Eine Sequenz pro Jahr müsste jedes Jahr von Hand angelegt werden, und der Abteilungsleiter ist ausdrücklich nicht technisch |
|
|
| Das rohe SQL benutzt `search_vector` und `updated_at` in Schlangenschrift, die Prisma-Modelle Höckerschrift | Prisma-Voreinstellung behalten: Tabellen in Höckerschrift-Pascal, Spalten in Höckerschrift. Das SQL quotet entsprechend (`"searchVector"`, `"updatedAt"`) |
|
|
| Kein Modell für Kanäle, obwohl `ChatMessage.channelId` existiert und `06-chat-und-realtime.md` Kanäle mit eigener Mindeststufe verlangt | Modell `Channel` ergänzt, dazu `ChatReaction` für den festen Reaktionssatz |
|
|
| `/api/health` soll den Discord-Gateway prüfen, aber die Web-App fragt nie live bei Discord an | Modell `ServiceHeartbeat`. Der Worker schreibt alle 30 Sekunden seinen Zustand, `web` liest ihn |
|
|
|
|
## Der wichtigste Widerspruch
|
|
|
|
`04-datenmodell.md` verlangt `CREATE RULE entry_version_no_update … DO INSTEAD NOTHING` auf
|
|
`CaseEntryVersion`. `05-akten-und-vorlagen.md` verlangt unter „Umklassifizierung wirkt
|
|
rückwirkend", dass beim Anheben von `minClearance` **alle bestehenden Versionen den neuen Wert
|
|
erben**. Beides zusammen geht nicht: die Regel würde genau die Nachführung verhindern, die das
|
|
Leck schließt.
|
|
|
|
Aufgelöst durch einen Trigger statt einer Regel. `CaseEntryVersion` bleibt anhängend, aber
|
|
`minClearance` darf nachgeführt werden, und nur nach oben. Jede andere Änderung wirft einen
|
|
Fehler. Das erhält beide Absichten: der Text ist unveränderlich, die Klassifizierung wirkt
|
|
rückwirkend.
|
|
|
|
Ein zweiter Trigger schützt `CaseEntry.authorId`, `createdAt`, `caseId` und `type` auf
|
|
Datenbankebene, nicht nur in der Anwendung.
|
|
|
|
## Suche
|
|
|
|
`08-suche-und-export.md` beschreibt einen `search_vector` je Akte aus Titel, Abschnitten und
|
|
Verlaufseinträgen. Ungeklärt bleibt, was mit Inhalten oberhalb der Stufe des Suchenden
|
|
passiert. Ein gemeinsamer Vektor über alle Abschnitte würde einen Nutzer mit Stufe 1 einen
|
|
Begriff finden lassen, der nur in einem als `[[c4]]…[[/c4]]` geschwärzten Bereich steht.
|
|
Das verstößt gegen harte Regel 6.
|
|
|
|
**Vorläufig aufgenommen wird nur, was jeder lesen darf, der die Akte überhaupt sehen darf:**
|
|
Aktenzeichen und Titel (Gewicht A), Abschnitte und Verlaufseinträge mit `minClearance = 0`
|
|
(B und C), Schwärzungs-Auszeichnungen vorher entfernt. Zurückgezogene Einträge fallen heraus.
|
|
|
|
Das ist sicher, aber für hohe Stufen zu sparsam: ein Nutzer mit Stufe 5 findet geheime Inhalte
|
|
nicht über die Volltextsuche. **Phase 7 muss das auflösen**, sinnvollerweise über getrennte
|
|
Vektorspalten je Stufe. Diese Entscheidung ist bewusst konservativ getroffen, weil ein Leck
|
|
hier schwerer zu bemerken ist als eine Lücke.
|
|
|
|
## Rollen auf dem Discord
|
|
|
|
Die Dokumente kennen genau eine Sorte Rolle: `RoleMapping` trägt Stufe **und** Rechte, die
|
|
Auflösung nimmt das Maximum der Stufen und die Vereinigung der Rechte. Auf dem Server gibt es
|
|
aber drei Sorten mit verschiedener Bedeutung:
|
|
|
|
| Sorte | Beispiele | Wirkung |
|
|
|---|---|---|
|
|
| Zugehörigkeit | `AfiS`, `Personalabteilung` | Türsteher. Ohne eine davon kein Login |
|
|
| Rang | `Director of internal Security` … `Probationary Agent` | Bestimmt die **Rechte** |
|
|
| Zugang | `Executive Oversight` … `Restricted Access` | Bestimmt die **Freigabestufe** |
|
|
|
|
Das bestehende Modell trägt das fast: Zugangsrollen bekommen eine Stufe und keine Rechte,
|
|
Rangrollen Rechte und Stufe 0, Maximum und Vereinigung ergeben das Richtige. Was fehlt, ist
|
|
der Türsteher. `RoleMapping` bekommt deshalb in Phase 1 zwei Felder:
|
|
|
|
- `grantsMembership` — diese Rolle berechtigt zur Anmeldung
|
|
- `category` — nur für die Gruppierung im Adminbereich, damit dort drei Abschnitte stehen
|
|
statt einer Liste aus achtzehn Rollen
|
|
|
|
Anmeldeprüfung damit: Mitglied der Guild **und** (eine Rolle mit `grantsMembership` **oder**
|
|
in `BOOTSTRAP_ADMIN_IDS`). Notfall-Admins umgehen die AfiS-Rolle, müssen aber Mitglied der
|
|
Guild bleiben — sonst bliebe die Hintertür auch nach einem Rauswurf offen.
|
|
|
|
Rangrollen bekommen im Auslieferungszustand **keine** Rechte. Das Rollen-Mapping entsteht
|
|
vollständig im Adminbereich, nichts wird nach Rollennamen geraten.
|
|
|
|
### Umzug vom Test- auf den echten Server
|
|
|
|
Entwickelt wird gegen einen Test-Discord mit nachgebauten Rollen. Rollen-IDs sind pro Server
|
|
eindeutig, beim Umzug ist das gesamte `RoleMapping` deshalb wertlos. Das ist kein Fehler,
|
|
sondern eine Anforderung an Phase 1: die Adminseite muss ein neu erkanntes `DISCORD_GUILD_ID`
|
|
verkraften und beim Übernehmen anbieten, die alte Zuordnung **anhand der Rollennamen**
|
|
vorzuschlagen. `RoleMapping.label` wird dafür ohnehin aus Discord gespiegelt.
|
|
|
|
## Der Ausschluss schlägt auch den Admin
|
|
|
|
`03-auth-und-rechte.md` nennt die Reihenfolge der Zugriffsprüfung: erst der
|
|
`CaseExclusion`, dann der Admin. Das Codebeispiel im selben Dokument gibt für Admins aber
|
|
`{}` zurück und übergeht den Ausschluss damit.
|
|
|
|
Umgesetzt ist die Reihenfolge, nicht das Beispiel. Der Ausschluss deckt gerade den Fall ab,
|
|
dass gegen einen hochrangigen Mitarbeiter ermittelt wird — und der Administrator ist einer.
|
|
Ein Admin, der sich selbst wieder Zugriff verschaffen will, muss dafür den Ausschluss
|
|
aufheben, und das steht dann im Protokoll.
|
|
|
|
## Versiegelte Akten im Where-Fragment
|
|
|
|
`05-akten-und-vorlagen.md` sagt, `SEALED` sei „nur mit Recht 'admin' oder Stufe 5 lesbar".
|
|
Das steht in `lib/access/caseFilter.ts` und nicht in einer Prüfung danach, sonst
|
|
verschwände die Akte zwar aus der Ansicht, aber nicht aus der Trefferzahl.
|
|
|
|
## Sprache der Fachbegriffe
|
|
|
|
`CLAUDE.md` verlangt eine deutsche Oberfläche. Die Begriffe der Domäne heißen auf dem Discord
|
|
aber englisch, und genau so werden sie im Rollenspiel gesprochen. Sie bleiben deshalb
|
|
englisch: Rollennamen, Dienstgrade und die Bezeichnungen der Freigabestufen.
|
|
|
|
Aus `docs/01-produkt.md` wird damit `Sensitive Access` statt `Geheim`. Die Bezeichnungen sind
|
|
Daten in `ClearanceLevelConfig`, nicht Oberflächentexte in `lib/i18n/de.ts` — sie sind im
|
|
Adminbereich änderbar, und ein Text, den der Leiter ändern kann, gehört nicht in eine
|
|
Übersetzungsdatei. `lib/i18n/de.ts` behält nur das Gerüst („Freigabestufe", „Geschwärzt,
|
|
ab Stufe 4 sichtbar").
|
|
|
|
## Tests laufen gegen eine eigene Datenbank
|
|
|
|
`CaseEntry`, `CaseEntryVersion` und `AuditLog` lassen sich nach harter Regel 2 nicht löschen.
|
|
Was ein Test dort anlegt, bleibt für immer stehen. Bei gemeinsamer Datenbank verstopfen die
|
|
Testakten binnen weniger Läufe die Fallliste, und aufräumen lässt sich das nicht mehr.
|
|
|
|
Tests laufen deshalb gegen `<datenbank>_test` auf demselben Server. `pnpm test` legt sie an,
|
|
migriert und seedet sie vorher (`pnpm db:test:prepare`). Über `TEST_DATABASE_URL` lässt sich
|
|
ein anderes Ziel setzen.
|
|
|
|
Das ist kein theoretisches Problem gewesen: die Testläufe der Phase 1 hatten in der
|
|
Entwicklungsdatenbank 70 erfundene Rollen, 112 Nutzer und 40 Akten hinterlassen, was die
|
|
Rollenverwaltung unbrauchbar machte.
|
|
|
|
## Fallakten werden nicht gelöscht
|
|
|
|
`03-auth-und-rechte.md` führte ein Recht `case.delete`, der Lebenszyklus in
|
|
`05-akten-und-vorlagen.md` kennt aber kein Löschen, nur `ARCHIVED` und `SEALED`. Dazu kommt
|
|
eine technische Sperre: eine Akte mit Verlaufseinträgen lässt sich gar nicht hart löschen,
|
|
weil `CaseEntry` nach harter Regel 2 unlöschbar ist und der Fremdschlüssel das Kaskadieren
|
|
blockiert.
|
|
|
|
Aufgelöst zugunsten des Lebenszyklus: **`case.delete` ist gestrichen.** Eine Akte, die weg
|
|
soll, wird archiviert (schreibgeschützt) oder versiegelt (nur Stufe 5 und Admin). Nichts
|
|
verschwindet je aus einer Akte. Das ist die konsequente Fortsetzung von Regel 2 auf die Ebene
|
|
darüber und passt zu einer Abteilung, die Korruption aufdeckt.
|
|
|
|
## Hochstufen ist bei der eigenen Stufe gedeckelt
|
|
|
|
Niemand klassifiziert eine Akte höher, als seine eigene Freigabestufe reicht — auch nicht,
|
|
um einen Fund zu schützen. Wer Stufe 3 hat, stuft höchstens auf 3 ein; für mehr muss jemand
|
|
mit entsprechender Stufe ran.
|
|
|
|
Die Alternative wäre gewesen, das Hochstufen zu erlauben und den Zugriff dabei zu verlieren.
|
|
Das erzeugt aber Akten, die ihr Urheber nicht mehr sehen kann, und macht die Frage „wer
|
|
konnte das lesen" im Nachhinein schwer beantwortbar.
|
|
|
|
## Zwei Freigaben im Kopf, aber nur eine sperrt
|
|
|
|
Die Dokumente der Abteilung tragen zwei Angaben nebeneinander: „Foundation-Freigabe 3" und
|
|
„Interne Freigabe: Restricted Access". Das Modell kennt nur eine Stufe je Akte.
|
|
|
|
Festgelegt: **den Zugriff steuert allein die interne Freigabe**, also `Case.classification`
|
|
aus den Zugangsrollen des Discord-Servers. Die Foundation-Freigabe ist ein gewöhnliches
|
|
Vorlagenfeld — sie steht im Kopf des Dokuments und im PDF, sperrt aber nichts.
|
|
|
|
Der Grund ist nicht Bequemlichkeit: mit zwei sperrenden Achsen bekäme jede
|
|
Sichtbarkeitsprüfung, jede Trefferzahl, jeder Facettenzähler und jeder Negativtest eine
|
|
zweite Dimension. Sichtbarkeit wird an genau einer Stelle entschieden, sonst ist sie nicht
|
|
mehr nachweisbar.
|
|
|
|
## Aktenzeichen mit Präfix je Fallart
|
|
|
|
Das Glossar schreibt `AfiS-<Jahr>-<4-stellig>` vor, die tatsächlichen Dokumente der
|
|
Abteilung tragen `AKT-PA-2026-62843` — Präfix nach Abteilung, fünfstellige Nummer.
|
|
|
|
Festgelegt: das Muster gehört zur **Vorlage**, nicht ins Programm. Jede Fallart bringt ihr
|
|
Präfix und ihre Stellenzahl mit, der Nummernkreis läuft je Präfix und Jahr. `CaseCounter`
|
|
bekommt dafür einen zusammengesetzten Schlüssel aus Präfix und Jahr statt nur dem Jahr.
|
|
Bestehende Aktenzeichen bleiben, wie sie sind.
|
|
|
|
## Dokumentarten über die Fallakte hinaus
|
|
|
|
**Noch nicht entschieden, noch nichts gebaut.** Geprüft vor Phase 3, weil die Antwort den
|
|
Zuschnitt von Phase 3 und 4 bestimmt.
|
|
|
|
Langfristig sollen weitere Dokumentarten dazukommen: Vorfallsberichte, Bußgelder,
|
|
Sonderarbeitsverträge, Beurteilungen. Die Frage ist, ob das Modell das trägt, ohne dass für
|
|
jede neue Art Code geschrieben wird.
|
|
|
|
### Was heute schon trägt
|
|
|
|
Eine neue **Fallart** braucht keine Zeile Code. `CaseTemplate.fields` ist ein JSON-Feld,
|
|
Vorlagen sind versioniert, und das Rendern der Abschnitte wird datengetrieben gebaut. Eine
|
|
neue Vorlage anzulegen ist eine Zeile in der Datenbank, kein Deployment. Mit dem Präfix je
|
|
Vorlage bekommt jede Fallart auch ihren eigenen Nummernkreis.
|
|
|
|
### Was heute nicht trägt
|
|
|
|
`CaseSection` hängt fest an `caseId`. **Alles, was kein Ermittlungsfall ist, hat keinen
|
|
Platz.** Ein Bußgeld gegen einen Mitarbeiter ist keine Ermittlung. Ein Sonderarbeitsvertrag
|
|
erst recht nicht.
|
|
|
|
`PersonnelFile.fields` ist ein nacktes JSON-Feld ohne Vorlage. Ein Vertrag, der dort landet,
|
|
hätte nichts von dem, was eine Fallakte selbstverständlich hat: keine Versionierung, keinen
|
|
Diff, keine Begründungspflicht, kein `minClearance` je Feld, keinen Eintrag im
|
|
Zugriffsprotokoll. Genau die Eigenschaften, um derentwillen das System gebaut wird.
|
|
|
|
Würde man Bußgelder und Verträge behelfsweise als `Case` führen, stünden sie in der
|
|
Fallliste, in der Fallzählung und in der Suche nach Ermittlungen. Das ist kein Schönheits-
|
|
fehler, sondern eine falsche Aussage über den Bestand.
|
|
|
|
### Zwei Bauformen von Dokument, nicht eine
|
|
|
|
Die beiden vorliegenden Beispiele sind grundverschieden aufgebaut:
|
|
|
|
| | Vorfallsbericht | Sonderarbeitsvertrag |
|
|
|---|---|---|
|
|
| Aufbau | beschriftete Felder, ausgefüllt | fester Vertragstext mit Platzhaltern |
|
|
| Vorlage liefert | Feldliste | den Wortlaut selbst, samt Paragraphen |
|
|
| Nutzer trägt ein | Werte | `[Name]`, `[Datum]`, `[Tätigkeit]` |
|
|
| Ausgabe | eine Seite | mehrere Seiten mit Unterschriftenzeilen |
|
|
|
|
Die Feldtypen aus `docs/05-akten-und-vorlagen.md` decken den **Bericht vollständig** ab
|
|
(`multiselect` für die Berichtsart, `select` für die Gefahreneinschätzung, `date`, `person`,
|
|
`longtext`). Für den **Vertrag reichen sie nicht**: fester Fließtext, Überschriften und
|
|
Unterschriftenzeilen sind keine Formularfelder. Dafür braucht die Vorlage einen Textkörper
|
|
mit Platzhaltern statt einer Feldliste.
|
|
|
|
### Entschieden
|
|
|
|
**Jede Akte ist ein Behälter, der Inhalt sind Beiträge: Kurzvermerk oder Dokument.** Das gilt
|
|
für Fallakten, Personalakten und alles, was später dazukommt.
|
|
|
|
Die dritte Ebene bleibt: eine Fallakte behält zusätzlich ihre **Abschnitte**, also
|
|
Vorlagenfelder direkt an der Akte, die „was wissen wir jetzt" beantworten. Die Trennung aus
|
|
`docs/05-akten-und-vorlagen.md` gilt damit weiter. Eine Fallakte hat also:
|
|
|
|
| Ebene | Was |
|
|
|---|---|
|
|
| Kopfdaten | Aktenzeichen, Titel, Status, Freigabestufe, Wiedervorlage |
|
|
| Abschnitte | aktueller Sachstand aus der Vorlage der Fallart |
|
|
| Verlauf | Kurzvermerke, chronologisch |
|
|
| Dokumente | aus einer Vorlage erzeugt, versioniert, einzeln exportierbar |
|
|
|
|
### Empfehlung für den Aufbau
|
|
|
|
Eine gemeinsame Dokumentebene, additiv statt als Umbau:
|
|
|
|
- `DocumentTemplate` — was heute `CaseTemplate` ist, plus `scope` (Fall, Person,
|
|
eigenständig), `numberPrefix`, und `kind` (Formular oder Textkörper)
|
|
- `Document` — eine Ausfertigung, mit Vorlage, Klassifizierung, Nummer und einem Besitzer:
|
|
Fall, Personalakte oder nichts
|
|
- `DocumentSection` und `DocumentSectionVersion` — dieselben Zusicherungen wie bei
|
|
`CaseSection`: versioniert, `minClearance` je Feld, Diff, Protokoll
|
|
|
|
`Case`, `CaseSection` und `CaseEntry` bleiben unangetastet. Eine Fallakte kann Dokumente
|
|
tragen, eine Personalakte auch, und ein Bußgeld steht für sich.
|
|
|
|
Damit ist eine neue Dokumentart wieder das, was sie sein soll: **ein Datensatz, kein
|
|
Deployment.**
|
|
|
|
### Gebaut in Phase 4
|
|
|
|
Umgesetzt wie empfohlen, additiv. Drei neue Modelle: `Document`, `DocumentSection`,
|
|
`DocumentSectionVersion`. `Case`, `CaseSection` und `CaseEntry` blieben unberührt.
|
|
|
|
Die offene Frage — ob die Abschnitte einer Fallakte langfristig ebenfalls über diese Ebene
|
|
laufen sollen — ist **verneint**. Dafür hätte gesprochen, dass es dann nur einen Mechanismus
|
|
gibt. Dagegen sprach mehr: `docs/04` und `docs/05` benennen `CaseSection` ausdrücklich, der
|
|
Umbau fasst bestehende Akten an, und die Regeln sind ohnehin dieselben. Statt die Tabellen
|
|
zusammenzulegen, liegen die Regeln jetzt einmal in `lib/sections/render.ts` und werden von
|
|
beiden Diensten aufgerufen. Dieselbe Darstellung, dieselben Zusicherungen, zwei Tabellen.
|
|
|
|
Zwei Umbenennungen kamen dazu, beide von Hand migriert, damit die vorhandenen Zeilen stehen
|
|
bleiben:
|
|
|
|
| Vorher | Jetzt | Warum |
|
|
|---|---|---|
|
|
| `CaseTemplate` | `DocumentTemplate` | Eine Vorlage beschreibt jetzt auch Dokumente. Ein Sonderarbeitsvertrag als `CaseTemplate` wäre eine falsche Aussage |
|
|
| `CaseCounter` | `NumberSeries` | Aktenzeichen und Dokumentnummern laufen über denselben Zähler, getrennt durch das Präfix aus der Vorlage |
|
|
|
|
`DocumentTemplate.uses` ist eine Liste, kein Einzelwert: dieselbe Vorlage kann an mehreren
|
|
Stellen sinnvoll sein. Der Vermerk hängt an einer Fallakte **und** an einer Personalakte, der
|
|
Bußgeldbescheid an einer Person **oder** für sich allein. `layout` bestimmt allein das
|
|
Aussehen und wird beim Anlegen in das Dokument kopiert — ein späterer Umbau der Vorlage darf
|
|
ein ausgefertigtes Dokument nicht nachträglich anders aussehen lassen.
|
|
|
|
### Ein Dokument hat höchstens einen Besitzer
|
|
|
|
`caseId` und `personnelFileId` sind beide optional, eine CHECK-Bedingung lässt nicht zu, dass
|
|
beide gesetzt sind. Zwei Besitzer wären eine Aussage über die Sichtbarkeit, die niemand
|
|
prüfen könnte: die Prüfung müsste sich für einen der beiden entscheiden, und welcher das
|
|
wäre, stünde nirgends.
|
|
|
|
Die Sichtbarkeit eines Dokuments hat deshalb **zwei** Bedingungen, die beide zutreffen
|
|
müssen: der Besitzer muss lesbar sein, und das Dokument selbst muss es sein. Ein Vertrag der
|
|
Stufe 1 an einer Personalakte der Stufe 4 bleibt verborgen — sonst verriete die
|
|
Dokumentenliste die Existenz der Person.
|
|
|
|
## Die eigene Personalakte ist keine Ausnahme
|
|
|
|
Eine Fallakte kennt den ausnahmsweisen Zugang über `CaseParticipant`. Eine Personalakte hat
|
|
nichts Vergleichbares, und insbesondere darf **die betroffene Person ihre eigene Akte nicht
|
|
allein deshalb lesen**, weil sie es ist.
|
|
|
|
Der Gedanke lag nahe: die eigene Akte kennt man doch. Er ist trotzdem falsch. Eine Beurteilung
|
|
auf Stufe 4 über einen Mitarbeiter der Stufe 1 wäre sonst genau für den einen Menschen
|
|
lesbar, vor dem sie geschützt werden soll. Es zählt allein die Freigabestufe.
|
|
|
|
## Rückverweise zählen nur, was sichtbar ist
|
|
|
|
Auf einer Personalakte stehen die Fälle, in denen die Person vorkommt. Diese Liste läuft
|
|
durch `visibleCaseWhere`, und die **Zahl daneben entsteht aus dem gefilterten Ergebnis**, nie
|
|
aus einem eigenen `count`. Eine Personalakte der Stufe 1, auf der „3 Fälle" steht, verriete
|
|
sonst die Existenz von Ermittlungen, die der Leser nicht sehen darf.
|
|
|
|
Auch die Rollengruppe verschwindet mit: stünde dort „Betroffener" ohne Eintrag darunter, wäre
|
|
die Ermittlung gegen die Person verraten, ohne dass ein Aktenzeichen sichtbar wird.
|
|
|
|
Eine Person kann auf zwei Wegen an einem Fall hängen — über ihre Personalakte oder über ihr
|
|
Discord-Konto. Beide zählen, sonst fehlen Fälle.
|
|
|
|
## Der Name eines Beteiligten gehört zur Akte
|
|
|
|
`listParticipants` zeigt den Namen einer beteiligten Person auch dann, wenn der Leser deren
|
|
Personalakte nicht sehen darf. Nur der **Verweis** auf die Personalakte entfällt dann.
|
|
|
|
Das ist Absicht: wer ein Ermittlungsverfahren lesen darf, muss wissen, gegen wen es läuft.
|
|
Der Name ist an dieser Stelle Inhalt der Fallakte, nicht Inhalt der Personalakte. Verborgen
|
|
bleibt, was darüber hinaus über die Person aktenkundig ist.
|
|
|
|
## Prisma 7
|
|
|
|
Die Dokumente nennen nur „Prisma + PostgreSQL 16". Eingesetzt wird Prisma 7, die
|
|
aktuelle Fassung. Sie unterscheidet sich in drei Punkten, die den Aufbau betreffen:
|
|
|
|
| Änderung | Folge hier |
|
|
|---|---|
|
|
| Verbindungsdaten stehen nicht mehr im Schema | `packages/db/prisma.config.ts`. Migrationen laufen über `DIRECT_DATABASE_URL`, also an pgbouncer vorbei |
|
|
| Keine Rust-Query-Engine mehr, stattdessen Treiber-Adapter | `@prisma/adapter-pg` über `pg`. Der Treiber wird für `LISTEN/NOTIFY` ohnehin gebraucht, und die Images müssen keine Engine nachladen |
|
|
| Neuer Generator `prisma-client` statt `prisma-client-js` | Der Client wird als ECMAScript-Module nach `packages/db/src/generated` erzeugt statt nach `node_modules/.prisma`. Nötig mit `importFileExtension = "js"`, sonst schreibt er endungslose Importe, die Node im ESM-Modus nicht auflöst |
|
|
|
|
Der erzeugte Client ist ein Bauartefakt und liegt nicht im Repository.
|
|
`pnpm --filter @afis/db run build` erzeugt ihn, die CI tut das vor lint, typecheck und test.
|
|
|
|
Für `prisma migrate deploy` braucht es weiterhin eine Rust-Schema-Engine. Sie wird beim
|
|
Bauen des Web-Images heruntergeladen und liegt unter `/app/migrator` im Image. Beim
|
|
Containerstart wird nichts nachgeladen: ein Container, der nur startet, wenn gerade eine
|
|
Registry erreichbar ist, wäre auf einem Homeserver die falsche Bauart.
|
|
|
|
## Betrieb
|
|
|
|
| Konflikt | Entscheidung |
|
|
|---|---|
|
|
| `11-betrieb.md` verlangt „Jeder Dienst braucht einen Healthcheck", die abgedruckte `compose.yaml` hat für `db-backup` und `offsite-backup` keinen | Ergänzt. `db-backup` über seinen HTTP-Port 8080, `offsite-backup` über Cron-Prozess und `restic version` |
|
|
| Das Startkommando `npx prisma migrate deploy` würde im Container aus dem Netz nachladen | Auf die im Image unter `/app/migrator` mitgelieferte Prisma-CLI umgestellt, zusammen mit Schema, Migrationsordner und Schema-Engine |
|
|
| `pnpm deploy` zieht die optionalen Peers von `@prisma/client` mit ins Image (CLI, Studio, TypeScript, rund 240 MB) | `--no-optional` beim Bündeln. Zur Laufzeit wird nichts davon gebraucht |
|
|
|
|
## Protokollierung
|
|
|
|
`09-performance-und-ux.md` verlangt eine Puffertabelle für Audit-Einträge, die alle zwei
|
|
Sekunden gebündelt geschrieben wird, zugleich aber, dass schreibende Aktionen ihren Eintrag
|
|
**in derselben Transaktion** erzeugen. Umgesetzt wird beides getrennt: schreibende Aktionen
|
|
protokollieren synchron in der Transaktion, Lesezugriffe ab Stufe 3 laufen über einen Puffer.
|
|
Der Puffer liegt im Prozessspeicher, nicht in einer Tabelle — eine Puffertabelle hätte
|
|
dieselbe Verlustgefahr bei einem Absturz, aber zusätzliche Schreiblast. Kommt in Phase 2.
|
|
|
|
## Vorlagen im Seed
|
|
|
|
`CLAUDE.md` beschreibt `pnpm db:seed` als „Testdaten inkl. Vorlagen", `TASKS.md` ordnet die
|
|
vier Startvorlagen aber Phase 3 zu. Der Seed legt vorerst nur an, was die Anwendung zum Laufen
|
|
braucht: die sechs Freigabestufen, die Änderungsrichtlinie und den Aktenzeichen-Zähler.
|
|
Die Vorlagen kommen mit dem Vorlagen-Editor in Phase 3.
|
|
|
|
## Vier neue Rechte für Dokumente
|
|
|
|
`document.create`, `document.edit`, `document.finalize` und `document.retract`. Die
|
|
Alternative wäre gewesen, Dokumente an `case.edit` und `personnel.edit` zu hängen — dann
|
|
hätte aber jeder, der eine Personalakte pflegen darf, auch Bußgeldbescheide ausstellen
|
|
können. Das ist nicht dieselbe Befugnis.
|
|
|
|
`document.retract` steht bei den zurückgehaltenen Rechten, zusammen mit den drei
|
|
Eintragsrechten. Gemeinsames Merkmal: sie greifen in bereits Geschriebenes ein.
|
|
|
|
Im Auslieferungszustand hat keine Rolle diese Rechte. Bis der Leiter sie im Adminbereich
|
|
verteilt, legt nur der Administrator Dokumente an.
|