Commit Graph
9 Commits
Author SHA1 Message Date
Harlekin 42dbf0f6b1 Anhaenge zurueckziehen statt sie liegen zu lassen
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
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.
2026-09-04 16:18:33 +02:00
Harlekin abf42c5187 Ranggefuege: Rollen, die aufeinander aufbauen
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
Bisher trug jede Rolle ihre Rechte vollstaendig selbst. Bei achtzehn Rollen
und zweiundzwanzig Rechten heisst das, dass eine hoehere Rangrolle die ganze
Liste der darunterliegenden noch einmal abhaken muss - beim Anlegen
fehleranfaellig und bei jeder spaeteren Aenderung doppelt zu pflegen.

Ein Gefuege ist eine benannte, geordnete Folge von Rollen. Jede Sprosse
erbt alles von jeder Sprosse unter ihr, verwaltet wird nur noch das, was
oben dazukommt. Geerbt wird nicht nur die Rechteliste, sondern auch
Freigabestufe und Anmeldung: sonst waere eine Befoerderung unter Umstaenden
eine Herabstufung, naemlich immer dann, wenn Discord beim Befoerdern die
alte Rangrolle abnimmt.

Die Ordnung ist bewusst eine Reihenfolge und kein Zeiger je Rolle - ueber
ganzen Zahlen gibt es keinen Zyklus, den man abfangen muesste. Aufloesen
eines Gefueges laesst die Rollen stehen und nimmt nur das Geerbte weg.

Die Rechnung steht als reine Funktion in lib/auth/ladders.ts und wird an
drei Stellen benutzt: bei der Rechteaufloesung, in der Verwaltung und in
der Vorschau des Editors, die im Browser mitrechnet. Eine Vorschau, die den
Stand vor dem Verschieben zeigt, waere schlimmer als keine.

Die Rollenseite zeigt Geerbtes kursiv und gesperrt an, damit dort niemand
blind Kaestchen setzt, die laengst gesetzt sind.

Nebenbei: Aenderungen an der Rollenzuordnung landen jetzt im Protokoll, mit
Vorher und Nachher. Wer Rollen aendert, aendert die Rechte anderer - genau
dafuer ist es da.
2026-09-04 14:53:29 +02:00
Harlekin e0b03ea336 Gedankenstriche durch Bindestriche ersetzt
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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00
Harlekin e21e478fd1 Vorlagen-JSON abweisen statt still verwerfen, Platzhalter ohne Prototypkette
Durchgesehen, was mit Text passiert, der nicht als Text ankommt. Die Grundlage
war schon in Ordnung: genau ein dangerouslySetInnerHTML mit einer Konstanten,
kein eval, kein $queryRawUnsafe, keine Weiterleitung aus einem Adressparameter,
Dateien mit nosniff und sandbox-CSP als Anhang statt inline.

Drei Ränder waren offen.

**Vorlagen.** Das zod-Schema hatte einen festen Satz Feldarten und speicherte
das Geprüfte, verwarf unbekannte Schlüssel aber stillschweigend. Mit
strictObject fällt ein `typ` statt `type` auf, statt eine Vorlage zu
hinterlassen, die nicht tut, was dasteht.

**Platzhalter lasen über die Prototypkette.** Beim Durchprobieren gefunden:
`constructor` ist ein gültiger Feldschlüssel, die Schlüsselregel erlaubt
Kleinbuchstaben. Ein Baustein mit {{constructor}} bekam den Quelltext der
geerbten Funktion in den Text geschrieben - kein ausgeführter Code, aber
Innenleben in einem Dokument, das weitergereicht wird. Jetzt Object.hasOwn.

**Farbmarken aus der Datenbank** landen als Name einer CSS-Variablen in einem
style-Attribut. Heute setzt das nur der Seed, und eine zweite Deklaration liesse
sich darüber ohnehin nicht einschleusen - aber der Kommentar sagt „im
Adminbereich pflegbar", und was aus der Datenbank in ein Stilattribut geht,
wird geprüft.

Dreizehn Prüfungen, die es mit echten Versuchen probieren. Bösartiger Text
bleibt dabei ausdrücklich erlaubt: eine Feldbeschriftung darf
<script>alert(1)</script> heissen und sieht dann genau so aus. Verboten ist,
dass daraus ein Element wird.

Nebenbei: eine Vorlagenänderung stand im Zugriffsprotokoll als roles.update.
2026-09-03 23:52:53 +02:00
Harlekin aef3df0d44 Abteilungen als Ebene über der Personalakte
Punkt 3 der Wünsche.

