Commit Graph
8 Commits
Author SHA1 Message Date
Harlekin 552f6352bb Eine pg-Version im Baum, @prisma/client im Web-Verzeichnis
Der Entwicklungsserver warnte bei jedem Start: „Package pg can't be external".
Die Ursache lag im Abhängigkeitsbaum - wir pinnten pg auf 8.13.1,
@prisma/adapter-pg verlangt ^8.16.3, also lagen zwei Kopien nebeneinander und
Next konnte pg nicht als extern behandeln. Unser Pin steht jetzt auf 8.23.0,
@types/pg entsprechend.

Damit wurde eine zweite Warnung sichtbar, gleiche Art: der generierte Client
verweist auf @prisma/client/runtime, und Next löst das aus dem
Projektverzeichnis auf - dort lag es aber nicht. @prisma/client ist jetzt auch
in web deklariert.

Beide Warnungen sind nachgeprüft weg, mit einem Dev-Server auf eigenem Port und
zwei Seitenaufrufen.

Nebenbei: Next hatte meine Wegwerf-Verzeichnisse von den Zeitmessungen
(.next-mess1 bis .next-pg2) in die include-Liste von web/tsconfig.json
geschrieben. Zurückgesetzt auf den ursprünglichen Stand.
2026-09-03 22:41:39 +02:00
Harlekin 4ba84049e6 Datenbank- und Worker-Tests auf den Chat in der Akte nachgezogen
Die Regel „eine Nachricht gehört zu genau einem Ort" wird jetzt stärker
geprüft als vorher: zwei Orte, kein Ort und genau einer. Vorher hing sie
allein an der Anwendung, jetzt an einem CHECK.

Der Worker-Test prüft ausdrücklich, dass die Meldung den Namen der Person
nicht nennt.
2026-09-03 15:19:04 +02:00
Harlekin 6af1e437e7 Runde Bedienelemente, Admin zusammengelegt, Kanäle entfallen
Vier der acht Änderungswünsche.

**Knöpfe.** Rund vierzig der siebzig Knöpfe waren blosser Text in Akzentfarbe,
von einem Link nicht zu unterscheiden. Jeder trägt jetzt Rahmen und
Grundfläche, über die Klasse .btn an einer Stelle definiert statt siebzigmal
als Klassenkette. Beim Drücken zieht der Knopf sich zusammen und bekommt einen
inneren Ring - er wirkt kurz dicker, ohne dass die Rahmenbreite sich ändert und
das Umfeld springt; die Rückmeldung setzt ohne Verzögerung ein und klingt
langsam ab, sonst bliebe ein kurzer Klick unsichtbar.

Dazu die neue Regel: Flächen bleiben eckig, alles Bedienbare ist rund. Damit
ist die Rundung eine Auskunft und keine Verzierung.

**Verwaltung.** Rollen, Vorlagen und Änderungsrichtlinie waren drei Einträge in
der Kopfzeile für etwas, das man selten und dann zusammenhängend braucht. Jetzt
ein Eintrag /admin mit Reiterleiste, dazu eine gemeinsame Tabs-Komponente, die
auch der Fallakte zeigt, welcher Reiter offen ist.

**Kanäle.** Ersatzlos entfernt, mitsamt Tabelle. Der Chat gehört dorthin, wo
der Vorgang steht; ein eigener Reiter mit freien Räumen war der Rückweg nach
Discord. Die Personalakte hat jetzt denselben Chat wie die Fallakte - ohne
„als Vermerk übernehmen", denn dafür braucht es einen Verlauf, und Meldungen
gehen nur an Erwähnte, nie in den allgemeinen Kanal: dass es zu jemandem eine
Personalakte gibt, ist selbst eine Auskunft. Ein CHECK erzwingt jetzt, dass
eine Nachricht zu genau einem Ort gehört.

**Kopfzeile.** Suchbalken statt Suchlink, als gewöhnliches Formular ohne ein
Byte im Bündel. Thema, Benachrichtigungen und Abmelden sitzen hinter dem
Profilbild, das von Discord kommt; wer keins hat, bekommt seine Initialen.

