diff --git a/.gitignore b/.gitignore index 91a221a..600c050 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ blob-report/ .DS_Store *.log + diff --git a/README.md b/README.md index 9cb5673..424c54f 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,71 @@ # AfiS-Aktensystem -Fallakten-, Personalakten- und Ermittlungsverwaltung für die Abteilung für innere Sicherheit -eines SCP-RP-Servers. Anmeldung und Berechtigungen laufen vollständig über Discord. +Fall- und Personalaktenverwaltung für die Abteilung für innere Sicherheit eines +SCP-RP-Servers. Anmeldung und Berechtigungen laufen vollständig über Discord. -## Los geht's +Interne Anwendung für eine geschlossene Gruppe von zehn bis fünfzig Personen, selbst +gehostet. Keine Mandantentrennung, keine öffentliche Registrierung. -1. Ordner in ein leeres Git-Repository legen -2. Discord-Application anlegen, Bot einladen, **Server Members Intent aktivieren** -3. Claude Code im Projektordner starten +## Was es kann -Gehostet wird als **ein Dockge-Stack** auf dem Homeserver. Die fertige `compose.yaml` und die -Einrichtungsschritte stehen in `docs/11-betrieb.md`. Die `.env` wird nicht im Repository -gepflegt, sondern im `.env`-Reiter von Dockge; `.env.example` ist nur die Vorlage. +- **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 -Claude Code liest `CLAUDE.md` von selbst. Die Fachdokumente liegen in `docs/`, der Bauplan -in `TASKS.md`. +## Zusicherungen -## Erste Anweisung an Claude Code +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. -> Lies CLAUDE.md und alle Dateien in docs/. Fass in fünf Sätzen zusammen, was gebaut wird, -> und nenn mir alles, was in den Dokumenten widersprüchlich oder unklar ist, bevor du -> anfängst. Baue noch nichts. - -Danach Phase für Phase aus `TASKS.md`. Siehe `docs/12-arbeiten-mit-claude-code.md`. +1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals + alle Datensätze laden und danach filtern. +2. **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. +3. **Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen + Protokolleintrag.** +4. **Keine Dateiendungen als Typprüfung.** Immer Magic Bytes. Kein SVG, kein HTML, keine + Archive. +5. **Keine Blockade-Spinner.** Jede Ansicht hat ein Skelett, jede Änderung eine sofortige + Rückmeldung. +6. **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. +7. **Keine Geheimnisse im Repository.** Alles über `.env`, Vorlage in `.env.example`. +8. **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 ``` -CLAUDE.md Projektanweisung, wird automatisch gelesen -TASKS.md Bauplan in neun Phasen -docs/ Fachdokumente, dazu 13-entscheidungen.md -compose.yaml Der Dockge-Stack -.env.example Vorlage für den .env-Reiter in Dockge - -web/ Next.js 15, App Router -worker/ discord.js, Rollen-Abgleich -pdf/ Playwright, PDF-Erzeugung +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. @@ -66,9 +92,13 @@ pnpm test:e2e ``` `pnpm test` legt sich eine eigene Datenbank `afis_test` an und lässt die -Entwicklungsdatenbank in Ruhe. Grund in `docs/13-entscheidungen.md`. +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 kurz gefasst +## Betrieb + +Gehostet als **ein** Dockge-Stack. Die `.env` wird nicht im Repository gepflegt, sondern im +`.env`-Reiter von Dockge. | | | |---|---| @@ -76,3 +106,4 @@ Entwicklungsdatenbank in Ruhe. Grund in `docs/13-entscheidungen.md`. | 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 | diff --git a/packages/db/prisma/migrations/20260902160000_personalakten_und_dokumente/migration.sql b/packages/db/prisma/migrations/20260902160000_personalakten_und_dokumente/migration.sql index 91c9018..0853df6 100644 --- a/packages/db/prisma/migrations/20260902160000_personalakten_und_dokumente/migration.sql +++ b/packages/db/prisma/migrations/20260902160000_personalakten_und_dokumente/migration.sql @@ -139,7 +139,7 @@ ALTER TABLE "DocumentSectionVersion" ADD CONSTRAINT "DocumentSectionVersion_sect -- -- Dieselbe Zusicherung wie beim Verlaufseintrag: kein DELETE, weder über -- die Anwendung noch über rohes SQL. Wer ein Dokument loswerden will, zieht --- es zurück; der Grabstein bleibt sichtbar. Siehe CLAUDE.md, harte Regel 2. +-- es zurück; der Grabstein bleibt sichtbar. Siehe Zusicherung 2 der README. -- =========================================================================== CREATE RULE document_no_delete AS ON DELETE TO "Document" DO INSTEAD NOTHING; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 7ce773a..b5ef069 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -512,7 +512,7 @@ model PersonnelFile { } /// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind -/// Daten, keine Oberflaechentexte — siehe CLAUDE.md, Sprachkonvention. +/// Daten, keine Oberflaechentexte — siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch. model PersonnelStatusTag { key String @id // "tot", "gekuendigt" name String // "Tot", "Gekuendigt" @@ -573,7 +573,7 @@ model FoundationClearance { /// eine CHECK-Bedingung in der Migration. /// /// Anhaengend wie ein Verlaufseintrag: es gibt kein DELETE, eine Zuruecknahme -/// laesst einen Grabstein stehen. Siehe CLAUDE.md, harte Regel 2. +/// laesst einen Grabstein stehen. Siehe Zusicherung 2 der README. model Document { id String @id @default(uuid(7)) documentNumber String @unique // SAV-2026-0007 @@ -821,7 +821,7 @@ model RecentView { /// Eine Tabelle statt eines direkten Aufrufs, weil der Worker ein eigener /// Prozess ist und laufen oder auch gerade neu starten kann. Ein Eintrag, der /// nicht rausging, bleibt liegen und wird beim naechsten Durchlauf abgeholt. -/// Kein Einmal-Container, siehe CLAUDE.md, harte Regel 8. +/// Kein Einmal-Container, siehe Zusicherung 8 der README. /// /// **Hier steht nie ein Inhalt.** Nur Kennungen und die Art des Ereignisses; /// den Text baut der Worker daraus, und Discord kennt unsere Klassifizierung diff --git a/packages/db/scripts/prepare-test-db.mjs b/packages/db/scripts/prepare-test-db.mjs index 936fa55..11f0f1b 100644 --- a/packages/db/scripts/prepare-test-db.mjs +++ b/packages/db/scripts/prepare-test-db.mjs @@ -10,7 +10,7 @@ import { resolveTestDatabaseUrl } from '../dist/testDatabase.js' * Wird von `pnpm test` vorgeschaltet. * * **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und - * Protokollzeilen sind nach harter Regel 2 aus CLAUDE.md nicht löschbar — was + * Protokollzeilen sind nach Zusicherung 2 der README nicht löschbar — was * ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen * standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge * begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche diff --git a/packages/db/src/testDatabase.ts b/packages/db/src/testDatabase.ts index b4b08c2..ea0de39 100644 --- a/packages/db/src/testDatabase.ts +++ b/packages/db/src/testDatabase.ts @@ -3,7 +3,7 @@ * Entwicklung. * * Der Grund ist nicht Ordnungsliebe: `CaseEntry`, `CaseEntryVersion` und - * `AuditLog` lassen sich nach Regel 2 aus CLAUDE.md nicht löschen. Was ein Test + * `AuditLog` lassen sich nach Zusicherung 2 der README nicht löschen. Was ein Test * dort anlegt, bleibt für immer stehen. In einer gemeinsamen Datenbank * verstopfen die Testakten binnen weniger Läufe die Fallliste, und aufräumen * lässt sich das nicht mehr. diff --git a/packages/db/tests/regeln.test.ts b/packages/db/tests/regeln.test.ts index 94ae9aa..f5dfb05 100644 --- a/packages/db/tests/regeln.test.ts +++ b/packages/db/tests/regeln.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createPrismaClient, type PrismaClient } from '../src/index.js' /** - * Prüft die harten Regeln aus CLAUDE.md dort, wo sie wirklich gelten: in der + * Prüft die harten Regeln der README dort, wo sie wirklich gelten: in der * Datenbank. Die Anwendung kann man umgehen, diese Regeln nicht. * * Läuft gegen eine echte Postgres-Instanz. Ohne DATABASE_URL wird übersprungen. diff --git a/web/app/(app)/admin/protokoll/page.tsx b/web/app/(app)/admin/protokoll/page.tsx index dfbde18..75b3cbd 100644 --- a/web/app/(app)/admin/protokoll/page.tsx +++ b/web/app/(app)/admin/protokoll/page.tsx @@ -13,7 +13,7 @@ type Params = Record * Das Zugriffsprotokoll. * * Es beantwortet die eine Frage, die eine Abteilungsleitung an ein Aktensystem - * hat: **wer hat was wo getan.** Nach harter Regel 3 aus CLAUDE.md entsteht + * hat: **wer hat was wo getan.** Nach Zusicherung 3 der README entsteht * die Spur bei jeder schreibenden Aktion und bei jedem Lesezugriff ab Stufe 3, * ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken. * diff --git a/web/components/Pending.tsx b/web/components/Pending.tsx index 0a66823..7366110 100644 --- a/web/components/Pending.tsx +++ b/web/components/Pending.tsx @@ -9,7 +9,7 @@ import { useLinkStatus } from 'next/link' * sei der Klick nicht angekommen — man klickt ein zweites Mal. Der Punkt sitzt * **im geklickten Link**, damit sichtbar bleibt, wohin es geht. * - * Das ist kein Blockade-Spinner im Sinn von Regel 5 aus CLAUDE.md: er ersetzt + * Das ist kein Blockade-Spinner im Sinn von Zusicherung 5 der README: er ersetzt * keinen Inhalt und hält nichts auf. Die Zielseite bringt ihr Skelett mit, das * ist die eigentliche Antwort auf das Warten — hier geht es nur darum, den * Klick zu quittieren. diff --git a/web/components/StatusTags.tsx b/web/components/StatusTags.tsx index e0ca1ce..e4d5da6 100644 --- a/web/components/StatusTags.tsx +++ b/web/components/StatusTags.tsx @@ -6,7 +6,7 @@ import { colorVariable } from '@/components/StampMark' * Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus. * Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im * Adminbereich pflegbar — deshalb werden sie hereingereicht und nicht aus - * `lib/i18n/de.ts` gelesen. Siehe CLAUDE.md, Sprachkonvention. + * `lib/i18n/de.ts` gelesen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch. */ export function StatusTags({ tags, diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index effef50..280e738 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -10,7 +10,7 @@ export default [ { rules: { // Sprachkonvention: Oberflaechentexte gehoeren nach lib/i18n/de.ts, - // nie hart ins JSX. Siehe CLAUDE.md. + // nie hart ins JSX. Siehe README. '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, diff --git a/web/lib/audit/query.ts b/web/lib/audit/query.ts index 6304edc..3c67363 100644 --- a/web/lib/audit/query.ts +++ b/web/lib/audit/query.ts @@ -6,7 +6,7 @@ import type { Prisma } from '@afis/db' * * Es ist die einzige Stelle im System, die vollständig Auskunft gibt: wer hat * wann was getan, und bei Stufe 3 aufwärts auch, wer was nur gelesen hat. Nach - * harter Regel 3 aus CLAUDE.md entsteht diese Spur bei jeder schreibenden + * Zusicherung 3 der README entsteht diese Spur bei jeder schreibenden * Aktion, ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken. * * Lesen darf sie **nur die Abteilungsleitung**. Das ist keine Bequemlichkeit, diff --git a/web/lib/departments/entries.ts b/web/lib/departments/entries.ts index b95cce6..7865f22 100644 --- a/web/lib/departments/entries.ts +++ b/web/lib/departments/entries.ts @@ -10,7 +10,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions' /** * Vermerke an einer Abteilung. * - * Harte Regel 2 aus CLAUDE.md gilt hier genauso wie im Verlauf einer Akte: + * Zusicherung 2 der README gilt hier genauso wie im Verlauf einer Akte: * **es gibt kein Löschen.** Jede Änderung legt den vorherigen Stand als * Version ab, jede Zurücknahme lässt einen Grabstein stehen, und beides sieht * jeder, der den Vermerk sehen darf. Die Regeln stehen in der Datenbank diff --git a/web/lib/documents/service.ts b/web/lib/documents/service.ts index 4042b01..b5f46a0 100644 --- a/web/lib/documents/service.ts +++ b/web/lib/documents/service.ts @@ -363,7 +363,7 @@ export async function finalizeDocument( /** * Zurückziehen. Es gibt kein Löschen: das Dokument bleibt als Grabstein - * stehen, mit Datum, Person und Begründung. Siehe CLAUDE.md, harte Regel 2. + * stehen, mit Datum, Person und Begründung. Siehe Zusicherung 2 der README. */ export async function retractDocument( p: EffectivePermissions, diff --git a/web/lib/files/magic.ts b/web/lib/files/magic.ts index 4255c32..26e9dc4 100644 --- a/web/lib/files/magic.ts +++ b/web/lib/files/magic.ts @@ -2,7 +2,7 @@ * Typprüfung über Magic Bytes. * * **Die Endung wird nie angesehen**, und der vom Browser gemeldete Typ nur, um - * ihn gegen den erkannten zu prüfen. Siehe CLAUDE.md, harte Regel 4. + * ihn gegen den erkannten zu prüfen. Siehe Zusicherung 4 der README. * * Bewusst eine eigene Signaturtabelle statt `file-type`: die Liste der * erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird diff --git a/web/lib/i18n/de.ts b/web/lib/i18n/de.ts index b9d9077..4c4f3e8 100644 --- a/web/lib/i18n/de.ts +++ b/web/lib/i18n/de.ts @@ -1,5 +1,5 @@ /** - * Alle Oberflaechentexte. Nie deutsche Strings hart ins JSX, siehe CLAUDE.md. + * Alle Oberflaechentexte. Nie deutsche Strings hart ins JSX, siehe README. * Die Begriffe folgen dem Glossar in docs/01-produkt.md. * * Schreibweise nach docs/10-design-system.md: diff --git a/web/lib/i18n/departments.ts b/web/lib/i18n/departments.ts index dde8656..cbb7996 100644 --- a/web/lib/i18n/departments.ts +++ b/web/lib/i18n/departments.ts @@ -1,7 +1,7 @@ /** * Abteilungen. Die Bezeichnungen der Abteilungen selbst stehen **nicht** hier: * das sind Daten, im Adminbereich änderbar, so wie die Namen der - * Freigabestufen. Siehe CLAUDE.md, Sprachkonvention. + * Freigabestufen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch. */ export const departmentText = { heading: 'Abteilungen', diff --git a/web/tests/i18n.test.ts b/web/tests/i18n.test.ts index 1c33c71..9d9b734 100644 --- a/web/tests/i18n.test.ts +++ b/web/tests/i18n.test.ts @@ -19,7 +19,7 @@ describe('Sprachkonvention', () => { it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => { // Umlaute und Eszett sind der billigste verlaessliche Marker fuer einen - // deutschen Text. Alles Sichtbare gehoert nach lib/i18n/de.ts, siehe CLAUDE.md. + // deutschen Text. Alles Sichtbare gehoert nach lib/i18n/de.ts, siehe README. // // Geprueft werden Komponenten und Seiten (.tsx) sowie Server Actions: // deren Fehlermeldungen landen in der Oberflaeche. Kommentare zaehlen diff --git a/web/tests/protokoll.test.ts b/web/tests/protokoll.test.ts index cd0c2d9..b9c5cd0 100644 --- a/web/tests/protokoll.test.ts +++ b/web/tests/protokoll.test.ts @@ -117,7 +117,7 @@ suite('Zugriffsprotokoll', () => { where: { actorId: akteur, action: 'case.create' }, }) - // Harte Regel 3 aus CLAUDE.md steht als Regel in der Datenbank, nicht nur + // Zusicherung 3 der README steht als Regel in der Datenbank, nicht nur // in der Anwendung: das Löschen läuft ins Leere statt zu wirken. await db.auditLog.deleteMany({ where: { id: zeile.id } }) const danach = await db.auditLog.findUnique({ where: { id: zeile.id } }) diff --git a/worker/src/notify.ts b/worker/src/notify.ts index 66153f0..7ee8c2d 100644 --- a/worker/src/notify.ts +++ b/worker/src/notify.ts @@ -12,7 +12,7 @@ import type { PrismaClient } from '@afis/db' * * Der Dauerläufer holt in zwei Takten: sofort, wenn `notification_queued` * kommt, und alle 30 Sekunden als Sicherheitsnetz. Kein Einmal-Container, - * siehe CLAUDE.md, harte Regel 8. + * siehe Zusicherung 8 der README. */ /** Nach so vielen Fehlversuchen wird eine Zeile liegengelassen. */