Die Abteilung stand als Freitext in der Personalakte. Damit liess sich nichts
fragen: „SHD", „Shd" und „Sicherheitsdienst" waren drei Abteilungen. Sie ist
jetzt ein eigenes Modell, im Adminbereich verwaltbar, und die drei vorhandenen
Freitexte sind in der Migration zu Abteilungen geworden.

**Eine Fallakte trägt keine Abteilung.** Sie gehört zu einer, wenn eine
beteiligte Person ihr angehört - abgeleitet über CaseParticipant und die
Personalakte. Eine eingetragene Zuordnung wäre nach zwei Wochen falsch: Leute
wechseln, Beteiligte kommen dazu, und niemand pflegt das nach. Dasselbe gilt
für die Dokumente: ein Dokument unmittelbar an der Abteilung wäre halb
verdrahtet, weil die Vorlagen über TemplateUse nur Fall- und Personalakte
kennen.

Die Abteilungsseite zeigt Mitglieder, abgeleitete Fälle und Dokumente,
Vermerke, Chat und ein Kreisdiagramm der Fallarten; die Übersicht eines über
alle Abteilungen. Das Diagramm ist serverseitig gerendertes SVG - eine
Bibliothek wäre das Teuerste auf der Seite gewesen. Die Segmente sind
Abstufungen von --steel und nicht die Stufenfarben: ein Tortenstück in Oxidrot
würde als „streng geheim" gelesen.

Die Zusammenfassung ist die gefährliche Stelle. Eine Abteilungsseite fasst
zusammen, was einzeln längst geschützt ist, und eine Zusammenfassung, die nicht
filtert, verrät mehr als jede Einzelansicht. Deshalb läuft hier jede Liste und
jede Zahl durch dieselben Fragmente wie überall; ein Test prüft, dass die Summe
der Diagrammanteile mit der Länge der Liste übereinstimmt. Wo jemand
Personalakten gar nicht sehen darf, steht null und keine Null.

Vermerke erben Regel 2 unverändert: kein DELETE, Version bei jeder Änderung,
Grabstein bei jeder Zurücknahme, als Regel und Trigger in der Datenbank. Ohne
Korrekturkette, Anhänge und Karenzzeit - was Beweiswert hat, gehört in eine
Fallakte.

Nebenbei: das Zugriffsprotokoll hängt jetzt am dafür vorgesehenen Recht
audit.read statt an admin.
2026-09-03 19:57:28 +02:00
Harlekin 89edaa6ae1 Protokoll im Adminbereich, ein Knopf für das ganze Blatt
Die restlichen zwei Änderungswünsche.

**Protokoll.** „Wer was wo macht" stand seit dem ersten Tag in AuditLog -
harte Regel 3 sorgt dafür. Was fehlte, war die Ansicht darauf. /admin/protokoll
ist genau das und keine zweite Aufzeichnung: filterbar nach Person, Aktion,
Art und Zeitraum, rückwärts blätternd.

Nur für die Leitung. Eine Zeile nennt Aktenzeichen und Namen, und schon die
Existenz einer Zeile verrät, dass es einen Vorgang gibt; ein gefiltertes
Protokoll für alle wäre ein Weg, die Sichtbarkeitsregeln von hinten
aufzurollen. Aus demselben Grund werden Kennungen ohne Sichtbarkeitsfilter zu
Aktenzeichen aufgelöst - ein Protokoll aus UUIDs liest niemand, und eines, das
niemand liest, ist keine Kontrolle.

**Speichern.** Jedes Feld trug seinen eigenen Knopf: ein Arbeitsvertrag mit
zwölf Feldern hiess zwölfmal speichern. Der Knopf steht jetzt am Blatt.

Geschrieben wird trotzdem je Feld und nicht als Transaktion, und das folgt aus
dem Datenmodell: Abschnitte sind einzeln versioniert, weil mehrere Leute
gleichzeitig an einer Akte arbeiten. Als Transaktion blockierte ein einziges
zwischenzeitlich geändertes Feld das ganze Blatt - elf gute Änderungen verloren,
weil jemand das zwölfte angefasst hat. So geht durch, was durchgeht, und die
Rückmeldung sagt, was offen blieb und wo. Lichtbilder bleiben ausgenommen: ein
Upload mit Prüfkette ist keine Feldänderung.

lib/audit.ts liegt jetzt als lib/audit/write.ts neben lib/audit/query.ts.
2026-09-03 15:16:10 +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 addef20e16 Phase 9 abgeschlossen: Übersicht, Beobachtungsliste, helles Thema, Messung
Die Übersicht beantwortet vier Fragen, die man morgens hat: woran arbeite ich,
was ist fällig, wo hat jemand geschrieben, was hat sich getan. Jede Liste
läuft durch visibleCaseWhere - das ist der Ort, an dem eine unachtsame
Abfrage am ehesten eine Akte verrät. Ein Test prüft jede einzeln gegen eine zu
hoch eingestufte Akte. Der bisherige Inhalt ("Dein Zugang") steht jetzt unter
/zugang.