Nebenbei: Aktenzeichen in Tests waren nur auf vier Stellen eindeutig und
kollidierten zwischen Läufen, weil Akten nicht löschbar sind.
2026-09-03 15:07:43 +02:00
Harlekin 88c257e793 Phase 6 abgeschlossen: Aktenchat, Kanäle, Echtzeit, Benachrichtigungen
Ein Ereignisstrom mit Themen (/api/stream), in dem der Dateistrom aus Phase 5
aufgegangen ist. Zwei Prüfungen: beim Verbinden, welche Themen der Nutzer
abonnieren darf, und je Ereignis wird der Inhalt mit seinen Rechten geladen.
Das zweite ist nicht nur Vorsicht - eine Umklassifizierung wirkt dadurch in
einer offenen Verbindung sofort. In der pg_notify-Nutzlast steht nie ein
Inhalt, nur Thema, Art und Kennung.

Der Aktenchat sendet optimistisch mit clientseitiger UUIDv7, die zugleich
Idempotenzschlüssel ist. Antworten, vier feste Reaktionen, Bearbeiten mit
sichtbarem editedAt, Zurückziehen mit Platzhalter - Löschen gibt es nicht, und
eine Regel in der Datenbank hält das fest. Eine zurückgezogene Nachricht kommt
ohne Text an, nicht ausgeblendet.

Aktenzeichen im Text werden zu Vorschaukarten, serverseitig aufgelöst. Eine
Akte, die der Leser nicht sehen darf, sieht darin genauso aus wie eine, die es
gar nicht gibt.

"Als Vermerk übernehmen" steht sichtbar an jeder Nachricht. Der Vermerk gehört
dem Verfasser, nicht dem Übernehmenden; ein Systemeintrag nennt den Ursprung,
damit der Wortlaut später nicht für die Akte geschrieben wirkt.

Bei den Discord-Benachrichtigungen entscheidet die Web-App, wer was erfährt,
und der Worker liefert nur aus. Die Alternative hätte geheissen, die
Sichtbarkeitsprüfung im Worker ein zweites Mal zu bauen. In meta steht ein
Titel nur, wenn er hinaus darf - ab Stufe 3 keiner. Postausgang als Tabelle,
angestossen über LISTEN, mit Takt als Sicherheitsnetz.

Nebenbei riss das Budget der Fallakte erneut (122 kB). Drei Schritte zurück
auf 119,6: Chattexte in eine eigene i18n-Datei, ein gemeinsames AddPanel statt
viermal derselbe Aufklapp-Code, und eine eigene UUIDv7 statt des Pakets - die
Sortierbarkeit, auf der die Nachlieferung beruht, steht damit im eigenen Code
und hat Tests.

Unterwegs habe ich NotifyMode und User.notifyMode ein zweites Mal angelegt,
obwohl es beides seit dem Grundgerüst gibt. Die Migration schlug fehl und die
Datenbank musste von Hand aufgeräumt werden; die Lehre steht in docs/13.

155 Tests im Web-Paket, 15 in der Datenbank, 7 im Worker, 32 e2e.
2026-09-02 21:06:03 +02:00
Harlekin 2505664822 Tests schreiben nicht mehr in die Entwicklungsdatenbank
Die Testläufe hatten dort 70 erfundene Rollen, 112 Nutzer und 40 Akten
hinterlassen. Die Rollenverwaltung zeigte deshalb "Gast" siebzehnmal, echte
Rollen doppelt und in willkürlicher Reihenfolge.

Aufräumen allein reicht nicht: CaseEntry, CaseEntryVersion und AuditLog lassen
sich nach harter Regel 2 nicht löschen. Was ein Test dort anlegt, bleibt für
immer stehen - in Phase 2 hätte das die Fallliste unbrauchbar gemacht.

Tests laufen deshalb gegen <datenbank>_test auf demselben Server. pnpm test legt
sie an, migriert und seedet sie vorher. Über TEST_DATABASE_URL lässt sich ein
anderes Ziel setzen.

