Zwei Funde beim Nachsehen, warum `pnpm db:migrate` hängt.
**Der Befehl hing nicht, er fragte.** `prisma migrate dev` fand einen
Unterschied zwischen schema.prisma und den Migrationen und wartete auf einen
Namen für eine neue Migration. Deren Inhalt wäre gewesen, den GIN-Index über
den Suchvektor zu löschen: er stand nur in der Migration, weil
Unsupported("tsvector") dazu verleitet, den Index für nicht darstellbar zu
halten. Er ist darstellbar. Wer die Frage in Eile beantwortet hätte, hätte die
gestufte Volltextsuche verloren und es erst gemerkt, wenn die Suche leer
bleibt.
packages/db/tests/schema-drift.test.ts prüft das jetzt und nennt im Fehlerfall
das fehlende Stück. Gegengeprobt: ohne die Zeile im Schema schlägt der Test an.
Die Sperre, an der der zweite Versuch scheiterte (P1002), hielt eine verwaiste
Verbindung eines abgeschossenen migrate-dev. Wie man den Halter findet und
freigibt, steht jetzt in docs/11.
**Die Testdatenbank wird verworfen und neu angelegt.** Weil Akten nicht
löschbar sind, hatte sie 2487 angesammelt, und drei Zusicherungen auf eine auf
zehn Einträge begrenzte Liste fanden ihre eigene Akte nicht mehr darin. Das
sieht aus wie ein Fehler im Code und ist keiner. Neu aufsetzen kostet
dreieinhalb Sekunden; KEEP_TEST_DB=1 behält den Bestand.
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.
Design-Tokens aus docs/10-design-system.md als CSS-Variablen: Grundpalette,
die sechs Klassifizierungsfarben, Eckenradius durchgehend 2 px, keine Schatten
ausser dem Fokusring. Der Schwärzungsbalken mit dem einmaligen Durchwischen
respektiert prefers-reduced-motion. Vier Tests halten die Tokens gegen das
Dokument.
IBM Plex selbst gehostet als woff2, latin und latin-ext getrennt über
unicode-range, font-display: swap, immutable-Cache.
Alle Oberflächentexte in lib/i18n/de.ts. Ein Test durchsucht app/ und
components/ nach Umlauten im JSX und schlägt fehl, wenn dort deutscher Text
steht. Er hat beim ersten Lauf gleich einen Verstoss gefunden.
/api/health prüft Datenbank, Discord-Gateway und ClamAV. Die Gesamtbewertung
fällt nur auf "down", wenn die Datenbank weg ist: die anderen Dienste haben
eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll nicht
den Web-Container als tot markieren.
Strikte CSP mit Nonce, kein unsafe-inline für Skripte. Die erste Route liegt
bei 100 kB JavaScript nach Kompression, Budget sind 120 kB.