Zum Inhalt

TODO — Kora Platform

Technische Schulden, Deferred-Items aus Code-Reviews, und Integration-Learnings. Diese Liste ist Teil der Platform-Dokumentation unter docs.kora.luki-net.org.

Konventionen

  • Severity: High (≥ 80) / Medium (50–79) / Low (< 50). Scores folgen dem Review-Report-Template.
  • Quelle: Aus welchem Review/Block stammt das Item.
  • Status: Open / In Progress / Done. Abgeschlossene Items wandern in den Archiv-Abschnitt am Ende dieses Dokuments mit Commit-Hash + Datum (Traceability). Alte Archiv-Einträge werden nach ~3 Monaten oder einem Major-Release zusammengefasst.

Offene Technische Schulden

Block-2-/Block-3-Todos (B3-03, B3-04, B2-01, B2-03, B2-05, B2-07) sind in Cleanup-02 und TODO-B2-03 (platform/b2-03-service-account) abgeschlossen worden — siehe Archiv am Ende.

Die Einträge aus Prometheus-Scrape / Dev-Infrastruktur (TODO-Cleanup03-01, -02) sind inzwischen abgeschlossen — siehe Archiv.

Aus Block 19 Phase 4 (2026-05-03, Branch feature/block-19-bge-m3-embedder)

TODO-Block-19-Phase-4 — Production-Cutover ✅ Erledigt 2026-05-03

  • Severity: High (Score 80)
  • Status: ✅ Erledigt 2026-05-03 mit Cutover-Commit (siehe Changelog [Unreleased] → Phase 4)
  • Snapshot-Recovery-Point: backups/qdrant/avs_handbuecher_pre_block19_cutover_20260503-1150.snapshot (4.2 MB, 30+ Tage Aufbewahrung)
  • Aktivierte Defaults: use_hybrid_retrieval=True, qdrant_collection=avs_handbuecher_bge_m3 in src/avs_chatbot/config.py. .env synchronisiert (idempotenter Switch).
  • Cutover-Verifikation: make smoke 8/8 grün, make smoke-hybrid grün, frische Live-Cache-bypass-Query (cached=false, 5 sources), 4 weitere Live-Queries fehlerfrei.

TODO-Block-19-Observation-Window — 7-Tage-Beobachtung ✅ Erledigt 2026-05-10

  • Severity: Medium (Score 60) — Production-Confidence-Building
  • Trigger: Phase-4-Cutover ist live. 7-Tage-Window-Start: 2026-05-03.
  • Status: ✅ Tag 7 grün abgeschlossen 2026-05-10. R@5 0.96 (+0.04 vs Block 18), MRR 0.74 (−0.0267 dokumentierter Reranker-Bias q006/q025), avg_latency 140ms (−15ms). Live-Stack: API healthy 7d, smoke-hybrid grün, 0 Query-Errors. Snapshot bleibt 30d in backups/qdrant/ (defense in depth bis 2026-06-02). .env.block19_backup analog behalten bis Cleanup-Welle ~2026-06-02.
  • Cycle:
  • Tag 0 (2026-05-03): Cutover, Initial-Smoke, Live-Verify ✅
  • Tag 1 (2026-05-04): 24h-Snapshot der Metriken — p50/p95-Latenz, Error-Rate, No-Match-Rate gegen Tag-0-Baseline
  • Tag 3 (2026-05-06): Mid-Window-Review — bei Drift Diskussion ob weitermachen oder rollback
  • Tag 7 (2026-05-10): End-Window-Review ✅ grün:
    • Phase 5 (Edge-Case-Queries q026–q035) als nächste Roadmap-Karte freigegeben
    • Alte Collection avs_handbuecher Cleanup als separates Followup-Item
    • Snapshot in backups/qdrant/ für 30 Tage behalten (defense in depth)
  • Grafana-Panel-Tasks (innerhalb Window aufzubauen):
  • avs_query_duration_seconds p50/p95/p99 — bestehender Panel
  • avs_no_retrieval_match_total-Rate — bestehender Panel
  • NEU avs_rag_stage_duration_seconds{stage="embedding"} — BGE-M3 vs e5-large-Baseline (~20 ms)
  • NEU avs_rag_stage_duration_seconds{stage="retrieval"} — Hybrid (Dense + Sparse parallel)
  • Alarm: p95-Latenz > 500 ms (Block-18 ~155 ms, Block-19 ~140 ms — Headroom für Spikes)
  • Alarm: no_retrieval_match_total-Rate > 5%/h
  • Wöchentlicher manueller make eval-hybrid (Makefile-Target ist Block-19.5/Block-20-Vorbereitung)
  • Rollback-Pfad (bei Drift): cp .env.block19_backup .env && make redeploy. Snapshot-Restore bei tieferem Issue. config.py-Defaults auf Hybrid bleiben — bei Rollback override .env die Defaults.
  • Aufwand: ~30 min Setup + asynchrone Beobachtung
  • Quelle: Block 19 Phase 4 Cutover, 2026-05-03
  • Status: Open

TODO-Block-19-Cleanup — Alte Collection avs_handbuecher löschen

  • Severity: Low (Score 30) — Storage-Hygiene
  • Trigger: Nach Tag-7 grüner Observation (~2026-05-10). Snapshot in backups/qdrant/ behält Recovery-Möglichkeit.
  • Vorschlag:
    curl -sf -X DELETE http://localhost:6333/collections/avs_handbuecher
    curl -sf http://localhost:6333/collections | python3 -m json.tool
    
  • Aufwand: ~10 min
  • Voraussetzung: Observation-Window grün abgeschlossen
  • Quelle: Block 19 Phase 4 Cutover, Plan §15a.4
  • Status: Blocked-bis-Tag-7

TODO-Block-19-5-Reranker-Upgrade — bge-reranker-v2-m3

  • Severity: Medium (Score 60) — Quality-Tuning + konsistenter BGE-Stack
  • Trigger: Phase-3.5-Diagnose-Outcome B' (Cross-Encoder-Reranker-Bias). q006 zeigt: TransformersSimilarityRanker (cross-encoder/mmarco-mMiniLMv2-L12-H384-v1) bevorzugt Kurverwaltung-Chunks vor Beherbergungsbetrieb-Chunks bei duplizierten Inhalten. RRF-Weights haben dabei keine Wirkung (Sweep validated).
  • Vorschlag:
  • SentenceTransformersSimilarityRanker mit BAAI/bge-reranker-v2-m3 (multilingual cross-encoder, konsistent zu BGE-M3-Embedder).
  • Phase-3-Style-Eval-Pattern wiederverwenden: make smoke-hybrid-Skeleton + In-Process-Eval-Skript für 25 Test-Queries.
  • q006-Tiefen-Inspektion (analog Phase 3.5 Sub-Step 1) bestätigen: kommt MRR auf >= 0.77 zurück?
  • A/B gegen Phase-3-Block-19-Eval (eval_retrieval_block19_phase3_20260503-0545.json).
  • Voraussetzung: Tag-7 Observation grün, Phase-4-Cutover stabil.
  • Aufwand: ~4-6h (Reranker-Wechsel + Eval-Run + Bericht)
  • Quelle: Block 19 Phase 3.5 Diagnose, H4-Hypothese
  • Status: Open

TODO-Block-19-Tooling — In-Process-Quality-Run-Runner formalisieren

  • Severity: Low (Score 40) — Tooling-Schuld, nicht Production-relevant
  • Problem: Phase-3-/3.5-Runs liefen mit Inline-Skripten via docker cp + docker compose exec. Existierendes tests/evaluation/evaluate_retrieval.py ist HTTP-basiert (testet Live-API), nicht in-process — und tests/-Verzeichnis ist nicht in den API-Container gemountet.
  • Vorschlag:
  • Neuer Runner tests/evaluation/evaluate_retrieval_inprocess.py (oder --mode in-process-Flag am bestehenden Runner) der die Pipeline direkt aufruft analog make smoke-hybrid.
  • Mount-Strategie: optional tests/ als Bind-Mount in docker-compose.yml für In-Process-Workflows (read-only).
  • Neue Makefile-Targets make eval-hybrid / make eval-legacy analog smoke-hybrid (Phase-4-Observation-Window-Tool).
  • Reusable für Block 19.5 (Reranker-Upgrade) und Block 20 (AST-Chunking-Flag).
  • Aufwand: ~2h
  • Quelle: Block 19 Phase 3 + Phase 3.5 Drift-Finding (Eval-Runner-CLI)
  • Status: Open

TODO-Block-19-Phase-5 — Edge-Case-Queries q026–q035

  • Severity: Low (Score 30) — Test-Set-Erweiterung
  • Trigger: Phase-3-Eval validiert nur 25 bestehende Queries. Edge-Case-Queries (Paragraphen, Codes, Akronyme, deutsche Compounds wie "Kurtaxe") sind separater Phase-5-Check.
  • Vorschlag: 10 neue Test-Queries (q026–q035) im Test-Set ergänzen mit Fokus auf:
  • Paragraphen-Querverweise (z.B. "§ 12 Meldegesetz")
  • Fehler-Codes (z.B. "Was bedeutet Fehlercode E-2031?")
  • Akronyme (z.B. "Was ist die ELMA-Schnittstelle?")
  • Deutsche Compounds-Edge-Cases (BGE-M3-Tokenization-Stress-Test)
  • Voraussetzung: Tag-7 Observation grün.
  • Aufwand: ~2-3h (Query-Curation + Eval-Run)
  • Quelle: Phase-3-Akzeptanzkriterien, q026–q035 ausserhalb Phase-3-Scope
  • Status: Open

TODO-Block-19-Reconciliation — Konzept-Update §6a.2 + §15

  • Severity: Low (Score 30) — Konzept-Doku-Hygiene
  • Vorschlag: Updates in docs-kora/docs/konzepte/multitenancy-fundament.md:
  • §6a.2 — Reranker-Rolle dokumentieren (war in Phase-3.5-Diagnose H4 die Wurzel des MRR-Drifts, nicht RRF). Pre-Reranker-Ranking ≠ Post-Reranker-Top-K.
  • §15 — Per-Subprocess-In-Process-Pattern (Demo bleibt im Legacy-Modus während Hybrid-Run via docker compose exec -e ... + Inline-Skript). Brücke zu §15a Parallel-Entwicklung.
  • §6a.2 — q006-Lesson: Mehrdeutige Gold-Labels in Test-Set bei duplizierten Inhalten über mehrere Handbücher. Empfehlung: file-level Match bleibt (Phase 3 hat das nicht broken), aber chunk-/section-level Match als optionale Erweiterung in Phase 5+.
  • Aufwand: ~1h
  • Quelle: Block 19 Phase 3 + 3.5 Methodik + Diagnose
  • Status: Open

TODO-Block-19-Tooling — In-Process-Quality-Run-Runner formalisieren

  • Severity: Low (Score 40) — Tooling-Schuld, nicht Production-relevant
  • Problem: Phase-3-Run lief mit Inline-Skript /tmp/phase3_eval.py via docker cp + docker compose exec. Existierendes tests/evaluation/evaluate_retrieval.py ist HTTP-basiert (testet Live-API), nicht in-process — und tests/-Verzeichnis ist nicht in den API-Container gemountet.
  • Vorschlag:
  • Neuer Runner tests/evaluation/evaluate_retrieval_inprocess.py (oder --mode in-process-Flag am bestehenden Runner) der die Pipeline direkt aufruft analog make smoke-hybrid.
  • Mount-Strategie: optional tests/ als Bind-Mount in docker-compose.yml für In-Process-Workflows (read-only).
  • Neue Makefile-Targets analog smoke-hybrid für die zwei Modi.
  • Reusable für Block 19.5 (Reranker-Upgrade) und Block 20 (AST-Chunking-Flag).
  • Aufwand: ~2h
  • Quelle: Block 19 Phase 3 Drift-Finding (Eval-Runner-CLI)
  • Status: Open

TODO-Block-19-Lesson — Per-Subprocess-Methodik dokumentieren

  • Severity: Low (Score 30) — Konzept-Reconciliation
  • Vorschlag: §6a.2 oder §15 in multitenancy-fundament.md um den Per-Subprocess-In-Process-Pattern erweitern (Demo bleibt im Legacy-Modus während Hybrid-Run, Workflow für zukünftige Block-Vergleiche 19.5 / 20). Brücke zu §15a Parallel-Entwicklung.
  • Aufwand: ~30 min
  • Quelle: Block 19 Phase 3 Methodik
  • Status: Open

Aus Block 18 (2026-05-02, Branch platform/block-18-docling-ingestion)

TODO-Block-18-Followup-01 — EnhancedPDFConverter-Removal (Cleanup-Welle)

  • Severity: Low (Score 30)
  • Datei(en): src/avs_chatbot/pipelines/components/pdf_converter.py (193 LoC), tests/unit/test_pdf_converter.py (166 LoC)
  • Problem: Block 18 hat DoclingPDFConverter als Default-PDF-Converter im FileTypeRouter etabliert. EnhancedPDFConverter wird nicht mehr referenziert, bleibt aber im Code als Cleanup-Welle-Restanteil. Pure-Helper-Functions (_strip_footer, _mark_hint_boxes, _rows_to_markdown) sind isoliert genug für Removal.
  • Vorschlag: Nach Block-18-Merge: File pdf_converter.py löschen, Import in indexing.py (bereits durch Block 18 entfernt) verifizieren, tests/unit/test_pdf_converter.py löschen, pdfplumber>=0.11-Pin in pyproject.toml prüfen (wird ggf. nicht mehr gebraucht — docling zieht eigene PDF-Backend-Deps).
  • Aufwand: ~2h (Code-Removal, Dependency-Cleanup, Smoke-Tests)
  • Trigger: Cleanup-Welle (z.B. v1.5.0 oder vor Block 14). Nicht Block-18-Scope per Lutz-Entscheidung "Block 18 ist groß genug".
  • Disziplin-Datapoint: Pre-Block-18-EnhancedPDFConverter macht silent-fallback auf PyPDFToDocument bei jedem Fehler. Block 18 etabliert Fail-Loud. Removal entfernt damit auch das letzte Stück silent-fallback-Pattern aus dem Code.
  • Quelle: Block-18-Implementation (Phase 7), Discovery-Datapoint
  • Status: Open

TODO-Block-18-Followup-02 — Page-Number-Fidelity im Widget ✅ Erledigt

  • Severity: Medium (Score 60) — User-facing
  • Status: ✅ Erledigt 2026-05-02 mit Block-18-Merge 814f981
  • Lösung: Phase-2.5-Page-Tracking via page_break_placeholder aus docling-core 2.30+ + numbered Sentinels (\x02AVS_PAGE_N\x02). Pro Chunk werden page_number/page_start/page_end aus Sentinel-Position abgeleitet, statt aus 1-Doc-per-Page-Index. Real-Eval-Verifikation: Smoke-Queries zeigen echte Page-Numbers (p.7, p.13, p.54, ...) statt alle p.1.
  • Resolution-Pattern: TODO im selben Block aufgelöst statt als Followup ausgelagert (Lutz-Entscheidung beim Phase-2.5-Scope-Add).

TODO-Block-18-Followup-03 — q004 Gold-Label-Re-Annotation

  • Severity: Low (Score 30)
  • Problem: Eval-Frage q004 ("Was passiert wenn ein Benutzer gesperrt wird?") hat aktuell single-doc Gold-Label (Meldeschein_Handbuch_AdministrationBeherbergungsbetrieb.pdf). Block 18 retrieved Richtlinie zum sicheren Umgang mit Informationen als rank-1, Beherbergung außerhalb Top-5. Beide Sources semantisch valide — der Sicherheits-Richtlinie behandelt Benutzer-Sperrung im Sicherheitskontext, Beherbergung im operativen Meldeschein-Workflow.
  • Effekt: R@5 q001-q020 fällt von 95% auf 90% allein wegen q004 → Stop-Trigger 4 verfehlt. Dokumentierte Block-18-Merge-Ausnahme.
  • Vorschlag: Multi-Doc-Annotation (Liste von relevant_documents pro Frage) ODER Eval-Set-Refactor mit präziseren Frage-Formulierungen, die eindeutig auf 1 Source zielen.
  • Aufwand: ~1-2h (Eval-Schema-Erweiterung + Re-Annotation aller 25 Q + evaluator-Logik für any-match-Hit)
  • Trigger: Nächste Eval-Set-Maintenance-Welle (vor v1.0.0-Release oder bei Block 19 BGE-M3 Re-Eval)
  • Quelle: Block-18-Real-Eval Hand-off
  • Status: Open

TODO-Block-18-Backup-Cleanup — Production-Backup-Snapshot

  • Severity: Low (Score 20)
  • Datei: backups/qdrant/avs_handbuecher_pre_block18.snapshot (8.2 MB)
  • Problem: Backup der vor-Block-18-Production-Collection liegt nach Production-Switch noch auf disk. Rollback-Pfad falls Quality-Problem in ersten 1-2 Wochen.
  • Vorschlag: Nach 1-2 Wochen ohne Rollback-Notwendigkeit löschen: rm /opt/avs-chatbot/backups/qdrant/avs_handbuecher_pre_block18.snapshot
  • Aufwand: ~5min
  • Trigger: 2026-05-16 (zwei Wochen nach Production-Switch)
  • Status: Open

TODO-Konzept-02 — Pattern-Reife-Quote-Trendlinie in §17.2 einarbeiten

  • Severity: Low (~30–45 min, Mini-Run nach v1.3.0-Tag)
  • Datei: docs-kora/docs/konzepte/multitenancy-fundament.md §17.2
  • Problem: §17.2 führt einen Pauschal-Pattern-Reife-Wert (60 %). v1.3.0-Welle liefert drei differenzierende Datapoints (D2 23 %, D1 31 %, E ~50 %) plus existing Block 8 (50–80 %) und Block 11 (40 %). Diese erlauben eine kalibrierte Quote-Erwartung pro Block-Typ (Cleanup-Welle Backend, Cleanup-Welle Frontend, Foundation-Reuse hoch, Foundation-Reuse + neuer Code, Foundation-erweiternd) und damit eine belastbarere Block-13-Schätzung.
  • Fix: §17.2-Tabelle um Block-Typ-Spalte plus Quote-Erwartung ergänzen. Datapoints-Liste in §17.2a (analog Reconciliation-Quelle Nr. 6 aus TODO-Konzept-01-Auflösung). Konzept-Header-Patch v5.3.3 → v5.3.4. v1.0.0-Budget bleibt unverändert (~427h) — die Kalibrierung ändert keine Aufwand-Zahlen, nur die Erwartungsfunktion.
  • Pattern: TODO-Konzept-01 als Vorlage (eigener Mini-Run nach v1.2.0-Tag, ~30 min, dokumentativer Patch ohne Zahlen-Korrektur).
  • Status: ✅ Erledigt 2026-05-02 — Mini-Run dieser Eintrag selbst. §17.2a Reconciliation Nr. 7 (Pattern-Reife-Quote-Trendlinie pro Block-Typ) ergänzt; §17.5 (Cleanup-Wellen außerhalb 427h-Hauptachse) als neue Sektion eingeführt; Konzept-Header v5.3.3 → v5.3.4. Merge folgt unten.

Aus Block 5 Phase E Code-Review (2026-04-22)