Dazu die Abschnitte der Rollenverwaltung in der Reihenfolge, in der die
Abteilung ihre Rollen aufzählt: Ränge, Zugehörigkeit, Zugang. Innerhalb eines
Abschnitts entscheidet die Position auf dem Discord-Server, oben die höchste.
Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt wird.
2026-09-01 21:11:40 +02:00
Harlekin e14868b5ea Notfallzugang gilt auch im Worker
Der Voll-Abgleich hat den Administrator alle fünf Minuten hinausgeworfen:
enforceMembership kannte BOOTSTRAP_ADMIN_IDS nicht und sah nur, dass keine
seiner Rollen zur Anmeldung berechtigt. Solange das Rollen-Mapping noch leer
ist, trifft das zwangsläufig zu - der Notfallzugang war damit genau in der
Lage wirkungslos, für die es ihn gibt.

Gefunden beim ersten Lauf gegen den echten Discord-Server, nicht im Test.
2026-09-01 19:24:13 +02:00
Harlekin c6aefb310c Phase 1: Anmeldung, Rechte und Rollenverwaltung
Auth.js übernimmt nur den OAuth-Ablauf mit state und PKCE, Scope identify. Das
Zugangstoken des Nutzers wird danach verworfen: die Rollen holt der Bot mit
seinem eigenen Token. Die Sitzung steht in der Datenbank, im Cookie liegt nur
userId und sessionId. Ohne die Zeile in Session ist das Cookie wertlos, und nur
so lässt sich eine Sitzung zurückziehen.

Wer sich anmelden darf, steckt in lib/auth/signInPolicy.ts statt im Callback:
das ist die Regel, an der alles hängt, und sie gehört einzeln prüfbar. Kein
Guild-Mitglied, kein Zugang. Keine Rolle mit grantsMembership, kein Zugang.
Notfall-Admins umgehen die Rolle, aber nicht die Guild-Mitgliedschaft.

RoleMapping trägt jetzt grantsMembership, category und isPresent. Der Worker
spiegelt jede Rolle der Guild hierher, damit im Adminbereich nichts von Hand
eingetragen werden muss. Gelöschte Rollen werden nicht entfernt, sondern auf
isPresent = false gesetzt: beim Umzug auf den echten Server bleiben sie als
Vorlage stehen.

Der Worker gleicht über Gateway-Ereignisse und zusätzlich alle fünf Minuten
voll ab. Verliert jemand seine AfiS-Rolle, beendet er dessen Sitzungen sofort
und setzt isActive auf false - der Datensatz bleibt, damit Autorenangaben in
alten Akten gültig bleiben. Das passiert im Worker und nicht in der Web-App,
weil es dort erst beim nächsten Request des Betroffenen griffe.

Zwei Widersprüche der Dokumente aufgelöst, festgehalten in
docs/13-entscheidungen.md: der Ausschluss schlägt auch den Admin (das
Codebeispiel in 03-auth-und-rechte.md übergeht ihn, die Reihenfolge eine Zeile
darüber nicht), und versiegelte Akten fallen im Where-Fragment heraus statt in
einer Prüfung danach.

45 Tests, davon die Negativfälle aus docs/03 gegen eine echte Datenbank: Stufe 2
sieht die Stufe-4-Akte nicht und findet sie auch nicht in der Trefferzahl, ein
Ausgeschlossener mit Stufe 5 kommt nicht heran, ein Rollenentzug beendet die
Sitzungen, und ein Ausfall der Discord-API sieht nicht aus wie eine verweigerte
Mitgliedschaft.
2026-09-01 19:11:53 +02:00
Harlekin 8375d1cb03 Worker- und PDF-Dienst
Der Worker hängt am Discord-Gateway und schreibt alle 30 Sekunden sein
Lebenszeichen in die Datenbank, damit /api/health im Web-Container den
Gateway-Zustand kennt, ohne selbst mit Discord zu sprechen. healthcheck.js
prüft genau dieses Lebenszeichen. Der eigentliche Rollen-Abgleich kommt in
Phase 1.

Der PDF-Dienst rendert die Druckansicht über Playwright. Kein PDF-Layout von
Hand, die Gestaltung kommt aus @media print der Seite selbst. Er kennt keine
Rechte: wer darf, entscheidet die Web-App, bevor sie hier anfragt, und schickt
das Sitzungs-Cookie des Abrufers mit, damit die gerenderte Seite dessen
Freigabestufe sieht.
2026-09-01 00:01:49 +02:00