Bisher gab es ueberhaupt keinen Weg, einen Anhang wieder loszuwerden. Wer die falsche Datei hochlud, musste damit leben. Das ist keine Strenge, sondern eine Luecke: die Regel des Hauses lautet nicht "nichts geht wieder weg", sondern "nichts geht unbemerkt weg". Also derselbe Weg wie beim Verlaufseintrag und beim Dokument. Zurueckziehen mit Pflichtbegruendung, der Anhang bleibt als Grabstein an seiner Stelle stehen - durchgestrichen, mit Zeitpunkt und Grund, fuer jeden sichtbar, der ihn sehen darf. Aufheben laesst es sich, beides steht im Protokoll. Aus dem Umlauf heisst wirklich aus dem Umlauf: die Auslieferung verweigert die Datei, sie verschwindet aus Lichtbildfeldern, und das Vorschaubild faellt weg. Ein Grabstein, unter dem der Download weiterlaeuft, waere eine Beschriftung und keine Ruecknahme. Fuer `audit.read` bleibt sie abrufbar - wie der Text eines zurueckgezogenen Eintrags, sonst waere es doch ein Loeschen. Im Ausdruck steht der Anhang weiter im Verzeichnis, mit dem Grund daneben. Ihn dort wegzulassen hiesse, den Ausdruck sauberer zu machen als die Akte. Die Datei auf der Platte bleibt liegen. Sie zu entfernen waere das Loeschen, das hier gerade nicht stattfindet - und ein Anhang, dessen Pruefsumme nicht mehr nachrechenbar ist, waere als Beweismittel wertlos. Dazu eine Regel in der Datenbank, die DELETE auf Attachment verbietet, und eine Bedingung, die einen Grabstein ohne Begruendung ausschliesst. Zuruecknehmen darf, wer die Datei hochgeladen hat - der haeufige Fall, und ohne das bliebe jeder Fehlgriff liegen, weil dafuer die Leitung zu rufen waere - sowie jeder, der am Besitzer schreiben darf.
AfiS-Aktensystem
Fall- und Personalaktenverwaltung für die Abteilung für innere Sicherheit eines SCP-RP-Servers. Anmeldung und Berechtigungen laufen vollständig über Discord.
Interne Anwendung für eine geschlossene Gruppe von zehn bis fünfzig Personen, selbst gehostet. Keine Mandantentrennung, keine öffentliche Registrierung.
Was es kann
- Fallakten mit Kopfdaten, Abschnitten nach Vorlage und einem Verlauf, der nur wächst
- Personalakten, überwiegend für Externe, mit Aktenblatt zum Ausdrucken
- Abteilungen als Ebene darüber: was zu ihnen aktenkundig ist, ergibt sich aus ihren Mitgliedern
- Dokumente nach Vorlage, an einer Fall- oder Personalakte
- Fünfstufige Klassifizierung mit feldgenauer Schwärzung
- Volltextsuche je Freigabestufe, Kommandopalette auf Strg+K
- Chat in jeder Akte, mit Übernahme einer Nachricht als Vermerk
- Export als PDF mit Wasserzeichen, jeder Abruf protokolliert
- Zugriffsprotokoll über jede schreibende Aktion und jeden Lesezugriff ab Stufe 3
Zusicherungen
Acht Regeln, die im Code und wo möglich in der Datenbank durchgesetzt sind. Sie sind der Grund, warum das System so gebaut ist, wie es gebaut ist - Codekommentare verweisen unter ihrer Nummer hierher.
- Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage. Niemals alle Datensätze laden und danach filtern.
- Verlaufseinträge dürfen nachträglich geändert werden, aber niemals unsichtbar. Jede
Änderung erzeugt eine Version, jede Zurücknahme lässt einen Grabstein stehen, beides ist
für alle sichtbar, die den Eintrag sehen dürfen. Es gibt kein
DELETE- das steht als Regel in der Datenbank, nicht nur in der Anwendung. - Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen Protokolleintrag.
- Keine Dateiendungen als Typprüfung. Immer Magic Bytes. Kein SVG, kein HTML, keine Archive.
- Keine Blockade-Spinner. Jede Ansicht hat ein Skelett, jede Änderung eine sofortige Rückmeldung.
- Existenz ist auch eine Information. Akten oberhalb der Sichtstufe tauchen weder in Listen noch in Trefferzahlen noch in Rückverweisen auf. Kein Zugriff heisst 404, nicht 403.
- Keine Geheimnisse im Repository. Alles über
.env, Vorlage in.env.example. - Keine Einmal-Container. Der Betrieb läuft über Dockge, das beendete Container nicht
dauerhaft anzeigt. Migrationen gehören ins Startkommando von
web, wiederkehrende Aufgaben in Dauerläufer mit eigenem Cron. Jeder Dienst braucht einen Healthcheck, weil das in Dockge die Statusanzeige ist.
Aufbau
web/ Next.js 15, App Router, React 19
worker/ discord.js, gleicht Rollen und Mitgliedschaft ab
pdf/ Playwright, erzeugt die PDF-Ausdrucke
packages/db/ Prisma-Schema, Migrationen, Seed
docker/ Dockerfiles für web, worker und pdf
compose.yaml Der Dockge-Stack
.env.example Vorlage für den .env-Reiter in Dockge
Stack: Next.js 15 mit React 19 und TypeScript, Auth.js v5 mit Discord-Provider, Prisma auf PostgreSQL 16, Tailwind ohne Komponentenbibliothek. ClamAV und Playwright laufen als eigene Container.
Einrichten
Discord-Application anlegen, Bot auf den Server einladen und Server Members Intent aktivieren - ohne das liefert der Bot keine Rollen.
Lokal entwickeln
Node 22 und pnpm 9.
corepack enable && pnpm install
docker run -d --name afis-db -e POSTGRES_DB=afis -e POSTGRES_USER=afis \
-e POSTGRES_PASSWORD=afis -p 5434:5432 postgres:16-alpine
cp .env.example .env
# DATABASE_URL und DIRECT_DATABASE_URL auf localhost:5434 zeigen lassen
pnpm --filter @afis/db run build # erzeugt den Prisma-Client
pnpm db:deploy && pnpm db:seed
pnpm dev
Prüfen wie die CI:
pnpm lint && pnpm typecheck && pnpm test
pnpm test:e2e
pnpm test legt sich eine eigene Datenbank afis_test an und lässt die
Entwicklungsdatenbank in Ruhe: Verlaufseinträge und Protokollzeilen sind nach Regel 2 nicht
löschbar, was ein Test dort anlegt, bliebe für immer stehen.
Betrieb
Gehostet als ein Dockge-Stack. Die .env wird nicht im Repository gepflegt, sondern im
.env-Reiter von Dockge.
| Verwaltung | Dockge, ein Stack namens afis unter /opt/stacks/afis/ |
| Erreichbarkeit | Cloudflare Tunnel, kein offener Port am Router |
| Aktualisieren | Dockge → Stack → Update, Migration läuft automatisch mit |
| Backup | zwei Dauerläufer mit Cron: nächtlicher Dump plus restic offsite |
| Wiederherstellung | scripts/restore-test.sh spielt einen Dump in eine Wegwerf-Datenbank und vergleicht Zeilenzahlen, Regeln und Trigger |