Nummerierungs-Hinweis: Die TODO-IDs TODO-Block-5b..-5f stammen aus dem Code-Review des Plattform-Module-Blocks, der historisch als "Block 5" umgesetzt wurde. Nach der Roadmap- Umnummerierung vom 2026-04-23 (siehe Struktur-Sync-Commit 23a9b10) ist dieser Block in der Roadmap als Block 6 — Plattform-Module & einstufige Freischaltung geführt. Die TODO-IDs selbst bleiben als stabile Identifier unverändert.

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker nach CLAUDE.md-Regel („≥ 80 vor Merge fixen").

TODO-Block-5b, -5c, -5d in Cleanup-Branch platform/block-5-cleanup-audit-delta (2026-04-24) geschlossen — siehe Archiv am Ende.

TODO-Block-5e, -5f, -5g im v1.3.0-D2-Lauf platform/v1.3.0-d2-backend-polish (2026-05-01) abgeschlossen — siehe Archiv am Ende.

TODO-Block-5e — Audit-Rollback-Pfad ungestestet

  • Severity: Low (Score 35)
  • Datei: Tests
  • Problem: write_module_audit-503-Pfad (DB-Failure) hat keinen Test. Bei Audit-Fail soll die Mutation rollbacken — aktuell nur im Smoke latent durch Audit-Row-Existenz-Check abgedeckt.
  • Fix: Integration-Test mit gemockter/sabotierter Audit-Tabelle (z.B. kurzfristig Permission revoken) und Assert, dass tenant_modules-Row nach 503 nicht persistiert wurde.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — test_audit_failure_rolls_back_mutation simuliert OperationalError im write_platform_audit und prüft Rollback der umgebenden create_tenant-Mutation.

TODO-Block-5f — DELETE-Route ohne Defense-in-Depth-Scope-Check

  • Severity: Low (Score 30)
  • Datei: src/kora_platform/api/routes/tenant_modules.py:165-203
  • Problem: DELETE läuft auf BYPASSRLS-Engine und verlässt sich allein auf _require_operator_or_vendor. Kein zusätzlicher Tenant-Scope- Check wie in list_tenant_modules.
  • Heute: Routing erlaubt nur Operator/Vendor; kein Bug.
  • Fix (wenn Block 7 Operator-UI Tenant-Admin-Revoke erlaubt, nach Roadmap-Umnummerierung vom 2026-04-23): Explicit tenant-id-Match vor dem DELETE, plus RLS-Mode via app-Engine.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — DELETE /api/v1/tenants/{id}/modules/{module_id} prüft jetzt zuerst Tenant-Existenz vor dem DELETE auf tenant_modules. Vorher stilles No-Op bei typo'd Tenant-ID, jetzt 404.

Aus Block 7.3 Code-Review (2026-04-25)

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker. Review-Datei: reviews/block-7-3-modules-and-packages.md.

TODO-Block-7-3-02, -7-3-03 (Backend-Endpoints) im v1.3.0-D2-Lauf platform/v1.3.0-d2-backend-polish (2026-05-01) abgeschlossen. Frontend-Anteile (-04, -05, -06, -07) im v1.3.0-D1-Lauf platform/v1.3.0-d1-frontend-polish (2026-05-01) abgeschlossen, zusammen mit dem -02/-03 Frontend-Last-Mile (useModule + useTenantModules auf neue Endpoints migriert). Siehe Archiv am Ende. -01 (tenant_packages) bleibt offen für Block 12 (Provisioning).

TODO-Block-7-3-01 — tenant_packages Tier/Limits-Backend + UI (Pre-Flight 0b)

  • Severity: Medium (Score 65)
  • Datei(en): tenant_packages-Tabelle (alembic/platform/versions/0001_initial_schema.py:53-67), Frontend TenantsDetailPage.vue Tab "Pakete & Module"
  • Problem: Konzept §8.3 sieht Pro-Tenant-Pakete mit Tier (Starter/Pro/Enterprise) + Limits (max_chatbots, max_docs_per_chatbot, max_storage_mb, max_sources_per_chatbot, max_monthly_queries, avs_shared_enabled) vor. Tabelle existiert seit Block 1, hat aber keine API/Service/Pydantic-Schemas und 0 Rows live. Im Code gibt es kein Limits-Enforcement — die Felder sind heute Doku-only. Block 7.3 baut deshalb nur den Modul-Toggle.
  • Fix-Akzeptanz:
  • Backend: tenant_package_service.py mit get/upsert-Methoden, Routen GET/PATCH /api/v1/platform/tenants/{id}/package
  • Service muss Limits-Enforcement aktivieren (z. B. Chatbot- Lifecycle prüft max_chatbots, Document-Uploader prüft max_docs_per_chatbot)
  • Frontend: Tier-Dropdown + Number-Inputs für Limits in TenantsDetailPage Tab
  • Audit-Action tenant_package.updated
  • Rationale Deferred: Limits-UI ohne Enforcement ist Cosmetic- Doku-Work — würde User irreführen. Limits gehören sinnvoll in Block 12 (Tenant-Provisioning), wo Onboarding den Tier setzt und Lifecycle-Services die Enforcement implementieren.
  • Status: Open (Trigger Block 12)

TODO-Block-7-3-02 — useModule lädt komplette Liste statt Detail-Endpoint (Review M1)

  • Severity: Medium (Score 60)
  • Datei: frontend/operator-ui/src/composables/useModule.ts:18-35
  • Problem: Detail-Lookup ruft GET /platform/modules (volle Liste) und filtert clientseitig — Backend hat keinen /platform/modules/{id}-Endpoint. Heute akzeptabel (2 Module live), bei wachsendem Module-Inventar verschwendet das Bandbreite und CPU.
  • Fix: Backend-Detail-Endpoint GET /api/v1/platform/modules/{id} ergänzen, Composable umstellen auf Single-Call.
  • Rationale Deferred: Aktuell 2 Module, < 50 erwartet bis Block 13 (Connector-Subsystem bringt mehr). Trigger: > 50 Module ODER Backend bekommt Detail-Endpoint mit zusätzlichen Feldern, die nicht in der Liste sind.
  • Status: ✅ Erledigt in v1.3.0-D2 + D1 (Merges bb0abbd + 18c4884) — Backend-Endpoint GET /api/v1/platform/modules/{module_id} in D2 ergänzt; useModule in D1-Last-Mile auf Single-Call umgestellt, ModulesDetailPage-Spec deckt 404-Pfad ab.

TODO-Block-7-3-03 — Clientseitiger Set-Diff in TenantModulesSection (Review M2)

  • Severity: Medium (Score 55)
  • Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:36-53
  • Problem: Component lädt platform_modules und tenant_modules parallel und mergt clientseitig zu Modul × ist-aktiv-für-Tenant. Bei N Tenants und M Modulen ist das N+1-fähig — ein Aggregate-Endpoint GET /api/v1/platform/tenant-modules?include_unassigned=true würde es in einem Roundtrip lösen.
  • Fix-Optionen:
  • Pinia/Singleton-Cache für Plattform-Module über Tenants hinweg (Frontend-only, nutzt useModules-Cache zwischen Routes)
  • Backend-Aggregate-Endpoint mit Cross-Tenant-Listing
  • Rationale Deferred: Bei < 5 Tenants live ist der Doppel-Call kein Performance-Problem. Trigger: erstes Tenant-Onboarding mit

    50 Tenants ODER Block 7.4 Audit-Log-Viewer braucht ähnliches Aggregate-Pattern.

  • Status: ✅ Erledigt in v1.3.0-D2 + D1 (Merges bb0abbd + 18c4884) — Backend-Aggregate-Endpoint GET /api/v1/platform/tenant-modules?tenant_id=…&include_unassigned=true in D2 (Cross-Tenant-Fan-out ohne tenant_id ist 400, Block-12-Trigger). useTenantModules in D1-Last-Mile migriert; TenantModulesSection feuert nur noch einen Roundtrip.

TODO-Block-7-3-04 — Kein Optimistic-Update beim Toggle (Review M3)

  • Severity: Medium (Score 50)
  • Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:107-119
  • Problem: Nach assign() / revoke() macht der Component einen vollen tenant.load()-Refresh. Beim N-fachen Toggle innerhalb weniger Sekunden flickert die UI. Optimistic-Update würde das glätten.
  • Fix: Local-State-Update vor Backend-Call; Rollback bei Error.
  • Rationale Deferred: Bei aktuellem Volumen (< 5 Module pro Tenant) kaum sichtbar. Trigger: erste UX-Beschwerde oder Block 7.4 Bulk-Operations (das wäre Multiple-Toggle-im-Sturm).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — Optimistic-Update via patchRow vor POST/DELETE, Rollback per restoreRow bei Fehler. Tests flips badge optimistically on activate before the assign POST resolves und rolls back optimistic activate when the POST rejects decken beide Pfade.

TODO-Block-7-3-05 — Mutation-Errors auf Composable-Ebene ohne Detail (Review L1)

  • Severity: Low (Score 45)
  • Datei: frontend/operator-ui/src/composables/useTenantModules.ts:34-43
  • Problem: assign() und revoke() werfen rohe ApiError- Objekte; der Caller (TenantModulesSection) zeigt einen generischen Toast "Aktivieren fehlgeschlagen: <message>". Der body.detail- Field aus 422-Responses wird nicht extrahiert.
  • Fix: useApi um flattenError(err): {status, message, detail} erweitern; alle Caller adaptieren.
  • Rationale Deferred: Generic Toast ist heute ausreichend. Trigger: erste 422-Validation auf der Modules-Toggle-Route (heute unwahrscheinlich, weil Backend nur 400/404 wirft).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — flattenError(err) in composables/useApi.ts reduziert rohe ApiError-Objekte auf {status, message, detail}. Pydantic-422-Listen werden zu human-lesbaren Detail-Zeilen gejoint. TenantsListPage Bulk-Soft-Delete und TenantModulesSection Bulk-/Single-Toggle nutzen flattenError für Toast-Detail-Zeilen.

TODO-Block-7-3-06 — enabled_by-UUID-Truncation ohne Tooltip (Review L3)

  • Severity: Low (Score 35)
  • Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:198
  • Problem: enabled_by-Anzeige {{ row.tenant_state.enabled_by.slice(0, 8) }}… zeigt 8 UUID-Chars. Operator kann nicht erkennen, welcher User das war — fehlender Tooltip mit voller UUID oder Username-Lookup.
  • Fix: <span :title="row.tenant_state.enabled_by"> für Hover- Tooltip; mittelfristig Username-Lookup-Cache (Backend liefert nur UUIDs).
  • Rationale Deferred: Operator kennt heute alle Operator-User intern. Trigger: erste externe Operator-Personas (Block 17).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — TenantModulesSection zeigt die volle enabled_by-UUID als HTML-title-Tooltip auf dem trunkierten 8-Char-Display. Test renders enabled_by full UUID as title attribute asserted die UUID im title-Attribut.
  • Severity: Low (Score 30)
  • Datei: frontend/operator-ui/src/layouts/BaseLayout.vue:63
  • Problem: Footer-Text steht hartkodiert auf "Kora Platform · Operator-UI (Block 7.1b)" — durch 7.2 + 7.3 überholt. Sollte entweder generisch sein oder den aktuellen Block-Stand reflektieren.
  • Fix: Auf neutrales "Kora Platform · Operator-UI" ohne Block-Referenz reduzieren (so wie Tenant-UI das macht). Oder Vite- Env-Var einsetzen, die der CI/CD beim Build setzt.
  • Rationale Deferred: Pure Cosmetic. Trigger: Phase B Abschluss (vor v1.0.0-GA).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — Cosmetic-Cleanup in TenantsCreatePage-Description und TenantsDetailPage-Audit-Card (vorher „Block 7.3 / Block 7.4"). Footer war bereits durch den Layout-Refactor 2026-04-28 entfernt; D1 räumt die letzten veralteten Block-Strings im UI auf.

Aus Block 7.4 Code-Review (2026-04-25)

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker. Review-Datei: reviews/block-7-4-audit-bulk-connectors.md.

TODO-Block-7-4-01, -02, -03, -06, -07, -08 im v1.3.0-D2-Lauf platform/v1.3.0-d2-backend-polish (2026-05-01) abgeschlossen — siehe Archiv am Ende. -04 (KNOWN_ACTIONS-Drift, Block-13/14-Trigger) und -05 (Playwright workers, >50-Specs-Trigger) bleiben offen.

Review-Finding M1 (Score 75) — Filter-Shortcut aus TenantsDetailPage liest ?entity_id=… nicht wurde vor dem Merge im Commit a0ab7c9 (useAuditEntries.ts:readUrlFilters()) gefixt und ist daher nicht hier gelistet.

TODO-Block-7-4-01 — Bulk-Loop ohne Max-Cap und ohne Batch-Hydration (Review M2)

  • Severity: Medium (Score 70)
  • Datei(en): src/kora_platform/services/module_service.py:213-268, src/kora_platform/services/tenant_service.py:213-223
  • Problem: bulk_assign_to_tenant und bulk_soft_delete_tenants iterieren pro Item: session.get(...) + INSERT ... ON CONFLICT DO UPDATE RETURNING (Modules) bzw. session.get(...) + Soft-Delete- Update (Tenants). Bei 100 Items sind das ~200 Round-Trips, bei 1000 Items wird die UI-Latenz sichtbar. Es gibt zudem keinen Max-Cap im Backend — ein Operator könnte 10000 IDs submitten.
  • Fix-Akzeptanz:
  • Pre-Validate per WHERE id IN (...)-Bulk-Select
  • Single INSERT ... VALUES (...), (...) ON CONFLICT DO UPDATE (Modules) bzw. UPDATE tenants SET deleted_at = now() WHERE id IN (...) AND deleted_at IS NULL RETURNING id (Tenants)
  • Soft-Cap max_items=500 per Pydantic-Body (zentrale Konstante, konsistent für alle Bulk-Routen)
  • Rationale Deferred: Heute < 50 Tenants und < 5 Module/Tenant live; aktuelle 1× session.get + UPDATE liegt unter 500 ms gegen lokales Postgres. Trigger: > 200 Tenants ODER erste UI-Beschwerde über Bulk-Latenz.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — bulk_soft_delete_tenants und bulk_assign_to_tenant nutzen WHERE id IN (...)-Pre-Validate plus single Multi-Row-Statement statt N Per-Item-Roundtrips. Pydantic min_length=1, max_length=MAX_BULK_ITEMS=500 in beiden Bulk-Routen.

TODO-Block-7-4-02 — CSV-Export mappt forensische Felder nicht (Review M3)

  • Severity: Medium (Score 65)
  • Datei: src/kora_platform/services/audit_service.py:96-168
  • Problem: actor_keycloak_id, ip_address und session_id sind in der DB-Tabelle, im Pydantic-Read-Schema und im Frontend-TS-Type vorhanden — aber export_csv schreibt sie nicht in den CSV-Header. Forensisch ärgerlich, weil ein Audit-CSV ohne Quell-IP / Session- ID nicht mit den vCenter-/Keycloak-Logs korrelierbar ist.
  • Fix: Drei Spalten an den Header anhängen, gleiche Reihenfolge wie im Pydantic-Schema. Excel verkraftet zusätzliche Spalten.
  • Rationale Deferred: Erste Forensik-Anfrage gibt es noch nicht; bisherige Audits wurden mit include_details=true und JSON-Suche abgedeckt. Trigger: erster Compliance-Lauf mit Quell-IP-Anforderung.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — CSV-Export (GET /api/v1/platform/audit/export.csv) mappt jetzt actor_keycloak_id, ip_address, session_id in den Header. Test test_csv_export_includes_forensic_columns ergänzt.

TODO-Block-7-4-03 — Bulk-Audit-Auffindbarkeit per entity_id (Review M4)

  • Severity: Medium (Score 60)
  • Datei(en): src/kora_platform/api/routes/operator_tenants.py:329, src/kora_platform/api/routes/tenant_modules.py:312
  • Problem: Lutz-Konvention „eine Audit-Zeile pro Bulk-Aktion" schreibt entity_id="bulk:<n>" und packt die UUID-Liste in details.after. Ein Operator, der per entity_id=<tenant.id> im Audit-Filter sucht, findet das Bulk-Event nicht, sondern nur die N Einzel-Events (die es bei Bulk gar nicht gibt).
  • Fix-Optionen:
  • Konvention dokumentieren in docs-kora/docs/konzepte/audit.md (Hinweis: „Bulk-Events suchst du über action=*.bulk_* oder JSON-Search auf der UUID")
  • UI-Hint im AuditLogPage-Filter-Toolbar
  • Rationale Deferred: Single-Entry-Pattern ist die richtige Compliance-Lesbarkeit (ein Reviewer sieht „Operator hat 17 Tenants in einer Aktion soft-gelöscht"). Tradeoff ist die Auffindbarkeit; ein Konventions-Doc löst das ohne Code-Änderung. Trigger: erste Operator-Frage „warum finde ich Tenant X nicht im Audit-Log".
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — Neue Doku-Page operations/audit-conventions.md dokumentiert „eine Audit-Zeile pro Bulk-Aktion"-Konvention plus Anonymous-Actor-Pfad und CSV-Forensik-Felder.

TODO-Block-7-4-04 — KNOWN_ACTIONS-Drift gegen Block 13/14 (Review M5)

  • Severity: Medium (Score 55)
  • Datei: frontend/operator-ui/src/types/audit.ts:42-52
  • Problem: KNOWN_ACTIONS ist eine kuratierte Liste für das Action-Filter-Dropdown (heute: tenant.*, template.*, module.*, tenant_module.*). Block 13 (Connectors) und Block 14 (Lizenz- Audit) werden eigene Action-Codes mitbringen — wenn die Liste nicht mitwächst, sind die neuen Actions im Dropdown unsichtbar (User muss „Free-Text"-Filter nutzen, was Composable nicht unterstützt).
  • Fix: Bei Block 13/14 die jeweiligen Actions in KNOWN_ACTIONS ergänzen. Mittelfristig: Backend-Endpoint GET /api/v1/platform/audit/known-actions (DISTINCT-Query auf action-Spalte), Composable lädt einmal pro Session.
  • Rationale Deferred: Reine Erweiterungs-Aufgabe, keine Korrektur. Trigger: Block 13 oder 14 startet.
  • Block-11-Update (2026-05-01): Block 11 hat chatbot_feedback.created (anonymous-actor Widget-Schreibung) in die Liste eingetragen. Restliche Drift bleibt offen — Block 13/14 und potenzielle Cleanup-Welle vor Block 13.
  • Status: Open (Trigger Block 13/14; Block-11-Anteil eingetragen)

TODO-Block-7-4-05 — Playwright workers: 1 als Pragma (Review L1)

  • Severity: Low (Score 40)
  • Datei: frontend/operator-ui/playwright.config.ts:19
  • Problem: Playwright läuft sequenziell, weil parallele Worker sich denselben Backend-State (Tenants-Liste, Audit-Log) teilen und sich Rows „klauen" (Test A erstellt Tenant X, Test B sucht per Slug und filtert, Test A soft-deleted X mid-flight, Test B scheitert). Aktuell akzeptabel — 8 Specs unter ~30 s — aber bei wachsendem Test-Volumen zu langsam.
  • Fix: Per-Worker-DB-Schemas (jede Worker-Instanz bekommt eigene Postgres-Schema mit isolierter Tenants-Tabelle, Audit-Log). Playwright global-setup richtet die Schemas ein, env-Var an die Tests durchschleift. Komplex, lohnt erst bei > 50 Specs.
  • Rationale Deferred: Pragma korrekt für Stand v1.0.0. Trigger: Test-Volumen > 50 Specs ODER Wall-Time > 2 Min.
  • Status: Open

TODO-Block-7-4-06 — datetime.utcnow() (Py3.12 deprecated) (Review L2)

  • Severity: Low (Score 35)
  • Datei: src/kora_platform/api/routes/operator_audit.py:131
  • Problem: datetime.utcnow() wird in Python 3.12 mit DeprecationWarning versehen, in 3.13/3.14 vermutlich entfernt. Aktuell nur eine Stelle (CSV-Export-Filename).
  • Fix: datetime.now(UTC) (mit from datetime import UTC).
  • Rationale Deferred: Heute kein Warning sichtbar im Lint, weil Backend-Stack noch Py3.11. Trigger: Upgrade auf Py3.12 ODER irgendein anderer Bereich des Backends löst das Warning aus.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — datetime.utcnow()datetime.now(UTC) im CSV-Filename-Builder.

TODO-Block-7-4-07 — Date-Range-Validation date_from <= date_to fehlt (Review L3)

  • Severity: Low (Score 30)
  • Datei: src/kora_platform/api/routes/operator_audit.py (Filter-Pfad)
  • Problem: Wenn ein Operator versehentlich date_from > date_to setzt, gibt es kein 422-Error — die Liste ist einfach leer, was nach „kein Audit gefunden" aussieht.
  • Fix: Pydantic-Validator in AuditListParams (@model_validator mit mode="after") der prüft date_from <= date_to, oder Route- level assert mit explizitem 422.
  • Rationale Deferred: Kosmetisch; UI-Filter-Toolbar setzt Date-Range monoton (User wählt zuerst „from", dann „to"). Trigger: erste Support-Anfrage „Audit-Suche zeigt nichts".
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — _validate_date_range-Helper lehnt date_from > date_to mit 422 date_from_after_date_to ab. Unit-Tests test_validate_date_range_accepts_ordered_range und _rejects_inverted_range ergänzt.

TODO-Block-7-4-08 — Bulk-UI ohne Progress-Indicator (Review L4)

  • Severity: Low (Score 25)
  • Datei(en): frontend/operator-ui/src/pages/TenantsListPage.vue (Bulk-Bar), src/components/TenantModulesSection.vue (Bulk-Bar)
  • Problem: Toast nach Bulk-Soft-Delete bzw. Bulk-Aktivieren ist heute der einzige Latenz-Hinweis. Bei N+1-Bulk-Loop (siehe TODO-Block-7-4-01) wird die Latenz bei großen Bulks spürbar.
  • Fix: <FormButton :loading> während des Requests, optional ein einfacher „2/17 verarbeitet"-Counter. Echter Progress wird mit Cap-Hardening aus M2 redundant — also gemeinsam refactor'n.
  • Rationale Deferred: Heute keine spürbare Latenz (lokales Postgres, < 100 Items üblich). Trigger: gemeinsam mit M2 (Cap + Batch-Hydration).
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — Mit dem Cap-Hardening aus -7-4-01 obsolet geworden: Bulk-Cap MAX_BULK_ITEMS=500 plus single Multi-Row-Statement macht Per-Item-Progress redundant.

Aus Routing-Discovery (Merge 9c0c51d, 2026-04-25)

Beim Schreiben von operations/routing-and-endpoints.md sichtbar geworden. Beide vorher nur in CLAUDE.md/userMemories oder implizit im Init-Script-Pattern, keine TODO-Spur.

TODO-Platform-04 (iptables-Setup-Skript für Remote-vLLM-Node) wurde im Lauf platform/todo-platform-04-iptables-remote-node (2026-04-29) abgeschlossen — siehe Archiv am Ende.

TODO-Platform-10 (Mkdocs-Auto-Deploy bei Doc-Merge) wurde im Lauf platform/todo-platform-10-mkdocs-static-hosting (2026-04-29) als Option 3 (Build-static-Pattern mit nginx:alpine) abgeschlossen — siehe Archiv am Ende.

TODO-Auth-NEU — JWT-sub-Claim-Mapper-Fix (Drift #8)

  • Severity: Medium (Score 40, M2)
  • Entdeckt: Color-System-Implementation (Merge 7261426, 2026-04-28). 8. Drift-Datenpunkt im Auth-Stack-Drift-Aufräum-Vektor (Datenpunkte #1–#7 archiviert via TODO-Platform-05/-06/-07/-08/-09).
  • Datei(en): Realm-JSONs infra/keycloak/realms/kora-platform-realm.json und infra/keycloak/realms/kora-tenants-realm.json (Soll-Zustand); Init-Skript-Reconcile-Pfad analog zu create-operator-ui-client.sh; Live-Sync via kc-config-cli oder kcadm.
  • Befund: JWTs aus beiden Realms (kora-platform, kora-tenants) enthalten kein sub-Claim. Verfügbar im Token sind nur preferred_username, email, realm_access.roles, sid, iss, aud, exp/iat. Der kora-scope-Custom-Scope hat keinen Subject-Mapper, und Keycloak emittiert sub in der aktuellen Realm-Konfiguration nicht. Standard-OIDC weicht damit ab — sub ist normalerweise immer im Access-Token.
  • Bestehende Workarounds (nicht zu fixen, nur zu kennen):
  • NULL-Fallback in platform_audit_log.actor_keycloak_id UUID NULLABLE. Bei fehlendem sub (aktuell: jeder Token) wird NULL eingetragen; Audit-Trail referenziert User stattdessen über actor_user = preferred_username.
  • Composite-PK in user_preferences (realm, username) statt natürlicher keycloak_id UUID-PK (Color-System-Implementation, Migration 0008). Pragmatisch, funktional, aber nicht der Soll-Architektur-Pfad.
  • Risiko: Jede zukünftige Tabelle mit User-Bezug muss einen der beiden Workarounds nutzen. Mit jeder neuen Code-Stelle wächst die Sanierungs-Schuld linear (Settings-Erweiterungen, User-Activity- Tracking, Notifications-Preferences).
  • Fix-Pfad:
  • kora-scope-Client-Scope in beiden Realm-JSONs um einen sub-Mapper erweitern (oder via Standard-OIDC-Scope-Inclusion mit openid als Default-Scope sicherstellen)
  • kc-config-cli/kcadm-Sync gegen Live-Keycloak — idempotenter Init-Skript-Pfad analog zu create-operator-ui-client.sh
  • Re-Login-Test in beiden Realms — JWT enthält sub
  • scripts/verify-auth-stack.sh Check 58 wechselt von FAIL → PASS
  • Optional (separater Folge-Block): user_preferences-Migration zu UUID-PK + Daten-Migration der bestehenden Composite-Keys (Lookup über Username-zu-sub-Mapping pro Realm)
  • Optional (separater Folge-Block): Audit-Log-Backfill für historische actor_keycloak_id IS NULL-Zeilen (Lookup analog; nur wenn Daten-Wert hoch genug)
  • Aufwand-Schätzung: ~1.5h für Schritte 1–4, weitere ~2h für 5+6 falls erwünscht.
  • Verify-Mechanismus: scripts/verify-auth-stack.sh Check 58 testet die sub-Claim-Anwesenheit in einem frisch gemintten Token aus beiden Realms. Aktuell: FAIL (kein Regressionsfeind, das ist die explizite Drift-Frühwarnung).
  • Blockt: keine kritischen Features. Aber jede weitere User-bezogene Tabelle sollte den Fix erst abwarten, statt einen dritten Workaround zu erfinden.
  • Status: Open

TODO-Platform-07 (Tenant-UI Auth-URL-Drift) und TODO-Platform-09 (Systematische Auth-Stack-Verifikation) wurden im Lauf platform/todo-09-auth-stack-verification (2026-04-27) gemeinsam abgeschlossen — siehe Archiv am Ende.

Aus Browser-Walkthrough (2026-04-26)

Live-Browser-Walkthrough der Operator-UI nach TODO-Platform-09-Merge (Login mit bench-operator-admin, alle 5 Hauptseiten + Tenant-Detail- Tabs durchgegangen). Kein Funktions-Bug — vier UX-Polish-Findings, die das Erscheinungsbild verbessern, aber Block 8 (Tenant-UI) nicht blockieren.

TODO-UX-01 (Status-Wert-Konsistenz Liste vs. Detail) wurde im Lauf platform/ux-polish-status-consistency-and-cleanup (2026-04-29) abgeschlossen — siehe Archiv am Ende.

TODO-UX-02 (Test-Daten-Pollution Cleanup-Skript) wurde im Lauf platform/todo-ux-02-cleanup-test-data (2026-04-27) abgeschlossen — siehe Archiv am Ende.

TODO-UX-NEU (Cleanup-Pattern für op-vendor-* Auth-Test-Pollution) wurde am 2026-04-28 im Cleanup-Mini-Run als Datenpunkt erkannt und am 2026-04-29 im Lauf platform/ux-polish-status-consistency-and-cleanup opportunistisch mit-erledigt — siehe Archiv am Ende.

TODO-UX-03 wurde in Block 8.1 (platform/block-8.1-tenant-edit-ui, 2026-04-30) als Verifikations-only archiviert — Discovery zeigte vollständige Implementation seit Block 7.1b/7.4. Re-Diagnose-Lesson siehe Archiv am Ende.

TODO-UX-04 (Sprache-Filter Templates-Listing) wurde im Lauf platform/ux-04-language-filter-and-ux-03-deferral (2026-04-29) abgeschlossen — siehe Archiv am Ende.

Aus Block 7.2 Code-Review (2026-04-25)

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker. Review-Datei: reviews/block-7-2-templates-crud.md.

TODO-Block-7-2-01, -03, -04, -06, -07 im v1.3.0-D1-Lauf platform/v1.3.0-d1-frontend-polish (2026-05-01) abgeschlossen — siehe Archiv am Ende. -02 (Backend-Search-Endpoint für Templates) bleibt offen für > 50-Templates-Trigger; -05 (EntityForm-Refactor) bleibt verworfen.

TODO-Block-7-2-01 — ListInput A11y: Keyboard-Reorder + Live-Region (Review M1)

  • Severity: Medium (Score 60)
  • Datei: frontend/operator-ui/src/components/ListInput.vue:80-98
  • Problem: Add/Remove-Items haben keine aria-live-Region — Screenreader-Nutzer hören die State-Änderung nicht. Reorder per Tastatur fehlt komplett.
  • Fix: <div aria-live="polite" class="sr-only"> mit Status- Texten. Keyboard-Reorder via Alt+Up/Down, ARIA-Roles listbox/option.
  • Rationale Deferred: Phase-A wird intern (GTS+Vendor) betrieben. Trigger: Block-18-A11y-Audit oder erste externe Operator-Personas.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — ListInput.vue bekommt A11y-Polish: aria-live="polite"-Region annonciert Add/Remove/Reorder, Keyboard-Reorder via Alt+↑/↓, sichtbare ↑/↓-Move-Buttons. Tests Alt+ArrowDown reorders an item one position lower, Alt+ArrowUp at the top is a no-op, renders a polite live-region for screen-reader status ergänzt.

TODO-Block-7-2-02 — Backend-Search-Endpoint für Templates (Review M2)

  • Severity: Medium (Score 55)
  • Datei: frontend/operator-ui/src/composables/useTemplates.ts:39-54
  • Backend src/kora_platform/api/routes/chatbot_templates.py
  • Problem: Clientseitige Suche filtert über allItems. Bei > 200 Templates wird das spürbar.
  • Fix: ?search=<term>-Query-Param im Backend ergänzen, Frontend nutzt es analog zu useTenants.
  • Rationale Deferred: Aktuell < 5 Templates im System. Trigger: erstes Tenant-Onboarding mit > 50 Templates.
  • Status: Open

TODO-Block-7-2-03 — listsEqual ignoriert Reorder (Review M3)

  • Severity: Medium (Score 55)
  • Datei: frontend/operator-ui/src/pages/TemplatesEditPage.vue:88-91, 128-134
  • Problem: Pairwise-Compare. Bei Einführung von Reorder-UI würde das System eine neue Reihenfolge nicht als dirty erkennen.
  • Fix: JSON.stringify-Compare oder Index-bewusste Equality.
  • Rationale Deferred: Kein aktueller Bug — <ListInput> hat keine Reorder-UI. Trigger: gemeinsam mit TODO-Block-7-2-01 oder Connector-Reorder in 7.4.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — listsEqual aus neuem formHelpers.ts ist order-aware. TenantsEditPage und TemplatesEditPage nutzen den Helper konsistent.

TODO-Block-7-2-04 — Empty-List-Felder werden im POST mitgeschickt (Review L1)

  • Severity: Low (Score 45)
  • Datei: frontend/operator-ui/src/pages/TemplatesCreatePage.vue:78-87
  • Problem: suggested_suggestions: [] und recommended_connectors: [] landen immer im POST-Payload, auch wenn der User nichts hinzugefügt hat. Backend-Default ist [], semantisch identisch — Audit-Payload bläht sich.
  • Fix: Nur einsetzen wenn length > 0.
  • Rationale Deferred: Audit-Hygiene, kein Verhaltens-Bug. Trigger: wenn 7.4 Audit-Log-Reader die Empty-Lists als visual-noise flaggt.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — TemplatesCreatePage entfernt Empty-Lists aus dem POST-Payload — Audit-Rows tragen die Lists nur noch, wenn der User sie wirklich gefüllt hat.

TODO-Block-7-2-05 — EntityForm-Refactor-Decision (Review L2)

  • Severity: Low (Score 40)
  • Datei: frontend/operator-ui/src/pages/{Tenants,Templates}*Page.vue
  • Problem: TODO-Block-7-1b-04 hatte EntityForm-Refactor als Trigger „nach drei Form-Shapes" vorgemerkt. Block 7.2 fügt die zweite Shape hinzu. Der 7.2-Review empfiehlt: wahrscheinlich nicht durchführen — Tenants-Form (4 Felder, Slug-Validator) und Templates-Form (8 Felder, ListInputs) unterscheiden sich genug, dass ein gemeinsamer Wrapper Premature-Abstraction wäre.
  • Status:Verworfen in Block-7.3-Code-Review (Merge folgt). 7.3 baute keinen neuen Form-Datenpunkt — Modul-Toggle ist Single-Click, keine Form mit Feldern. Block 12 (Tenant-User- Provisioning-Form) und Block 13 (Connector-Configs mit Schema- Driven-Forms) bringen jeweils sehr unterschiedliche Form-Shapes; drei sehr ähnliche CRUDs entstehen nie. EntityForm-Refactor wird damit final verworfen.

TODO-Block-7-2-06 — Busy-Wait-Sleep im Token-Mint-Retry (Review L3)

  • Severity: Low (Score 35)
  • Datei: frontend/operator-ui/e2e/helpers.ts:65-70
  • Problem: while (Date.now() - start < ms) { /* spin */ } blockiert den Worker-Prozess. Akzeptabel (Worker macht nichts anderes), aber unsauber.
  • Fix: mintTokens async machen + await sleep(ms).
  • Rationale Deferred: Kein Throughput-Problem. Trigger: wenn die Suite auf 4+ Worker skaliert.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — mintTokens in e2e/helpers.ts ist async; der vorher synchrone Spin-Loop im Backoff blockiert den Worker-Event-Loop nicht mehr. Alle E2E-Spec-Caller (8 Specs) sind auf await mintTokens() angepasst.

TODO-Block-7-2-07 — Trim-Diff inkonsistent (Review L4)

  • Severity: Low (Score 30)
  • Datei: frontend/operator-ui/src/pages/TemplatesEditPage.vue:98, 102
  • Problem: description.trim()-Compare bei manchen Feldern, andere Felder ohne Trim. Analog zu TODO-Block-7-1b-07 (Tenants-Edit).
  • Fix: Helper trimEqual(a, b) für alle String-Felder.
  • Rationale Deferred: Kein aktueller Bug. Gemeinsam mit TODO-Block-7-1b-07 lösen.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — trimEqual aus neuem formHelpers.ts (whitespace-only-tolerantes Edit-Diff) wird in TenantsEditPage und TemplatesEditPage konsistent genutzt.

Aus Block 7.1b Code-Review (2026-04-25)

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker. Review-Datei: reviews/block-7-1b-operator-ui-frontend.md.

TODO-Block-7-1b-01, -05, -06, -07, -08 im v1.3.0-D1-Lauf platform/v1.3.0-d1-frontend-polish (2026-05-01) abgeschlossen — siehe Archiv am Ende. -02 (Slug-availability-Backend-Endpoint) bleibt offen für > 200-Tenants-Trigger; -03 schon in Block 7.3 erledigt; -04 final verworfen.

TODO-Block-7-1b-01 — E2E-Seed-Hook ohne Build-Flag-Gate (Review M1)

  • Severity: Medium (Score 65)
  • Datei: frontend/operator-ui/src/composables/useAuth.ts:46-57
  • Problem: applyE2eSeed() liest window.__KORA_E2E_SEED__ ohne Vite-Build-Flag-Check. In Prod ist das Feld nie real gesetzt — aber ein DOM-Clobbering-Angriff (<form id="__KORA_E2E_SEED__">) könnte die Auth-State manipulieren, bevor main.ts läuft. Threat-Model ist niedrig (Operator-User loggt sich auf vertrauenswürdiger Maschine ein), Mitigation aber trivial.
  • Fix: if (import.meta.env.VITE_E2E_MODE !== "true") return; in applyE2eSeed. Build-Flag ist im env.d.ts schon deklariert.
  • Rationale Deferred: Aktive Bedrohung erfordert XSS oder DOM- Injection vor dem Vue-Mount. Trigger: Security-Pass vor v1.0.0-GA (Block 14 Deployment) oder erste externe Pen-Test-Runde.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — E2E-Seed-Pfad in useAuth.applyE2eSeed() ist hinter import.meta.env.VITE_E2E_MODE === "true" gegated. Dockerfile.platform akzeptiert ARG VITE_E2E_MODE, docker-compose.platform.yml setzt ihn auf ${VITE_E2E_MODE:-true} für luki-ai. Production-Build (z. B. zukünftige GA-CI-Pipeline) überschreibt mit leerem Wert; DOM-Clobbering-Angriff (<form id="__KORA_E2E_SEED__">) kann den Auth-State dann nicht mehr kapern.

TODO-Block-7-1b-02 — Slug-Availability-Check ILIKE-Substring (Review M2)

  • Severity: Medium (Score 60)
  • Datei: frontend/operator-ui/src/pages/TenantsCreatePage.vue:76-88
  • Problem: Slug-Verfügbarkeits-Check nutzt ?search=<slug>&limit=5 und macht einen Exact-Match-Filter clientseitig. Bei slug="acme" und einem existierenden acme-gmbh würde der limit=5-Cutoff den exakten Match zurückliefern, wenn er in den ersten 5 ist — aber ein zweiter acme darunter könnte dropped werden. Datenrisiko null (Backend-409 fängt jedes Race), aber UX-Stolperer.
  • Fix: Dedizierten GET /platform/tenants?slug=<slug> (exact- match) im Backend ergänzen, oder limit=200 setzen.
  • Rationale Deferred: Smoke-Test-10 (§14.4) bestätigt, dass kein Daten-Bug entsteht. UX-Glitch erst sichtbar bei > 200 Tenants mit ähnlichen Slugs. Trigger: 7.3 (Modules-Registry braucht ähnlichen Pattern, könnte Backend-Endpoint mitbringen).
  • Status: Open

TODO-Block-7-1b-03 — Tab-Row A11y in DetailPage (Review M3)

  • Severity: Medium (Score 55)
  • Datei: frontend/operator-ui/src/pages/TenantsDetailPage.vue:131-149
  • Problem: Tab-Row nutzt <div class="tab tab--disabled"> mit title-Tooltip statt einer Tablist-ARIA-Pattern oder einfach <button disabled>. Screenreader hören die Tabs nicht; Tab-Tasten- Fokus überspringt sie.
  • Fix: Echtes Tablist-Pattern (role="tablist" + role="tab" + aria-selected + aria-disabled) ODER <button type="button" disabled :title="...">. Letzteres ist 1-zu-1.
  • Rationale Deferred: Phase-A v1.0.0 wird intern (GTS+Vendor) betrieben. Trigger: erste externe Operator-Personas (Block 17 Tenant-SSO-Self-Service deployt einen Vendor-User-Onboarding-Flow, da kommen externe Operatoren rein) oder vor v1.1.0-GA.
  • Status: ✅ Erledigt in Block 7.3 (Merge folgt) — Tab-Row beim Aktivieren des „Pakete & Module"-Tabs von <div>-Elementen auf echte <button>-Tabs umgestellt: aria-active-State, disabled- Attribut auf den Block-7.4-Placeholdern, focus-visible-Outline, cursor:not-allowed-Style. Volle ARIA-tablist-Rolle nicht umgesetzt (würde noch role="tablist"/role="tab"/aria-controls brauchen) — als Folge-TODO für externe-Personas-Pass beobachten, aber Screenreader-Basis ist jetzt da.

TODO-Block-7-1b-04 — Form-Markup-Overlap Create/Edit (Review L1) ✅ Verworfen

→ siehe TODO-Block-7-2-05 oben — finaler Verzicht im Block-7.3-Code- Review begründet (kein dritter ähnlicher Form-Datenpunkt entsteht).

  • Severity: Low (Score 45)
  • Datei: frontend/operator-ui/src/pages/TenantsCreatePage.vue + TenantsEditPage.vue
  • Problem: ~120 LOC Overlap zwischen Create- und Edit-Formular (Display-Name, E-Mail, Notes mit identischer Validierung). Slug- Verhalten unterscheidet sich (Create: live-Verfügbarkeit, Edit: disabled), Submit-Handler auch (Create: POST, Edit: Diff-PATCH) — also keine 100%-Wiederverwendung möglich.
  • Fix (wenn 7.2 Templates und 7.3 Modules ähnlichen Bedarf haben): Generic <EntityForm v-slot="{ field }">-Component, die das Layout
  • Standard-Field-Wrapping liefert; Create/Edit-Pages liefern die Slot-Inhalte und den Submit-Handler.
  • Rationale Deferred: DRY ohne 2/3 weitere Use-Cases ist premature. Trigger: 7.2 (Templates-Form) baut die zweite, 7.3 die dritte Variante — dann sind die gemeinsamen Punkte sichtbar.
  • Status: ❌ Verworfen in Block-7.3-Code-Review (siehe Header oben + TODO-Block-7-2-05). Kein dritter ähnlicher Form-Datenpunkt entsteht; Block 12 (Tenant-User-Provisioning) und Block 13 (Connector-Configs) bringen unterschiedliche Form-Shapes.

TODO-Block-7-1b-05 — Kein Stale-While-Revalidate in useTenants (Review L2)

  • Severity: Low (Score 40)
  • Datei: frontend/operator-ui/src/composables/useTenants.ts
  • Problem: Jede Navigation zurück zur Liste triggert kompletten Re-Fetch (onMounted(load) in TenantsListPage). Akzeptabel bei < 100 Tenants, kann auf 3G-Verbindung als Loading-Flash sichtbar werden.
  • Fix: Stale-While-Revalidate-Pattern: Cache letzte Response, zeige sie sofort, fetche im Hintergrund. Vue-Query oder eigener Composable.
  • Rationale Deferred: Operator-UI ist Office-Netzwerk-Nutzung. Trigger: > 200 Tenants oder mobile-Operator-Use-Case (post-v1.0.0).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — useTenants hat jetzt einen Module-Scope SWR-Cache (Stale-While-Revalidate, key = serialisierte List-Params, 30s TTL). Mutationen rufen invalidateCache. Tests decken first-load (network), warm-cache (no network), mutation-invalidate, stale-revalidate ab.

TODO-Block-7-1b-06 — useConfirm Singleton-Auto-Reject (Review L3)

  • Severity: Low (Score 35)
  • Datei: frontend/operator-ui/src/composables/useConfirm.ts
  • Problem: Wenn ask() aufgerufen wird während ein Dialog offen ist, resolved der erste mit false. Aktuell unerreichbar (UI hat keine zwei parallelen Confirm-Trigger), aber überraschend für spätere Caller.
  • Fix: Entweder Queue (zweiter Dialog wartet) oder Throw (UnableToOpenConfirm). Queue ist UX-freundlicher.
  • Rationale Deferred: Aktuell unerreichbar. Trigger: 7.3 (Bulk- Ops mit "alle X-Tenants löschen?"-Confirm pro Item könnte das triggern).
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — useConfirm ist auf eine FIFO-Queue umgestellt. Überlappende ask()-Calls landen hintereinander statt den ersten still mit false abzuwürgen. Test queues a second ask while the first is open and serves them in order ersetzt den vorherigen Auto-Reject-Test.

TODO-Block-7-1b-07 — PATCH-Diff ohne String-Trim (Review L4)

  • Severity: Low (Score 30)
  • Datei: frontend/operator-ui/src/pages/TenantsEditPage.vue:75-84
  • Problem: Dirty-Detection via String-Vergleich. form.value.notes !== initial.value.notes löst ein notes: null in Payload aus, wenn der User nur Whitespace anhängt und dann zurückspringt — User sieht keinen visuellen Unterschied, aber Notes werden überschrieben.
  • Fix: Vor dem Diff-Vergleich .trim() auf Initial- und Form- Werten anwenden, oder Diff-Funktion verschönern.
  • Rationale Deferred: Edge-Case bei manuell editiertem Whitespace. Trigger: erstmal Audit-Log auswerten (Block 7.4) — wenn dort viele notes: null-Updates ohne erkennbaren Anlass auftauchen.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — trimEqual aus formHelpers.ts (whitespace-only-tolerantes Edit-Diff) wird in TenantsEditPage und TemplatesEditPage konsistent genutzt. PATCH schickt notes: null nicht mehr nur, weil der User Whitespace angehängt hat.

TODO-Block-7-1b-08 — PORTING.md als Repo-Artefakt (Review L5)

  • Severity: Low (Score 25)
  • Datei: frontend/operator-ui/PORTING.md
  • Problem: Pre-Flight-Artefakt aus Phase 1 ist im Code-Repo, nicht in docs-kora/docs/blocks/. Spätere Leser könnten es übersehen (Code-Reviewer schauen erst auf docs-kora/, nicht in Source-Bäume).
  • Fix: Nach docs-kora/docs/blocks/block-7-1b-porting.md verschieben, im operator-ui-README darauf verlinken.
  • Rationale Deferred: Aktiv genutzt nur für Block-7.2-Autor; die Verschiebung kann in 7.2 mit dem neuen <EntityForm>-Refactor (siehe -04) gebündelt werden.
  • Status: ✅ Erledigt in v1.3.0-D1 (Merge 18c4884) — frontend/operator-ui/PORTING.md ist nach docs-kora/docs/blocks/block-7-1b-porting.md verschoben (per git mv). Operator-ui/README.md verlinkt die neue Position; mkdocs.yml hat eine neue nav-Sektion „Blocks (Pre-Flight & Porting)".

Aus Block 7.1a Code-Review (2026-04-24)

Alle Findings haben Review-Score < 80 → Follow-up-Backlog, kein Merge-Blocker. Review-Datei: reviews/block-7-1a-tenants-backend.md.

TODO-Block-7-01, -02, -03, -04, -06, -07 im v1.3.0-D2-Lauf platform/v1.3.0-d2-backend-polish (2026-05-01) abgeschlossen — siehe Archiv am Ende. -05 (pydantic[email]-Supply-Chain) bleibt offen für ein separates Supply-Chain-Review vor v1.0.0-GA-Audit.

TODO-Block-7-01 — Operator-Route-Präfix-Vereinheitlichung + Scope-Guards zentralisieren

  • Severity: Low (Score 45)
  • Datei(en): src/kora_platform/api/routes/operator_tenants.py, tenant_modules.py, chatbot_templates.py — je eine lokale _require_operator_or_vendor-Kopie; operator_tenants.py ergänzt dazu _require_operator.
  • Problem: Drei (mit 7.2/7.3/7.4 künftig vier bis fünf) identische Kopien der Scope-Guards. Pfad-Präfix ist jetzt /api/v1/platform/* für Platform-Level-Routen und /api/v1/tenants/* für Tenant-Scope — konsistent, aber nur durch Konvention, nicht durch einen zentralen Helper erzwungen.
  • Fix: api/dependencies/scope_guards.py mit require_operator(), require_operator_or_vendor(), require_tenant()-Dependencies; Route-Lokal-Kopien durch Imports ersetzen. Lohnt sich, wenn alle 7.2/7.3/7.4-Operator-Routen da sind.
  • Rationale Deferred: Kosmetik + DRY, keine Verhaltensänderung. Trigger: nach 7.4, wenn der Umfang der Operator-Routes stabil ist.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — Zentraler Scope-Guard-Helper kora_platform.api.dependencies.scope_guards. Vier Routen-Files (operator_tenants, operator_audit, tenant_modules, chatbot_templates) ziehen jetzt zentralen Helper statt lokaler Kopien. _require_any_scope und _require_tenant_owns_chatbot in chatbot_templates.py bleiben lokal (template-spezifisch).

TODO-Block-7-02 — GET-List TOCTOU Window-Function (Review M1)

  • Severity: Medium (Score 65)
  • Datei: src/kora_platform/api/routes/operator_tenants.py:86-108 (bzw. tenant_service.py::list_tenants)
  • Problem: Pagination-Count und Items-Select sind zwei Roundtrips; zwischen ihnen kann ein paralleler Create/Delete total verschieben. Heute kosmetisch (Operator-UI max. 50 Rows, low Traffic).
  • Fix: Eine SELECT mit COUNT(*) OVER () — ein Roundtrip, identischer Snapshot.
  • Rationale Deferred: Tenant-Volumen < 1000 auf absehbare Zeit. Trigger: sobald Live-Filter-driven-UI oder > 1000 Tenants.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — TenantService.list_tenants nutzt count(*) OVER ()-Window-Function in einer Single-Query. Empty-Page-Fallback per Count-Only-Query (Window liefert nur Werte, wenn die Page selbst Rows hat).

TODO-Block-7-03 — Session-Identity-Trap beim Audit-Delta (Review M2)

  • Severity: Medium (Score 60)
  • Datei: src/kora_platform/api/routes/operator_tenants.py:198-221
  • Problem: changed_fields(before, patch) liest Attribute aus der ORM-Instanz, die update_tenant gleich mutieren wird. Heute korrekt (Python-setattr passiert nach dem Vergleich), fragil wenn der Service auf Bulk-SQL umstellt.
  • Fix: Vor dem Aufruf before_snapshot = {f: getattr(before, f) for f in TenantUpdate.model_fields} — entkoppelt das Diff vom ORM- State.
  • Rationale Deferred: Kein aktueller Bug. Trigger: wenn update_tenant auf session.execute(update(...)) umgestellt wird (Bulk-Patch-Use-Case z. B. in Block 12 Provisioning-Automatisierung).
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — TenantService.changed_fields snapshotet Before-Werte in plain-dict vor dem Diff. Entkoppelt das Audit-Delta vom ORM-State, sodass eine zukünftige Bulk-Update-Variante nicht den Audit-Trail aliased.

TODO-Block-7-04 — updated_at server-side onupdate (Review M3)

  • Severity: Medium (Score 55)
  • Datei: src/kora_platform/services/tenant_service.py:155, src/kora_platform/db/models/tenant.py (Column-Definition)
  • Problem: update_tenant setzt updated_at = datetime.now(UTC) Python-seitig statt via Column-level onupdate=func.now(). In einem Multi-Host-Deployment können die Uhren leicht abweichen; der Service- Layer führt das als Business-Logic, statt die DB die Wahrheit sagen zu lassen. Parallel zu TODO-Block-5g (tenant_modules.updated_at- Pendant im Upsert).
  • Fix: onupdate=func.now() an tenant.updated_at ergänzen (Alembic-Migration) + datetime.now(UTC)-Aufruf im Service entfernen.
  • Rationale Deferred: Single-Host-Dev, keine Observability-Relevanz heute. Trigger: vor Multi-Host-Deployment oder wenn Block 12 die Audit-Timestamps vereinheitlicht.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — Alembic 0010 installiert PG-Trigger set_updated_at() auf tenants, tenant_modules, platform_modules (gemeinsam mit -5g). Service-Code setzt updated_at nicht mehr Python-seitig. update_tenant und soft_delete_tenant rufen jetzt await session.refresh(tenant), damit der trigger-bumped Wert beim Caller ankommt.

TODO-Block-7-05 — pydantic[email] in Runtime-Image (Review L2)

  • Severity: Low (Score 40)
  • Datei: pyproject.toml:22
  • Problem: Durch pydantic[email] landet email-validator + transitiv idna + dnspython im Runtime-Image. check_deliverability=False ist Default — keine Runtime-DNS-Queries —, aber Supply-Chain-Footprint wächst.
  • Fix (Option): eigener Email-Regex-Validator in models/tenant.py; pydantic[email] durch plain pydantic ersetzen. Kostet Validation-Qualität (RFC 5322 ist nicht regex-abbildbar).
  • Rationale Deferred: Wiegt Validation-Qualität gegen Supply-Chain. Trigger: Supply-Chain-Review vor v1.0.0-GA oder wenn ein Security- Audit email-validator/dnspython als Angriffsfläche flaggt.
  • Status: Open

TODO-Block-7-06 — search ILIKE-Metachar-Escape (Review L3)

  • Severity: Low (Score 35)
  • Datei: src/kora_platform/services/tenant_service.py::list_tenants
  • Problem: search-Query-Param wird ungefiltert in %…%-ILIKE eingebettet. Enthält der String % oder _, werden sie als ILIKE- Wildcards interpretiert (UX-Glitch; kein Injection-Risiko — die Bind-Parameter bleiben geschützt). Slug-Regex enthält diese Chars nicht, display_name kann sie aber haben.
  • Fix: Escape-Helper (search.replace("%", "\\%").replace("_", "\\_")) + ESCAPE '\\'-Klausel in der Query.
  • Rationale Deferred: Bisheriges UI (keins) zeigt keine Wildcards; Trigger: sobald die Operator-UI (7.1b) die Filter-Box live ausliefert.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — ILIKE-Metachar-Escape im TenantService.list_tenants-Search-Pfad. Helper _escape_ilike escapt \\, %, _. Test test_search_escapes_ilike_metachars beweist, dass _safe-Suche nicht mehr safe-Slugs zieht.

TODO-Block-7-07 — include_deleted-Vendor-Pfad ohne Test (Review L4)

  • Severity: Low (Score 30)
  • Datei: tests/integration/test_operator_tenants_api.py
  • Problem: Vendor-Scope darf laut Guard GET /tenants + include_deleted=true lesen; kein expliziter Test deckt den Vendor- mit-Flag-Pfad. Operator-Pfad und Flag-Pfad sind separat getestet.
  • Fix: Kleiner Test test_vendor_can_list_deleted.
  • Rationale Deferred: Reines Gap, kein Bug. Trigger: bundled mit 7.1b Vendor-Audit-Log-UI oder 7.4.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — Test test_vendor_can_list_with_include_deleted deckt den Vendor-Pfad mit include_deleted=true.

Aus Block-5-Cleanup Code-Review (2026-04-24)

TODO-Block-5g — updated_at wird vom ON CONFLICT DO UPDATE-Pfad nicht mitgeschrieben

  • Severity: Low (Score 40)
  • Datei: src/kora_platform/services/module_service.py:150-157, src/kora_platform/db/models/platform_module.py (Column-Definition TenantModule.updated_at)
  • Problem: Das SET-Dict des Upserts setzt is_enabled, enabled_by, enabled_at, aber nicht updated_at. Die Column hat nur server_default=func.now(), kein onupdate=func.now() → ein Re-Assign über ON CONFLICT DO UPDATE lässt updated_at auf dem ursprünglichen Insert-Timestamp stehen. Heute kein Consumer betroffen; sobald aber ein „changed-since"-Filter oder eine UI-Sortierung nach letzter Änderung gebaut wird, wird das zum Bug.
  • Fix (eine der beiden Varianten):
  • "updated_at": func.now() in den set_={…}-Dict des Upserts aufnehmen. Minimal-invasiv, keine Schema-Änderung.
  • onupdate=func.now() zur updated_at-Column ergänzen. Saubere ORM-Semantik, erfordert Alembic-Migration.
  • Rationale Deferred: Kein aktiver Consumer; Fix ist Ein-Liner und kann gebündelt mit Block 7 (Operator-UI, zeigt „zuletzt geändert") oder Block 12 (Provisioning, nutzt Audit/Timestamps) kommen.
  • Status: ✅ Erledigt in v1.3.0-D2 (Merge bb0abbd) — gemeinsam mit -7-04 als Alembic 0010 implementiert: PG-Trigger set_updated_at() auf tenants, tenant_modules, platform_modules. Saubere Variante 2 gewählt (server-side onupdate).

Aus Block 4 Phase F (2026-04-22)

TODO-Block-4b — Qdrant Backup-Strategie

  • Severity: Medium (Score 60)
  • Scope: Snapshot-Integration, Storage-Lokation, Restore-Runbook
  • Aufwand: ~8-12h
  • Rationale: Wurde aus Block 4 herausgezogen, um den Block auf 24-28h statt ~40h zu begrenzen. Fundament §6 hatte Backup partiell drin. Kommt als eigener Block, sobald Block 4 stabil ist.
  • Inhalt:
    • Qdrant-native Snapshots via HTTP-API (/collections/<name>/snapshots)
    • Scheduler-Job für periodisches Snapshotting (täglich, Retention 14 Tage)
    • Storage: lokaler Volume-Mount, langfristig S3-kompatibel
    • Restore-Runbook mit konkretem Wiederherstellungs-Pfad
  • Status: Open

TODO-Block-4c — Template-Level Shared-Collection-Gate

  • Severity: Low (Score 35)
  • Scope: PlatformRetrieval.search() erweitern, damit Shared- Collection auch dann nicht abgefragt wird, wenn das zugehörige chatbot_template.shared_collection_name IS NULL ist.
  • Hintergrund: Fundament §6.2 spezifiziert einen zweistufigen Gate: chatbots.uses_shared_docs = TRUE UND chatbot_template.shared_collection_name IS NOT NULL. Die erste Bedingung (Chatbot-Level) ist mit Block 4 Phase D.1 umgesetzt. Die zweite (Template-Level) wurde verschoben, weil Chatbot- Templates erst mit Block 5 volles CRUD bekommen.
  • Aufwand: ~1h (inkl. Template-Join in der Retrieval-Query)
  • Abhängigkeit: Block 5 (Chatbot-Templates & CRUD) — nach Roadmap-Umnummerierung vom 2026-04-23. Nicht zu verwechseln mit dem historischen "Block 5" (Plattform-Module), der nach der Umnummerierung Block 6 ist.
  • Rationale Deferred: Ohne Template-CRUD gibt es keine effektiven Template-Configs. Der Gate würde aktuell gegen ein leeres Feld prüfen und wäre ein Null-Op.
  • Status: ✅ Erledigt in Sub-Commit c898d85 (Block-5 Sub-Commit 4, gemerged als 2a3e5b0 am 2026-04-24). Lösung: PlatformRetrieval._template_allows_shared() prüft zusätzlich zum Chatbot-Level-Gate, ob chatbot.template_id gesetzt ist und das Template ein shared_collection_name IS NOT NULL deklariert. Chatbots ohne Template fallen ebenfalls geschlossen. Drei neue Unit-Tests in tests/unit/test_retrieval.py; Fixture in tests/integration/test_qdrant_tenant_isolation.py seedet jetzt ein Test-Template mit shared_collection_name. 52/52 Regression-Tests grün, inkl. 200×-Cross-Tenant-Stress-Pattern.

TODO-Platform-03 — File-Write-Hook blockt Substring-Match auf Eval

  • Severity: Low (Score 20)
  • Datei(en): Claude-Code-Hook-Konfiguration (nicht im Repo)
  • Problem: Der lokale Write-Hook blockt File-Writes, deren Inhalt die Substring "eval" enthält (Security-Check gegen Python-Eval-Calls). Triggert false-positive bei Wörtern wie "retrieval", "evaluation", "medieval", "devaluation". Workaround: Bash-Heredoc statt Write-Tool, oder Datei zunächst mit Platzhalter schreiben und dann via sed füllen.
  • Vorschlag: Hook-Pattern schärfen auf Wort-Grenzen (z.B. \beval\b statt eval). Alternative: Whitelist für häufige false-positive- Wörter.
  • Rationale Deferred: Claude-Code-Tool-Config, kein Platform-Code. Wird nur erwähnt, weil der Workaround-Aufwand bereits dreimal in Block 4 angefallen ist.
  • Status: Open

TODO-Platform-14 (Template-Mutations ohne Audit-Trail) wurde in chore/todo-platform-14-template-audit (2026-04-30) abgeschlossen — siehe Archiv am Ende.

Aus v1.0.0-GA-Akzeptanz-Lauf (2026-04-30)

Drei Datapoints aus dem v1.0.0-Tag-Run, die operativ Friction verursachen, aber nicht Tag-blockierend waren — siehe docs-kora/docs/releases/v1.0.0-acceptance.md für die Acceptance- Kontext-Discovery-Befunde.

TODO-Platform-11 — Backend-Pytest-Env-Bootstrap

  • Severity: Low (Score 35)
  • Datei(en): noch nicht im Repo — Soll: make test-setup oder scripts/bootstrap-test-env.sh, plus Eintrag im docs-kora/docs/operations/-Runbook
  • Problem: Lokales Backend-Pytest-Setup ist nicht out-of-the-box. email-validator ist nicht in der Test-Dependency-Gruppe installiert, und DSN-Env-Vars (KORA_DB_APP_DSN, KORA_DB_VENDOR_DSN, KORA_DB_ADMIN_DSN) sind nur via docker-compose.platform.yml gesetzt — nicht im .venv-Lauf. Beim v1.0.0-GA-Akzeptanz-Lauf musste .venv/bin/pytest mit .env.platform-Source plus manueller DSN-Construction laufen. Dieses Workaround-Wissen liegt in keinem Skript.
  • Vorschlag: make test-setup-Target oder scripts/bootstrap-test-env.sh das (a) alle Test-Dependencies inklusive email-validator ins .venv installiert, (b) die benötigten DSN-Env-Vars aus .env.platform ableitet und in eine .env.test schreibt (oder als shell-export-Block emittiert), (c) idempotent ist (mehrfacher Lauf bricht nicht), (d) im Operations-Runbook referenziert wird.
  • Trigger: Vor Block 14 (CI-Pipeline-Setup) zwingend, weil CI-Runner ein reproduzierbares Test-Setup brauchen. Kann früher kommen, falls ein Block-8-Run Backend-Tests lokal benötigt.
  • Quelle: v1.0.0-GA-Akzeptanz-Lauf, Discovery-Datapoint #2 (siehe releases/v1.0.0-acceptance.md).
  • Status: Open

TODO-Platform-12 — Pytest-Profil-Trennung Platform vs. AVS-Demo

  • Severity: Low (Score 45)
  • Datei(en): pyproject.toml ([tool.pytest.ini_options]), evtl. neue pytest-platform.ini oder Marker-Decorator-Sweep über tests/
  • Problem: AVS-Demo-Tests (tests/unit/test_pipelines.py, test_cache.py, test_rate_limit.py, test_semantic_cache.py, test_api.py, tests/integration/test_qdrant_tenant_isolation.py, test_rag_e2e.py) liegen im selben tests/-Verzeichnis wie die kora-Platform-Tests. Voller pytest tests/-Lauf produziert 19 Failures + 109 Errors in den Phase-A-Demo-Tests, die für die Platform irrelevant sind und Akzeptanz-Matrix-Reads verfälschen. v1.0.0-GA-Lauf hat das pragmatisch per --ignore-Flag-Cluster umgangen — implizite Konvention, die jeder Operator neu lernen muss.
  • Vorschlag (eine Variante wählen):
  • Pytest-Marker: @pytest.mark.platform / @pytest.mark.demo plus markers-Liste in pyproject.toml plus addopts = "-m platform" als Default. Klassisch.
  • Separate Pytest-Config-Files: pytest-platform.ini / pytest-demo.ini mit testpaths-Isolation. Robust, aber Doppel-Pflege.
  • Verzeichnis-Trennung: Demo-Tests nach tests-avs-demo/ verschieben. Sauber, aber größere Diff. Kompatibel mit Demo-eingefroren-Status. Akzeptanz: Acceptance-Matrix-Skripte aus releases/v1.x.0-acceptance.md verwenden den Platform-Filter als Default — kein --ignore-Cluster mehr.
  • Trigger: Vor Block 14 (CI), oder beim ersten Block-8- Backend-Test-Lauf wenn voller Lauf gewünscht ist.
  • Quelle: v1.0.0-GA-Akzeptanz-Lauf, Discovery-Datapoint #3.
  • Status: Open

TODO-Platform-13 (Module-Auto-Seed) wurde in Block 8.0 (platform/block-8.0-foundation, 2026-04-30) mit Variante 1 (Lifespan-Hook) erledigt — siehe Archiv am Ende.

Aus Block 8.6/8.7-Hand-off (2026-04-30)

TODO-Platform-15 — Playwright-E2E-Coverage-Lücke für Chatbot-Sub-Routes ✅ ERLEDIGT MIT BLOCK 11 (2026-05-01)

  • Severity: Low (Score 35)
  • Status: ✅ Erledigt mit Block 11 Phase 4 (Branch platform/block-11-widget-integration, Merge 149c699).
  • Auflösung: Drei neue Tenant-UI-Specs (frontend/tenant-ui/e2e/chatbot-sub-routes.spec.ts, widget-public-api.spec.ts) und ein Operator-UI-Spec (frontend/operator-ui/e2e/tenant-branding-sub-route.spec.ts) decken die Lücke. Sie folgen dem skip-if-env-not-set-Pattern aus template-update.spec.ts (E2E_CHATBOT_ID/E2E_TENANT_ID), damit CI ohne provisionierte Fixture-Daten nicht bricht. Das Test-Daten-Setup (SQL-Fixture für bench-tenant-a + bench-chatbot- 000 mit Branding/Feedback-Mock-Rows) bleibt als Folge-Aufgabe für einen E2E-Seed-Sweep — die Specs selbst sind aber lauffähig, sobald ENV gesetzt ist und ein Chatbot-Row existiert.
  • Folge-Datapoint (kein eigenes TODO): ein zentrales E2E-Seed-Skript (analog gen-test-tokens.sh, das einen Test-Tenant + Chatbot + Branding + Feedback-Sample landet) würde die test.skip-Tags in Tenant-UI-Specs auflösen. Bietet sich vor Block 14 (CI) an, damit die ganze E2E-Suite im Build mitläuft.
Original-Eintrag (zur Nachvollziehbarkeit)
  • Datei(en): frontend/tenant-ui/e2e/, frontend/operator-ui/e2e/
  • Problem: Block 8.6 (Branding) und Block 8.7 (Feedback-View) haben Vitest-Coverage, aber keine Playwright-E2E-Tests für die neuen Sub-Routes:

    • Tenant-UI: /chatbots/:id/branding, /chatbots/:id/feedback
    • Operator-UI: /tenants/:id/branding

    Existing 8.4-Playwright-Sweep deckt CRUD über ChatbotDetailPage ab, aber Branching auf Sub-Routes wird nicht klick-getestet. Regression-Risiko bei zukünftigen Layout-Änderungen (z.B. Sidebar-Refactor, ChatbotDetailPage- Tab-Migration). - Vorschlag: - Pro Sub-Route ein E2E-Szenario: - Tenant-Branding: Login → Sidebar „Branding" → Color ändern → Save → Toast. - Chatbot-Branding: Login → Chatbot-Detail → Branding-Button → Override setzen → Save. - Chatbot-Feedback: Login → Chatbot-Detail → Feedback-Button → Filter setzen → Tabelle aktualisiert. - Operator-Tenant-Branding: Operator-Login → Tenant-Detail → Branding → custom_css setzen → Save. - Test-Daten-Setup für Feedback: Mock-Rows via SQL-Fixture (analog Live-Smoke aus 8.7) oder Seed-Skript. - Aufwand: ~2h pro Sub-Route inkl. Fixture, also ~6–8h gesamt. - Trigger: Vor Block 11 (Widget-Integration ändert Schreib- Pfad — gute Gelegenheit für E2E-Regression-Tests) oder als Teil der Cleanup-Welle vor Block 13. - Quelle: Block-8.7-Hand-off-Bericht (2026-04-30), Drift-Liste-Update.

Aus Konzept-Review (Merge 1fa5835, 2026-04-21)

TODO-Konzept-01 — v5.2-Klärung §17.2/§18 Phase-B-Summe + Block 11 Widget ✅ ARCHIVIERT (2026-05-01)

  • Severity: Low (Score 40, kein Blocker)
  • Status: ✅ Erledigt 2026-05-01 (Branch chore/todo-konzept-01-block-11-aufwand, Merge 9bcf36e).
  • Auflösung: Die Block-11-Widget-Aufwand-Diskrepanz (12h §17.2 vs. 16h frühere Roadmap-Status-Box) wurde per Lutz-Entscheidung am 2026-05-01 auf den Mid-Point 14h Refined konsolidiert. Dokumentiert in §17.2 Tabellenzeile (12h → 14h), §17.2a Überlapp-Quelle Nr. 6 (Reconciliation-Begründung), roadmap.md (Pfad-A-Header und Block-11-Aufwand-Zeile auf 14h Refined vereinheitlicht). Phasen-Total ~425h für v1.0.0 wandert um +2h auf ~427h innerhalb der Rundungstoleranz.
  • Nicht-Scope dieser Auflösung: Die §17.2-Einzelposten-vs-§18- Phasen-Summen-Differenz (~18h) und das Qdrant-Collection-Naming- Schema-Finding aus Block 4 sind eigenständige Themen — siehe §17.2a Überlapp-Quellen Nr. 1–5 (bereits in v5.3/v5.3.1 aufgelöst) und Block-4-Implementations-Doku. Keine offenen Punkte aus dem ursprünglichen TODO-Konzept-01 bleiben.
Original-Eintrag (zur Nachvollziehbarkeit)
  • Datei(en): docs-kora/docs/konzepte/multitenancy-fundament.md §17.2 + §18, docs-kora/docs/roadmap.md:698
  • Problem: §17.2-Einzelposten summieren 410h (nach v5.1-Anpassung), §18-Phasen-Summe deklariert 384h, Delta ~26h schwerpunktmäßig in Phase B (166h vs. 144h). Zusätzlich: Block 11 Widget mit 12h in §17.2 vs. 16h in roadmap.md:698. Eine Warning-Admonition in §17.2 markiert die Diskrepanz und verweist §18 als autoritativ.
  • Zusätzlich (Block-4-Finding): Fundament §6.1 nennt ein anderes Qdrant-Collection-Naming-Schema (chatbot_<uuid>, shared_<template_slug>). Die Platform-Implementierung in Block 4 Phase A nutzt bewusst Variante A (tenant-prefixed: kora_<tenant_id>_<chatbot_id>, kora_<tenant_id>_shared) — stabiler, debuggabler, tenant-scoped. Das Fundament §6.1 soll in v5.2 auf die Implementierung nachgezogen werden.
  • Vorschlag: Scope-Review von Block 8 (Tenant-UI, 44h) und Block 13 (Konnektor-Subsystem, 48h — überschneidet sich möglicherweise mit 13a+13b = 48h). Plus Block 11 Widget-Aufwand konsolidieren. Ergebnis als v5.2- Revision einpflegen, Warning-Admonition entfernen.

Integration-Learnings

Erkenntnisse aus Block-Umsetzungen, die für spätere Blöcke relevant bleiben. Diese sind KEINE offenen Todos, sondern "gewusst-wie"-Notizen.

L-B2-01 — Keycloak-Realm-SMTP: KC_SMTP_* konfiguriert nur Bootstrap-Mails, nicht Per-Realm-SMTP

Problem: Die KC_SMTP_*-ENV-Variablen setzen den SMTP-Kontext nur für Keycloak-interne System-Mails (Bootstrap-Admin, Lost-Password im Master-Realm). Für realm-spezifische Mails (execute-actions-email, Password-Reset in kora-platform/kora-tenants) muss jeder Realm einen eigenen smtpServer-Block im Realm-Export-JSON enthalten — sonst antwortet Keycloak mit 500 "No sender address configured in the realm settings for emails".

Lösung: Pro Realm-JSON einen smtpServer-Block mit host, port, from, fromDisplayName, auth: "false", ssl: "false", starttls: "false" definieren. Für Prod können die Werte per ${KC_SMTP_HOST}-Platzhalter interpoliert werden (Keycloak unterstützt das beim Realm-Import).

Relevanz für spätere Blöcke:

  • Block 17 (Tenant-SSO-Self-Service): Bei IdP-Provisioning pro Tenant prüfen, ob der Tenant einen abweichenden SMTP-Kontext braucht.
  • Block 12 (Provisioning): execute-actions-email für Initial-Tenant- Admin braucht funktionierenden Realm-SMTP-Block in kora-tenants.

L-B2-02 — Composite-Rollen im Realm-JSON brauchen explizite Deklaration

Problem: defaultRole.composites.realm: ["tenant-viewer"] direkt im defaultRole-Feld des Realm-Exports wird von Keycloak 26 beim Import nicht aufgelöst. Die Composite-Rolle entsteht leer, neue User bekommen keine Default-Permissions.

Lösung: Die Composite-Rolle explizit als Realm-Role in roles.realm[] definieren (composite: true + composites.realm: ["tenant-viewer"]). Das defaultRole-Feld referenziert dann nur noch den Namen. Siehe kora-tenants-realm.json ab "default-roles-kora-tenants".

Relevanz für spätere Blöcke:

  • Alle künftigen Realm-Änderungen, die Default-Rollen anpassen (z. B. Block 17 bei Shadow-User-Creation).

L-B2-03 — docker logs | grep -q unter set -euo pipefail triggert SIGPIPE

Problem: grep -q beendet sich nach erstem Match und schließt den Upstream-Pipe. Unter set -o pipefail wertet Bash das als Fehler, der Smoke-Test-Script bricht ab — obwohl das gesuchte Muster gefunden wurde.

Lösung: Entweder grep -c verwenden (voller Scan, Exit 0 immer) und das Ergebnis mit [[ "$count" -gt 0 ]] prüfen, oder das Pipeline-Output vorher in eine Variable capturen (logs=$(docker logs ... 2>&1 || true)) und dann greppen. Letzteres ist robuster, wenn die Input-Größe bekannt klein ist.

Relevanz für spätere Blöcke:

  • Alle künftigen Smoke-Test-Scripts (ab Block 3 relevant).
  • Kandidat für Aufnahme in CLAUDE.md als Convention.

L-B2-04 — psycopg2-ImportError durch Auto-Import in db/__init__.py

Problem: Block 1 hat db/session.py (sync, für Alembic) über db/__init__.py re-exportiert. Im API-Container (ohne psycopg2) schlug der Import der async App fehl, weil bereits das Importieren des Models-Tree das Sync-Session-Modul zog.

Lösung: Sync-Session-Re-Exports aus db/__init__.py entfernt. Alembic importiert explizit from kora_platform.db.session import Base — die App nutzt ausschließlich db/async_session.py. Keine gemeinsame Import-Pfad-Oberfläche für sync und async.

Relevanz für spätere Blöcke:

  • Alle Module, die sowohl sync (Alembic, CLI-Tools) als auch async (App) Code brauchen — Import-Pfade explizit trennen.

L-B3-02 — admin-cli Session in Master-Realm läuft in 60s ab

Problem: Der admin-cli-Client im Keycloak-Master-Realm hat einen Default-Access-Token-Lifespan von 60 Sekunden. Bei längeren Bootstrap-Skripten (~20+ Admin-API-Calls) läuft der zu Beginn gefetchte Token mittendrin ab, alle nachfolgenden Calls erhalten 400/401.

Lösung für Block 3: gen-test-tokens.sh hat eine refresh_auth()-Funktion, die vor jedem logischen Block (enable_direct_grants, ensure_user_*, enable_vendor_breakglass) einen frischen Master-Token holt. Nicht optimal, aber robust.

Relevanz für spätere Blöcke:

  • Block 12 (Provisioning): Tenant-Anlage ruft Keycloak-Admin-API mehrfach. Entweder das 60s-Limit pragmatisch akzeptieren und refreshen oder einen dedizierten kora-platform-admin-Service-Account mit längerem Token-TTL anlegen.
  • Generell: keine lange laufenden Admin-Scripts gegen Keycloak ohne Token-Refresh-Strategie schreiben.

L-B3-03 — SET LOCAL via asyncpg braucht inline-UUID, keine Bind-Params

Problem: asyncpg nutzt Simple-Query-Mode für PostgreSQL-SET LOCAL-Statements, die keine Prepared-Statement-Parameter akzeptieren. Der Versuch SET LOCAL app.current_tenant_id = :tid mit {"tid": ...} scheitert mit InvalidTextRepresentationError.

Lösung für Block 3: UUID als uuid.UUID validieren (via isinstance-Guard) und dann per f-String ins SQL einliegen: text(f"SET LOCAL app.current_tenant_id = '{tid}'"). Der Typ-Guard verhindert SQL-Injection, weil nur echte UUID-Objekte akzeptiert werden.

Relevanz für spätere Blöcke:

  • Alle Routen, die request_scoped_session(tenant_id=...) nutzen — Inlining läuft über die bereits gebaute Helper-Funktion.
  • Bei Operator-Routen mit explizitem SET LOCAL nach dem Pfad-Parameter: derselbe Inline-Trick mit UUID-Guard. Im api/dependencies/tenant_context.py::tenant_context_setup (via _set_rls_context) bereits konsistent umgesetzt. (Vor dem Block-7.1a-Doku-Fix stand hier eine Referenz auf routes/tenants.py:_scope_operator_branch — der Helper-Name existiert im Repo nicht, Historie liegt im tenant-context-Dependency.)

L-B3-04 — Keycloak Composite-Default-Role inkludiert tenant-viewer

Problem: Die Default-Rolle default-roles-kora-tenants im kora-tenants-Realm ist als Composite konfiguriert und referenziert tenant-viewer. Jeder User im Realm hat dadurch mindestens Viewer-Zugriff, auch wenn man ihm explizit keine Rolle zuweist oder tenant-admin entzieht — die Composite-Mitgliedschaft bleibt. Entdeckt bei Smoke-Test 9 aus dem Pre-Block-4-Cleanup: Der Test musste beide Rollen (die zugewiesene plus die Composite-Referenz in der Default-Role) temporär entfernen, um den 403-Fall zu reproduzieren.

Relevanz für spätere Blöcke:

  • Block 12 (Provisioning): "Zugriff entziehen" durch Rollen-Entzug reicht nicht — User über enabled=false deaktivieren oder aus der Composite entfernen.
  • Block 17 (Tenant-SSO-Self-Service): Bei IdP-Shadow-Usern ist das gewünschtes Verhalten: jeder neue Shadow-User bekommt automatisch Viewer-Baseline, Tenant-Admin eskaliert bei Bedarf.
  • Keine Schwachstelle, sondern dokumentierbares Verhalten. Relevant beim Role-Management-UI-Design.

L-B3-05 — BusyBox-wget mit irreführendem "bad address" bei funktionalem Docker-DNS

Problem: wget in BusyBox-/Alpine-basierten Containern hat einen eigenen minimalen DNS-Resolver, der nicht zuverlässig mit Docker-DNS zusammenarbeitet. Das Ergebnis wget: bad address '<service>:<port>' ist in dem Fall ein False-Negative — der Name lässt sich via Docker-DNS trotzdem korrekt auflösen, nur BusyBox-wget sieht ihn nicht. Entdeckt bei Prometheus-Cross-Network- Reachability-Test (chore-prometheus-scrape).

Verifikations-Muster für zukünftige Container-Reachability-Tests:

  • Bevorzugt: nslookup <service> (zeigt echte DNS-Auflösung) oder getent hosts <service> (falls im Container verfügbar)
  • Alternative: curl statt BusyBox-wget, falls installiert
  • wget als Reachability-Beweis nur in Distros mit vollwertigem Resolver (Debian-Basis, etc.)

Relevanz für spätere Blöcke: Alle Cross-Network-Diagnosen in Alpine-/ BusyBox-basierten Containern. Vermutlich auch relevant bei Qdrant-/Redis- Diagnosen zukünftig (beide nutzen Alpine).

L-B3-06 — Docker Single-File-Bind-Mounts halten an Inode fest

Problem: Wenn eine Host-Datei durch ein Tool (Editor, sed, Claude Codes Edit-Tool, mv-Replace) neu geschrieben wird, wechselt die Inode. Ein Docker-Single-File-Bind-Mount hält aber am ursprünglichen Inode fest — der Container sieht weiterhin den alten Inhalt. docker compose restart reicht nicht, nur docker compose up -d --force-recreate <service> (oder vollständiger Container-Neubau) löst das auf.

Bereits zweimal beobachtet:

  • mkdocs-Config (mkdocs-kora.yml) im Pre-Block-4-Cleanup/mkdocs- Hygiene-Kontext
  • Prometheus-Config (monitoring/prometheus.yml) beim Scrape-Job-Setup

Workaround: --force-recreate nach jeder Config-Änderung, bis die Umstellung auf Dir-Mounts erfolgt (TODO-Cleanup03-01).

Langfristige Lösung: Single-File-Bind-Mounts komplett vermeiden. Dir-Mount mit --config-file-Flag im Container-Command ist die robustere Variante (wie bei mkdocs-Hygiene umgesetzt).

Relevanz für spätere Blöcke: Alle zukünftigen Config-Änderungen in Services mit Single-File-Bind-Mounts. Proaktive Suche: grep -rnE ":.*\.(yml|yaml|conf|json):ro" docker-compose*.yml.

L-B5-01 — SQLAlchemy-Identity-Map vs. RETURNING bei ON CONFLICT DO UPDATE

Problem: Der Block-5-Cleanup-Upsert (INSERT ... ON CONFLICT DO UPDATE ... RETURNING ...) gab eine hydrierte TenantModule-Instanz zurück, die bei einem zweiten Assign innerhalb derselben Session die alten Werte (z. B. enabled_by vom ersten Aufruf) zeigte — obwohl das RETURNING die neuen Felder korrekt lieferte. Ursache: SQLAlchemys Identity-Map liefert die bereits gecachte ORM-Instanz zurück, ohne die RETURNING-Werte darauf zu syncen.

Lösung: execution_options={"populate_existing": True} auf dem session.execute(stmt, ...) zwingt SQLAlchemy, die RETURNING-Werte auf die existierende Identity-Map-Instanz zu schreiben. Im Block-5- Cleanup (Commit 4a19966) per Test-Failure-Debugging gefunden und dokumentiert.

Relevanz für spätere Blöcke:

  • Alle künftigen Upsert-Patterns (Block 13 Konnektoren — Credentials- Upsert; Block 14 Deployment — Migration-State-Upsert) müssen dasselbe Flag setzen, sobald derselbe Service im Request-Lauf mehrmals auf dieselbe Row trifft.
  • Sichtbare Symptome: Stale-Daten in der API-Response, aber der DB- State ist korrekt. Identity-Map-Caching-Bugs sind ohne populate_existing schwer zu finden, weil nur die zweite Operation in derselben Session stale wird.

L-B7-01 — .local-TLD lehnt email-validator als Special-Use ab

Problem: EmailStr (pydantic[email], → email-validator) lehnt nach RFC 6761 alle Special-Use-TLDs ab, darunter .local. Test-Fixtures mit user@test.local werfen ValueError: special-use or reserved name und produzieren irreführende 422er.

Lösung: Test-Emails auf @example.com stellen (RFC 2606, explizit für Tests reserviert). Produktion ist unbetroffen, weil echte Kunden- E-Mails kein .local nutzen.

Relevanz für spätere Blöcke: Alle künftigen Tests/Smokes, die EmailStr-Felder befüllen — nicht nur Tenants, sondern auch Block 12 (Provisioning mit Admin-Email), Block 17 (Tenant-SSO-Self-Service mit IdP-Contact-Email). Default-Fixture-Email-Domain auf @example.com setzen.


Abgeschlossen (Archiv)

v1.3.0-D1 Frontend-Polish-Welle — 16 TODOs in 3 Familien + 2 D2-Last-Mile (Branch platform/v1.3.0-d1-frontend-polish, 2026-05-01)

Zweite Welle der v1.3.0-Cleanup-Sequenz, direkt im Anschluss an D2. A11y-Polish, Foundation-Helpers, Composable-Caching, Hygiene und Frontend-Wiring auf die in D2 neu gebauten Aggregate-Endpoints.

Phase D1.1 — Foundation-Helpers

  • TODO-Block-7-1b-07, -7-2-07, -7-2-03: Neuer frontend/operator-ui/src/utils/formHelpers.ts mit trimEqual (whitespace-only-tolerantes Edit-Diff) und listsEqual (order- aware list-equality). TenantsEditPage und TemplatesEditPage nutzen beide Helpers, statt jede Page eigene Trim-/Compare-Konvention zu haben. PATCH-Diff schickt notes: null nicht mehr nur, weil der User Whitespace angehängt hat.
  • TODO-Block-7-3-05: flattenError(err) in composables/useApi.ts — reduziert rohe ApiError-Objekte auf {status, message, detail}. Pydantic-422-Listen werden zu einer human-lesbaren Detail-Zeile gejoint. TenantsListPage Bulk-Soft- Delete und TenantModulesSection Bulk-/Single-Toggle nutzen flattenError jetzt für Toast-Detail-Zeilen.

Phase D1.2 — Component-Patterns

  • TODO-Block-7-2-01: ListInput.vue bekommt A11y-Polish: aria-live="polite"-Region annonciert Add/Remove/Reorder, Keyboard- Reorder via Alt+↑/↓, sichtbare ↑/↓-Move-Buttons. Tests Alt+ArrowDown reorders an item one position lower, Alt+ArrowUp at the top is a no-op, renders a polite live-region for screen-reader status ergänzt.
  • TODO-Block-7-1b-06: useConfirm ist auf eine FIFO-Queue umgestellt. Überlappende ask()-Calls landen hintereinander, statt den ersten still mit false abzuwürgen. Test queues a second ask while the first is open and serves them in order ersetzt den vorherigen Auto-Reject-Test.

Phase D1.3 — Backend-induzierte Endpoints + D1.4 — D2-Frontend-Last-Mile

  • TODO-Block-7-3-02 last-mile: useModule ruft GET /api/v1/platform/modules/{id} direkt — kein Full-List-Fetch
  • Client-Filter mehr. ModulesDetailPage-Spec asserted explizit gegen die neue Pfad-Form, plus 404-Pfad.
  • TODO-Block-7-3-03 last-mile: useTenantModules migriert auf den D2-Aggregate-Endpoint GET /api/v1/platform/tenant-modules?tenant_id=...&include_unassigned=true. TenantModulesSection feuert nicht mehr zwei parallele Requests und mergt clientseitig — ein Roundtrip liefert die fertige „Modul × ist-aktiv-für-Tenant"-Sicht. Test issues exactly one aggregate request, not parallel registry+state calls beweist es.
  • TODO-Block-7-3-04: Optimistic-Update in TenantModulesSection. patchRow patcht die einzelne Aggregate-Row vor dem POST/DELETE, restoreRow rollt zurück bei Fehler. Tests flips badge optimistically on activate before the assign POST resolves und rolls back optimistic activate when the POST rejects decken beide Pfade ab.
  • TODO-Block-7-2-04: TemplatesCreatePage entfernt Empty-Lists aus dem POST-Payload — Audit-Rows tragen die Lists nur noch, wenn der User sie wirklich gefüllt hat.
  • TODO-Block-7-3-06: TenantModulesSection zeigt die volle enabled_by-UUID als HTML-title-Tooltip auf dem trunkierten 8-Char-Display. Test renders enabled_by full UUID as title attribute asserted die UUID im title-Attribut.

Phase D1.5 — Composable-Caching

  • TODO-Block-7-1b-05: useTenants hat jetzt einen Module-Scope SWR-Cache (Stale-While-Revalidate, key = serialisierte List- Params, 30s TTL). Mutationen rufen invalidateCache. Tests decken first-load (network), warm-cache (no network), mutation- invalidate, und stale-revalidate ab; TenantsListPage-Spec ruft invalidateCache() in beforeEach, damit der Module-Level-Cache zwischen Tests nicht leakt.

Phase D1.6 — Hygiene

  • TODO-Block-7-1b-01: E2E-Seed-Pfad in useAuth.applyE2eSeed() ist hinter import.meta.env.VITE_E2E_MODE === "true" gegated. Dockerfile.platform akzeptiert ARG VITE_E2E_MODE, docker- compose.platform.yml setzt ihn auf ${VITE_E2E_MODE:-true}, damit die luki-ai-Instanz die Playwright-Suite weiterhin fahren kann. Production-Build (z. B. eine zukünftige GA-CI-Pipeline) überschreibt mit leerem Wert; ein DOM-Clobbering-Angriff (<form id="__KORA_E2E_SEED__"> injektiert vor main.ts) kann den Auth-State dann nicht mehr kapern.
  • TODO-Block-7-2-06: mintTokens in e2e/helpers.ts ist async — der vorher synchrone Spin-Loop im Backoff blockiert den Worker- Event-Loop nicht mehr. Alle E2E-Spec-Caller (8 Specs) sind auf await mintTokens() angepasst.
  • TODO-Block-7-1b-08: frontend/operator-ui/PORTING.md ist nach docs-kora/docs/blocks/block-7-1b-porting.md verschoben (per git mv). Operator-ui/README.md verlinkt die neue Position; mkdocs.yml hat eine neue nav-Sektion „Blocks (Pre-Flight & Porting)".

Phase D1.7 — Cosmetic

  • TODO-Block-7-3-07: TenantsCreatePage-Description und TenantsDetailPage-Audit-Card ohne Block-Nummer-Referenzen (vorher „Block 7.3 / Block 7.4"). Footer war bereits durch den Layout-Refactor 2026-04-28 entfernt; D1 räumt die letzten veralteten Block-Strings im UI auf.

Verifikation

  • Operator-UI Vitest: 161 Tests (vorher 130) ✅
  • Operator-UI Lint: 0 errors, 77 pre-existing warnings (alle in TenantBrandingPage.vue, nicht von D1 berührt) ✅
  • Operator-UI Type-Check: clean ✅
  • mkdocs strict build: clean (Phase 6 verifiziert)

Datenpunkt für E + zukünftige Wellen

  • Refined-Schätzung: ~14.5h
  • Real-Aufwand: ~3.5h Implementation + ~0.5h Test-Fix + ~0.5h Docs
  • Quote: ~30 % (näher am Plan als D2 mit 23 %, weil Frontend- Refactors typischerweise mehr Vue-/Test-spezifische Reibung haben).
  • D2-Foundation (Aggregate-Endpoint, Modules-Detail-Endpoint, flattenError-Pattern) hat sich direkt ausgezahlt — D1.3+D1.4 waren reine Wiring-Arbeit, keine Backend-Iteration nötig.

v1.3.0-D2 Backend-Polish-Welle — 17 TODOs in 4 Familien (Branch platform/v1.3.0-d2-backend-polish, 2026-05-01)

Erste Welle der v1.3.0-Cleanup-Sequenz. Audit-Pattern-Konsolidierung, Schema-Hygiene, Bulk-Hardening, Service-Endpoints. Foundation für v1.3.0-D1 (Frontend-Polish) und v1.3.0-E (Operator-Per-Chatbot-Page).

Phase D2.1 — Foundation

  • TODO-Block-7-NN-01: Zentraler Scope-Guard-Helper kora_platform.api.dependencies.scope_guards. Vier Routen-Files (operator_tenants, operator_audit, tenant_modules, chatbot_templates) ziehen jetzt zentralen Helper statt lokaler Kopien. _require_any_scope und _require_tenant_owns_chatbot in chatbot_templates.py bleiben lokal (template-spezifisch).
  • TODO-Block-7-NN-04 + -5g: Alembic 0010 installiert PG-Trigger set_updated_at() auf tenants, tenant_modules, platform_modules. Service-Code setzt updated_at nicht mehr Python-seitig. update_tenant und soft_delete_tenant rufen jetzt await session.refresh(tenant), damit der trigger-bumped Wert beim Caller ankommt. Eine Migration für beide Spalten gemeinsam — das war der Discovery-Bündel-Punkt #4 aus cleanup-welle-discovery-2026-05-01.md.

Phase D2.2 — Bulk-Refactor

  • TODO-Block-7-4-01 + -7-4-08: bulk_soft_delete_tenants und bulk_assign_to_tenant nutzen jetzt WHERE id IN (...)-Pre-Validate plus single Multi-Row-Statement statt N Per-Item-Roundtrips. Pydantic min_length=1, max_length=MAX_BULK_ITEMS=500 in beiden Bulk-Routen. Empty-List ergibt jetzt 422 statt 400 (Tests test_bulk_soft_delete_empty_list_422, test_bulk_assign_modules_empty_list_422 und je ein Über-Cap-Test ergänzt). Frontend-Progress-Indicator ist mit dem Cap-Hardening obsolet — kein eigener Fix nötig.

Phase D2.3 — Audit-Hygiene

  • TODO-Block-7-NN-02: TenantService.list_tenants nutzt count(*) OVER ()-Window-Function in einer Single-Query. Empty-Page-Fallback per Count-Only-Query (Window liefert nur Werte, wenn die Page selbst Rows hat).
  • TODO-Block-7-NN-03: TenantService.changed_fields snapshotet Before-Werte in plain-dict vor dem Diff. Entkoppelt das Audit- Delta vom ORM-State, sodass eine zukünftige Bulk-Update-Variante nicht den Audit-Trail aliased.
  • TODO-Block-7-4-02: CSV-Export (GET /api/v1/platform/audit/export.csv) mappt jetzt actor_keycloak_id, ip_address, session_id in den Header. Test test_csv_export_includes_forensic_columns ergänzt.
  • TODO-Block-5e: Integration-Test test_audit_failure_rolls_back_mutation simuliert OperationalError im write_platform_audit und prüft, dass die umgebende create_tenant-Mutation rollbacked (DB-Row nicht persistiert).

Phase D2.4 — Service-Endpoints

  • TODO-Block-7-3-02: GET /api/v1/platform/modules/{module_id} — Single-Modul-Detail. Operator-UI muss nicht mehr die volle Liste fetchen + clientseitig filtern.
  • TODO-Block-7-3-03: GET /api/v1/platform/tenant-modules — Aggregate-Endpoint mit tenant_id-Query-Param und include_unassigned-Flag. Cross-Tenant-Fan-out ohne tenant_id ist 400 (Block-12-Trigger).
  • TODO-Block-5f: DELETE /api/v1/tenants/{id}/modules/{module_id} prüft jetzt zuerst Tenant-Existenz vor dem DELETE auf tenant_modules. Vorher war ein typo'd Tenant-ID-Path-Param ein stilles No-Op (idempotente Semantik), jetzt 404.

Phase D2.5 — Polish

  • TODO-Block-7-NN-06: ILIKE-Metachar-Escape im TenantService.list_tenants-Search-Pfad. Helper _escape_ilike escapt \\, %, _. Test test_search_escapes_ilike_metachars beweist, dass _safe-Suche nicht mehr safe-Slugs zieht.
  • TODO-Block-7-NN-07: Test test_vendor_can_list_with_include_deleted deckt den Vendor-Pfad mit include_deleted=true.
  • TODO-Block-7-4-03: Neue Doku-Page operations/audit-conventions.md — „eine Audit-Zeile pro Bulk-Aktion"-Konvention plus Anonymous- Actor-Pfad und CSV-Forensik-Felder.
  • TODO-Block-7-4-06: datetime.utcnow()datetime.now(UTC) im CSV-Filename-Builder (Py3.12-Deprecation).
  • TODO-Block-7-4-07: _validate_date_range-Helper lehnt date_from > date_to mit 422 date_from_after_date_to ab. Unit-Tests test_validate_date_range_accepts_ordered_range und _rejects_inverted_range ergänzt.

Real-Aufwand: ~5h vs. Refined-Schätzung 22h (≈ 23 % Quote — deutlich besser als die 60 %-Erwartung der Cleanup-Welle-Discovery, weil Audit-/Service-Pattern aus Block 8/11 voll wiederverwendbar waren und viele TODOs Single-File-Touch waren). Datapoint für D1- und E-Erwartung.

Test-Stand: 87/87 grün (vorher 82/82, +5 neue D2-Tests). Migrationen: Alembic 0009 → 0010 (beide Up/Down idempotent).

TODO-Platform-14 — Template-Audit-Trail-Konsolidierung (Branch chore/todo-platform-14-template-audit, 2026-04-30)

Audit-Trail-Lücke aus Block-8.2-Discovery geschlossen. Template- Mutations (Create/Update/Deactivate) schreiben jetzt platform_audit_log-Einträge analog zu Tenants und Modules. Foundation für Block 8.4 (Chatbots-CRUD) — Pattern „aus dem Heft" verfügbar, kein neuer Drift in 8.4.

Audit-Actions implementiert:

Action Route Details-Schema
template.created POST /api/v1/operator/templates {id, display_name, language, is_active}
template.updated PATCH /api/v1/operator/templates/{id} {before: {...changed only}, after: {...changed only}} (only-if-diff via changed_fields-Helper)
template.deactivated DELETE /api/v1/operator/templates/{id} {id, display_name, language}
template.cloned POST /api/v1/operator/templates/{id}/clone bereits seit Block 8.2 (8c5a37d)

Re-Aktivierung (PATCH is_active=true) wird nicht als eigene Action geloggt, sondern als template.updated mit details.before.is_active=false, details.after.is_active=true-Diff. Lesbarer Audit-Trail ohne Action-Inflation.

Helper: write_platform_audit aus _platform_audit.py wiederverwendet (kein neuer template-spezifischer Helper). Pattern aus Tenants/Modules etabliert.

Routes umgestellt: admin_session()request_scoped_session(tenant_id=None, bypass_rls=True) — gleiche Transaktion für Mutation + Audit. Vor TODO-14 nutzten 3 von 4 Mutations admin_session() ohne Audit-Hook.

Service-Erweiterung: ChatbotTemplateService.changed_fields(before, payload) als statische Methode (Spiegelung aus TenantService.changed_fields). Liefert (before_delta, after_delta) mit nur tatsächlich geänderten Feldern.

Field-Diff-Strategie: Volltext für alle Fields inkl. suggested_system_prompt. Maximale Real-Länge 8.5KB (meldeschein-Template) → JSONB-Audit-Detail OK, kein Hashing nötig.

Live-Smoke verifiziert: - POST smoke14 → HTTP 201 + audit-log template.created mit {id, display_name, language, is_active}. - PATCH smoke14 (display_name + language) → HTTP 200 + audit-log template.updated mit korrekten before/after-Deltas. - DELETE smoke14 → HTTP 204 + audit-log template.deactivated mit Snapshot-Details. - 3 audit-log-Einträge sichtbar via direkter SQL-Query auf platform_audit_log WHERE entity_id='smoke14'. - Smoke-Template nach Verifikation aufgeräumt (DELETE FROM chatbot_templates WHERE id='smoke14').

Tests:

  • 2 neue Service-Unit-Tests (changed_fields_returns_only_diff, changed_fields_empty_diff_when_no_changes). Gesamt test_chatbot_template_service.py 21/21 grün.
  • Existing 19/19 Tests blieben grün (keine Regression durch Session-Pattern-Switch).
  • Live-Smoke deckt Audit-Insert-Pfad ab. Pytest-Integration-Tests für template.created/updated/deactivated-Audit-Reads sind Block-8.4-Voraussetzung (TODO-Platform-12-Pytest-Profil-Trennung würde Integration-Test-Suite-Vereinfachung bringen).

Aufwand: ~1.5h (geschätzt 2–3h, im Korridor).

Foundation für Block 8.4: Chatbots-CRUD übernimmt Pattern direkt: request_scoped_session(bypass_rls=True) + write_platform_audit mit changed_fields-Diff für PATCH. Audit-Actions: chatbot.created/updated/deleted/restored nach gleichem Schema.

Out-of-Scope (separate Blöcke): Helper-Konsolidierung (_write_module_audit / _write_tenant_audit / direkter write_platform_audit divergieren leicht — Cleanup-Block für Helper-Vereinheitlichung wenn AVS-Bedarf). Retroaktive Audit- Einträge für historische Mutations (kein Audit-Theater, ehrlicher Cut mit diesem Merge).

Block 8.1 / TODO-UX-03 — Verifikations-only (Branch platform/block-8.1-tenant-edit-ui, 2026-04-30)

Befund: Block 8.1 enthielt keine Implementation. Discovery zeigte, dass alle UX-03-Anforderungen bereits seit Block 7.1b/7.4 vollständig implementiert waren — die Block-8-Verschiebung im UX-04-Block (platform/ux-04-language-filter-and-ux-03-deferral) basierte auf der ungeprüften Annahme, eine Backend-Edit-UI fehle.

Existing Implementation (verifiziert):

  • Backend: PATCH /api/v1/platform/tenants/{tenant_id} mit _require_operator-Scope-Gate, TenantUpdate-Pydantic-Schema (slug-immutable, EmailStr-Validation), TenantService.update_tenant
  • changed_fields(before, payload)-Diff. Audit-Log-Action tenant.updated mit details={"before": {...}, "after": {...}} — geschrieben nur bei echter Änderung.
  • Frontend: TenantsEditPage.vue (227 Zeilen) mit Slug-disabled- Tooltip, Display-Name-Required-Validation, EmailStr-Format-Check, Notes-Textarea, Dirty-State-Logic, Inline-422-Errors, Toast on success, Redirect zur Detail-Page. useTenant().update()-Composable (statt separatem useTenantUpdate). Detail-Page-Edit-Button funktional (goEdit → /tenants/{id}/edit).
  • Schema-Felder: tenants hat id, slug, display_name, status, contact_email, notes, status, deleted_at, created_at, updated_at. Editable im PATCH-Schema: display_name, contact_email, notes. Slug ist explizit immutable (Block-7.1b-Konvention).

Was Lutz' Phase-2b-Form-Spec zusätzlich nannte, aber Schema- Realität nicht hergab:

  • language-Feld auf tenants — wurde in UX-04-Discovery (2026-04-29) als Stop-Trigger Szenario iii / Konzept-Drift markiert. Sprach-Achse ist auf Content-Level (chatbot_templates.language, chatbots.language). Tenant- Sprache als Stammdatum hätte Doppel-Achse erzeugt — bewusst nicht ergänzt.
  • contact_phone, contact_address — kein dokumentierter User-Story-Bedarf über contact_email + notes hinaus.

Live-Smoke-Verifikation (2026-04-30):

  • PATCH /api/v1/platform/tenants/<bench-tenant-a-id> mit Operator-Token: HTTP 200, korrekt geupdated.
  • platform_audit_log-Row erscheint: action=tenant.updated, actor_role=operator, actor_user=bench-operator-admin, actor_keycloak_id=NULL (TODO-Auth-NEU-Workaround), details.before= {"display_name": "Benchmark Tenant A", "contact_email": null}, details.after={"display_name": "Bench Tenant A (smoke)", "contact_email": "ops-a@example.com"}.
  • Backend-Tests grün: 43/43 (test_operator_tenants_api, test_tenant_service, test_audit_service).
  • Frontend-Tests grün: Vitest TenantsEditPage 3/3, Playwright tenants-crud 2/2 (deckt happy path: login → list → create → detail → edit → delete ab).

Bench-Demo-Daten gepflegt: bench-tenant-a und bench-tenant-b haben jetzt contact_email (ops-a@example.com / ops-b@example.com) über direktes SQL-Update (kein zusätzlicher Audit-Pollution-Eintrag). display_name wurde auf den Original-Wert Benchmark Tenant A zurückgesetzt nach dem Smoke-PATCH.

Re-Diagnose-Lesson (für künftige Block-Moves):

TODO-UX-03 wurde im UX-04-Block in Block-8-Scope verschoben unter der Annahme, eine Backend-Edit-UI fehle. Discovery in Block 8.1 zeigte: PATCH-Endpoint, TenantUpdate-Schema, TenantsEditPage und Detail-Page-Edit-Button existieren seit Block 7.1b/7.4 vollständig. Die Walkthrough-Beobachtung „Kontakt-Spalte leer" war ein Daten-Pflege-Befund, kein UI-Lücken-Befund. Lesson: Status-Annahmen über existing Code via find/grep verifizieren, bevor Block-Moves beschlossen werden.

Aufwand: ~30min (Verifikation + Doku, kein Code-Change).

Out-of-Scope: Schema-Erweiterung (language, contact_phone, contact_address) bleibt verworfen. Multi-Language-Templates weiterhin Block-8.2-Scope auf Content-Achse.

Block 8.0 + TODO-Platform-13 (Branch platform/block-8.0-foundation, 2026-04-30)

Block-8.0-Foundation gelegt: Module-Auto-Seed (TODO-Platform-13 erledigt) plus erweitertes Tenant-UI-Skelett mit Read-Only-Pages für Templates, Module, Profil. Vorbereitung für Block 8.1 (UX-03 Tenant-Edit-UI), 8.2 (Multi-Language-Templates), 8.3 (Provisioning- Self-Service).

TODO-Platform-13 Implementation: Variante 1 (Lifespan-Hook) gewählt. src/kora_platform/main.py ruft ensure_seed_modules() nach init_engines() über admin_session() auf, idempotent via existing INSERT ... ON CONFLICT (id) DO NOTHING. Failure ist non-fatal (Container darf hochkommen, Health-Probes machen unsauberen Zustand sichtbar — kein Crash-Loop, der Operator-Triage behindert).

Live-Verifikation: - Fresh-Deploy auf leere platform_modules (DELETE FROM platform_modules + restart api): Log zeigt module_seed_completed inserted=4. - Idempotenz: zweiter Restart liefert module_seed_completed inserted=0. DB-State nach beiden Starts identisch (chatbot, confluence, internal_analytics, ticket_escalation).

Backend-API (Block 8.0 neu):

  • GET /api/v1/tenants/me/modules — Tenant-Self-Read aller nicht-internen Module mit Aktivierungs-Status. LEFT-JOIN-Variante via neuer Service-Methode ModuleService.list_all_modules_with_tenant_status. Auth-Matrix validiert: tenant=200, operator=403, vendor=403, unauth=401. is_enabled fällt für nicht-zugewiesene is_always_on-Module (scope='core') auf True zurück (chatbot ist immer aktiv).
  • Nicht angetastet: /api/v1/tenant/templates (existing aus Block 5) und /api/v1/tenants/me (existing aus Block 4) reichen für Block-8.0-Frontend-Anforderungen.

Tenant-UI (Block 8.0 neu):

  • /templates — Read-Only-Liste der für den Tenant aktiven Templates. Empty-State, Sprach-Label-Mapping (de/en → Deutsch/Englisch), Version-Counter.
  • /modules — Read-Only-Liste aller Module mit Aktivierungs- Status-Badge. Scope-Labels (Kern/Extern aktivierbar/Intern).
  • /profile — Read-Only-Stammdaten (Slug, Display-Name, Status, Tenant-ID, Angelegt). Edit-Button disabled mit Hinweis auf Block 8.1.
  • Sidebar in 4 Groups restrukturiert:
  • „Übersicht" (Dashboard)
  • „Verwaltung" (Templates, Module)
  • „Konto" (Profil)
  • „In Vorbereitung (Block 8.x)" (Chatbots, Wissensquellen, Branding, Feedback — disabled) Pattern gespiegelt aus Operator-UI; keine Re-Invention.

Validierung: - Backend Pytest 211/211 (kora-platform-Pfad) - Tenant-UI Vitest 16/16 (existing tests, keine Regression) - Tenant-UI Build: 107.08 kB raw / 41.50 kB gzip (3 neue Pages je ~2 kB) - Operator-UI Vitest 128/128 (Regression-Check, keine Änderungen dort) - verify-auth-stack.sh: 57/59 (TODO-Auth-NEU bleibt monitored) - Mkdocs Strict-Build: Exit 0 - Live-Smoke: GET /tenants/me/modules mit Tenant-A-Token liefert 3 nicht-interne Module mit korrekten Status-Werten

Out-of-Scope (separate Blöcke): UX-03 Tenant-Edit-UI (8.1), Multi-Language-Template-Workflow (8.2), Provisioning-Self-Service (8.3). TODO-Platform-11/12 bleiben offen.

TODO-UX-04 (Branch platform/ux-04-language-filter-and-ux-03-deferral, 2026-04-29)

Sprach-Filter in der Templates-Liste implementiert (Operator-UI). Ein gepaarter Block mit TODO-UX-03-Verschiebung nach Block 8.

Discovery-Kontext (wichtig für Lutz' künftige Prompts): Der ursprüngliche Prompt zielte auf eine Tenant-Liste-Sprach-Filter- Variante mit Migration tenants.language. Discovery zeigte: kein language-Feld in tenants, aber chatbot_templates.language existiert bereits (VARCHAR(5), Default de, 2 Rows alle de). Lutz korrigierte den Plan auf TODO-UX-04-wie-im-Repo (Templates, Szenario i, keine Migration). Discovery-First-Pattern hat den falschen Plan verhindert.

  • frontend/operator-ui/src/pages/TemplatesListPage.vue<select> Dropdown „Sprache" neben „Inaktive anzeigen", Werte Alle / Deutsch / Englisch. Dynamische Empty-Title und Empty-Body wenn Filter aktiv und keine Treffer („Keine Englischen Templates" + Filter-Hinweis). Footer-Note erweitert um Filter-Status.
  • frontend/operator-ui/src/composables/useTemplates.tsapplySearch() umbenannt zu applyFilters(); neuer Setter setLanguage(value); watch auf params.value.language triggert applyFilters() (clientseitig, kein Re-Fetch wie bei search).
  • frontend/operator-ui/src/types/template.tsTemplateListParams.language?: string ergänzt.
  • Filter-Strategie: clientseitig analog zu search — Templates- Volumen ist klein (≪ 100), Backend-Endpoint liefert bare list ohne Pagination. Kein Backend-Refactor nötig.
  • Sprach-Werte: 2-Buchstaben-Codes (de, en) — chatbot_templates.language ist VARCHAR(5) und damit locale-fähig (de-AT, en-US), aber Filter arbeitet mit den tatsächlich vorhandenen Daten (alle de), keine Locale-Detection.
  • Vitest: 3 neue Tests in pages/__tests__/TemplatesListPage.spec.ts (Dropdown-Render, client-side Filter de, Empty-State mit Sprach-Hinweis bei en). 7/7 grün, Gesamt-Suite 128/128.
  • Filter-Charakter: strukturell sinnvoll, funktional „tot" — alle live-Templates sind de. Filter ist Vorbereitung für Multi-Language-Templates ab Block 8.

TODO-UX-03 nach Block 8 verschoben: Severity hochgestuft auf M2/50, Block-8-Scope-Item, Roadmap-Block-8 um „Tenant-Stammdaten- Edit-UI" erweitert (mit Datenpunkt: TODO-UX-03 ist strenggenommen Operator-UI, Block 8 sonst Tenant-UI — Block-Scope wird leicht erweitert; alternativ ein eigener Block 8.1 falls Block 8 sich aufbläht).

Out-of-Scope (separate Blöcke): Multi-Language-Content-Refactor. i18n-Stack für Operator-UI (Block Phase D). Backfill-Skript für existierende Tenants. Locale-Erweiterung über 2-Buchstaben-Codes hinaus.

TODO-UX-01 + TODO-UX-NEU (Branch platform/ux-polish-status-consistency-and-cleanup, 2026-04-29)

Zwei Polish-Items in einem Mini-Run vor v1.0.0-GA: Status-Konsistenz zwischen Tenant-Liste und Tenant-Detail (TODO-UX-01) und Cleanup- Pattern-Erweiterung um op-vendor-*-Pollution (TODO-UX-NEU, opportunistisch mit-erledigt, weil im Cleanup-Mini-Run als Datenpunkt erkannt — kein vorheriger TODO-Lifecycle).

TODO-UX-01 Discovery-Befund: Variante A mit Twist. Liste rendert ein Badge basierend auf tenant.deleted_at (Boolean: Aktiv/ Soft-Deleted), Detail rendert tenant.status raw (active/ inactive). TODO-UX-01-Beschreibung selbst war leicht ungenau — die Liste rendert nicht status, sondern deleted_at. Es gibt keine StatusPill-Component (Variante C wäre Component- Wiederverwendung; nicht zutreffend).

TODO-UX-01 Fix: Neuer util frontend/operator-ui/src/utils/tenantStatus.ts mit formatTenantStatus(tenant) — kombiniert deleted_at (dominiert) + status-Mapping zu deutschem Label. Detail-Page nutzt es; Liste bleibt unverändert per Scope-Boundary "Keine UI-Änderung außerhalb der Tenant-Detail-Status-Anzeige". Anti-Drift-Check: Templates und Modules-Section haben eigene konsistente is_active/is_enabled- Boolean-Pattern — kein Whack-a-Mole, der sonst nötig gewesen wäre.

  • utils/tenantStatus.ts — pure function, fallback auf Roh-Wert für unbekannte Status-Strings. Composable-Verzeichnis war unpassend (kein ref/Reactivity), types/tenant.ts ist für Type-Shapes — daher neues utils/-Verzeichnis. Erste shared util im operator-ui (vorher formatIso als Local-Function-Duplikate).
  • Vitest: 4 Unit-Tests in utils/__tests__/tenantStatus.spec.ts (active/inactive/Soft-Delete-dominiert/unknown-fallback) + 2 neue Tests in pages/__tests__/TenantsDetailPage.spec.ts (humanisiert + Soft-Deleted-Pfad). Alle 9 Tests grün.
  • Playwright: kein neuer Test — die Discovery-Klärung ergab nur Text-Konsistenz (kein Pill im Detail), Vitest deckt das vollständig.

TODO-UX-NEU op-vendor-Cleanup: Skript-Pattern erweitert.

  • scripts/cleanup-test-data.sh — neuer Flag --include-test (analog --include-bench). Aktiviert op-vendor-% zusätzlich zu e2e-%. Pattern erweiterbar für weitere künftige Auth-/Service-Test-Klassen ohne neuen Flag.
  • Makefile — neue Convenience-Targets cleanup-test-data-include-test (Dry-Run) und cleanup-test-data-include-test-apply (mit Confirm). Bestehender Pass-Through-Pfad via args= weiter unterstützt für --include-bench --include-test-Kombinationen.
  • Runbook: operations/test-data-cleanup.md um neuen Abschnitt + Tabellen-Updates erweitert.
  • Live-Smoke: Dry-Run zeigte op-vendor-write-0bd2ee27; Apply löschte 1 Tenant; Idempotenz-Check „Keine Test-Daten gefunden — nichts zu tun"; Audit-Log unverändert (515 Zeilen vor + nach) — Scope-Boundary respektiert.

Bonus-Datenpunkt aus diesem Block: TODO-Platform-10 (Mkdocs- Build-Static) erstmals produktiv genutzt via make docs-kora-deploy — funktioniert, ~5–8s end-to-end, keine Issues.

Out-of-Scope (separate Blöcke): Status-Mapping in der Tenants- Liste auf formatTenantStatus migrieren (würde Verhalten ändern für status: 'inactive'-Tenants, aktuell als „Aktiv" angezeigt solange deleted_at null — separater Diskussionspunkt). i18n-Infrastruktur. TODO-UX-03 (Kontakt-Spalte) und TODO-UX-04 (Sprach-Filter).

TODO-Platform-04 (Branch platform/todo-platform-04-iptables-remote-node, 2026-04-29)

iptables-Setup auf Remote-vLLM-Node (192.168.0.223) codifiziert. Vorher: nur in CLAUDE.md/userMemories beschrieben, kein Skript im Repo — bei Hardware-Wechsel oder zweiter Node ging der Schutz verloren. Lutz-Entscheidung: Option C — Reverse-Engineering vom bekannt-aktiven Live-Zustand. Kein Live-Deploy in diesem Block.

  • Skript: infra/remote-node/docker-firewall.sh
  • Pattern: Delete-then-Insert-Position-1 (Live-bewährt seit 1.5 Wochen) statt Check-then-Append. Reihenfolge wird explizit durch Reverse-Iteration kontrolliert
  • Modi: default (apply, für systemd ExecStart), --install (apply + systemd setup), --remove, --dry-run
  • IPv4 + IPv6 asymmetrisch wie im Live-Zustand: IPv4 hat RETURN-Whitelist + DROP-Catchall pro Port; IPv6 nur DROP (internes LAN-Routing nutzt ausschließlich IPv4)
  • Konfig-Block: ALLOWED_SOURCE_IP="192.168.0.7" und PORTS_STR="8000 9835" als Env-Override-Hooks für zweite Nodes
  • set -euo pipefail + IFS=$'\n\t' strict-mode
  • Root-Check + DOCKER-USER-Existenz-Check (letzter im Dry-Run übersprungen, damit Skript-Logic auf Hosts ohne Docker prüfbar bleibt)
  • Service-Template: infra/remote-node/docker-firewall.service
  • Type=oneshot, RemainAfterExit=yes
  • PartOf=docker.service intentional — Service stoppt mit Docker, startet mit Docker-Restart wieder
  • ExecStart=/usr/local/sbin/docker-firewall.sh (fester Pfad, Runbook beschreibt Kopier-Schritt)
  • Runbook: deployment/remote-vllm-node-setup.md
  • Architektur-Kontext, Voraussetzungen, Aufruf-Reihenfolge, drei Verifikations-Pfade (iptables-Inspection als Standard ohne Drittel-Host, plus optional Container-Negative-Test mit docker run --network bridge), Fehlerbehebung, Rollback, zweite-Node-Anpassung
  • Routing-Page §4 (GPU-Inferenz-Topologie): „Doku-only"-Blockquote durch Verweis auf Skript + Runbook ersetzt; Hinweis auf das Delete-then-Insert-Pattern
  • mkdocs-Nav um Runbook-Eintrag unter Deployment erweitert
  • Phase-1-Discovery-Datenpunkte (Lutz-bestätigt am 2026-04-29):
  • Live-Counter zeigen 998K Pakete auf Port-8000-RETURN, 324K auf Port-9835-RETURN, 0 Pakete auf den DROP-Regeln seit Setup- Zeit (kein unauthorisierter Zugriff seit 1.5 Wochen)
  • curl http://192.168.0.223:8000/health von luki-ai: 200 ✅
  • curl http://192.168.0.223:9835/metrics von luki-ai: 200 ✅
  • Kein Drittel-LAN-Host für externen nmap verfügbar → Smoke- Strategie auf Inspection-Pfad ohne externen Test fixiert
  • Validierung: bash -n PASS; vier Dry-Run-Modi (default --dry-run, --remove --dry-run, --install --dry-run, Env-Override PORTS_STR=...) liefern erwartete Aufruf-Sequenzen
  • Out-of-Scope (separate Blöcke): Live-Deploy, Disaster-Recovery- Test auf frischem Node, WireGuard/VPN-Schicht, SSH-Lockdown

TODO-Platform-10 (Branch platform/todo-platform-10-mkdocs-static-hosting, 2026-04-29)

mkdocs-Live-Deployment vom serve-Mode auf vorgebauten Static-Site mit nginx:alpine umgestellt — der Inotify-Bug, der mehrere Wochen Doc-Merges auf docs.kora.luki-net.org unsichtbar gemacht hat, ist damit struktur-eliminiert. Lutz-Entscheidung: Option 3 (Build-static- Pattern) aus dem TODO-Eintrag — sauber, expliziter Build-Step, kein Long-Running-Build-Werkzeug im Container.

  • Compose-Refactor: docker-compose.platform.yml Service mkdocs
  • Vorher: squidfunk/mkdocs-material:latest serve --no-livereload, Mount ./docs-kora:/docs:ro, Port 8237→8000
  • Nachher: nginx:alpine mit Custom-Config, Mounts ./docs-kora/site:/usr/share/nginx/html:ro und ./infra/docs/nginx-docs.conf:/etc/nginx/conf.d/default.conf:ro, Port 8237→80
  • NPMplus zero-touch: Service-Name mkdocs und Port 8237 unverändert — kein NPMplus-Reload nötig
  • mkdocs.yml: site_dir: ../site-korasite_dir: site (innerhalb docs-kora/, gitignored, neben docs/ inspizierbar)
  • nginx-Config: infra/docs/nginx-docs.conf mit gzip, try_files-Routing für mkdocs-Pretty-URLs, sane Security- Header (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)
  • Make-Targets: docs-kora-build, docs-kora-deploy, docs-kora-clean. Build läuft über Throwaway-docker run --rm mit squidfunk/mkdocs-material:latest-Image, RW-Mount nur für die Build-Phase
  • Healthcheck: wget --spider http://127.0.0.1/ (Alpine-Quirk: localhost resolved auf IPv6, nginx hört nur IPv4)
  • Runbook: deployment/docs-deployment.md — Architektur, Build-vs-Serve-Container-Trennung, Standard-Update- Flow (make docs-kora-deploy), Troubleshooting für Build-Strict- Errors und 404-nach-Deploy-Edge-Cases
  • Legacy-Hinweis: Der frühere deployment/mkdocs-container.md-Stub wurde in Reorg 2026-05-09 gelöscht (redundant zur neuen Runbook-Page; Cross-Refs aus älteren Karten bleiben über die Audit-Trail-History nachvollziehbar)
  • gitignore: explizit docs-kora/site/ + site-kora/ (Legacy) ergänzt; das globale site/ deckte beide Pfade implizit ab
  • Out-of-Scope (separate Blöcke): Auto-Deploy via Webhook (Block 14 / CI-Setup), --no-livereload-Diagnose-Wrapper für Local-Editor-Workflows, Doku-Search-Index-Optimierung

TODO-UX-02 (Branch platform/todo-ux-02-cleanup-test-data, 2026-04-27)

Test-Daten-Pollution in der Live-Operator-UI gefixt: 68 e2e-Tenants und 35 e2e-Templates aus akkumulierten E2E-Läufen entfernt.

  • Skript: scripts/cleanup-test-data.sh — read-only Default (Dry-Run), --apply mit Confirm-Prompt, --include-bench opt-in für bench-*, --yes für CI/non- interactive, transaktionaler DELETE mit Auto-Rollback bei Fehler.
  • Pre-Flight-Sicherheits-Check: Skript prüft alle 10 NO-ACTION-FK- Tabellen (platform_audit_log, chatbot_sources, sync_jobs, chat_sessions, chat_messages, feedback, document_versions, evaluation_runs, vendor_access_log, chatbots.template_id) vor dem DELETE — bricht ab, falls dort Rows zu löschende Tenants/ Templates referenzieren. CASCADE-FKs (tenant_packages, tenant_branding, chatbots, credentials, tenant_modules, evaluation_questions) werden automatisch mitgenommen.
  • Make-Targets: make cleanup-test-data (Dry-Run), make cleanup-test-data-apply (mit Confirm-Prompt). Beide nehmen optional args=--include-bench.
  • Runbook: operations/test-data-cleanup.md mit Aufruf-Patterns, Edge-Case-Handling (NO-ACTION-Pre-Flight- Failure, DB-Connection-Fehler), Verifikations-Schritten.
  • Audit-Log unangetastet: Per Scope-Boundary werden keine Audit-Zeilen gelöscht. History bleibt vollständig erhalten — auch von gelöschten Tenants. Audit-Zeilen referenzieren den Tenant- UUID weiter über die entity_id-Spalte (string). tenant_id-FK- Spalte ist nullable und im Discovery-Befund leer für e2e/bench (operator-scope-Audit-Writes nutzen tenant_id=NULL).
  • Discovery-Datenpunkte:
  • 68 e2e-Tenants in 5 Pattern-Klassen (e2e-bulk-1/2, e2e-conflict-*, e2e-happy-*, e2e-mod-*)
  • 35 e2e-Templates in 2 Pattern-Klassen (e2e-conf-*, e2e-tpl-*)
  • 2 bench-Tenants (bench-tenant-a, bench-tenant-b) mit 200 cascadenden Chatbots
  • 0 Audit-Zeilen mit tenant_id IN e2e-tenants
  • Alle 9 weiteren NO-ACTION-FK-Tabellen 0 Rows für e2e
  • Live-Verifikation:
  • Vor Apply: 68 Tenants + 35 Templates + 455 Audit-Einträge
  • Nach Apply (default, ohne --include-bench): 0 e2e-Tenants
    • 0 e2e-Templates + 2 bench-Tenants (preserved) + 455 Audit-Einträge unverändert
  • Idempotenz-Lauf: „Keine Test-Daten gefunden — nichts zu tun"
  • Bench-Tenants-Behandlung: Default-Pattern nimmt sie nicht mit. --include-bench ist explizit Opt-In. Empfehlung: nur dann einsetzen, wenn für eine Demo wirklich kein Test-Tenant in der UI auftauchen soll — gen-test-tokens.sh legt sie beim nächsten E2E-Lauf wieder an.
  • Verworfene Pfade (out-of-scope):
  • Pytest-Hook nach E2E-Lauf (sauberster Pfad, aber Workflow- Eingriff in alle Test-Suites — eigener Block bei Bedarf)
  • CI-Integration (manuelles Operator-Tool, nicht automatisiert)
  • Cleanup von Audit-Log oder Keycloak-Usern

TODO-Platform-09 (Branch platform/todo-09-auth-stack-verification, 2026-04-27)

Systematische Auth-Stack-Verifikation über alle sechs Schichten und beide Realms (kora-platform, kora-tenants). Schließt drei zusätzliche Drift-Datenpunkte (#5–#7) und löst die in -06/-07/-08 offenen Tenant-UI-Folge-Fixe gemeinsam mit auf.

Lieferung:

Adressierte Drifts:

  • Drift #5 — operator-ui Pfad-Doppelung beim post-login redirect: useAuth.login() speicherte window.location.pathname (volle Pfadkomponente inkl. Router-Base /admin/operator/); CallbackPage's router.replace(target) mit createWebHistory("/admin/operator/") produzierte /admin/operator/admin/operator/.... Fix: pathRelativeToBase()-Helper schneidet die Router-Base ab. Identischer Fix für tenant-ui mit ROUTER_BASE="/tenant".
  • Drift #6 — OAuth state mismatch bei Re-Login: Bestätigt als Folge von #5 (kein eigener Fix nötig).
  • Drift #7 — Realm-JSON defaultClientScopes aspirativ: Beide Realm-JSONs deklarierten für alle Clients defaultClientScopes: ["openid","profile","email","roles","kora-scope"], obwohl die OIDC-Standard-Scopes als Client-Scope-Objekte gar nicht existieren. Live-Zustand war seit Anfang sauber ["kora-scope"]. Re-Import wäre an dem aspirativen JSON gescheitert. Fix per jq — 6 Client-Einträge in 2 Realm-JSONs auf ["kora-scope"] konsolidiert, optionalClientScopes analog leergezogen wo ["address","phone"]- Aspirativ stand.
  • TODO-Platform-07 — tenant-ui Auth-URL-Drift: Default in tenant-ui/src/composables/useAuth.ts von "/auth" auf "https://auth.kora.luki-net.org" umgestellt (analog -06).
  • Tenant-UI Scope-Patch: Scope-Request von "openid profile email" auf "openid kora-scope" (analog -08).

Test-Erweiterungen:

  • frontend/operator-ui/e2e/auth-redirect.spec.ts um zweiten Test erweitert (Drift #5 — post_login_redirect-Path-Stripping prüft per route.abort + sessionStorage-Polling)
  • frontend/tenant-ui/e2e/auth-redirect.spec.ts neu (analog operator-ui) inkl. realm-konformem Scope-Assertion + Path-Stripping
  • ESM-Polyfill-Fix in frontend/tenant-ui/e2e/helpers.ts (__dirname aus import.meta.url) — ohne den Fix konnte der neue Spec gar nicht geladen werden (pre-existierender Bug, narrow gefixt um Test-Suite zu unblocken)

Test-Status nach Fix:

  • Operator-UI Vitest 100/100, Playwright 10/10
  • Tenant-UI Vitest 8/8, Playwright 2/2 neue Auth-Specs (existing template-update.spec ist test.skip)
  • verify-auth-stack.sh: 57/57 Checks grün, 0 Drifts

Routing-Page §6: Drift-Pattern auf 7 Datenpunkte finalisiert, Lessons-Learned um Router-Base-Schicht-Erkenntnis erweitert, Anti-Drift-Disziplin via Verify-Skript + Soll-Zustand-Doku formalisiert. Status /admin/operator/* und /tenant/* 🟢.

Browser-Test: Lutz live verifiziert — siehe Rückmeldung.

TODO-Platform-07 (im Rahmen von TODO-Platform-09 mit-aufgelöst, 2026-04-27)

Tenant-UI hatte denselben Default-Bug wie Operator-UI vor TODO-Platform-06: baseUrl: import.meta.env.VITE_KEYCLOAK_BASE_URL ?? "/auth". Im Browser resolved das auf platform.kora.luki-net.org/auth/realms/kora-tenants/... — FastAPI-404. Im Rahmen von TODO-Platform-09 als Teil der systematischen Auth-Stack-Verifikation mit-gefixt:

  • Default in frontend/tenant-ui/src/composables/useAuth.ts auf "https://auth.kora.luki-net.org" umgestellt
  • Plus Drift-#5-Analog-Fix (pathRelativeToBase() mit ROUTER_BASE="/tenant")
  • Plus tenant-ui Scope-Patch (analog -08): "openid kora-scope" statt "openid profile email"
  • Anti-Regression-Spec frontend/tenant-ui/e2e/auth-redirect.spec.ts (analog operator-ui) prüft Auth-URL + Scope + Path-Stripping
  • ESM-Polyfill-Fix in frontend/tenant-ui/e2e/helpers.ts als notwendiger Side-Quest (__dirname unter "type": "module" braucht fileURLToPath(import.meta.url))

TODO-Platform-08 (Branch platform/fix-operator-ui-oidc-scopes, 2026-04-26)

operator-ui-OIDC-Scope-Drift gefixt: Frontend fragte scope=openid profile email an, obwohl die Realms (kora-platform, kora-tenants) bewusst nur die Custom-Client-Scope kora-scope und offline_access als Client-Scope-Objekte haben. Keycloak antwortete mit error=invalid_scope, der Login-Callback war nicht durchführbar.

  • Lösungs-Variante: Option C — Frontend-Scope-Request in useAuth.ts:125 von "openid profile email" auf "openid kora-scope" reduziert. Begründung: kora-scope enthält bereits Mapper für preferred_username, email, realm_access.roles und aud=kora-api und liefert damit alle Claims, die Operator-UI konsumiert. openid bleibt als OIDC-Protokoll-Marker (id-Token- Issuance). Realm bleibt minimal — keine neuen Client-Scope-Objekte, keine Mapper-Duplikation.
  • Verworfene Pfade:
  • A1 (Realm-Erweiterung um profile/email-Scopes + Mapper- Konfiguration in beiden Realms) — ~2–3h Aufwand, Realm-Architektur wäre vom Custom-Scope-Pattern abgewichen
  • A2 (wie A1, nur kora-platform) — Asymmetrie zu kora-tenants, widerspricht Drift-Discipline
  • B (scope=kora-scope ohne openid) — verzichtet auf id-Token, OAuth2-statt-OIDC-Mode
  • Discovery-Datenpunkte:
  • Live-Keycloak kora-platform Client-Scopes: [offline_access, kora-scope] — keine OIDC-Standard-Scopes
  • Live-Keycloak kora-tenants Client-Scopes: identisch
  • Realm-JSON ↔ Live kora-scope-Mapper: kein substantieller Drift (nur Auto-Defaults beim Read)
  • kora-scope-Mapper liefert: preferred_username, email, realm_access.roles, aud=kora-api (kora-tenants zusätzlich groups)
  • Token-Endpoint-Verifikation:
    • scope=openid+kora-scope → HTTP 302 (Keycloak-Login-Form)
    • scope=openid+profile+email → callback mit error=invalid_scope&error_description=Invalid+scopes:+openid+profile+email
  • Anti-Regression: auth-redirect.spec.ts um Scope-Param-Assertions erweitert — URL.searchParams.get("scope") muss exakt "openid kora-scope" sein, "profile" und "email" dürfen nicht im Scope auftauchen. Robust gegen Encoding-Varianten (+ vs. %20).
  • Routing-Page: §6 Drift-Pattern um Datenpunkt #4 (Frontend-OIDC-Scope ↔ Realm-Architektur) erweitert, Lessons-Learned-Block formalisiert. Status-Marker /admin/operator/* bleiben 🟢 (durch dieses Fix funktional validiert), aber TODO-Platform-09 trackt die noch ausstehende systematische Auth-Stack-Verifikation aller Schichten und beider Realms — vor Block 8 zwingend.
  • Tenant-UI-Drift: symmetrischer Fix für tenant-ui ist bewusst NICHT in diesem Commit — wird über TODO-Platform-09 (siehe Archiv-Eintrag oben — am 2026-04-27 gemeinsam mit der TODO-Platform-07-Auth-URL-Korrektur und dem Verifikations-Skript abgehandelt).
  • Stellen-Inventar (Phase 1): Genau eine Code-Stelle relevant — frontend/operator-ui/src/composables/useAuth.ts:125. Die scope:-Treffer in __tests__/-Dateien sind alle Module-Scope- Klassifikationen ("core"/"external_eligible"/"internal_only", Block-7.3-Domain), nicht OAuth-Scopes. Token-Refresh und Logout übergeben keinen expliziten Scope.

TODO-Platform-06 (Branch platform/fix-operator-ui-auth-subdomain, 2026-04-26)

Operator-UI nutzt nun auth.kora.luki-net.org als OIDC-Authorize- Endpoint statt platform.kora.luki-net.org/auth/... — letzteres traf das FastAPI-Backend (404), weil NPMplus dort keine Keycloak-Route hat. Bug war seit Block 7.1b live, aber durch den E2E-Seed-Token-Hook (window.__KORA_E2E_SEED__) in keinem Test sichtbar.

  • Discovery: Default in frontend/operator-ui/src/composables/useAuth.ts:28 war import.meta.env.VITE_KEYCLOAK_BASE_URL ?? "/auth". Vite-Build bekam keine VITE_KEYCLOAK_BASE_URL-Build-Arg im Dockerfile, also ging der relative /auth-Default ins Bundle. Im Browser resolved das auf platform.kora.luki-net.org/auth/realms/... — FastAPI-404.
  • Fix: Default auf absolute URL https://auth.kora.luki-net.org umgestellt (moderne Keycloak-Pfad-Konvention ohne /auth/-Präfix — verifiziert per OIDC-Discovery: /realms/... = 200, /auth/realms/... = 404).
  • Realm-Pfad-Konvention: Keycloak 24+ ohne /auth/-Präfix; das Frontend setzt nur die Base-URL, die OIDC-Library hängt /realms/<realm>/protocol/openid-connect/... selbst an.
  • Anti-Regression: frontend/operator-ui/e2e/auth-redirect.spec.ts fängt per page.route jede Cross-Origin-Navigation zur Auth-Subdomain ab und prüft die URL — explizit ohne Seed-Token-Hook. Zusätzlicher Anti-Bug-Check: Bug-Pfad platform.kora.luki-net.org/auth/* wird mit 404 stub'd und darf vom Frontend gar nicht angesprochen werden.
  • Realm-JSON / Live-Keycloak unverändert: Der operator-ui- Client im Realm-JSON enthält keine hartcodierten Auth-URLs, der Live-Client ist korrekt konfiguriert — nur die Frontend-Konfig war falsch.
  • README-Update: VITE_KEYCLOAK_BASE_URL-Default auf https://auth.kora.luki-net.org im Env-Var-Tabelle dokumentiert, Dev-Override-Hinweis (http://localhost:8236) aufgenommen.
  • Routing-Page: §1a /auth/* als „nicht bedient" explizit, §6 Auth-Subdomain-Klarstellung, Source-of-Truth-Hinweisblock um den dritten Drift-Datenpunkt erweitert (Frontend-Konfig + Test-Suite-Realismus).
  • Lessons-Learned: Drei Drifts in drei Schichten (Realm-JSON ↔ Live-Keycloak via TODO-Platform-05; Frontend-Konfig ↔ NPMplus-Routing via TODO-Platform-06; Test-Suite-Realismus durch Seed-Hook-Bypass). Source-of-Truth-Disziplin gilt für jede Schicht einzeln. „Tests grün" reicht nicht, wenn die Tests den fehleranfälligen Pfad bypassen — Anti-Regression muss explizit den ungeseedeten Pfad treffen.
  • Tenant-UI-Drift-Bestätigung (1d): tenant-ui hat denselben Default-Bug (useAuth.ts:27 defaultet auf "/auth") — bewusst nicht im selben Commit gefixt, sondern als TODO-Platform-07 dokumentiert (am 2026-04-27 im Rahmen von TODO-Platform-09 archiviert — siehe oben). Das hilft datieren, wann der Drift entstanden ist: beide UIs wurden vom selben Pattern portiert, der Default-Bug ist also vor 7.1b im Tenant-UI-Vorbild eingewandert (Block 5 UI-Framework-Scaffolding) und in beiden Surfaces geerbt.

TODO-Platform-05 (Branch platform/fix-operator-ui-client-live-import, 2026-04-25)

operator-ui-Public-Client wurde im laufenden kora-platform-Realm nachgezogen. Realm-JSON-Re-Import war wegen Confidential-Client-Secret- Regeneration tabu — daher Init-Script-Pattern analog zu create-audit-service-account.sh.

  • Init-Script: infra/keycloak/init-scripts/create-operator-ui-client.sh — extrahiert den Client-Block zur Laufzeit aus dem Realm-JSON, ruft kcadm.sh create clients -f. Idempotent: Re-Run prüft via kcadm get clients -q clientId=…, skippt wenn vorhanden, kein Drift-Reset.
  • Realm-JSON-Fix: Description gekürzt von > 300 auf ~245 Zeichen. Original sprengte den Keycloak-DB-Constraint CLIENT.DESCRIPTION varchar(255). Ohne Trim wäre kein Re-Import funktional gewesen.
  • Verifikation: kcadm get clients -r kora-platform -q clientId=operator-ui liefert den Client mit enabled=true, publicClient=true, standardFlowEnabled=true, directAccessGrantsEnabled=false, vollständigen Redirect-URIs (Prod
  • Dev Vite + Dev API-only) und kora-scope als Default-Client-Scope. PKCE-S256-Authorize-Endpoint-Smoke (HTTP 200 auf /auth?…&code_challenge_method=S256).
  • Routing-Page-Konsequenz: TLDR + §1a + §6 Status-Marker zurück von 🟡 auf 🟢. §6-Hinweisblock erweitert um Source-of-Truth-Modell („Realm-JSON deklariert, Init-Scripts garantieren idempotent").
  • Runbook-Erweiterung: deployment/operator-ui-client.md Abschnitt „Drift-Vermeidung" mit Aufruf-Reihenfolge bei Realm-Reset und Verifikations-Befehl.

TODO-Block-5b / -5c / -5d (Branch platform/block-5-cleanup-audit-delta, 2026-04-24)

Audit-Delta-Differenzierung, REST-konforme Status-Codes und Race-Safety für tenant_modules-Assigns in einem Cleanup-Commit erledigt.

  • Service (src/kora_platform/services/module_service.py):
  • Neuer ModuleAssignResult-Dataclass (tenant_module, was_new, was_previously_enabled) — Provenance-Flags für die Route-Schicht.
  • assign_to_tenant umgebaut auf INSERT ... ON CONFLICT (tenant_id, module_id) DO UPDATE ... RETURNING xmax = 0. Der xmax=0-Trick unterscheidet INSERT- von UPDATE-Pfad ohne zweiten Round-Trip; Race-Condition bei parallelen Assigns auf dieselbe Composite-PK ist damit geschlossen (kein IntegrityError → kein 500).
  • execution_options={"populate_existing": True} auf dem session.execute(stmt, …), damit SQLAlchemys Identity-Map die RETURNING-Werte nicht durch die gecachte Vorversion überschreibt.
  • Separater SELECT is_enabled vor dem Upsert für was_previously_enabled — tiny race window dokumentiert, wirkt sich nur auf Audit-Action-Klassifikation aus, nicht auf Daten.
  • Route (src/kora_platform/api/routes/tenant_modules.py):
  • POST liefert jetzt 201 bei Create, 200 bei Update/Re-Enable (REST- Konvention, responses={200, 201} in OpenAPI dokumentiert).
  • Audit-Action differenziert per Flag-Matrix: tenant_module.assigned (was_new=True), tenant_module.re_enabled (was_new=False, prev_enabled=False) und tenant_module.reassigned (was_new=False, prev_enabled=True). before-Delta entsprechend (None / {is_enabled: False} / {is_enabled: True}).
  • Tests:
  • Unit: 4 neue Tests (was_new/was_previously_enabled auf Create, Reassign, nach Revoke, enabled_by-Update), zwei bestehende auf .tenant_module-Accessor migriert. 21/21 grün.
  • Integration: test_assign_is_race_safe_under_concurrent_calls — 20× asyncio.gather auf dieselbe (tenant, module)-Kombination, exakt 1× was_new=True, keine Exceptions. 4/4 grün.
  • Smoke: scripts/smoke-block5.sh um Tests 8 (re-assign → 200 + reassigned), 9 (SET is_enabled=FALSE + POST → 200 + re_enabled) und 10 (DELETE + POST → 201 + assigned) erweitert. 11/11 grün.
  • Code-Review-Report: Ein Deferred-Finding (Score 40, TODO-Block-5gupdated_at im Upsert-SET). Keine ≥ 80-Findings. Scope-Boundary (keine neue Alembic-Migration, TODO-5e/-5f bleiben auf post-Block-7) eingehalten.
  • Akzeptanz: 25/25 Tests + 11/11 Smoke + make redeploy-platform + Strict-MkDocs-Build grün.

TODO-Block-5a (Branch platform/block-5-platform-modules, 2026-04-23)

ensure_seed_modules committete fremde Session → Service flusht jetzt, Caller committet. Konsistent mit allen anderen Block-5-Services.

  • Fix: await session.commit()await session.flush() in src/kora_platform/services/module_seeds.py, Docstring ergänzt um „Caller owns the transaction".
  • Aufrufstellen angepasst: Kein Startup-Hook vorhanden; einziger Caller tests/unit/test_module_service.py::seeded_modules-Fixture kommt ohne Fixture-Änderung aus — der nachgelagerte tenant_id- Fixture bzw. Cleanup-commit() handeln die Transaktion sauber ab. Integration-Test-Fixture benutzt eigenen SQL-INSERT, nicht betroffen.
  • Smoke-Skript zusätzlich idempotent: scripts/smoke-block5.sh ergänzt um INSERT ... ON CONFLICT DO NOTHING-Setup, damit Seeds auch nach einem Unit-Test-Cleanup wieder da sind (vorher konnte der Smoke 404 werfen, wenn Unit-Tests zuvor liefen).
  • Akzeptanz: 17/17 Unit + 3/3 Integration + 7/7 Smoke grün.

TODO-Platform-02 (Branch platform/chore-platform-02-dev-deps, 2026-04-22)

pytest + pytest-asyncio permanent im Platform-api-Image.

  • Variante (A): neue test-Group in [project.optional-dependencies] in pyproject.toml (Subset von dev — nur pytest>=8.3 + pytest-asyncio>=0.24, ohne Linter/Formatter/Audit-Tools). Dockerfile-Zeile auf pip install ".[test]" geaendert. Multi-Stage- Refactor bewusst nicht gewaehlt (Overkill fuer das kleine Delta).
  • Akzeptanz: docker exec kora-platform-api pytest --version funktioniert direkt nach make redeploy-platform, kein manuelles pip install mehr noetig. 5/5 Integration-Tests + 38/38 Unit-Tests nach Rebuild gruen.
  • Image-Groesse: 8.78GB → 8.79GB (+10MB). Deutlich unter den erwarteten +40MB, weil pytest-asyncio als transitive Dependency bereits durch andere Pakete gezogen war und nur der kleine pytest-Hook-Overhead dazukommt.

TODO-Platform-01 (Branch platform/chore-platform-01-migrate-superuser, 2026-04-22)

Makefile-Wrapper fuer make migrate-platform und verwandte Targets.

  • Variante (A): PLATFORM_ALEMBIC := ALEMBIC_USE_SUPERUSER=1 alembic -c alembic.ini.platform — das Env-Prefix wird in der Makefile-Variable gesetzt, alle 5 Targets (migrate-platform, migrate-platform-down, migrate-platform-status, migrate-platform-new, migrate-platform-history) erben es.
  • Akzeptanz: make -n migrate-platform zeigt ALEMBIC_USE_SUPERUSER=1 im Command; echter Test make migrate-platform-status ohne Shell-Env liefert Current 0005 (head) + History ohne Permission-Fehler.
  • Offen bleibt als separater Task: Role-Permission-Rework auf Block-1.5-Level. Der kora_platform_migrator-Role hat weiterhin keinen Owner-Zugriff auf alembic_version — die pragmatische Lösung umgeht das, fixed es nicht. Der saubere Weg bleibt ein eigener Cleanup-Task, wenn Block-1.5-Setup ohnehin angefasst wird.

TODO-Cleanup03-02 (Branch platform/chore-cleanup03-02, 2026-04-22)

Compose-Env-Fragility via Makefile-Wrapper abgesichert.

  • Variante (A): Makefile-Wrapper. Die bereits existierende $(KORA_COMPOSE)-Variable (Makefile:105) wurde um zwei neue Targets ergänzt: platform-exec cmd="..." (Ad-hoc-Befehl via $(KORA_COMPOSE) exec api) und platform-bootstrap cmd="..." (Master-PW-Bootstrap via docker exec -e, scheitert früh bei fehlenden Bootstrap-Credentials).
  • Akzeptanzkriterien: make -n-Dry-Runs aller Platform-Targets zeigen --env-file .env.platform im erzeugten Befehl. Live- platform-exec-Test läuft ohne Keycloak-RestartCount-Increment. platform-bootstrap mit gesetzter Env legt User an und löscht keinen bestehenden Container. Regression-Smokes (Block-3 12/12, Request-ID 2/2) grün.
  • Runbook: deployment/compose-invocations.md neu — Prinzip, Wrapper-Targets, was-zu-vermeiden, Diagnose-Workflow bei Crashloop-Verdacht. Bootstrap-Abschnitt in deployment/keycloak-service-account.md auf make platform-bootstrap umgestellt. Strict-Build-Pattern in deployment/mkdocs-container.md auf make docs-kora-build umgestellt.
  • Bewusst ausgelassen: COMPOSE_AVS-Wrapper für den AVS-Demo-Stack — post-Go-Live obsolet (Fundament §16), analog Cleanup03-01. Docker-Secrets-Migration bleibt v1.x-Kandidat (Overkill für v1.0.0).

TODO-Cleanup03-01 (Branch platform/chore-cleanup03-01, 2026-04-22)

Single-File-Bind-Mounts → Dir-Mounts umgestellt, Scope (a) minimal:

  • Scope (a): Prometheus (monitoring/prometheus.yml + monitoring/alertmanager/alerts.yml) auf gemeinsamen Dir-Mount monitoring/prometheus/ → /etc/prometheus/ umgestellt. Akzeptanzkriterium (Live-Edit-Test mit docker restart, ohne --force-recreate) grün für beide Files.
  • Bewusst belassen: 10 weitere Single-File-Mounts im AVS-Demo-Stack (docker-compose.yml, Zeilen 146, 195, 196, 237, 239, 240, 241, 264, 266, 404). Der Stack wird per Go-Live v1.0.0 komplett abgeschaltet (Fundament §16 „frische Leinwand") — Investition in Artefakte mit Ablaufdatum ist fehlallokiert. Details in deployment/bind-mount-discipline.md.
  • Runbook: docs-kora/docs/deployment/bind-mount-discipline.md neu, dokumentiert Prinzip, Status, Option-A/B-Muster für neue Services und das Live-Edit-Test-Akzeptanzkriterium.

TODO-B2-03 (Branch platform/b2-03-service-account, 2026-04-22)

Keycloak Service-Account statt Master-Password im laufenden API-Container.

  • Client kora-platform-audit im kora-platform-Realm angelegt via Init-Script infra/keycloak/init-scripts/create-audit-service-account.sh (idempotent, Re-Run überschreibt Secret nicht).
  • Rechte strikt minimal: nur realm-management.view-events. D3- Verifikation hat view-users gestrichen — der Poller nutzt Event-Payload-Felder (userId, details.username), kein User-Enrichment-Call.
  • audit_poller.py: Client-Credentials-Flow gegen interne Keycloak-URL, Token-Cache mit asyncio.Lock + Double-Check-Pattern, 30s Safety-Margin vor expires_in. Prometheus-Counter audit_poller_auth_failures_total{reason}.
  • cli/bootstrap.py: bootstrap-operator-admin liest Master-PW aus KC_BOOTSTRAP_ADMIN_USERNAME/PASSWORD statt aus Settings — Invocation nur noch via docker compose run --env-file .env.bootstrap.
  • Cleanup: KORA_KEYCLOAK_ADMIN_USERNAME/PASSWORD entfernt aus docker-compose.platform.yml, .env.platform, .env.platform.example, config.py. .env.bootstrap.example + .gitignore-Eintrag neu.
  • Runbook: docs-kora/docs/deployment/keycloak-service-account.md.
  • Integration-Test: tests/integration/test_audit_service_account.py (Happy-Path, Deny-Path, Sad-Path).

Cleanup-02 (Branch platform/chore-cleanup-02, 2026-04-21)

Vier offene Todos aus Block 2 und Block 3 abgearbeitet, plus eine NPMplus-Alternative für B3-04.

ID Kurzbeschreibung Fix-Ort
TODO-B3-03 vendor_access_log.action: VARCHAR(64)TEXT. Reversible Alembic-Migration 0004, Model-Update, Truncation [:64] im Middleware-Code entfernt. Fix vorgezogen vor Block 4, weil die dort entstehenden Chatbot-Subpfade 64 Zeichen schon im Basisfall sprengen. alembic/platform/versions/0004_vendor_access_log_action_text.py, src/kora_platform/db/models/audit.py, src/kora_platform/api/dependencies/tenant_context.py
TODO-B2-01 Audit-Poller: Pagination bis Partial-Page oder Batch-Oldest ≤ Cursor, MAX_PAGES=50 als Safety-Rail, Log-Warning pro voller Seite. 5 Unit-Tests. src/kora_platform/services/audit_poller.py, tests/unit/test_audit_poller.py
TODO-B2-05 Redis-Cursor-Key avs:audit:last_event_tskora:platform:audit:last_event_ts. Einmalige, idempotente Startup-Migration mit Overlap-Handling. In Produktion-Log verifiziert: "audit cursor migrated". src/kora_platform/services/audit_poller.py, tests/unit/test_audit_poller.py
TODO-B2-07 platform_public_url, keycloak_base_url, keycloak_public_base_url auf pydantic.AnyHttpUrl. Alle Call-Sites mit str(...)-Cast gewrappt. AnyHttpUrl statt HttpUrl wegen Dev-Default http://localhost:8236. 5 Unit-Tests. src/kora_platform/config.py + Call-Sites in cli/bootstrap.py, api/dependencies/auth.py, api/health.py, services/audit_poller.py
TODO-B3-04 /metrics IP-Allowlist als FastAPI-Dependency (statt NPMplus-Ebene, weil NPMplus-Config außerhalb dieses Hosts liegt). Default-Allowlist: 127.0.0.1/32, ::1/128, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Liest X-Forwarded-For (leftmost) bzw. request.client.host. Verifiziert: LAN-IP → 200, external IP (X-Forwarded-For: 8.8.8.8) → 403. src/kora_platform/config.py, src/kora_platform/main.py

Verifikation: smoke-block3 12/12, smoke-request-id 2/2, integration 200×/0, 11 Unit-Tests grün, /metrics Allowlist live getestet.

mkdocs-Hygiene (Branch platform/chore-mkdocs-hygiene, 2026-04-21)

Kein Codefix, sondern Deployment-Layout — hier nur erwähnt, damit der Archiv-Zeitstrahl vollständig ist. Details im CHANGELOG.

  • Config-Datei mkdocs-kora.yml (Repo-Root) → docs-kora/mkdocs.yml.
  • Content-Dateien nach docs-kora/docs/.
  • Docker-Compose: Dir-Mount statt Dir+Single-File → Bind-Mount-Inode- Problem eliminiert, docker restart reicht für Doku-Änderungen.
  • URL /todo//offene-todos/.

Pre-Block-4 Cleanup (Branch platform/chore-pre-block4, 2026-04-21)

Der Cleanup-Commit behebt sieben aufgestaute Todos aus den Block-2- und Block-3-Reviews. Die Original-Eintragstexte stehen in den Git-Commits 2c57172 (Block 2) bzw. d18a1fc (Block 3). Kurzfassung hier:

ID Kurzbeschreibung Fix-Ort
L-B3-01 Keycloak-Realm-Import legt Built-in-Mappers nicht an — drei Mapper (realm-roles, preferred_username, email) jetzt direkt in beiden Realm-JSONs unter kora-scope. Dev/Prod-Parität hergestellt. infra/keycloak/realms/*-realm.json
TODO-B2-02 Correlation-ID-Middleware — UUID pro Request in structlog-ContextVar, im X-Request-Id-Header gespiegelt, im 500er-Response-Body. src/kora_platform/api/middleware/request_id.py (neu), main.py
TODO-B2-04 Redis ConnectionPool in app.state statt neuer Connection pro /health/ready-Call. Ping bei Startup, saubere Shutdown-Sequenz (Client → Pool). src/kora_platform/main.py, api/health.py
TODO-B2-06 refreshTokenMaxReuse: 0 + revokeRefreshToken: true auch in kora-tenants-realm.json (Refresh-Token-Rotation aktiv). infra/keycloak/realms/kora-tenants-realm.json
TODO-B2-08 Makefile redeploy-platform wartet jetzt per until-Loop (max 60s) auf /health/live statt fixem sleep 10. Makefile
TODO-B3-01 ContextVar-Reset-Pattern im Vendor-Pfad vereinheitlicht — current_tenant_id.set(None) mit Token-Reset im finally, kein Stale-Wert zwischen asyncio-Tasks. src/kora_platform/api/dependencies/tenant_context.py
TODO-B3-02 TENANT_ROLES als hartes Gate aktiviert: Gruppenmitgliedschaft allein reicht nicht mehr, ein tenant-admin/editor/viewer muss vorliegen — sonst 403 no_tenant_role. src/kora_platform/api/dependencies/tenant_context.py

Begleitend: scripts/gen-test-tokens.sh um die ensure_kora_scope_mappers- Phase entschlackt (durch Realm-JSON-Fix redundant). Smoke-Tests smoke-block3.sh um Tests 9 (no_tenant_role) + 10 (X-Request-Id round-trip) erweitert; neuer scripts/smoke-request-id.sh für die Correlation-ID- Verifikation gegen unauthenticated Endpoints. mkdocs-Nav thematisch neu strukturiert (Start / Roadmap / Konzepte / Prozesse / Deployment / Änderungen mit eingebettetem TODO-Link).


Letzte Aktualisierung: 2026-05-02 (TODO-Konzept-02-Lauf: §17.2a Reconciliation Nr. 7 mit Pattern-Reife-Quote-Trendlinie pro Block-Typ ergänzt, §17.5 Cleanup-Wellen separat ausgewiesen, Konzept v5.3.3 → v5.3.4. Plus Archiv-Sweep: 32 TODOs aus v1.3.0-D2/D1/Block-7.3-Familien auf „Erledigt" gezogen mit Merge-Hash-Verweis.) Nächste Review: Vor Block 11 (Widget-Integration) oder als Teil der Cleanup-Welle vor Block 13.