Beobachten ist eine Merkliste, kein Recht: nur was sichtbar ist, lässt sich
merken, und steigt eine Akte später über die eigene Stufe, verschwindet sie
aus der Liste, ohne dass jemand aufräumt.

Das helle Thema tauscht nur die semantische Ebene; die Grundpalette bleibt.
Die Klassifizierungsfarben sind für hellen Grund eine Spur dunkler, im Farbton
aber dieselben - eine Stufe muss in beiden Themen wiedererkennbar sein. Drei
Zustände, "wie das System" ist der Auslieferungszustand, und ein Skript im
Kopf setzt die Wahl, bevor das erste Mal gemalt wird.

Die Messung misst nicht den Nutzer. web-vitals schickt Zahlen und das
Routenmuster, keine Kennung und keinen konkreten Pfad: wer wann welche Akte
geöffnet hat, steht im Zugriffsprotokoll, und eine zweite Spur daneben wäre
eine Überwachungsfunktion ohne Auftrag. Ausgewertet wird Median und 75.
Perzentil, nicht der Mittelwert.

Mobil: die breiten Tabellen scrollen in sich, statt die Seite breiter zu
machen. Ein e2e-Test hält fest, dass keine Seite waagerecht überläuft.

scripts/restore-test.sh spielt eine Sicherung in eine Wegwerf-Datenbank und
prüft ausdrücklich, dass Regeln und Trigger mitkommen: eine Wiederherstellung
ohne entry_no_delete wäre eine Datenbank ohne die Zusicherung, um derentwillen
das System gebaut wird. Einmal durchgespielt, alles angekommen.

186 Tests im Web-Paket, 17 in der Datenbank, 7 im Worker, 54 e2e.
Grösste Route 116,2 von 120 kB.
2026-09-03 10:28:50 +02:00
Harlekin 55bcb0d95c Phase 5 abgeschlossen: Dateien mit vollständiger Prüfkette
Upload → quarantine/ → Typprüfung → ClamAV → Neukodierung → store/. Es gibt
keinen Pfad, auf dem eine Datei ungeprüft in store/ landet, und keinen
Schalter, der die Kette abschaltet.

Die Typprüfung liest Magic Bytes aus einer eigenen Signaturtabelle statt aus
file-type: bei einer Positivliste aus fünf Formaten hilft es nicht, hundert
erkennen zu können. Die Signatur muss an Position 0 stehen - ein Vorspann vor
%PDF- ist der klassische Weg, eine Datei für zwei Programme zugleich lesbar zu
machen. Wer trotzdem durchkommt, kommt als text/plain durch und wird unter der
Endung des erkannten Typs ausgeliefert, nicht unter der hochgeladenen.

Drei Ausgänge statt zwei: BLOCKED heisst "ist gefährlich", FAILED heisst
"wissen wir nicht". Nur FAILED lässt sich freigeben, und die Freigabe
wiederholt die Kette ohne den Scanner - Typprüfung und Neukodierung laufen
dabei mit. Ein fehlender clamd macht damit keine Datei sauber; ein Test hält
das fest.

Auslieferung nur über /api/files/[id], mit Sichtbarkeitsprüfung der
zugehörigen Akte, Protokoll ab Stufe 3 und den harten Kopfzeilen aus docs/07.
Vorschaubilder dürfen inline, weil sie serverseitig als WebP neu kodiert
wurden.

Das Lichtbild im Aktenblatt ist damit gefüllt: der Feldwert ist die Kennung
eines geprüften Anhangs, serverseitig zu einer Adresse aufgelöst.

/api/events liefert attachment.scanned über pg_notify - die Grundlage, auf der
Phase 6 den Aktenchat aufsetzt. Die Sichtbarkeit wird je Ereignis geprüft,
sonst verriete schon die Kennung die Existenz.

Nebenbei riss das Bündelbudget (121 kB gegen 120 erlaubte): lib/i18n/de.ts war
ein Objekt, und eine Client-Komponente zieht die ganze Datei mit. Die Texte
stehen jetzt in sechs Dateien nach Bereichen, de.ts setzt sie nur zusammen.
Grösste Route wieder bei 118 kB, und sie wächst nicht mehr mit jedem neuen
Bereich mit.

133 Tests im Web-Paket, 15 in der Datenbank, 28 e2e.
2026-09-02 20:27:22 +02:00