README erklärt das Projekt, Zusicherungen unter Nummern
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s

Die README beschrieb das Vorgehen statt der Anwendung. Sie erklärt jetzt, was
das System kann, wie es aufgebaut ist und wie man es lokal und im Betrieb
fährt.

Dazu tragen die acht Zusicherungen dort ihre Nummern — Berechtigungsprüfung in
der Abfrage, kein DELETE im Verlauf, Protokollpflicht ab Stufe 3, Magic Bytes
statt Dateiendungen, Skelette statt Spinner, Existenz als Information, keine
Geheimnisse im Repository, keine Einmal-Container. Achtzehn Dateien verwiesen
für ebendiese Regeln auf eine Datei, die nicht mehr mitverfolgt wird; sie
zeigen jetzt auf die README.

Arbeitsunterlagen bleiben auf der Platte, werden aber nicht mehr verfolgt. Die
Ausschlussregel steht in .git/info/exclude statt in .gitignore, damit sie nicht
selbst im Repository steht.
This commit is contained in:
Harlekin
2026-09-04 08:42:07 +02:00
parent d7fdbb54f4
commit 319ef6a997
20 changed files with 80 additions and 48 deletions
+1
View File
@@ -27,3 +27,4 @@ blob-report/
.DS_Store .DS_Store
*.log *.log
+59 -28
View File
@@ -1,45 +1,71 @@
# AfiS-Aktensystem # AfiS-Aktensystem
Fallakten-, Personalakten- und Ermittlungsverwaltung für die Abteilung für innere Sicherheit Fall- und Personalaktenverwaltung für die Abteilung für innere Sicherheit eines
eines SCP-RP-Servers. Anmeldung und Berechtigungen laufen vollständig über Discord. 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 ## Was es kann
2. Discord-Application anlegen, Bot einladen, **Server Members Intent aktivieren**
3. Claude Code im Projektordner starten
Gehostet wird als **ein Dockge-Stack** auf dem Homeserver. Die fertige `compose.yaml` und die - **Fallakten** mit Kopfdaten, Abschnitten nach Vorlage und einem Verlauf, der nur wächst
Einrichtungsschritte stehen in `docs/11-betrieb.md`. Die `.env` wird nicht im Repository - **Personalakten**, überwiegend für Externe, mit Aktenblatt zum Ausdrucken
gepflegt, sondern im `.env`-Reiter von Dockge; `.env.example` ist nur die Vorlage. - **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 ## Zusicherungen
in `TASKS.md`.
## 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, 1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals
> und nenn mir alles, was in den Dokumenten widersprüchlich oder unklar ist, bevor du alle Datensätze laden und danach filtern.
> anfängst. Baue noch nichts. 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
Danach Phase für Phase aus `TASKS.md`. Siehe `docs/12-arbeiten-mit-claude-code.md`. 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 ## Aufbau
``` ```
CLAUDE.md Projektanweisung, wird automatisch gelesen web/ Next.js 15, App Router, React 19
TASKS.md Bauplan in neun Phasen worker/ discord.js, gleicht Rollen und Mitgliedschaft ab
docs/ Fachdokumente, dazu 13-entscheidungen.md pdf/ Playwright, erzeugt die PDF-Ausdrucke
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
packages/db/ Prisma-Schema, Migrationen, Seed packages/db/ Prisma-Schema, Migrationen, Seed
docker/ Dockerfiles für web, worker und pdf 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 ## Lokal entwickeln
Node 22 und pnpm 9. 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 `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 | | Erreichbarkeit | Cloudflare Tunnel, kein offener Port am Router |
| Aktualisieren | Dockge → Stack → Update, Migration läuft automatisch mit | | Aktualisieren | Dockge → Stack → Update, Migration läuft automatisch mit |
| Backup | zwei Dauerläufer mit Cron: nächtlicher Dump plus restic offsite | | 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 |
@@ -139,7 +139,7 @@ ALTER TABLE "DocumentSectionVersion" ADD CONSTRAINT "DocumentSectionVersion_sect
-- --
-- Dieselbe Zusicherung wie beim Verlaufseintrag: kein DELETE, weder über -- Dieselbe Zusicherung wie beim Verlaufseintrag: kein DELETE, weder über
-- die Anwendung noch über rohes SQL. Wer ein Dokument loswerden will, zieht -- 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; CREATE RULE document_no_delete AS ON DELETE TO "Document" DO INSTEAD NOTHING;
+3 -3
View File
@@ -512,7 +512,7 @@ model PersonnelFile {
} }
/// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind /// 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 { model PersonnelStatusTag {
key String @id // "tot", "gekuendigt" key String @id // "tot", "gekuendigt"
name String // "Tot", "Gekuendigt" name String // "Tot", "Gekuendigt"
@@ -573,7 +573,7 @@ model FoundationClearance {
/// eine CHECK-Bedingung in der Migration. /// eine CHECK-Bedingung in der Migration.
/// ///
/// Anhaengend wie ein Verlaufseintrag: es gibt kein DELETE, eine Zuruecknahme /// 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 { model Document {
id String @id @default(uuid(7)) id String @id @default(uuid(7))
documentNumber String @unique // SAV-2026-0007 documentNumber String @unique // SAV-2026-0007
@@ -821,7 +821,7 @@ model RecentView {
/// Eine Tabelle statt eines direkten Aufrufs, weil der Worker ein eigener /// Eine Tabelle statt eines direkten Aufrufs, weil der Worker ein eigener
/// Prozess ist und laufen oder auch gerade neu starten kann. Ein Eintrag, der /// Prozess ist und laufen oder auch gerade neu starten kann. Ein Eintrag, der
/// nicht rausging, bleibt liegen und wird beim naechsten Durchlauf abgeholt. /// 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; /// **Hier steht nie ein Inhalt.** Nur Kennungen und die Art des Ereignisses;
/// den Text baut der Worker daraus, und Discord kennt unsere Klassifizierung /// den Text baut der Worker daraus, und Discord kennt unsere Klassifizierung
+1 -1
View File
@@ -10,7 +10,7 @@ import { resolveTestDatabaseUrl } from '../dist/testDatabase.js'
* Wird von `pnpm test` vorgeschaltet. * Wird von `pnpm test` vorgeschaltet.
* *
* **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und * **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 * 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 * 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 * begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche
+1 -1
View File
@@ -3,7 +3,7 @@
* Entwicklung. * Entwicklung.
* *
* Der Grund ist nicht Ordnungsliebe: `CaseEntry`, `CaseEntryVersion` und * 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 * dort anlegt, bleibt für immer stehen. In einer gemeinsamen Datenbank
* verstopfen die Testakten binnen weniger Läufe die Fallliste, und aufräumen * verstopfen die Testakten binnen weniger Läufe die Fallliste, und aufräumen
* lässt sich das nicht mehr. * lässt sich das nicht mehr.
+1 -1
View File
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createPrismaClient, type PrismaClient } from '../src/index.js' 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. * Datenbank. Die Anwendung kann man umgehen, diese Regeln nicht.
* *
* Läuft gegen eine echte Postgres-Instanz. Ohne DATABASE_URL wird übersprungen. * Läuft gegen eine echte Postgres-Instanz. Ohne DATABASE_URL wird übersprungen.
+1 -1
View File
@@ -13,7 +13,7 @@ type Params = Record<string, string | string[] | undefined>
* Das Zugriffsprotokoll. * Das Zugriffsprotokoll.
* *
* Es beantwortet die eine Frage, die eine Abteilungsleitung an ein Aktensystem * 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, * die Spur bei jeder schreibenden Aktion und bei jedem Lesezugriff ab Stufe 3,
* ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken. * ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken.
* *
+1 -1
View File
@@ -9,7 +9,7 @@ import { useLinkStatus } from 'next/link'
* sei der Klick nicht angekommen — man klickt ein zweites Mal. Der Punkt sitzt * sei der Klick nicht angekommen — man klickt ein zweites Mal. Der Punkt sitzt
* **im geklickten Link**, damit sichtbar bleibt, wohin es geht. * **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 * 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 * ist die eigentliche Antwort auf das Warten — hier geht es nur darum, den
* Klick zu quittieren. * Klick zu quittieren.
+1 -1
View File
@@ -6,7 +6,7 @@ import { colorVariable } from '@/components/StampMark'
* Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus. * Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus.
* Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im * Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im
* Adminbereich pflegbar — deshalb werden sie hereingereicht und nicht aus * 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({ export function StatusTags({
tags, tags,
+1 -1
View File
@@ -10,7 +10,7 @@ export default [
{ {
rules: { rules: {
// Sprachkonvention: Oberflaechentexte gehoeren nach lib/i18n/de.ts, // 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': [ '@typescript-eslint/no-unused-vars': [
'error', 'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
+1 -1
View File
@@ -6,7 +6,7 @@ import type { Prisma } from '@afis/db'
* *
* Es ist die einzige Stelle im System, die vollständig Auskunft gibt: wer hat * 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 * 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. * Aktion, ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken.
* *
* Lesen darf sie **nur die Abteilungsleitung**. Das ist keine Bequemlichkeit, * Lesen darf sie **nur die Abteilungsleitung**. Das ist keine Bequemlichkeit,
+1 -1
View File
@@ -10,7 +10,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
/** /**
* Vermerke an einer Abteilung. * 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 * **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 * 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 * jeder, der den Vermerk sehen darf. Die Regeln stehen in der Datenbank
+1 -1
View File
@@ -363,7 +363,7 @@ export async function finalizeDocument(
/** /**
* Zurückziehen. Es gibt kein Löschen: das Dokument bleibt als Grabstein * 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( export async function retractDocument(
p: EffectivePermissions, p: EffectivePermissions,
+1 -1
View File
@@ -2,7 +2,7 @@
* Typprüfung über Magic Bytes. * Typprüfung über Magic Bytes.
* *
* **Die Endung wird nie angesehen**, und der vom Browser gemeldete Typ nur, um * **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 * Bewusst eine eigene Signaturtabelle statt `file-type`: die Liste der
* erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird * erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird
+1 -1
View File
@@ -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. * Die Begriffe folgen dem Glossar in docs/01-produkt.md.
* *
* Schreibweise nach docs/10-design-system.md: * Schreibweise nach docs/10-design-system.md:
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Abteilungen. Die Bezeichnungen der Abteilungen selbst stehen **nicht** hier: * Abteilungen. Die Bezeichnungen der Abteilungen selbst stehen **nicht** hier:
* das sind Daten, im Adminbereich änderbar, so wie die Namen der * 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 = { export const departmentText = {
heading: 'Abteilungen', heading: 'Abteilungen',
+1 -1
View File
@@ -19,7 +19,7 @@ describe('Sprachkonvention', () => {
it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => { it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => {
// Umlaute und Eszett sind der billigste verlaessliche Marker fuer einen // 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: // Geprueft werden Komponenten und Seiten (.tsx) sowie Server Actions:
// deren Fehlermeldungen landen in der Oberflaeche. Kommentare zaehlen // deren Fehlermeldungen landen in der Oberflaeche. Kommentare zaehlen
+1 -1
View File
@@ -117,7 +117,7 @@ suite('Zugriffsprotokoll', () => {
where: { actorId: akteur, action: 'case.create' }, 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. // in der Anwendung: das Löschen läuft ins Leere statt zu wirken.
await db.auditLog.deleteMany({ where: { id: zeile.id } }) await db.auditLog.deleteMany({ where: { id: zeile.id } })
const danach = await db.auditLog.findUnique({ where: { id: zeile.id } }) const danach = await db.auditLog.findUnique({ where: { id: zeile.id } })
+1 -1
View File
@@ -12,7 +12,7 @@ import type { PrismaClient } from '@afis/db'
* *
* Der Dauerläufer holt in zwei Takten: sofort, wenn `notification_queued` * Der Dauerläufer holt in zwei Takten: sofort, wenn `notification_queued`
* kommt, und alle 30 Sekunden als Sicherheitsnetz. Kein Einmal-Container, * 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. */ /** Nach so vielen Fehlversuchen wird eine Zeile liegengelassen. */