Lessons aus realen Prilog-Vorfällen
Jede Regel hier ist aus einem konkreten Fall entstanden. Datum, Symptom, Was-wir-jetzt-tun. Wachsendes Dokument — bei jedem neuen Vorfall einen Eintrag ergänzen.
L-001 · „LIVE" ohne Hand-Test
Datum: 2026-05-31 (Zeugnis Sprint 5/6) Symptom: Sprint wurde als LIVE gestempelt nach: TSC clean + Tests grün + Deploy erfolgreich. User klickte zum ersten Mal als Lehrer auf „Anton Schmidt" und bekam 404. Backend war korrekt, Frontend war korrekt — aber die Klick-Pfade waren nie zusammen durchgegangen worden. Plus: ID-Slices statt Klarnamen überall (Anton war als cmp6s0aor000576qg… zu sehen).
Was wir jetzt machen:
- LIVE-Stempel nur nach Phase 6 (UX-Audit) der Pflicht-Checkliste
- Mindestens 1× per Hand pro betroffene Rolle durchgeklickt
- Real-DB-Probe gegen Live-Tenant
- Klarnamen statt IDs —
id.slice(0, n)…nur als font-mono-Fallback
Memory-Eintrag: feedback_live_acceptance_check.md
L-002 · Stundenplan-Bridge ohne Lehrer-Zuordnung → alles still
Datum: 2026-05-31 (Zeugnis Sprint 7) Symptom: Auf leander hatte 8b 0 InstructionGroups. Die Stundenplan- Bridge fand 0 Lehrer-Zuordnungen → alle 900 SubjectEntries hatten teacherId=null. Lehrer öffnete Editor → kein Fehler, aber er konnte nicht schreiben (Sicht-Resolver verweigerte 403, aber als 404 dargestellt).
Was wir jetzt machen:
- Stiller Hang = immer Bug. Diagnose-Banner im Editor wenn
teacherId=null: „Kein Lehrer zugeordnet — Stundenplan-Bridge fand nichts, bitte Klassenleitung fragen" setup-status-Endpoint mit Coverage-Check: „N/M Lehrer zugeordnet" — wenn < 50 % → rot, sichtbar im DSGVO-Tab
L-003 · 0 approved TextRules → Generator macht nichts
Datum: 2026-05-31 Symptom: Generator-Button erzeugte leeren Text. User sah keinen Fehler, dachte „komisch", schrieb weiter mit leerem Vorschlag.
Was wir jetzt machen:
- Generator zeigt Diagnose-Banner wenn 0 approved TextRules: „Generator wird leer liefern — erst im Tab Textbausteine welche anlegen + freigeben"
- Default-Seed beim Modul-Install muss alle Voraussetzungen abdecken oder explizit als „leer-zum-Ausfüllen" markiert sein
L-004 · 404 statt verständlicher Diagnose
Datum: 2026-05-31 Symptom: Sicht-Resolver gab {error:'not_found'} zurück (statt 403, um IDs nicht enumerierbar zu machen). Frontend zeigte Toast „HTTP 404" ohne weitere Hilfe.
Was wir jetzt machen:
- Backend-Asserter liefern strukturierte Diagnose:
{ error, message, reasons[], remedies[] }. 404 bleibt für Enumeration-Schutz, aber Body erklärt verständlich. - Frontend:
AccessDeniedBanner-Komponente rendert die Diagnose ausformuliert mit Reasons + „Was du tun kannst"-Liste - Toast für
error='no_visibility'unterdrückt (Banner reicht)
L-005 · UserAccount.admin gesetzt, JWT bleibt alt
Datum: 2026-05-31 Symptom: „Zum Admin machen"-Button setzte UserAccount.admin=true, aber die JWT-Rolle wurde nur aus order.adminUsername abgeleitet. Folge: User klickte „Admin", nichts passierte, weil JWT noch ['member'].
Was wir jetzt machen:
deriveRoles(tenantId, localPart, adminUsername)prüft beide Quellen (adminUsername + UserAccount.admin)- Toast nach „Zum Admin machen": 12s Info „User muss sich neu einloggen, damit das JWT die admin-Rolle bekommt"
- Generell: jede DB-Änderung, die JWT-Inhalte betrifft, braucht einen Re-Login-Hinweis
L-006 · Import erzeugt nur Person, keine UserAccounts
Datum: 2026-05-31 Symptom: Schule importierte 50 Lehrer via ude-migration. Alle landeten als Person ohne UserAccount. Daher konnte sich niemand einloggen, keine TeacherQualifikation gepflegt werden, Teaching- Section blieb leer.
Was wir jetzt machen:
- Bulk-Tool „Logins erzeugen" für Personen ohne Account
- Sidebar-Filter „Ohne Login" + „Mit Login" mit Live-Counts
- Bei externen Personen in Kontakte-Detail: erklärender Hinweis statt schweigender Section („Person hat keinen Prilog-Login, …")
L-007 · /api/api/ durch doppelten Prefix
Datum: 2026-05-31 Symptom: Neue fetch-Aufrufe machten ${env.platformBaseUrl}/api/.... In Production ist platformBaseUrl='/api' → /api/api/... → 404. In Dev verbarg sich der Bug, weil platformBaseUrl='https://api.prilog.chat/api' → /api/api/... → ebenfalls 404, aber Symptom war anders.
Was wir jetzt machen:
- Konvention: immer
${env.platformBaseUrl}/platform/v1/...(ohne zusätzliches/api/) - Wenn möglich:
requestJson-Helper nutzen statt direktefetch-Calls - In Tests: einmal gegen Production-Konfiguration durchspielen, nicht nur gegen Dev
L-008 · Migration referenziert Tabellen-Namen falsch
Datum: 2026-05-31 Symptom: Migration 0130 hatte REFERENCES "Tenant"("id") (mit großem T). Echte Tabelle heißt tenants (Plural, lowercase). Migration scheiterte auf Live-DB.
Was wir jetzt machen:
- Vor PR-Merge: Migration einmal auf einer Test-DB (Kopie der Live-DB) durchspielen
- In Migration-Files konsequent die Prisma
@@map-Namen verwenden - Tipp:
grep "REFERENCES" prisma/migrations/0001_baseline/migration.sqlals Referenz nutzen
L-009 · Schema-Migration ohne Backfill für bestehende Daten
Datum: 2026-05-30 (mehrere Fälle) Symptom: Neue NOT NULL-Spalte hinzugefügt, vergessen dass bestehende Reihen leer sind → Migration scheiterte oder die App gibt undefined- Errors auf bestehenden Daten.
Was wir jetzt machen:
- Jede Migration mit neuer Pflicht-Spalte enthält Backfill-SQL unmittelbar danach (z. B.
UPDATE x SET col = ... WHERE col IS NULL) - Erst danach
ALTER COLUMN SET NOT NULL
L-010 · CI fällt aus (GitHub-Billing) — manueller Deploy
Datum: 2026-05-31 Symptom: GitHub-Actions deaktiviert wegen Billing-Problem. Web-Client- Artifact wurde nicht gebaut. Reconcile-Sweep überschreibt sonst um 03:30 mit altem stable-Tarball.
Was wir jetzt machen:
- Tarball-Build lokal, scp ins
prilog-artifacts-Repo,promote-web-client.sh, Reconcile-Sweep manuell - Memory-Eintrag (
feedback_webclient_promote_stable.md) erklärt den Pfad - Backlog: CI auf anderen Provider migrieren oder lokaler Fallback automatisieren
L-011 · nginx-Pfad-Drift bei docs.prilog.chat
Datum: 2026-05-31 Symptom: docs.prilog.chat lieferte 404 obwohl Files lokal builden + rsynct werden. nginx-root war auf /var/www/prilog_docs/docs/.vitepress/dist konfiguriert, Files lagen flach in /var/www/prilog_docs/.
Was wir jetzt machen:
- Nach Site-Refactor immer einmal die Live-URL prüfen (
curl -sI ...), nicht nur den lokalen Build - Memory
reference_docs_hosting.mdaktuell halten
L-012 · Mehrere Identitäts-Konzepte parallel (Person vs UserAccount vs UserDirectoryEntry)
Datum: laufend (CRM-Foundation-Migration) Symptom: Es gibt 3 Konzepte für „Person ist in unserem System":
Person(CRM-Datensatz, optional ohne Login)UserAccount(CRM-v2-Login)UserDirectoryEntry(Legacy-Login)
Je nachdem welcher Endpoint angesteuert wird, wird das eine oder andere abgefragt → Inkonsistenzen.
Was wir jetzt machen:
- Plan zur Vereinheitlichung in
prilog_docs/umsetzung/crm-foundation/ - Bis Migration durch ist: jeder neue Endpoint mit
isCrmV2(tenantId)- Branch nutzen - Long-term: nur noch
Person+UserAccount,UserDirectoryEntryarchivieren
L-013 · TypeScript-Casing-Fehler in Prisma-Enums
Datum: 2026-05-31 Symptom: Code prüfte person.kind !== 'person', aber Prisma-Enum liefert 'PERSON' (Großbuchstaben). TSC fing es nach prisma generate, aber Strict-Compare lieferte vorher false positives.
Was wir jetzt machen:
- Bei Prisma-Enums immer den generierten Type verwenden (
import { PersonKind } from '@prisma/client') prisma generatevor jedem TSC-Lauf
L-014 · Tabellen-Drift bei Tenant-Migration (Backup-Stille 14 Tage)
Datum: 2026-06-01 (Tenant-Board-Anomalie auf shared-2) Symptom: Im Admin-Portal-Tenant-Board waren die Backup-Dots der zwei shared-2-Tenants rot. Diagnose: 9 (rssw) bzw. 14 (test-2) Tage kein verified-Backup, jeder nächtliche Cron-Lauf gleicher Fehler: „tenant-box <slug> existiert nicht".
Root Cause: Bei der Tenant-Migration (shared-1 → shared-2) wurde server_orders.shared_host_id korrekt auf 5 aktualisiert, abertenant_box_registry.shared_host_id blieb auf 1. Der Backup-Cron liest tenant_box_registry → versuchte Backup über Agent auf shared-1 → fand tenant-box dort nicht (sie war auf shared-2 umgezogen) → status='failed'.
Detection-Schwäche: Die Anomalie wurde nicht durch einen Alert bemerkt, sondern erst beim manuellen Tenant-Board-Blick des Owners — 14 Tage nach Beginn. Ohne diese zufällige Sichtung wäre shared-2 mit veralteten Backups in einen tatsächlichen Crash gegangen.
Was wir jetzt machen:
Code-Fix:— durch Schritt 4 obsolet, Code-Block ist im 0132-Commit wieder entfernt worden.tenant-migration.service.tsSchritt 6b' aktualisierttenant_box_registry.shared_host_idmit.- Drift-Recovery: SQL-One-Shot in der Live-DB hat die zwei stehengebliebenen Einträge korrigiert; danach manuell
tenant-box-daily-backup-Cron getriggert → alle drei Tenantsverified. - Alert-Cron: Neuer
backup-stale-watch-Cron (täglich 07:30 UTC) prüft pro Tenant das jüngste verified-Backup. >24h → Mail anADMIN_ALERT_EMAIL. - Drift physisch eliminiert (Migration
0132_tenant_box_registry_drift_eliminated, 2026-06-01):tenant_box_registry.shared_host_id/synapse_port/minio_portganz gedroppt. Truth wandert vollständig nachserver_orders(inklusiveminio_port). Alle Reader joinen jetzt, alle UNIQUE-Constraints sitzen aufserver_orders. Drift mathematisch ausgeschlossen. Konzept:umsetzung/tenant-box-registry-drift-eliminiert.md.
Manifest-Konsequenz:
- Schema-Drift zwischen zwei Tabellen mit „Wahrheit auf einer Seite" ist ein Anti-Pattern. Plan A: redundante Spalte ganz weg, JOIN. Plan B: Trigger in der DB. Plan C: beide Updates in derselben Transaktion. App- Sync ist die schwächste der drei Optionen — wer immer eine zusätzliche Stelle vergessen kann, vergisst sie.
- Jede neue „läuft im Hintergrund"-Operation braucht einen Alert für den Failure-Pfad. Status-Polls auf der Karte allein reichen nicht — niemand öffnet das Tenant-Board jeden Tag.
Wie pflegen
Pro Vorfall:
- Inkrementeller ID (L-014, L-015, …)
- Datum + 1-Zeilen-Symptom
- „Was wir jetzt machen" — die abgeleitete Regel oder das Code-Pattern
- Falls passt: Memory-Eintrag verlinken
Wachstum ist gewollt — das ist die Inschrift, was schon mal schiefging.