Fachdokumente nach docs/, Entscheidungen festgehalten

CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen aber im
Wurzelverzeichnis. Verschoben.

docs/13-entscheidungen.md hält fest, wo die Dokumente sich widersprechen und
wie es aufgelöst wurde. Der wichtigste Punkt: 04-datenmodell.md verlangt eine
Regel, die jedes UPDATE auf CaseEntryVersion verwirft, 05-akten-und-vorlagen.md
verlangt, dass eine Umklassifizierung rückwirkend auf alle Versionen wirkt.
Beides zusammen geht nicht. Aufgelöst über einen Trigger, der die Tabelle
anhängend hält, aber das Anheben von minClearance zulässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harlekin
2026-09-01 00:00:58 +02:00
co-authored by Claude Opus 5
commit 4446e3f0ac
16 changed files with 1919 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Architektur
## Dienste
```
Browser
└─ cloudflared (Tunnel, TLS und Kompression liegen bei Cloudflare)
└─ web Next.js 15, App Router, Server Actions, SSE-Endpunkt
├─ pgbouncer ─ db PostgreSQL 16
├─ files Volume, getrennt in quarantine/ und store/
├─ clamav clamd, angesprochen über Unix-Socket
└─ pdf Playwright-Container für PDF-Erzeugung
worker discord.js, hängt am Gateway, schreibt Rollen in db
db-backup Dauerläufer mit Cron, nächtlicher Dump
offsite-backup Dauerläufer mit Cron, restic auf zweites Ziel
```
Alles liegt in **einem** Dockge-Stack. Kein Reverse Proxy im Stack: der Tunnel terminiert TLS,
Cloudflare komprimiert, und Next.js liefert seine statischen Dateien selbst mit korrekten
Cache-Headern aus. Am Router wird kein Port geöffnet.
Migrationen laufen im Startkommando von `web`, nicht in einem Init-Container. Begründung und
die vollständige `compose.yaml` in `docs/11-betrieb.md`.
## Warum ein eigener Worker
Die Web-App fragt nie live bei Discord an. Der Worker hält `GuildMember` in Postgres aktuell:
- Gateway-Ereignisse `guildMemberUpdate`, `guildMemberRemove`, `guildMemberAdd`, `roleUpdate`
- Voller Abgleich alle 5 Minuten als Sicherheitsnetz
- Bei jeder Änderung: `pg_notify('member_changed', userId)`, damit die Web-App laufende
Sessions invalidieren kann
Dadurch ist eine Rechteänderung in Discord in unter einer Sekunde in der App wirksam,
ohne dass ein einziger Request in der App auf die Discord-API warten muss.
## Verzeichnisstruktur
```
.
├─ app/
│ ├─ (auth)/ Login, Callback, Fehlerseiten
│ ├─ (app)/
│ │ ├─ dashboard/
│ │ ├─ faelle/
│ │ │ ├─ page.tsx Liste
│ │ │ ├─ loading.tsx Skelett
│ │ │ ├─ neu/
│ │ │ └─ [caseNumber]/
│ │ │ ├─ page.tsx Akte
│ │ │ ├─ loading.tsx
│ │ │ └─ @chat/ Paralleler Slot, lädt unabhängig
│ │ ├─ personal/
│ │ ├─ suche/
│ │ └─ admin/ Rollen-Mapping, Vorlagen, Protokoll
│ └─ api/
│ ├─ auth/[...nextauth]/
│ ├─ files/[id]/ Ausliefernder Endpunkt mit Rechteprüfung
│ ├─ upload/
│ └─ stream/ SSE
├─ lib/
│ ├─ auth/ Session, Rechteauflösung
│ ├─ access/ Zugriffsprüfung als Prisma-Where-Fragmente
│ ├─ db.ts
│ ├─ audit.ts
│ ├─ i18n/de.ts
│ └─ realtime/
├─ components/
├─ worker/ Eigenes Paket, discord.js
├─ prisma/
├─ docs/
└─ docker/
```
## Datenflüsse, die man kennen muss
**Seitenaufruf einer Akte.** Server Component lädt Kopfdaten und Abschnitte in einer Abfrage.
Verlauf und Chat hängen in eigenen `<Suspense>`-Grenzen und streamen nach. Die Seite ist
sichtbar, bevor der Verlauf da ist.
**Neuer Vermerk.** Server Action schreibt `CaseEntry`, danach `revalidateTag('case:<id>')`
und `pg_notify` für alle offenen SSE-Verbindungen dieser Akte. Der Client hat den Eintrag
schon optimistisch angezeigt und ersetzt ihn beim Eintreffen anhand der Client-ID.
**Upload.** Datei geht per XHR mit Fortschritt an `/api/upload`, landet in `quarantine/`,
Prüfkette läuft asynchron, Statuswechsel kommt über SSE zurück.
**Rechteänderung in Discord.** Worker schreibt, `pg_notify`, Web-App verwirft den Cache der
betroffenen Session, nächster Request rechnet die Stufe neu.