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>
3.5 KiB
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.