Files
AfiS-Aktensystem/docs/02-architektur.md
T
HarlekinandClaude Opus 5 4446e3f0ac 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>
2026-09-01 00:00:58 +02:00

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.