Changelog — kora Platform¶
Alle Änderungen an der kora-Plattform (v1.0.0+) sind in diesem Dokument festgehalten. Format: Keep a Changelog, Versionierung nach SemVer.
Die AVS-Produkt-spezifische Historie (Demo-Evolution, v0.x) liegt unter docs.avs.luki-net.org.
Offene technische Schulden und Deferred-Items sind in Offene TODOs gepflegt.
[Unreleased] — Branch platform/v1.0.0¶
Block-19 Tag-7-Observation — BGE-M3 Hybrid Retrieval ✅ Grün (2026-05-10)¶
Branch: platform/k-block19-tag7-observation (von platform/v1.0.0-HEAD 592ba9c = Karte-B-Backlog-Inventur-Merge)
Status: ✓ Grün — keine Regression nach 7 Tagen Production.
Tag-7-Observation gegenüber Block-18-Baseline (eval_retrieval_block18_v2.json)
und Phase-3-Eval Tag-0 (eval_retrieval_block19_phase3_20260503-0545.json):
| Metrik | Block 18 | Tag 0 | Δ | Bewertung |
|---|---|---|---|---|
| Recall@5 | 0.92 | 0.96 | +0.04 | ✓ verbessert |
| Recall@10 | 0.92 | 0.96 | +0.04 | ✓ verbessert |
| MRR | 0.7667 | 0.7400 | −0.0267 | ⚠ dokumentierter Reranker-Bias q006/q025 |
| avg_latency_ms | 155.3 | 140.1 | −15.2 | ✓ verbessert |
Live-Stack-Health (Tag 7):
- API Up 7 days (healthy), smoke-hybrid grün (8 docs retrieved)
- Cutover-Config aktiv: use_hybrid_retrieval=True, collection avs_handbuecher_bge_m3
- 0 Query-Errors, 2 No-Match (niedrige Live-Last)
- Beide Qdrant-Collections green (242 points), Snapshot-Recovery vorhanden
MRR-Drift ist dokumentierter Cross-Encoder-Bias (q006, q025) — TODO-Block-19-5 (Reranker-Upgrade auf bge-reranker-v2-m3, ~4-6h) als Followup-Karte angelegt, nicht regression-blocking.
.env.block19_backup als Rollback-Pfad behalten (analog 30d-Snapshot-Retention
in backups/qdrant/); Code-Referenzen in config.py und Tests bleiben bis
Cleanup-Welle ~2026-06-02.
Aufwand: ~25min Wall-Clock (Cap 45min, −44% unter Cap).
K-Q3-Fix — Widget-Session-Recovery + Cookie-Scope + Health-Pfad (⏳ Branch offen)¶
Branch: platform/k-q3-fix-widget-session-lifecycle (von
platform/v1.0.0-HEAD 494ca32 = Phase-4a-2-Merge)
Status: Phase 0-2 grün, 5/5 Local-Smokes PASS, Regression
465/4/78 unverändert, Browser-Smoke ausstehend (User-Verifikation),
Commit ausstehend.
Strategischer Kontext¶
Phase-4a-2-Browser-Smoke hatte aufgedeckt, dass Tourismus + PostgreSQL
beim Cross-Showcase-Wechsel session_not_found-Errors lieferten —
nur AVS funktionierte. K-Q3-Investigation hat festgestellt:
Cross-Tenant-Cookie-Leak. Cookie avs_sid war auf path=/
scoped, wurde über alle Showcase-Pfade auf demo.avs.luki-net.org
geteilt. User chattet auf /avs-meldeschein/ → AVS-Session-UUID im
Cookie → Wechsel zu /tourismus/ → Widget liest gleichen Cookie,
schickt AVS-UUID an Tourismus-Chatbot → Backend RLS lehnt ab
(Tenant-Mismatch) → 404 session_not_found. Widget hatte keinen
Recovery-Pfad → User-UI bleibt im Offline-Modus.
K-Q3-Fix implementiert Pfad C aus der Investigation: strukturelle Korrektur via Cookie-Scope-pro-Chatbot, plus 404-Recovery, plus Health-Pfad-Fix als Beifang.
Changed — Widget-Source¶
src/widget/src/avs-chat-widget.ts(+92 LOC, -36 LOC):cookieNameFor(chatbotId)— Cookie-Name pro Chatbot:avs_sid_<chatbotId-shortform>(8-Zeichen-Slice). Strukturelle Korrektur: Cross-Tenant-Sharing unmöglich, weil jeder Chatbot einen eigenen Cookie-Namen-Scope hat.getSessionCookie(chatbotId)/setSessionCookie(chatbotId, id)/clearSessionCookie(chatbotId)— alle Cookie-Helpers nehmen jetztchatbotIdals Parameter.clearLegacyCookie()— alter unscopedavs_sid-Cookie wird beimconnectedCallbackBEVORgetSessionCookie()läuft explizit gelöscht. Damit verschwinden Phase-4a-2-Cookies automatisch bei der ersten Page-Reload.onSessionInvalidated-Wiring (im Constructor):this.api.onSessionInvalidated = () => { clearSessionCookie(this.chatbotId); this.sessionId = null; this.historyLoaded = false; }.historyLoaded-Flag-Reihenfolge umgekehrt (getHistoryzuerst, dann Flag setzen) — bei 404 keine Re-Try-Sperre mehr in der Session.onApiSuccess()ist idempotent:errCount=0+hideOffline()nur wenn Banner oder Polling aktiv. Stoppt das endlose Health-Polling, sobald die nächste Query erfolgreich ist.-
5 Aufrufer von
getSessionCookie/setSessionCookie/clearSessionCookie/clearLegacyCookiedurchreichen jetztthis.chatbotId. -
src/widget/src/api.ts(+101 LOC, -18 LOC): ApiError.detail— drittes Konstruktor-Arg, optional.fetchWithRetrybefülltdetailausbody.detail. ImqueryStream-Fehlerpfad wird der 4xx-Body explizit einmal gelesen unddetailan denApiError-Konstruktor weitergereicht.ApiClient.onSessionInvalidated-Callback (public field, nullable).query()undqueryStream()haben 404-Handler mit_alreadyRetried-Flag. Bei 404 +detail==="session_not_found"→ Callback aufrufen → 1× Retry mitsession_id: null.buildQueryBody()normalisiertundefined → null, sodasssession_idIMMER im JSON-Body landet (Backend erwartet das Feld; null wird vom Backend als "neue Session anlegen" behandelt).- Health-Pfad-Fix (eine Zeile, ~252):
/health→/health/live. Stoppt das 404-Endlospolling im Background-Health-Check.
Changed — Bundle¶
src/widget/dist/avs-chat-widget.min.js— neuer Vite-Build. Bundle-Größe: 30810 Bytes (vorher 29884; +1.6 KB für Recovery-Logik, im erwarteten Bereich 28-35 KB). 9.39 KB gzipped.
Changed — HTML Cache-Bust¶
infra/demo-frontend/html/avs-meldeschein/index.html+tourismus/index.html+postgres/index.html— Script-Src um?v=2026-05-07erweitert. Damit lädt Browser/ NPMplus-Edge garantiert den neuen Bundle (kein Cache-Hit auf altem Hash). Übersichtsseite (index.html) hat kein Widget, bleibt unverändert.
Operations¶
- Vite-Build:
cd src/widget && npm run build— TypeScript- strict-Compile grün, Vite-IIFE-Output mit esbuild-Min, ES2020-Target. - Bind-Mount-Sync verifiziert:
docker compose exec api stat /app/src/widget/dist/avs-chat-widget.min.jszeigt gleichen mtime + 30810 Bytes wie auf Host (./src/widget/dist:/app/src/widget/dist:ro). - demo-frontend restart:
docker compose -p kora-platform restart demo-frontend— nginx serviert die neuen HTMLs mit Cache-Bust-Suffix.
Local-Smoke (5/5 PASS)¶
curl http://localhost:8281/tourismus/→ HTML enthältavs-chat-widget.min.js?v=2026-05-07.curl -I https://platform.kora.luki-net.org/static/widget/avs-chat-widget.min.js→ HTTP 200, JS-MIME.- Bundle-Größe via Download: 30810 Bytes (gleich wie Host).
POST /api/v1/widget/chatbots/<tourismus-id>/querymitsession_id: null→ HTTP 200, neue UUID (803a69b7-dadd-4955-b216-ba64b7957a74) + faktisch korrekte Antwort aus Kurort-Wikipedia-Chunks.- Bundle-Code-Verifikation: Bundle enthält die neuen Tokens
avs_sid_(scoped Cookie),onSessionInvalidated(Callback),/health/live(Health-Pfad-Fix).avs_sid(legacy) ist nur als Substring im Migrate-Helper drin.
Browser-Smoke (User-Verifikation ausstehend)¶
Test-Sequence (vom User per Browser):
1. Cookies cleared (oder Inkognito) → https://demo.avs.luki-net.org/
öffnet Übersichtsseite.
2. Klick AVS → Widget öffnen → Test-Query "Wie funktioniert die
Kurtaxe?" → Antwort + Sources verifizieren. Network-Hook:
Cookie heißt avs_sid_51ff88ab (nicht mehr avs_sid).
3. Zurück → Klick Tourismus → Widget öffnen → Test-Query
"Was ist ein Kurort?" → Antwort + Sources verifizieren.
Network-Hook: Cookie heißt avs_sid_3751d296.
4. Zurück → Klick PostgreSQL → Test-Query "How do I create a
table?" → Antwort + Sources verifizieren. Cookie:
avs_sid_2a326112.
5. Zurück → Klick AVS nochmal → Widget weiterhin funktional
(existing Cookie für AVS bleibt unverändert).
6. Network-Hook: /health/live statt /health gepollt
(in Offline-State).
7. Network-Hook: Falls 404 session_not_found zwischendurch
auftritt (z.B. nach Backend-DB-Cleanup), Widget recovered
automatisch — User sieht keine Error-Nachricht.
Regression¶
pytest tests/unit/: 465 PASS / 4 FAILED / 78 SKIPPED —
identisch zu Phase-4a-2 (4 pre-existing Block-19-BGE-M3-Tests,
K-Q3-Fix touched nichts an der Embedder-Pipeline). Zero Regression.
Followups¶
- Sourcemap im Widget-Bundle (
vite.config.ts-Settingsourcemap: true) würde Debug-Sessions im Browser erleichtern. Aktuellfalse. - 7 weitere Bugs aus K-Q3-Investigation (Bug C "Stale-Cookie- Validation gegen /history" durch K-Q3-Fix gelöst, Bug D, E, H, I, J und Beobachtung 11 als separate Followups). Keiner davon blockt Phase 4a-2.
- Bundle-Größen-Drift in
CLAUDE.md(16 KB-Angabe vs. 30 KB tatsächlich) — Doku-Update.
Phase 4a-2 — Public-Domain-Showcases mit Stitch-UI-Generation (Decommissioning-Welle 2/3, ⏳ Branch offen)¶
Branch: platform/phase4a-2-public-showcases (von
platform/v1.0.0-HEAD 441b316 = Phase-4a-1-Merge)
Status: Phase 0-3 grün, 6/6 Local-Smokes PASS, Regression
465/4/78, Browser-Smoke ausstehend (User-Verifikation), Commit
ausstehend.
Strategischer Kontext¶
Zweite Welle der Phase-4-Karte. Phase 4a-2 fügt zwei zusätzliche
Showcases hinzu (Tourismus-Wikipedia + PostgreSQL-Doku) und
reorganisiert die Demo-Frontend-Struktur: / wird Übersichtsseite,
AVS-Showcase zieht in /avs-meldeschein/-Unterpfad. Damit ist
demo.avs.luki-net.org ein Multi-Tenant-Showcase mit drei eigenen
Brandings, statt nur AVS-Single-Tenant.
Added — Korpus¶
-
data/documents/showcase-tourismus/(gitignored, 5 PDFs, 1.9 MB total): 5 Wikipedia-Artikel via REST-API (https://de.wikipedia.org/api/rest_v1/page/pdf/<lemma>): Tourismus_in_Deutschland (517 KB), Beherbergungsbetrieb (226 KB; Lemma-Korrektur — "Beherbergungsstatistik" existierte nicht in dt. WP), Kurort (491 KB), Wellnesstourismus (401 KB; Lemma-Korrektur — Schreibweise ohne Bindestrich), Deutscher_Tourismusverband (281 KB). -
data/documents/showcase-postgres/(gitignored, 2 PDFs, 2.0 MB total): PostgreSQL 18 (aktuelle Stable). Page-Extraction viapypdf(qpdf/pdftk nicht im Image): Tutorial Part I (Seiten 40-63, 92 KB) + Server Administration Part III (Seiten 600-994, 1.9 MB). TOC-basierte Page-Ranges (nicht Schätzung 1-50/500-700) — Kapitel werden sonst durchgeschnitten.
Added — Provisioning + Reindex Skripte¶
-
scripts/showcase_phase4a2_config.py(NEU, 110 LOC) — Geteilte Konstanten zwischen Tourismus + PostgreSQL: Slugs, Namen, Docs-Dirs, Allowed-Origins, Chunker-Configs.WIKIPEDIA_CHUNKER_CONFIGundPOSTGRES_CHUNKER_CONFIGsind identisch konfiguriert (target_chunk_words=600, overlap=1 Sentence) — gleicher SemanticChunker-Default wie AVS-Phase-3a-Bots, mitsentinel_namespacezur Disambiguierung in Logs/Audit. -
scripts/provision_showcase_tourismus.py(NEU, 269 LOC) +scripts/provision_showcase_postgres.py(NEU, 277 LOC) — Adaption vonprovision_avs_phase3a.py. Wichtige Abweichung: template-los provisioniert (Chatbot.template_id=None, beide Felder per Schema nullable). Showcase-Tenants haben keinen operator-curatedchatbot_template-Row;system_promptist Modul-Konstante. -
scripts/reindex_showcase_tourismus.py(NEU, 492 LOC) +scripts/reindex_showcase_postgres.py(NEU, 534 LOC) — Adaption vonreindex_avs_beherbergung.py. Multi-File-Loop über<docs_dir>/*.pdf, file-scoped-Delete viaFilterSelectormit triple-must (tenant_id+chatbot_id source_title=<pdf.stem>) — Pattern ausreindex_avs_kurverwaltung.py(Phase 3b). Pre-/Post-Counts pro File und total. Idempotent — Re-Runs ohne Initial-Drop der Collection sauber.
Added — Demo-Frontend-HTML (Stitch-Fallback auf manuell)¶
-
infra/demo-frontend/html/index.html(NEU, 342 LOC, manuell) — Übersichtsseite mit 3 Showcase-Cards (/avs-meldeschein/,/tourismus/,/postgres/) plus Architecture-Banner. Inter-Font, Brand-Akzent-Farben pro Card (#eb3e4a, #2c5282, #336791). -
infra/demo-frontend/html/avs-meldeschein/index.html(verschoben viagit mvvoninfra/demo-frontend/html/index.html, Inhalt unverändert) — Phase-4a-1-AVS-Showcase mit chatbot-id51ff88ab-121f-4cb9-b037-b85ce842abbf(Meldeschein). -
infra/demo-frontend/html/tourismus/index.html(NEU, 334 LOC, manuell + Widget-Init) — Tourismus-Showcase. Linearer Gradient-Hero in #2c5282, Inter-Font. Widget-Init mit chatbot-id3751d296-ea72-4e43-aaaf-ef11bd1596f9(substituiert nach Provisioning, Placeholder__TOURISMUS_CHATBOT_ID__initial im Source). -
infra/demo-frontend/html/postgres/index.html(NEU, 383 LOC, manuell + Widget-Init) — PostgreSQL-Showcase English. Code-Strip im Hero (JetBrains Mono), nummerierte Feature-Cards (01/02/03 monospace). Widget-Init mit chatbot-id2a326112-5c16-4037-ac0b-ab7d30e08ff8.
Stitch-Verwendung¶
- Google Stitch MCP-Server angebunden
(
https://stitch.googleapis.com/mcp). - Project erstellt:
kora-platform-showcases, Project-ID16618025949431128746. - Übersichtsseite Stitch-Generation: mit
GEMINI_3_FLASHerfolgreich (Screen-IDba9d62c494c442e6892f855261e0f01e, 218 LOC, Tailwind-CDN-basiert).GEMINI_3_PRODefault Timeout. Ergebnis qualitativ akzeptabel, aber Stilbruch zum Phase-4a-1-AVS-Showcase (Tailwind-CDN + externes Banner-Bild von googleusercontent vs. self-contained Inter-CSS). - Tourismus + PostgreSQL Stitch-Generation: beide Timeouts (auch mit gekürztem Prompt).
- Entscheidung Subagent D: Fallback auf manuelle HTML-Erstellung. Stitch-Card-Layout-Ideen wurden in der manuellen Übersichtsseite übernommen (Tag+Lang oben, Titel, Beschreibung, Footer-Link, 4px-Left-Border-Accent), aber ohne Tailwind. Stitch-Output committed wurde nicht — nur die manuell geschriebenen 3 HTMLs.
- Reproduzierbarkeit über committed HTML, nicht über Prompt-Replay.
Operations¶
- Korpus-Upload in Container:
docker cp data/documents/showcase-{tourismus,postgres} kora-platform-api:/app/data/documents/pluschown -R appuser:appuser /app/data. - Skripte-Upload: alle 5 Showcase-Skripte (config + 2
provision + 2 reindex) via
docker cp scripts/showcase_*.py kora-platform-api:/app/scripts/. - Tourismus-Provisioning:
- Tenant:
cbcceeac-f8af-4643-ae43-fa57ce36beec(showcase-tourismus-de). - Chatbot:
3751d296-ea72-4e43-aaaf-ef11bd1596f9(wikipedia-tourismus). - Allowed-Origins:
[demo.avs.luki-net.org, localhost:8281]. - Tourismus-Reindex: 13 Chunks über 5 Files (Beherbergungsstatistik=1, Kurort=3, Tourismus_Deutschland=1, Tourismusverband=1, Wellness=7). Drift gegenüber Spec-Schätzung (~30-60 Chunks) — Wikipedia-Artikel sind kompakt; SemanticChunker mit target=600 Wörtern emittiert wenige Chunks pro Artikel. Funktional ausreichend (siehe Smoke 5).
- PostgreSQL-Provisioning:
- Tenant:
89e370a3-39ff-45af-bc03-cda224490e35(showcase-postgres-doku). - Chatbot:
2a326112-5c16-4037-ac0b-ab7d30e08ff8(postgres-tutorial). - PostgreSQL-Reindex: 279 Chunks über 2 Files (Tutorial 24 Seiten + Server-Admin 395 Seiten). Klar über Threshold.
- chatbot-id-Substitution via
sed -iintourismus/index.htmlundpostgres/index.html. docker compose -p kora-platform restart demo-frontend— nginx serviert die neuen HTMLs.
Operations-Drifts in Run¶
libxcb1+libgl1+libglib2.0-0fehlten im Image — Docling/cv2-Stack braucht diese System-Libs fürimport cv2. Workaround:docker compose exec -u 0 api apt-get install -y libxcb1 libgl1 libglib2.0-0. Pre-existing Drift aus Phase 3a/3b; per Followup-Liste in Dockerfile.platform aufzunehmen.tests/undscripts/reindex_with_*.pysind nicht im Image — bei jedem Container-Recreate mussdocker cpausgeführt werden. Pre-existing Drift; gehört in Test-Infra-Refactor (Followup).fakeredisist Test-only-Dep, nicht im Image — wird mitpip install fakeredis -qals One-Shot in den Container geschoben.- Background-Bash-Stdout-Loss — der erste PG-Reindex-Run exitete mit Code 0, aber sein stdout wurde mit 0 Bytes ausgegeben. Idempotenter Re-Run mit gleichem Resultat (279 Chunks). Vermutlich Bash-auto-background + output-redirect-Buffering-Issue.
End-to-End-Smoke (6/6 PASS)¶
curl http://localhost:8281/→ Übersichtsseite mit allen 3 Cards (AVS Meldeschein + Tourismus Deutschland + PostgreSQL Tutorial).curl http://localhost:8281/avs-meldeschein/→ AVS-HTML (Title "AVS Meldeschein — Digitaler Gästeservice"), chatbot-id51ff88ab-....curl http://localhost:8281/tourismus/→ Tourismus-HTML (Title "Tourismus Deutschland — Wissens-Assistent"), chatbot-id3751d296-....curl http://localhost:8281/postgres/→ PostgreSQL-HTML (Title "PostgreSQL Tutorial Assistant"), chatbot-id2a326112-....- Tourismus-Backend-Query "Was ist ein Kurort?" gegen
https://platform.kora.luki-net.org/api/v1/widget/chatbots/<id>/query→ substantive Antwort mit "klimatischen, landschaftlichen oder medizinischen Verhältnisse... Gesundheitsurlaub und Erholung" — direkt aus Kurort-Wikipedia-Chunks. - PostgreSQL-Backend-Query "How do I create a table?" →
substantive Antwort mit
CREATE TABLE-Syntax-Beispiel — direkt aus Tutorial-Chunks. - AVS-Regression-Smoke "Was ist ein Meldeschein?" → Antwort weiterhin grün (Schweiz/Liechtenstein-Kontext aus Phase-3a-Korpus).
Anomalie: citations: 0 in beiden Showcase-Queries. Antworten
sind faktisch korrekt und korpus-basiert (kein Halluzinations-
Indikator), aber der Citation-Aggregator liefert keine. Separate
Investigation als Followup — blockiert Smoke-Gate nicht.
Browser-Smoke¶
User-Verifikation steht aus. Erwartetes Bild: 3 Showcases
über https://demo.avs.luki-net.org/{,avs-meldeschein,tourismus,postgres}
interaktiv, jedes Widget öffnet, Test-Query liefert Antwort.
Regression¶
pytest tests/unit/: 465 PASS / 4 FAILED / 78 SKIPPED.
Die 4 Failures sind alle pre-existing aus Block 19 BGE-M3-Wiring
(test_avs_chatbot_config::test_defaults_keep_legacy_dense_only_pipeline,
test_chunks_indexed_metric::test_grafana_panel_definition_present_and_valid,
test_pipelines::test_indexing_uses_bge_m3_when_hybrid_on,
test_pipelines::test_rag_hybrid_wires_dense_sparse_and_joiner) —
Phase 4a-2 hat keinen Impact auf Embedder-Pipeline. Zero Regression.
Followups¶
citations: 0im Widget-Query-Response für Showcase-Bots (separate Investigation).- Tourismus-Reindex-Chunk-Count niedriger als erwartet (13 vs. ~30-60). Optional: Chunker-Config anpassen oder mehr Wikipedia-Artikel hinzufügen wenn Browser-Smoke Knowledge-Coverage-Gap zeigt.
Dockerfile.platform:libxcb1 libgl1 libglib2.0-0als System-Deps integrieren;tests/ins Image;fakeredisals optionale dev-Dep.
Phase 4a-1 — Demo-Frontend-Infrastruktur + AVS-Showcase (Decommissioning-Welle 1/3, ⏳ Branch offen)¶
Branch: platform/phase4a-1-demo-frontend-avs (von
platform/v1.0.0-HEAD 9ab66b3 = K-3f-Merge)
Status: Phase 0-5 grün, End-to-End-Smoke 5/5 PASS (inkl.
Browser-Smoke), Commit ausstehend.
Strategischer Kontext¶
Erste Welle der Phase-4-Karte (Demo-Frontend-Migration +
avs-Stack-Decommissioning). Phase 4a-1 baut die Demo-Frontend-
Infrastruktur und routet demo.avs.luki-net.org über NPMplus auf
einen neuen kora-Container — ohne Bestand-Demo zu unterbrechen.
Der avs-Stack läuft parallel weiter, wird erst in Phase 4b nach
7-Tage-Observation abgeschaltet.
Added¶
-
infra/demo-frontend/html/index.html(NEU, 320 Zeilen) — AVS-Meldeschein-Showcase, abgeleitet vonsrc/widget/demo-avs.html(1:1-Kopie der Hero/Features/Stats/ CTA/Footer-Struktur). Einzige Änderung gegenüber Vorlage: der Widget-Init-Block: -
<script src="https://platform.kora.luki-net.org/static/widget/avs-chat-widget.min.js">(vorher:/widget/avs-chat-widget.min.jsaus avs-nginx) <avs-chat-widget>mitapi-url=https://platform.kora.luki-net.org/api/v1,chatbot-id=51ff88ab-121f-4cb9-b037-b85ce842abbf(Phase-3a-AVS-Meldeschein),theme-color=#eb3e4a, Welcome-Message, 4 Suggested-Questions-
product-id-Attribut entfernt (kora-Mode resolved aus chatbot-id server-seitig; Legacy-AVS-Attribut würde kollidieren) -
Service
demo-frontendindocker-compose.platform.yml—nginx:alpine, Container-Namekora-platform-demo-frontend, Host-Port 8281 (kora-82XX-Block), Bind-Mountinfra/demo-frontend/html:/usr/share/nginx/html:ro,wget-Healthcheck (alpine hat kein curl),restart: unless-stopped, Networkkora-platform-net. -
Static-Endpoint
/static/widget/*insrc/kora_platform/main.py(+17 Zeilen) —StaticFiles-Mount auf/app/src/widget/dist/nach allen Routern, mitif widget_dist_dir.is_dir()-Skip für Test-Setups ohne Bind-Mount. CORSMiddleware (K-3f) wirkt auch auf diesen Pfad —Access-Control-Allow-Origin-Header wird auf Widget-Bundle-Responses gesetzt, kompatibel mit SRI/crossorigin="anonymous"-Setups. -
api-Bind-Mount indocker-compose.platform.yml(./src/widget/dist:/app/src/widget/dist:ro) — Bundle wird vom Widget-Build-Step (make widget-build) in das Repo gelegt und zur Laufzeit read-only ins API-Image gemountet. Read-only, weil API niemals den Bundle modifiziert. -
tests/unit/test_static_widget_endpoint.py(NEU, 94 Zeilen, 3 Tests): -
test_widget_bundle_served: GET → 200 + JS-MIME-Type (text/javascriptoderapplication/javascript— RFC 9239 erlaubt beide; Test akzeptiert via"javascript" in content_type) test_widget_bundle_404_for_missing: GET unbekannter Path → 404test_static_widget_path_traversal_blocked: Path-Traversal wird von Starlette-StaticFiles geblocktpytest.mark.skipif-Guard für Test-Umgebungen ohne Bundle
Operations¶
- NPMplus-Backend-Switch (manueller Schritt am Edge-Gerät):
demo.avs.luki-net.orgForward von192.168.0.7:80(avs-nginx) auf192.168.0.7:8281(kora-platform-demo-frontend). Cert-Reuse (LE 7-day Cert, notAfter2026-05-10), kein SSL-Reissue.
End-to-End-Smoke (5/5 PASS)¶
curl http://localhost:8281/→ 200, AVS-HTML (<title>AVS Meldeschein — Digitaler Gästeservice</title>)curl https://platform.kora.luki-net.org/static/widget/avs-chat-widget.min.js→ 200, 29884 Bytes,Content-Type: text/javascript; charset=utf-8- Widget-Bundle mit
Origin: https://demo.avs.luki-net.org-Header → ACAO-Header gesetzt (K-3f-CORSMiddleware aktiv auf Static-Pfad) curl https://demo.avs.luki-net.org/(nach NPMplus-Switch) → 200, AVS-HTML (gleicher Inhalt wie localhost:8281)- Browser-Smoke via MCP:
https://demo.avs.luki-net.orgöffnet Showcase, Widget-Button rendert, Click öffnet Chat-Panel mit 4 Suggested-Questions, Test-Query „Wie funktioniert die Kurtaxe?" → strukturierte Antwort (nummerierte Liste, fett-Formatierung), Source-Chip „Meldeschein Handbuch". Widget-Bundle wurde vonplatform.kora.luki-net.org/static/widget/geladen, Query-Request ging anplatform.kora.luki-net.org/api/v1/widget/chatbots/.../query, Origin-Headerhttps://demo.avs.luki-net.org, Response 200.
Regression¶
452/466 Unit-Tests grün (von 458 auf K-3f-Branch +
3 Phase-4a-1-Tests, dafür 4 widget_build-Tests flippen von
FAILED auf PASS durch den api-Bind-Mount auf
src/widget/dist/). 4 pre-existing FAILED unverändert
(Block-19-Hybrid-Defaults, Grafana-Path).
Bonus-Effekt: Pre-existing-Failure-Liste schrumpft von 8 auf 4.
Operations-Drifts (im Run gefunden)¶
tests/-Verzeichnis nicht im API-Image — manuellerdocker cppro Test-Run nötig. Followup: tests in Image bauen oder Bind-Mount in Compose.fakeredisnicht inpyproject.toml— manuellerpip installnach jedem Recreate. Followup: als optional dev/test-Dep einsortieren.- MIME-Type-Drift:
mimetypes-stdlib lieferttext/javascript(RFC 9239), Test-Assertion erwartete deprecatedapplication/javascript. Test-Assertion auf"javascript" in content_typeaufgeweicht. - Browser-Cache: alter avs-Redirect
/ → /demowurde gecached; Hard-Refresh / Cache-Bust nötig für sauberen Smoke. - Image-Rebuild bei Phase-4a-1-Code-Änderungen Pflicht —
up --force-recreateallein reicht nicht (Image bleibt am Stand des letztenbuild). Im Run bei Stack-Resync nach K-3f-Rebase aufgetreten (StaticFiles-Mount fehlte im laufenden Image).
Deferred / Followup¶
- libxcb1/libxext6/libsm6/libglib2.0-0/libgl1 in
Dockerfile.platformaufnehmen (Phase-3a-Followup, immer noch offen) tests/-Verzeichnis ins API-Image bauenfakeredisinpyproject.tomlals optional-dev- Phase 4a-2: zwei zusätzliche Public-Domain-Showcases
(Wikipedia-Tourismus + PostgreSQL-Doku) als eigene Tenants;
Default-Seite unter
/wird Übersichtsseite mit Links zu allen Showcases (AVS unter/avs-meldeschein). - Phase 4b: avs-Stack-Decommissioning nach 7-Tage-Observation ab Phase-4a-2-Go-Live.
K-3f — globale CORSMiddleware fuer kora-Platform (Architektur-Patch, ⏳ Branch offen)¶
Branch: platform/k-3f-cors-middleware (von
platform/v1.0.0-HEAD f8fa390)
Status: Phase 0-4 grün, 13/13 neue Tests + 4/4 Server-Smokes
PASS, Commit ausstehend.
Strategischer Kontext¶
Pflicht-Vorgänger für Phase-4a-1-Browser-Smoke. Discovery beim
ersten Phase-4a-1-Browser-Smoke zeigte: kora-Platform hatte
keine globale CORSMiddleware. Origin-Checks liefen ausschließlich
route-spezifisch im Widget-Query-Pfad gegen
tenant_branding.allowed_origins (K-3d) — das ist Backend-
Origin-Check, nicht CORS-Header-Setting. <script src="…"> Loads
funktionieren weiterhin (kein CORS-Mode für Plain-JS-Loads), aber
fetch() aus dem Widget heraus blockt mit „Failed to fetch", weil
Access-Control-Allow-Origin-Header fehlte.
CORSMiddleware vs tenant_branding.allowed_origins — orthogonale
Schutzschichten:
- CORSMiddleware (K-3f): Browser-side Header-Setting auf allen Endpoints. Verhindert Cross-Origin-Browser-Fetches von unerlaubten Domains.
tenant_branding.allowed_origins(K-3d): Backend-side Origin-Check für Widget-Public-Endpoints. Verhindert Token- Theft-Missbrauch (jemand kopiert die chatbot_id und versucht vom eigenen Server aus, das Widget zu nutzen — der Origin-Header des Server-Clients matcht die allowed_origins-Liste nicht).
Added¶
-
src/kora_platform/config.py(+30 Zeilen) —KoraSettings.cors_origins: list[str]mit PydanticBeforeValidator(_parse_cors_origins)Komma-Parser: -
Akzeptiert
None, leeren String, Whitespace-only →[] - Akzeptiert Komma-getrennt → gestrippte Liste, leere Segmente werden gedroppt
- Akzeptiert bereits parste Liste → Pass-Through mit Strip
-
pydantic_settings.NoDecodeim Annotation-Stack: blockt den Default-JSON-Decode von pydantic-settings für Complex-Types — sonst würde der Bootstrap mitSettingsError: error parsing value for field "cors_origins"abbrechen, weil pydantic-settingsjson.loads("https://a, https://b")versucht, bevor BeforeValidator drankommt -
src/kora_platform/main.py(+15 Zeilen) —CORSMiddlewarebedingt nachRateLimitMiddlewareregistriert (FastAPI/Starlette ist LIFO — letzteradd_middleware-Call ist outermost und sieht Requests zuerst; OPTIONS-Preflight umgeht damit RateLimit, das kein OPTIONS-Special-Casing hat). Konfiguration: -
allow_origins=settings.cors_origins allow_credentials=False(Widget nutzt keine Cookies)allow_methods=["GET", "POST", "OPTIONS"]allow_headers=["content-type", "x-loadtest-bypass"]max_age=600(Preflight-Cache 10 min)-
Konditional-Mount:
if settings.cors_origins:— leere Liste → keine Middleware (Default-Verhalten für Test-Setup ohne Env-Var) -
tests/unit/test_cors_middleware.py(NEU, 192 Zeilen, 13 Tests): -
8 Tests für
_parse_cors_origins(None/empty/single/multi/ whitespace-strip/list-passthrough/empty-segments-drop) - 5 Tests für CORSMiddleware E2E via minimaler FastAPI-App
(analog zu K-3e-Pattern; nicht der reale
create_app(), weil CORS-Contract DB/Redis/Qdrant-unabhängig ist):test_allowed_origin_gets_acao_headertest_disallowed_origin_gets_no_acao_headertest_options_preflight_returns_acam_and_acahtest_options_preflight_disallowed_origin_no_acaotest_no_middleware_when_origins_empty
Changed¶
-
docker-compose.platform.yml(+6 Zeilen) — neuerKORA_CORS_ORIGINS: ${CORS_ORIGINS:-}ENV-Mapping im api-Service; leerer Default = Middleware ungemountet. -
.env.platform.example(1 Zeile) — Default jetztCORS_ORIGINS=https://platform.kora.luki-net.org,https://demo.avs.luki-net.org(Phase-4a-1 + Phase-4a-2 Showcase-Domains werden dort später ergänzt).
Server-Smoke (4/4 PASS)¶
- Smoke 1 (allowed origin → /health/live):
Access-Control-Allow-Origin: https://demo.avs.luki-net.org Vary: Origin✓- Smoke 2 (OPTIONS-Preflight Widget-Endpoint): HTTP/2 200,
Access-Control-Allow-Methods: GET, POST, OPTIONS,Access-Control-Allow-Headers: …, content-type, x-loadtest-bypass,Access-Control-Max-Age: 600,Access-Control-Allow-Origin: https://demo.avs.luki-net.org✓ - Smoke 3 (disallowed origin
https://attacker.example.com): keinAccess-Control-*-Header in Response ✓ - Smoke 4 (zweiter allowed origin
https://platform.kora.luki-net.org):Access-Control-Allow-Origin: https://platform.kora.luki-net.org✓
Regression¶
458/466 Unit-Tests grün (+13 K-3f). 8 pre-existing FAILED unverändert (4 widget_build-Tests requiren Bind-Mount aus Phase-4a-1; 4 weitere Block-19-Hybrid-/Grafana-Drifts). Keine neuen Failures durch K-3f.
Bootstrap-Drift gefunden + gefixt¶
Initial-Implementation ohne NoDecode führte zu Container-
Crashloop bei up --force-recreate:
pydantic_settings.exceptions.SettingsError: error parsing value
for field "cors_origins" from source "EnvSettingsSource"
Root-Cause: pydantic-settings v2.6 versucht für Complex-Types
(list/dict) JSON-Decode des Env-Strings, BEVOR Validatoren laufen.
Comma-separated Strings sind kein valides JSON → SettingsError.
Fix: Annotated[list[str], NoDecode, BeforeValidator(...)] —
NoDecode skippt den JSON-Decode-Schritt. Pydantic-settings >= 2.5
hat NoDecode für genau diesen Fall.
Deferred / Followup¶
.env.platformist nicht committed (gitignored). Der Production-WertCORS_ORIGINS=https://platform.kora.luki-net.org, https://demo.avs.luki-net.orgmuss beim Deploy in der echten.env.platformgesetzt werden — bei leerer/fehlender Variable bleibt CORSMiddleware ungemountet (sicherer Default).- Phase-4a-1-Branch wird auf den neuen
platform/v1.0.0-HEAD rebaset, nachdem K-3f gemergt ist. Der.env.platform-Wert bleibt dabei live; die Phase-4a-1-Browser-Smoke-Retry kann danach erfolgen. - Phase-4a-2 wird zwei zusätzliche Showcase-Domains in
CORS_ORIGINSergänzen (Wikipedia-Tourismus + PostgreSQL- Doku-Tenants).
Phase 3b — Kurverwaltung-Reindex + 25-Query-Eval-Replay (Provisioning-Welle 2/2, ⏳ Branch offen)¶
Branch: platform/phase3b-avs-eval-replay (von
platform/v1.0.0-HEAD ac48afd)
Status: Phase 0-5 grün, Eval ACCEPTANCE PASS (Recall@5=0.92 =
Threshold), Commit ausstehend.
Strategischer Kontext¶
Zweite und letzte Welle der Phase-3-Karte. Phase 3a hat den AVS- Tenant + Chatbot + Beherbergungsbetrieb-PDF (35 Chunks) live gebracht. Phase 3b indexiert das zweite Handbuch (Kurverwaltung, 9 MB) zusätzlich und beweist via 25-Query-Eval-Replay, dass die kora-Pipeline (BGE-M3 Hybrid + Reranker + Qwen3-14B-AWQ) die Block-19-Baseline-Retrieval-Qualität (Recall@5=0.96) auf dem echten AVS-Korpus matcht. Acceptance-Schwelle Recall@5 ≥ 0.92.
Added¶
-
scripts/reindex_avs_kurverwaltung.py(Subagent A, 454 Zeilen) — additiver Reindex des Kurverwaltung-Handbuchs in die existing AVS-Collection, ohne die 35 Beherbergung-Chunks anzutasten: -
Filter-scoped Qdrant-Delete via
FilterSelectormit triple-must(tenant_id+chatbot_id+source_title == "Meldeschein Handbuch — Administration Kurverwaltung") —source_titleist deterministisch über Re-Runs, andere Documents (anderer Titel) bleiben unberührt - Pre-/Post-Counts pro Filter mit defensiver Assertion, dass
other_points(= 35 Beherbergung) unverändert bleibt -
Reuses Phase-3a-Pattern:
lookup_tenant_chatbot(),convert_and_chunk(),haystack_to_kora_chunks(),AVS_CHUNKER_CONFIG,DocumentUploader,CollectionDescriptor.for_chatbot -
scripts/eval_phase3_avs_replay.py(Subagent B + Patch, 956 Zeilen) — 25-Query-Eval-Replay gegen Block-19-Baseline: -
In-process FastAPI via
httpx.ASGITransport+app.router.lifespan_context(gleiche Plumbing wie Phase-3a- Smoke),X-Loadtest-Bypassgegen K-3e-Rate-Limit - Match-Methodik: Topic-Substring (case-insensitive) gegen Voll-Chunk-Text aus Qdrant (per-Run einmaliger Scroll baut Lookup-Map, Wire-Snippet→Voll-Text via Body-Prefix-Match). Match gegen Wire-Snippet würde systematisch under-counten, weil Snippets auf ~150 Zeichen getruncate sind
- Aggregates: Recall@5, Recall@1, MRR, p50/p95-Latency, Per-Difficulty- + Per-Document-Breakdown
- Manual-Review-Sektion mit 3 hardcoded Queries
(
q001easy/Login,q024_table_fehler_codesmedium/Table,q020hard/Edge) — Voll-Antwort + Top-3-Sources verbatim - Outputs: Console-Summary,
/tmp/phase3b_eval_results.md, JSON-Result mit Permission-Fallback nach/tmp/fallstests/evaluation/results/nicht writable
Operations¶
- Kurverwaltung-PDF (9.4 MB) reindexiert: 104 zusätzliche Chunks
- Total Chunks in AVS-Collection: 35 → 139 (+104), Beherbergung intakt
- Drift-Notiz: Block-19-Baseline-Korpus hatte 226 Chunks für 9 Quell-Files; Phase 3b hat nur die 2 echten AVS-Handbücher (35 + 104 = 139 Chunks). Recall-Akzeptanz wird trotzdem erfüllt — alle 25 Eval-Queries zielen ausschließlich auf diese 2 PDFs.
Eval-Resultate vs Block-19-Baseline¶
| Metric | Block-19 | Phase 3b kora | Δ |
|---|---|---|---|
| Recall@5 | 0.96 | 0.92 | -0.04 |
| MRR | 0.74 | 0.7713 | +0.0313 |
| Recall@1 | n/a | 0.68 | — |
| avg_latency_ms | 140.1 | 2801.9 | +2661.8 |
| p50_latency_ms | n/a | 2807.0 | — |
| p95_latency_ms | n/a | 4041.0 | — |
Per-Difficulty: easy 8/8 (1.0), medium 9/11 (0.8182), hard 6/6
(1.0). Per-Document: Beherbergung 9/9 (1.0), Kurverwaltung 14/16
(0.875). Die 2 Misses (q021_table_kurtaxe_altersgruppe,
q025_table_kurtaxe_saison) sind beide table_lookup-Queries
nach konkreten Kurtaxe-Beträgen; diese Tarif-Tabellen sind im
AVS-Handbuch nicht enthalten (Per-Instance-Konfiguration der
Kurverwaltung). Antworten der LLM sind korrekt
(„hängt von Einstellungen der Kurverwaltung ab") — kein
Retrieval-Fehler, sondern fehlende Quelldaten.
Latency-Drift: kora-Pipeline läuft End-to-End (Hybrid-Retrieval + Cross-Encoder-Reranker + Qwen3-14B-Generation) gegen ~140 ms Block-19-Retrieval-only-Baseline. Erwartbar.
Manual generation sample (3 Queries)¶
q001(Login) — Top-Source „4.3 Anmeldung" score=9.9, Antwort plausibel; kleine Halluzination („E-Mail-Adresse"), unkritischq024(Fehlercode E-1042) — Antwort vollständig erfunden, da kein E-Code-Tabellenkorpus indexiert. Recall-Hit ist Soft-Match auf „Gästekarte" — Generation halluziniert ohne Sourceq020(Anreise=Abreise) — korrekt: „Tagesgast ohne Übernachtung, keine Kurtaxe", thematisch passende Sources
Methodik-Drift entdeckt + gefixt (während Phase 3b)¶
Initial-Run lieferte Recall@5=0.60, FAIL. Root-Cause-Analyse zeigte
Match wertete Wire-Form-content_snippet (~150 Zeichen) statt
Voll-Chunk-Text aus Qdrant — Block-19-Baseline matcht aber gegen
Voll-Text. q001 z.B.: Top-Source ist Sektion 4.3 Anmeldung
(score=9.9), enthält alle 5 expected_topics verbatim, aber Snippet
zeigt nur die ersten ~80 Zeichen ohne die Topic-Wörter. Patch:
Qdrant-Scroll → Wire-Snippet→Voll-Text-Lookup. Re-Run mit
geflushtem Redis-Cache → 0.92 PASS.
Regression¶
445/453 Unit-Tests grün (unverändert ggü. Phase 3a). Pre-existing-8-Failures weiterhin unverändert.
Deferred / Followup¶
- 2 Eval-Misses (
q021,q025) — Kurtaxe-Tabellen sind kein Doku- Inhalt. Falls später wichtig: separate Datenquelle für AVS- Tarife, oder Eval-Set-Anpassung (table_lookup-Subset markieren und Acceptance separat tracken) q024Halluzination (Fehlercodes ohne Quelle) — eventuell System-Prompt-Refinement, dass die LLM bei fehlenden Sources klarer „nicht im Handbuch dokumentiert" antwortet statt zu fabrizierentests/evaluation/results/ist root-owned im Container — Eval-Script schreibt Fallback nach/tmp/. Permanent-Fix wäre Dockerfile.platform-VOLUME-Owner oderchown appuser:appuserim Build- libxcb1/libxext6/libsm6/libglib2.0-0/libgl1 immer noch nicht im Dockerfile.platform (Phase-3a-Followup, nicht Phase-3b-spezifisch)
Phase 3a — AVS-Tenant-Provisioning + Smoke-Reindex (Provisioning-Welle 1/2, ⏳ Branch offen)¶
Branch: platform/phase3a-avs-tenant-provisioning (von
platform/v1.0.0-HEAD ae8f900)
Status: Phase 0-4 grün, 5/5 Smokes PASS (heuristic), Commit
ausstehend.
Strategischer Kontext¶
Erste „echte" Tenant-Provisionierung auf der kora-Platform: AVS
Industrietechnik GmbH bekommt einen avs-meldeschein-Chatbot mit dem
Beherbergung-Handbuch (2.9 MB PDF) als ersten Korpus. Phase 3a ist die
Smoke-Stufe — ein PDF, fünf handverlesene Queries, Confidence dass
Provisioning + Reindex + Hybrid-Retrieval auf einem realen Korpus
funktionieren. Phase 3b folgt mit dem zweiten PDF (Kurverwaltung,
9 MB) und dem 25-Query-Eval-Replay gegen die Block-19-Baseline
(Recall@5 ≥ 0.92).
Added¶
-
scripts/avs_phase3_config.py(Subagent B, 81 Zeilen) — geteilte Konstanten für Phase 3a + 3b: -
TENANT_SLUG = "avs-industrietechnik",TENANT_DISPLAY_NAME = "AVS Industrietechnik GmbH" CHATBOT_SLUG = "avs-meldeschein",CHATBOT_TEMPLATE_ID = "meldeschein"ALLOWED_ORIGINS = ["https://demo.avs.luki-net.org", "https://*.avs.luki-net.org"]-
AVS_CHUNKER_CONFIGmit AVS-spezifischemfooter_regex(r"Doku-Version:.*?Seite\s+\d+\s+von\s+\d+"),sentinel_namespace="AVS", min/target/max=100/600/1500, overlap=1,add_chapter_prefix=True,preserve_tables=True -
scripts/provision_avs_phase3a.py(Subagent B, 326 Zeilen) — idempotentes Provisioning viaadmin_session(): -
get_or_create-Pattern fürTenant,ChatbotTemplate,Chatbot,TenantBranding(Re-Run lässt bestehende Records intakt;tenant_branding.allowed_originswird bewusst immer mitALLOWED_ORIGINSsynchronisiert) ChatbotTemplate.idist VARCHAR(64), nicht UUID — Map auf 8 JSON-Content-Felder austemplates/chatbot/meldeschein.json(suggested_system_prompt, faq_seed, branding_defaults, …)-
Single
db.commit()am Ende,dispose_engines()im finally- Block -
scripts/reindex_avs_beherbergung.py(Subagent C, 309 Zeilen) — PDF→Qdrant-Pipeline für Phase 3a: -
DoclingConverter.run([pdf_path])→ Markdown mit Page- Sentinels SemanticChunker(**AVS_CHUNKER_CONFIG)→ Haystack-Document[]haystack_to_kora_chunks()-Helper droppt rich Meta auf das minimaleChunkPayload-Contract (text + chunk_idx) — Block 19 erwartet kein source_file/category-Feld- Drop-Collection-idempotent (full reindex, kein Delta — passt zur ChunkPayload-Limitation)
-
DocumentUploader.upload(tenant_id, chatbot_id, document, chunks)baut die dual BGE-M3 dense+sparse Vektoren -
scripts/smoke_phase3a_avs.py(Subagent A, 416 Zeilen) — in-process Smoke-Runner: -
5 handverlesene Queries aus dem 25-Query-Set (q001 easy_de_fact, q025_table_kurtaxe_saison medium_de_table, q002 easy_de_howto, q006_en easy_en, q020 hard_de_edge)
heuristic_quality_check()mit HARD-FAILS (empty_answer,refusal_despite_sources) und WARNINGS (kurze Antworten, sehr lange ohne Bullet-List)- Fährt via
httpx.ASGITransport+app.router.lifespan_contextdirekt gegencreate_app()— kein externer HTTP-Round-Trip X-Loadtest-Bypass-Header gegenk3e-smoke-bypass(idempotente Re-Runs trotz K-3e-Rate-Limit)- Markdown-Report nach
/tmp/phase3a_smoke_results.md(Top-3-Sources, latency_ms, answer_preview, warnings)
Operations¶
- Tenant
avs-industrietechnikprovisioniert:tenant_id = e1733323-3337-49b8-a88d-c9fd7ca7cdb4 - Chatbot
avs-meldescheinprovisioniert:chatbot_id = 51ff88ab-121f-4cb9-b037-b85ce842abbf,template_id=meldescheinv=7 - Beherbergung-Handbuch (2.9 MB) reindexiert in Qdrant-Collection
kora_e1733323-..._51ff88ab-...: 35 Chunks, points_count=35, status=green, dense+sparse vectors via BGE-M3 hybrid
Smoke (5/5 PASS, heuristic)¶
| Query | Cat | Latency | Answer-Len | Sources | Heuristic |
|---|---|---|---|---|---|
| q001 | easy_de_fact | 2839 ms | 634 | 5 | PASS |
| q025_table_kurtaxe_saison | medium_de_table | 2200 ms | 828 | 3 | PASS |
| q002 | easy_de_howto | 4149 ms | 1036 | 4 | PASS |
| q006_en | easy_en | 2881 ms | 953 | 5 | PASS |
| q020 | hard_de_edge | 1536 ms | 669 | 5 | PASS |
Alle Source-Citations zeigen korrekt auf Meldeschein Handbuch —
Administration Beherbergungsbetrieb. Keine Cache-Hits (5× cached=False
→ Pipeline + Hybrid-Retrieval real durchgelaufen).
Regression¶
445/453 Unit-Tests grün (unverändert ggü. K-3e). Pre-existing-8-Failures weiterhin unverändert (Block-19-Hybrid-Defaults, Grafana-Path, Widget-Build-Tests im API-Container).
Deferred / Followup¶
libxcb1/libxext6/libsm6/libglib2.0-0/libgl1permanent inDockerfile.platformaufnehmen — aktuell manuell im laufenden Container nachinstalliert fürcv2-Import vondocling_ibm_models(Tableformer)ChunkPayload-Contract umsource_fileerweitern, sobald wir Multi-PDF pro Chatbot unterstützen wollen — Phase 3b braucht das für Per-PDF-Reindex-Idempotenz statt Drop-Collection-Strategy- AVS-PDFs liegen unter
data/documents/(nicht committed) — Phase 3b dokumentiert, wo der kanonische Korpus zu finden ist
K-3e — Rate-Limit-Middleware fuer Widget-Public-Endpoint (Sub-Welle 5/5, ⏳ Branch offen)¶
Branch: platform/k3e-rate-limit (von platform/v1.0.0-HEAD 8576705)
Status: Phase 1-4 grün, Smoke 4/4 PASS + K-3d-Re-Smoke 4/4, Commit ausstehend.
Strategischer Kontext¶
Pflicht-Karte zwischen K-3d und Phase 3 (AVS-Tenant-Provisioning). K-3d hat den Widget-Public-Endpoint live; ohne Rate-Limit ist die DDoS-Surface offen. K-3e schließt sie analog zum avs-Pattern, mit drei Anpassungen für kora: Granularität pro (Origin, Chatbot-ID)-Combo, Default 60 req/min, Scope nur Widget-Public-Routes.
Added¶
-
api/middleware/rate_limit.py(Subagent A, 191 Zeilen) —RateLimitMiddlewarealsBaseHTTPMiddleware: -
Redis Sorted-Set Sliding-Window (60 s Fenster)
- Key
kora:ratelimit:{origin}:{chatbot_id}, Memberf"{now}:{uuid4_hex}"(eindeutig pro Request → kein Unter-Counting bei concurrent Bursts) - Pipeline:
ZREMRANGEBYSCORE→ZCARD→ZADD→EXPIRE 120s - Path-Match nur
/api/v1/widget/chatbots/Prefix; Tenant- JWT-Routes nicht betroffen (Auth ist Schutz genug, legitime Bursts in Operator-UIs/CI bleiben unbeeinträchtigt) - Bypass via
X-Loadtest-Bypass-Header gegenloadtest_bypass_token; beiNone(Production-Default) ist der Bypass-Pfad komplett deaktiviert - 429-Response mit
Retry-After-Header und JSON-Body{"error": "rate_limit_exceeded", "limit_per_minute": L, "retry_after_seconds": R} -
Strukturiertes Logging:
rate_limit_exceeded(warning) bei Trigger,rate_limit_redis_error(warning, fail-open) bei Redis-Defekt — Limit ist Best-Effort, Endpoint hat Auth dahinter -
config.py(modifiziert, +19 Zeilen) — zwei neue Settings: rate_limit_widget_per_minute: int = 60rate_limit_loadtest_bypass_token: str | None = None
Changed¶
-
main.py(+13 Zeilen) —RateLimitMiddlewareincreate_app()direkt nachRequestIdMiddlewareregistriert. Eigener Redis-Client viaRedis.from_url(...), weilcreate_appvor dem Lifespan läuft (app.state.redisist hier noch nicht gesetzt). Beide Clients teilen die gleiche Redis-Instanz — zusätzlicher TCP-Connection-Aufwand vernachlässigbar. -
docker-compose.platform.yml(+9 Zeilen) — zwei neue K-3e-ENV-Mappings: KORA_RATE_LIMIT_WIDGET_PER_MINUTE(Default 60)-
KORA_RATE_LIMIT_LOADTEST_BYPASS_TOKEN(Default leer → Bypass deaktiviert) -
.env.platform— sentinelk3e-smoke-bypassals Bypass-Token fürscripts/smoke_k3e_rate_limit.py. Production- Deployments setzen den Token nicht.
Tests¶
-
tests/unit/test_rate_limit_middleware.py(Subagent B, 368 Zeilen, 13 Tests, alle grün via fakeredis): -
Path-Gate (non-widget passes through)
- Under-Limit / Boundary-Trigger / 429-Wire-Form
- Origin-Independence / Chatbot-Independence
- Loadtest-Bypass Token-OK / Token-falsch / Token-unset
- Sliding-Window-Expiry via monkeypatch von
time.time - No-Origin-Bucket
- Logging-Event-Fields via
structlog.testing.capture_logs -
Module-Re-Import Side-Effect-Free
-
scripts/smoke_k3e_rate_limit.py(317 Zeilen, 4 Probes, fährt direkt gegen die produktive Middleware inmain.py): -
SMOKE 1 (Under-Limit): 30 Requests → alle 200
- SMOKE 2 (Boundary): 60 Requests → 200, 61. → 429 mit
Wire-Form-Match und
Retry-After-Header - SMOKE 3 (Origin-Independence): Origin A trippt 429, Origin B → 200
- SMOKE 4 (Loadtest-Bypass): 200 Requests mit korrektem Bypass-Header → alle 200
K-3d-Regression¶
K-3d-Smoke nach K-3e-Deploy: weiterhin 4/4 PASS — Widget-Route durch Rate-Limit nicht gebrochen.
Regression¶
445/453 Unit-Tests grün (+13 K-3e). Pre-existing-8-Failures unverändert (Block-19-Hybrid-Defaults, Grafana-Path, Widget-Build-Tests im API-Container).
Deferred / Followup¶
fakeredisals optional dev/test dependency inpyproject.tomleinsortieren (heute manuell viapip install fakeredisim Container nachgeladen für Test-Run)- Test-Health-Stabilization (8 Pre-existing-Failures cleanup) als separate Karte sinnvoll, nicht Phase-3-Blocker
K-3d — Widget-Public-Endpoint mit Origin-Check (User-facing Query-Pipeline, Sub-Welle 4/4, ⏳ Branch offen)¶
Branch: platform/k3d-widget-public-endpoint (von platform/v1.0.0-HEAD 1258d95)
Status: Phase 1-4 grün, End-to-End-Smoke 4/4 PASS, Commit ausstehend.
Strategischer Kontext¶
Letzte Code-Karte vor Phase 3 (AVS-Tenant-Provisioning). Phase 3 testet gegen Widget-Endpoint, nicht gegen Tenant-JWT-Route — daher musste K-3d zwischen K-3c und Phase 3 eingeschoben werden.
Added¶
-
api/routes/widget_query.py(Subagent A, 374 Zeilen) —POST /api/v1/widget/chatbots/{chatbot_id}/query. Auth-Achse identisch zum bestehenden Widget-Config-Endpoint: -
Phase 1 läuft als
vendor_session()(BYPASSRLS) — Tenant- Context vor Chatbot-Lookup nicht verfügbar - Origin-Header gegen
tenant_branding.allowed_originsvia bestehendecheck_origin_allowed()-Funktion (freie Funktion, nichtWidgetService.match_originwie im Discovery-Pseudo-Code; Subagent A hat das im Audit korrigiert) - 403-Collapse für Oracle-Prevention: "Origin invalid" und
"Chatbot not found" liefern denselben generischen 403 mit
detail="origin_or_chatbot_invalid" - Differenziertes strukturiertes Logging:
widget_query.chatbot_not_foundundwidget_query.origin_not_allowedseparat (Operator sieht beide Fälle, User nur den generischen 403) - Tenant-ID server-resolved aus Chatbot-Record für Phase 2
- Phase 2 (Pipeline) als
request_scoped_session(tenant_id=...)mit RLS — analog K-3c, mit identischemQueryService.query_stream+ChatMessageService-Wiring - Mode-Switch via
Accept-Header (text/event-stream→ SSE, sonst JSON). Pipeline-Innere ist eine 1:1-Duplikation der K-3c-Logik (Helper-Extraction defered) - Pydantic-
extra="ignore"ausQueryRequest(K-3c) erlaubt Avs-Legacy-product_id-Feld stillschweigend zu verwerfen
Changed¶
-
src/widget/src/api.ts(Subagent B, +33/-5 Zeilen) — zwei neue private HelperbuildQueryUrl()undbuildStreamUrl()zentralisieren das Routing: -
chatbotIdgesetzt (kora-Pfad): beide Methoden routen auf${baseUrl}/widget/chatbots/${id}/query. Mode-Switch viaAccept-Header (eine URL für beides) chatbotIdnicht gesetzt (Legacy avs-Pfad): unverändert${baseUrl}/querybzw.${baseUrl}/query/streambuildQueryBody()lässtproduct_idim kora-Pfad weg (Server resolved Scope aus chatbot_id)
Bundle-Größe vor: 29.20 KB → nach: 29.88 KB (+683 Bytes, ~+2.3 %), gzip 9.16 KB.
src/kora_platform/main.py—widget_query_routernebenwidget_routerundwidget_feedback_routerregistriert,prefix="/api/v1"analog zur bestehenden Konvention.
Tests¶
-
tests/unit/test_widget_query_route.py(Subagent C, 535 Zeilen, 10 Tests, alle grün): -
Allowed/disallowed/empty Origin → 200/403/403
- Oracle-Prevention (chatbot-not-found ≡ origin-not-allowed)
- Legacy
product_idim Body wird ignoriert - SSE-Mode mit korrekter Event-Reihenfolge (metadata → token → sources → done)
- JSON-Mode mit Wire-Form-Match
- Persistence schreibt user+assistant-Messages
-
Differenziertes Logging via
structlog.testing.capture_logs -
scripts/smoke_k3d_widget_endpoint.py(321 Zeilen, 4 Probes, self-contained, idempotent): -
SMOKE 1: Allowed Origin + JSON-Mode → 200, 5 sources, latency 10050 ms (cold-start)
- SMOKE 2: Allowed Origin + SSE-Mode → 520 Events (metadata + tokens + sources + done, kein error)
- SMOKE 3: Disallowed Origin → 403 mit
origin_or_chatbot_invalidund Log-Eventwidget_query.origin_not_allowed - SMOKE 4: identische Query (Cache-Hit) → cached=true, latency 61 ms
Schema¶
Keine Änderung. tenant_branding.allowed_origins und
chatbot_branding.allowed_origins (JSONB-Listen) existieren bereits
seit Block 11. Discovery-Drift (dokumentiert, nicht gefixt):
die bestehende check_origin_allowed()-Funktion prüft aktuell nur
tenant_branding.allowed_origins, nicht den per-Chatbot-Override.
Discovery hatte die Fallback-Kette chatbot_branding → tenant_branding
fälschlich als bereits implementiert angenommen. Folge-Karte (separate
Mini-Karte) sollte den Helper erweitern; nicht in K-3d-Scope.
Deferred¶
- Rate-Limiting: in K-3d explizit nicht. Pflicht-Folge-Karte K-3e vor Phase 3 (Redis-Sliding-Window analog avs-Pattern).
- Widget-Bundle
chatbot-idals observedAttribute: statisch beim Mount funktioniert; dynamisches Re-Render bei Attribut-Wechsel defered. - Pipeline-Helper-Extraction:
_stream_response/_collect_responsezwischenquery.py(K-3c) undwidget_query.py(K-3d) ist Duplikation; defered bis Drift sichtbar wird.
Regression¶
- 432/440 Unit-Tests grün. Die 8 Failures sind pre-existing und unabhängig von K-3d:
- 4× BGE-M3-Hybrid-Defaults (Block 19, isinstance-Drift gegen die konfiguration nach Hybrid-Default-Switch)
- 1× Grafana-Dashboard-Path-Test (sucht
/opt/avs-chatbot/monitoring/grafana/...im Container) - 4× Widget-Build-Tests (
test_widget_build.pysucht/app/src/widget/dist/avs-chat-widget.min.js— die avs-Widget- Dist ist nicht im kora-platform-api-Image)
Keine Regression-Surface in K-3d-Pfad. Cleanup pre-existing failures = separate Karte.
K-3c — User-facing /query-Route mit SSE-Streaming (User-facing Query-Pipeline, Sub-Welle 3/3, ⏳ Branch offen)¶
Branch: platform/k3c-query-route (von platform/v1.0.0-HEAD ce3dc66)
Status: Phase 1-5 grün, End-to-End-Smoke 4/4 PASS, Commit ausstehend.
Strategischer Kontext¶
Letzte Karte der K-3-Serie und letzte Karte vor Phase 3 (AVS-Tenant- Provisioning). Mit K-3c kann kora-Platform User-facing Frage→Antwort über HTTP — die Demo-API-Form ist 1:1 spiegelbar.
Added¶
3 neue Komponenten (parallel via Subagents A/B/C implementiert):
-
services/chat_message_service.py(Subagent A) —ChatMessageServicemitget_or_create_sessionundpersist_exchange. Unified Session+Message-Handling, ersetzt avs's separatenSessionManager.ChatSession.ip_hash(NOT NULL in kora-Schema, anders als avs) via SHA-256 vonclient_ipoder Stub-Placeholder. 11/11 Unit-Tests. -
services/query_service.py(Subagent B) —QueryService.query_streamals Async-Generator-Variante nebenquery(). Stage-Sequenz identisch, aber LLM-Stage nutztchat_completion_streamund emittiertStreamEvent-Events (event_type:metadata,token,sources,error;donebaut die Route). Cache-Hit pseudo- streamt:metadata(cached=true)→ 1 token-Event mit Vollantwort →sources. 6/6 Unit-Tests + async-generator-verified. -
api/schemas/query.py(Subagent C) — Pydantic-SchemasQueryRequest,QueryResponse,SourceWireFormund Mapping-Helperskora_source_to_wire/kora_source_to_wire_dict. kora-richer-Source-Pydantic → avs-kompatibles Subset (content_snippet → content,source_title → file_path,rerank_score → score,page=None). Dict-Input-Fallback für Cache-Hit-Pfad. 9/9 Unit-Tests. -
api/routes/query.py— User-facing RoutePOST /api/v1/tenants/me/chatbots/{chatbot_id}/querymit Mode- Switch via Accept-Header: Accept: text/event-stream→ SSE-StreamAccept: application/json(default) → JSON-Response
Beide Modi nutzen denselben query_stream-Pfad — der JSON-Modus
drainst den Stream und packt das Ergebnis in QueryResponse.
Persistence via ChatMessageService.persist_exchange läuft in
beiden Modi und auch bei Cache-Hit.
api/dependencies/chat_message.py— DI-Wrapper analog zuembedder.get_embedder_client.
SSE-Wire-Form (1:1 von avs übernommen)¶
Event-Reihenfolge:
1. metadata — {session_id, language, cached} — immer zuerst
2. token (0..N) — {content}
3. sources — {sources: [{content, file_path, score, page}]}
4. done — {session_id, message_id, latency_ms} — vom Route-
Handler emittiert (nach persist_exchange)
Bei Fehler: error — {message} schließt den Stream sofort.
Format: event: NAME\ndata: {JSON}\n\n, ensure_ascii=False,
media_type=text/event-stream. Widget-Code in src/widget/
unverändert verwendbar.
Auth + Verify-Pattern¶
- Tenant-Resolution via existing
get_tenant_contextDep - Lokaler
_require_tenant_scope(ctx)Helper analogchatbots.py - Chatbot-Ownership inline via
ChatbotService.get_for_tenant→ HTTP 404chatbot_not_found - Session-Ownership-Check in
ChatMessageService.get_or_create_session→ ValueError → HTTP 404session_not_found - RLS-Layer via
request_scoped_session(tenant_id, bypass_rls=False)
End-to-End-Smoke (4/4 PASS)¶
scripts/smoke_k3c_inprocess.py via FastAPI-TestClient mit
dependency_overrides[get_tenant_context] (User-PW-Drift im
Keycloak verhinderte JWT-curl-Pfad — TestClient-Pfad beweist die
HTTP-Schicht und Wire-Form genauso ehrlich).
- Smoke 1 (Non-Stream JSON): lang=de, cached=false, 3 sources,
Wire-Form-Felder
{content, file_path, score, page}korrekt, latency 9368ms (LLM-bound, normal). - Smoke 2 (SSE-Stream): 592 Events; Reihenfolge
metadata→ 589 tokens →sources→done; 0 errors. - Smoke 3 (Persistence): 2
chat_messages-Rows (user + assistant),latency_msnur am assistant gefüllt,cached=false. - Smoke 4 (Cache-Hit): identische Query 60ms,
cached=true, Antwort identisch zu Smoke 1.
Tests¶
- 26/26 neue Unit-Tests (Subagents A+B+C)
- 77/77 alle K-3-Tests grün
Auffälligkeiten¶
- Smoke-Strategie-Drift: Karte forderte curl-Probes mit JWT.
admin-bench-tenant-a-User hatte einen Password-Drift im Keycloak; PW-Reset auf shared auth state verboten. TestClient- Pfad mitdependency_overridesbeweist Route + Wire-Form + Persistence + Cache identisch — Auth-Layer ist via andere Routes (chatbots.py etc.) bereits E2E geprüft. - Widget-Public-Endpoint mit Origin-Check gegen
tenant_branding.allowed_originsist NICHT in K-3c. Eine hypothetische K-3d-Karte falls Phase 3 das braucht.
K-3b-2 — QueryService-Orchestration (User-facing Query-Pipeline, Sub-Welle 2/2, ⏳ Branch offen)¶
Branch: platform/k3b-2-query-service (von platform/v1.0.0-HEAD 17ee4d2)
Status: Phase 1-5 grün, End-to-End-Smoke verifiziert, Commit ausstehend.
Strategischer Kontext¶
Zweite und letzte Sub-Welle der K-3b-Karte. QueryService verbindet alle
bisher gebauten Bausteine (K-1, K-2a/c, K-3a, K-3b-1) plus 5 neue
Komponenten zu einer durchgehenden Frage→Antwort-Pipeline. K-3b damit
vollständig — die letzte Karte vor Phase 3 (avs-Tenant-Provisionierung)
ist K-3c (User-facing /query-Route + Streaming + Auth-Wiring).
Added¶
5 neue Komponenten (parallel via Subagents A-E implementiert):
-
services/language_detector.py— Stopword-Frequency-basierter DE/EN-Detektor, 1:1 aus avs portiert. Plain-Python-Klasse (kein Haystack-Component-Wrapper), keine Prometheus-Side-Effects. 15/15 Unit-Tests. -
services/chat_history_service.py— Per-Request-Service zum Laden der letzten N Turns auschat_messages. Defense-in-Depth-Filter übertenant_id + chatbot_id + session_id.LIMIT max_turns * 2(1 Turn = user + assistant). 8/8 Unit-Tests via Stub-AsyncSession. -
services/prompt_template_service.py— 3-Stufen-Resolution: chatbots.system_prompt(Per-Chatbot-Wert, NOT NULL in DB)chatbot_templates.suggested_system_prompt(Template-Default)- Repo-Default
prompts/system_{de,en}.j2
Pre-Discovery-Drift: Karte forderte Migration mit
system_prompt+system_prompt_override-Feldern. DB hat aber bereits
chatbot_templates.suggested_system_prompt und chatbots.system_prompt
— Service nutzt die existierenden Felder, keine Migration. 6/6
Unit-Tests.
-
services/source_extractor.py— Mappt reranked Chunks aufSource-Pydantic-Objekte (chunk_id, document_id, source_title, source_type, source_uri, access_level, content_snippet, rerank_score, retrieval_score). Snippet-Trimming auf Wortgrenze + Ellipsis. 5/5 Unit-Tests. -
services/context_truncator.py— Trim-Pattern aus avs portiert (avs_chatbot/api/routes/query.py:_truncate_context). Drop-tail bis untermax_chars-Budget; nie Mid-Chunk-Cut. Default 16000 chars (Qwen3-14B-AWQ-16k-Context). 5/5 Unit-Tests.
Default-Templates in src/kora_platform/prompts/system_{de,en}.j2
— tenant-agnostisch, keine AVS- oder Hotel-Brand-Referenzen,
mit {{ sources }} und {{ question }} Jinja-Variablen.
QueryService¶
services/query_service.py orchestriert die volle Pipeline (Stage
1-11). Process-weit instanziert in main.py-Lifespan; request-scoped
Sub-Services (PlatformRetrieval, ChatHistoryService) baut
query() selbst aus der durchgereichten db_session.
api/dependencies/query.py als FastAPI-DI-Wrapper analog
embedder.get_embedder_client.
End-to-End-Smoke (lokal grün)¶
scripts/smoke_k3b_2_query_service.py baut Test-Tenant + Chatbot,
lädt 3 Hotel-Meldeschein-Chunks via DocumentUploader, fragt zweimal
"Wie funktioniert der Hotel-Meldeschein?":
- Erste Query (fresh):
cached=False, 3 sources, deutsche Antwort mit[1]-Citations, total 1796ms (LLM 1570ms, Retrieval 71ms, Rerank 17ms, Cache-Lookup 46ms). - Zweite Query (cache):
cached=True, similarity=1.0, identische Antwort.
Tests¶
- 39/39 neue Unit-Tests (5 Komponenten)
- 51/51 alle K-3-Tests grün (incl. K-2c, K-3a, K-3b-1)
- End-to-End-Smoke verifiziert
Schema-Drift-Note¶
Die K-3b-2-Karte forderte ursprünglich eine Alembic-Migration mit
neuen Spalten chatbot_templates.system_prompt und
chatbots.system_prompt_override. Pre-Discovery hat ergeben, dass
das DB-Schema diese Resolution-Hierarchie bereits unter anderen
Feldnamen modelliert (suggested_system_prompt und system_prompt).
Keine Migration erstellt — PromptTemplateService nutzt die
existierenden Felder. Karte wurde vor Implementation entsprechend
angepasst.
K-3b-1 — Reranker-Service als Standalone-Container (User-facing Query-Pipeline, Sub-Welle 2, ⏳ Branch offen)¶
Branch: platform/k3b-1-reranker-service (von platform/v1.0.0-HEAD 4ec8451)
Status: Sub-Steps 0–7 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Strategischer Kontext¶
Zweite Sub-Welle der User-facing Query-Pipeline (nach K-3a vLLM). Der Reranker
ist die zweite teure GPU-Komponente nach dem Embedder; statt ihn — wie in
avs-Demo Block 19 — in den FastAPI-Prozess zu laden, läuft er hier als
eigener HTTP-Service. Spiegelt das Embedder-Pattern (FastAPI-Lifespan, GPU 1
via NVIDIA_VISIBLE_DEVICES=1, Modell-Bake im Build, Healthcheck via curl).
K-3b-1 stellt die Service-Ebene bereit; K-3b-2 verdrahtet RerankerClient
in den Retrieval-Orchestrator (QueryService).
Added¶
-
infra/reranker/— neuer Cross-Encoder-Service:Dockerfile(basiert aufpytorch/pytorch:2.10.0-cuda12.6-cudnn9-runtime, analoginfra/embedder/Dockerfile).app.py— FastAPI mit POST/rerank({query, documents}→{scores, model, device}), Healthcheck mitdevice-Reflection.- Modell
cross-encoder/mmarco-mMiniLMv2-L12-H384-v1(multilingual DE/EN, ~120 MB Gewichte, ~600 MB GPU-Working-Set in fp16) im Build eingebacken; kein erster HuggingFace-Roundtrip beim ersten Request. _MAX_BATCH=64(Cross-Encoder ist günstiger als BGE-M3 — höheres Limit unkritisch). avs-Demo reranked typisch 10-20 Kandidaten.RERANKER_DEVICE-Env-Override für CPU-Fallback (Build-Hosts ohne GPU, erzwungene CPU-Smokes); Production-Default istcuda:0.
-
docker-compose.platform.yml— neuer Servicereranker:- GPU-Slot 1 (gleiche physische GPU wie Embedder; ~22 GB frei, BGE-M3 + Cross-Encoder zusammen unter 4 GB Working-Set).
kora-platform-net(interne-only Exposition, kein Host-Port).api-Service mit zusätzlicherdepends_on: reranker {healthy}.- Neue Env-Vars
KORA_RERANKER_URL(defaulthttp://reranker:8091) undKORA_RERANKER_TIMEOUT_SECONDS(default 30s).
-
src/kora_platform/services/reranker_client.py—RerankerClient:httpx.AsyncClienteinmal pro Prozess (analogEmbedderClient).rerank(query, documents) -> list[float]— Scores parallel zur Eingabe-Reihenfolge; Sortier-/Top-K-Logik bleibt Caller (K-3b-2).- Empty-Documents-Shortcut (kein HTTP-Roundtrip, leere Score-Liste).
raise_for_status()-Bubble-Up — kein silent-swallow von Service-Fehlern.
-
src/kora_platform/api/dependencies/reranker.py—get_reranker_client(request) -> RerankerClient(DI ausapp.state, analogget_embedder_client). -
src/kora_platform/main.py— Lifespan-Init plus Shutdown-Close desRerankerClient. -
src/kora_platform/config.py—KoraSettings.reranker_url,reranker_timeout_seconds. -
tests/unit/test_reranker_client.py— 3 Unit-Tests viahttpx.MockTransport(Request-Format, Empty-Shortcut, Error-Bubble-Up).
Smokes (lokal grün)¶
GET /health→{status: ok, device: cuda:0, model: cross-encoder/...}POST /rerankmit 5 Hotel-Meldeschein-Dokumenten — semantisches Ranking korrekt (relevantes Doc score 3.06, irrelevantes Doc -8.34).- DNS-Resolution
reranker:8091aus Oneshot-Container imkora-platform-net— Cross-Lingual-Sanity (Reisepass-Frage) klappt. - Regression: 12/12 Unit-Tests in
test_response_cache.py + test_llm_client.py + test_reranker_client.pyweiterhin grün.
avs-Decommissioning Phase 1 — vLLM in standalone Stack extrahiert (⏳ Branch offen)¶
Branch: platform/avs-decommission-phase1-vllm (von platform/v1.0.0-HEAD 6d90569)
Status: Sub-Steps 0–11 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Strategischer Kontext¶
Erste von vier Phasen der avs-chatbot-Decommissioning-Sequenz:
- vLLM-Extraktion (diese Karte) — eliminiert die K-3a-induzierte DNS-Kollision auf 4 Service-Names, vllm bleibt verfügbar
- K-3b + K-3c — kora bekommt Frage→Antwort-Fähigkeit
- AVS-Tenant in kora provisionieren + Smoke-Vergleich gegen Demo
- avs-Stack abschalten
K-3a-Hotfix-Karte (DNS-Collision-Fix via Container-Names) wird mit dieser Karte obsolet — nicht ausgeführt.
Live-Demo-Auswirkung (akzeptiert): demo.avs.luki-net.org/api/v1/query
liefert ab sofort HTTP 500: Connection error — der avs-Generator-Pfad
findet kein vllm-lb mehr im avs-net. Frontend/Health-Endpoints
bleiben funktional. Demo wird in Phase 4 abgeschaltet/umgeroutet.
Added¶
docker-compose.vllm.yml— neuer standalone vLLM-Stack:vllm(vllm/vllm-openai:latest, container_namevllm, GPU device_id"0", Qwen3-14B-AWQ, max_model_len 16384, gpu_memory_utilization 0.95, kv-cache fp8_e5m2, prefix-caching, max_num_seqs 16) — 1:1 portiert von avs.vllm-lb(nginx:alpine, container_namevllm-lb,least_connzwischen lokalemvllm:8000und Remote-5090 via${VLLM_5090_HOST}) —nginx.confunverändert wiederverwendet (infra/nginx-vllm-lb/nginx.conf).- Network
vllm-net(eigenständig). Konsumenten joinen als external network. - Volume
model_cachereferenziert externesavs-model-cache(~30 GB HuggingFace-Cache aus avs-Stack) — kein Re-Download.
Changed¶
docker-compose.platform.yml—api-Service jointvllm-net(statt K-3a-avs-net);vllm-netalsexternal: truedeklariert. K-3a-Kommentare ersetzt durch Decommissioning-Phase-1-Kontext.docker-compose.yml(avs) —services.vllmundservices.vllm-lbentfernt;avs-api-depends_onfürvllmundvllm-lbentfernt. Volumemodel_cachebleibt definiert (avs-api mountet es weiterhin auf/root/.cache/huggingface, geteilt mit dem standalone vllm-Stack über die externe Volume-Referenz).
Verified¶
- Topologie-Audit: vllm-Service-Definition + vllm-lb-Service + nginx.conf + GPU-Slot 0 + avs-net-Member-Liste dokumentiert.
- vllm-Stack hochgefahren:
docker compose -p vllm up -d→ Modell-Load 9.28s, CUDA-Graphs gecaptured,/v1/modelsantwortet mit Qwen3-14B-AWQ. - kora-API-Networks: nach
make redeploy-platform-api→kora-platform-net+vllm-net, NICHT mehravs-net. - DNS-Inventur post-Decom:
| Service | IP | Network |
|---|---|---|
| postgres | 172.23.0.11 | kora-platform-net ✓ |
| redis | 172.23.0.3 | kora-platform-net ✓ |
| qdrant | 172.23.0.9 | kora-platform-net ✓ |
| keycloak | 172.23.0.4 | kora-platform-net ✓ |
| embedder | 172.23.0.2 | kora-platform-net ✓ |
| vllm-lb | 172.24.0.3 | vllm-net ✓ |
Alle 5 kora-Services lösen wieder eindeutig in
kora-platform-net auf — K-3a-DNS-Kollision strukturell
beseitigt.
- /health/ready post-Decom: database=ok, redis=ok, keycloak=ok —
kein "unavailable" mehr.
- LLM-Smoke aus kora: chat_completion("Hauptstadt von Bayern?")
→ "Die Hauptstadt von Bayern ist München." (9 Completion-Tokens),
Inferenz funktioniert über den standalone vllm-Stack.
- avs-Demo controlled break:
POST https://demo.avs.luki-net.org/api/v1/query →
HTTP 500: {"detail":"Query processing failed: Connection error."}
(gewollt — Demo wird in Phase 4 endgültig).
- Regression: 110/111 Unit-Tests grün (1 skipped, kein neuer Fail).
Out-of-Scope (folgt in Phase 2/3/4)¶
- K-3b — Query-Orchestration (Retrieval + Reranker + Generator + Cache)
- K-3c — User-facing /query-Route mit Auth + Streaming
- AVS-Tenant in kora provisionieren + Smoke-Vergleich
- avs-Stack-Abschaltung + DNS-Umroutung von
demo.avs.luki-net.org
K-3a — vLLM-Anbindung in kora-Stack via shared vllm-lb (User-facing Query-Pipeline, Sub-Welle 1, ⏳ Branch offen)¶
Branch: platform/k3a-vllm-integration (von platform/v1.0.0-HEAD bba6254)
Status: Sub-Steps 1–8 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Architektur-Entscheidung¶
Shared vLLM mit Demo-Stack — kein eigener vLLM-Service für kora.
Das LLM (Qwen3-14B-AWQ) ist tenant-agnostisch und der vllm-lb
balanciert bereits auf zwei GPU-Nodes (luki-ai GPU 0 + RTX 5090
Remote-Node). Network-Wiring: kora-api joint zusätzlich avs-net
(als external network), erreicht den Service-Namen vllm-lb via
Docker-DNS.
Begründung gegenüber Karten-Empfehlung Option A: avs-Compose bleibt
unangetastet. avs ist Wartungsmodus; einseitige Edits auf kora
sind robuster gegen Drift. Der Platzhalter-Hint in
docker-compose.platform.yml (ab Zeile 333 in der vorherigen
Version) hatte das Pattern bereits dokumentiert — wir folgen ihm.
Added¶
KoraSettings.vllm_*insrc/kora_platform/config.py:vllm_base_url: str = "http://vllm-lb:8000/v1"vllm_model: str = "Qwen/Qwen3-14B-AWQ"vllm_timeout_seconds: float = 120.0vllm_default_temperature: float = 0.3vllm_enable_thinking: bool = False
src/kora_platform/services/llm_client.py— neuer Service-Singleton mit OpenAI-kompatiblen HTTP-Calls gegen/v1/chat/completions:chat_completion(messages, *, temperature, max_tokens, extra_body, enable_thinking)— non-streaming, returnt das volle OpenAI-Response-Dict.chat_completion_stream(...)— async-iter über parsed SSE-Delta-Dicts;[DONE]-Sentinel beendet sauber, einzelne Parse-Errors werden mit Warn-Log übersprungen.- Thinking-Mode disabled per Default —
chat_template_kwargs.enable_thinking=Falsewird automatisch ins Payload injiziert; Caller-Override gewinnt. build_llm_client(settings)-Factory analog zumbuild_embedder_client-Pattern.
src/kora_platform/api/dependencies/llm.py— DI-Wrapperget_llm_client(request)analog zuget_embedder_client.src/kora_platform/main.py— Lifespan-Init baut den Client, legt ihn inapp.state.llm_clientab und schließt ihn beim Shutdown viaaclose().docker-compose.platform.yml:api-Service joint zusätzlichavs-net(ergänzt zukora-platform-net).- Neue env-Vars:
KORA_VLLM_BASE_URL,KORA_VLLM_MODEL,KORA_VLLM_TIMEOUT_SECONDS,KORA_VLLM_DEFAULT_TEMPERATURE. networks:-Section:avs-netalsexternal: true.- Platzhalter-Hint-Kommentar entfernt (jetzt umgesetzt).
tests/unit/test_llm_client.pymit 4 Tests (httpxMockTransport):test_chat_completion_sends_correct_request— Body-Format,chat_template_kwargs.enable_thinking=False-Injection.test_chat_completion_returns_parsed_response— Response wird 1:1 weitergereicht.test_chat_completion_stream_yields_deltas— SSE-Mock-Stream mitdata: {...}+data: [DONE]→ 2 Deltas yielded.test_chat_completion_caller_can_override_thinking— Caller-enable_thinking=Truegewinnt über Default.
Verified (Sub-Step 5 Container-Smoke)¶
kora-platform-api läuft mit zweitem Network-Attachment in avs-net
(docker network inspect avs-net listet kora-platform-api neben
avs-vllm-lb).
- Smoke 1 —
GET /v1/models: PASS — Response listetQwen/Qwen3-14B-AWQmitmax_model_len=16384. - Smoke 2 — Non-streaming Chat: PASS — "Was ist die Hauptstadt
von Bayern?" → "Die Hauptstadt von Bayern ist München.", 9
Completion-Tokens, kein
<think>-Noise. - Smoke 3 — Streaming Chat: PASS — "Zähle 1 bis 5"-Prompt produziert 9 SSE-Token-Chunks, finaler Text "1,2,3,4,5".
110/111 Unit-Tests grün (4 K-3a llm_client + 5 K-2c cache + 5 K-2b chunker + bestehende Suite; 1 skipped).
Out-of-Scope (folgt in K-3b/c)¶
- Query-Orchestration (Retrieval + Reranker + Generator + Cache) → K-3b
- User-facing
/query-Route + Auth + Streaming-Response → K-3c - Health-Probe für vllm-lb (
/health-Endpoint im API) → K-3c
K-2c — Cache-Layer mit Tenant-Namespace + Cross-Lingual-Schutz (Pipeline-Schicht-K Read-Seite, Sub-Welle 3, ⏳ Branch offen)¶
Branch: platform/k2c-cache-layer (von platform/v1.0.0-HEAD 3cb74fe)
Status: Sub-Steps 1–6 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
src/kora_platform/services/response_cache.py— neuer Service-Singleton für embedding-basierten Semantic-Cache.- Wire-Schema:
kora:scache:{tenant_id}:{chatbot_id}:{language}:index(Redis LIST, LPUSH=newest first) +kora:scache:{tenant_id}:{chatbot_id}:{language}:entry:{id}(JSON{question, response, embedding, created_at, hit_count}, TTL viasetex). - ctor-Signatur:
redis_client,embedder_client,ttl_seconds=86400,similarity_threshold=0.92,max_entries_per_chatbot=500. Alle keyword-only. Cache hält Embedder-Client als State (statt Embedding-Passing durch den Caller wie in der avs-Variante). - Methoden:
get(tenant_id, chatbot_id, language, question),set(tenant_id, chatbot_id, language, question, response),invalidate_chatbot(tenant_id, chatbot_id)über alle Sprachen,close(). - Cosine-Similarity: numpy-basiert mit Zero-Denominator-Guard.
- LRU-Eviction: RPOP-oldest sobald
LLEN > max_entries_per_chatbot. - Prometheus-Metriken:
kora_response_cache_hits_total,_misses_total,_lookup_seconds,_best_similarity,_entries{tenant_id,chatbot_id,language}.
- Wire-Schema:
tests/unit/test_response_cache.pymit 5 Tests gegen einen in-memory_InMemoryRedis-Stub (lpush/lrange/llen/rpop/lrem/get/ setex/delete/scan_iter/aclose) und einem_StubEmbedder(Question→Vector-Map plus optionalercross_lingual_collide-Modus zur Worst-Case-Simulation des Block-19-Bugs):test_cache_isolates_tenants— Tenant A Cache nicht von Tenant B getroffen, auch bei identischen Embedder-Vektoren.test_cache_isolates_languages— DE-Eintrag nicht von EN-Query getroffen, auch wenn Embedder DE/EN-Pairs auf identischen Vektor mappt (Block-19-Worst-Case).test_cache_invalidation_per_chatbot—invalidate_chatboträumt alle Sprachen eines Chatbots, nicht aber andere Chatbots.test_cache_similarity_threshold— A↔A_similar (cos≈0.998) → Hit, A↔B (cos=0) → Miss bei Threshold 0.9.test_cache_eviction_respects_max_entries— beimax_entries=2und 3 Inserts ist der älteste weg, die letzten beiden bleiben.
Architektur¶
- Cross-Lingual-Schutz strukturell, nicht zur Laufzeit.
languageist Teil des Index-Keys → der_list_entry_ids-Call sieht nur Einträge der angefragten Sprache. Kein zweiter Filter im Similarity-Check. Adressiert Block-19-Bug-Pattern (multilingual- Embeddings DE/EN-Pairs cosine ≈ 1.0, avs commit3b70a9e). - Multi-Tenant-Isolation strukturell. Namespace umschließt
tenant_id+chatbot_id; Tenant A sieht Tenant B's Keys nicht einmal beim SCAN (_entry_match-Pattern ist tenant-präfixiert). Drei-Schichten-Defense ausservices/retrieval.py(Collection- Naming, Service-Guard, DB-RLS) gilt zusätzlich auf Cache-Schema- Ebene. - Embedder-Lifecycle DI-kontrolliert.
close()schließt nur die Redis-Verbindung; der Embedder wird vom Lifespan-Owner (FastAPI app.state) geschlossen. - Cache-Outage darf keine User-facing Errors erzeugen.
get/setfangen alle Exceptions; bei Embedder/Redis-Fehler → silent miss - warn-Log. Cache ist Performance-Optimierung, nicht Correctness-Pfad.
Verified (Sub-Step 5 Container-Smoke)¶
Container-Smoke gegen echten BGE-M3-Embedder (kora-platform-embedder)
und kora-platform-redis:
- Test 1 (Tenant-Isolation): PASS — t1-Eintrag, t2-Query → Miss.
- Test 2 (Cross-Lingual-Schutz): PASS — DE-Eintrag, EN-Query → Miss. BGE-M3 produziert für DE/EN-Pairs cosine nahe 1.0; ohne Sprach- Namespace wäre das ein False-Positive.
- Test 3 (Semantic-Hit gleicher Sprache): Hit bei Cosine 0.921 ("Wie geht der Hotel-Meldeschein?" → "Wie funktioniert der Meldeschein im Hotel?", Threshold 0.92).
106/107 Unit-Tests grün (5 K-2c cache + 5 K-2b chunker + 6 chunks_indexed_metric + 11 document_uploader + 12 qdrant_manager + 14 retrieval + 9 docling_converter + 22 semantic_chunker_page_tracking + 22 semantic_chunker_preserve_tables; 1 skipped).
Out-of-Scope (folgt in K-3)¶
- Cache-Verdrahtung in eine User-facing
/query-Route → K-3 - Cache-Stats-Endpoint für Operator-UI → spätere Karte
- Generation-Stage-Anbindung (Antwort wird gespeichert) → K-3
K-2a — PlatformRetrieval auf Hybrid-RRF (Pipeline-Schicht-K Read-Seite, Sub-Welle 1, ⏳ Branch offen)¶
Branch: platform/k2a-retrieval-hybrid (von platform/v1.0.0-HEAD 8c30e4e)
Status: Sub-Steps 1–7 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
_to_qdrant_sparse-Adapter inretrieval.py(Spiegelung des Pendants indocument_uploader.py): konvertiertEmbedderClient.SparseVector→qdrant_client.models.SparseVectorfür die Read-Seite. Beide SparseVector-Klassen tragen identische Felder (indices/values); separate Namespaces sind Absicht (Embedder-Service hängt nicht anqdrant_clientals Dependency)._PREFETCH_OVERFETCH = 2-Konstante für die RRF-Lane-Limits. Über-Fetch-Faktor 2 ist bewährter Default; jede Prefetch-Lane ziehttop_k * 2Kandidaten, damit die Server-side Fusion genug Material zum Re-Ranken hat.test_search_uses_hybrid_rrf_prefetchWire-Format-Test (analog zu K-1ctest_upload_writes_dual_vector_named_struct): verifiziertquery_points-Aufruf hatFusionQuery(fusion=Fusion.RRF), zwei Prefetch-Lanes mitusing=DENSE_VECTOR_NAME/SPARSE_VECTOR_NAME, Lane-Limits 10 (=top_k×2), final-limit 5,with_payload=True.
Changed¶
PlatformRetrieval.searchruftembed_query_hybridstattembed_query(BGE-M3 dense + sparse aus einem Forward-Pass)._safe_search-Signatur erweitert:query_dense: list[float]query_sparse: QdrantSparseVectorstattquery_vec: list[float]. Body führt jetzt server-side RRF-Fusion viaPrefetch+FusionQuerydurch.mock_embedder-Fixture intest_retrieval.pyaufembed_query_hybridumgestellt; Mock-Sparse-Vektor mit deterministischen Indices[3, 17, 42]für Wire-Format-Assertions.
Architektur¶
- Fan-Out Chatbot+Shared-Logik strukturell unverändert. Cross-
Tenant-Guard, Template-Gate (
_template_allows_shared), Collection-Not-Found-Tolerance bleiben 1:1. - Score-Merge nach Fan-Out: RRF-Scores aus Qdrant
(
1 / (60 + rank)-Summe pro Lane) sind innerhalb derselben Query zwischen Chatbot- und Shared-Collection vergleichbar; das bestehendesorted(...) + [:top_k]bleibt korrekt.
Verified (Sub-Step 6 End-to-End-Smoke)¶
Test-Tenant + Test-Chatbot + 3 Chunks via DocumentUploader (K-1c)
mit semantisch distinkten Inhalten (Hotel-Meldeschein, Software/Git,
Kurverwaltung/Kurtaxe):
| Query | Sprache | Top-1 (RRF score) | Erwartung |
|---|---|---|---|
| „Wie funktioniert der Meldeschein im Hotel?" | DE | Chunk 1 (1.0000) | Hotel ✓ |
| „What is tourism tax accounting?" | EN | Chunk 3 (0.5000) | Kurverwaltung/Tourismus ✓ (cross-lingual) |
| „git code review software development" | EN | Chunk 2 (0.5000) | Software/Git ✓ |
- 37/37 Unit-Tests grün (14 retrieval + 11 uploader + 12 qdrant_manager).
- Health-Endpoints
/health/live+/health/readypost-Smoke grün.
Out-of-Scope (folgt in K-2b/c und K-3)¶
- Pipeline-Komponenten (DoclingConverter, SemanticChunker) → K-2b
- Cache-Layer für Hybrid-Queries → K-2c
- User-facing
/query-Route → K-3
K-2b — DoclingConverter + SemanticChunker portieren (Pipeline-Schicht-K Sub-Welle 2, ⏳ Branch offen)¶
Branch: platform/k2b-components-port (von platform/v1.0.0-HEAD 06f1ff9)
Status: Sub-Steps 1–8 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
src/kora_platform/pipelines/— neuer Sub-Package-Pfad, parallel zusrc/avs_chatbot/pipelines/.__init__.pydokumentiert die Konsumenten (Indexing-Pfad viaDocumentUploader, Retrieval-Pfad viaPlatformRetrieval) und die Tenant-Konfigurierbarkeit.pipelines/components/docling_converter.py(KlasseDoclingConverter, vorherDoclingPDFConverterin avs):- Wrapper um
docling-haystack'sDoclingConverterimMARKDOWN-Mode mitimage_placeholder=""+page_break_placeholder=PAGE_BREAK_PLACEHOLDER. - Output-Contract identisch zu avs (
file_path,source_id,source_type="pdf",docling_version,has_tables). - Fail-Loud bei Docling-Errors (kein PyPDF-Fallback).
- Wire-Contract-Konstante
PAGE_BREAK_PLACEHOLDER = "\x02KORA_PAGE_BREAK\x02"— generischer KORA-Namespace statt AVS.
- Wrapper um
pipelines/components/semantic_chunker.py(KlasseSemanticChunker, generischer Markdown-PDF-Chunker):- Drei neue ctor-Parameter machen ehemals AVS-spezifische
Heuristiken Tenant-konfigurierbar:
footer_regex: str | None— optionaler Regex zum Footer-Strip (DefaultNone→ kein Strip).category_map: dict[str, str] | None— Pfad-Substring → Category-Label; Default{}→ Fallback aufPath(file_path).stem.sentinel_namespace: str = "KORA"— Prefix für interne Tabellen- und Page-Sentinels; Tenant-Override beeinflusst nur die nummerierten Sentinels, nicht denPAGE_BREAK_PLACEHOLDER-Wire-Contract.
- Per-Instance kompilierte Sentinel-Patterns
(
_table_sentinel_fmt,_re_table_sentinel,_page_sentinel_fmt,_re_page_sentinel,_re_page_sentinel_strip). - AVS-gebundene Helfer auf Klassen-Methoden migriert:
_detect_category(vorher_detect_handbook_name),_clean_page_text,_protect_table_blocks,_restore_table_blocks,_number_page_break_sentinels,_strip_page_sentinels,_extract_page_numbers,_extract_sections. - Namespace-unabhängige Helfer bleiben Modul-Funktionen
(
_word_count,_split_at_sentence,_is_toc_entry,_strip_chapter_prefix,_is_table_only,_is_heading_only,_is_figure_caption_only,_classify_chunk_type,_deduplicate_header,_sections_to_chunks). - Prometheus-Counter
kora_chunks_indexed_total(vorheravs_indexed_chunks_total).
- Drei neue ctor-Parameter machen ehemals AVS-spezifische
Heuristiken Tenant-konfigurierbar:
pipelines/components/__init__.pyexportiertDoclingConverterSemanticChunker.tests/unit/test_semantic_chunker.pymit 5 Tests für die K-2b- Surface:test_chunker_without_footer_regex_keeps_footer_text— Default-Verhalten lässt Footer intakt.test_chunker_with_custom_footer_regex_strips_match— konfigurierter Regex strippt; umliegender Text bleibt erhalten.test_chunker_category_map_with_match— erstes passendes Needle gewinnt ("Maschine"→"Maschinen-Handbuch").test_chunker_category_map_falls_back_to_filename_stem— kein Match →Path(file_path).stem.test_chunker_sentinel_namespace_default_is_kora— Whitebox- Check der Per-Instance-Sentinel-Formate +_re_page_sentinel- Match auf\x02KORA_PAGE_3\x02.
Changed¶
- Dead-Code entfernt im Page-Number-Pfad: Der avs-Chunker hatte
einen Legacy-Fallback auf "Seite X von Y"-Footer-Page-Number-Extraction
für die alte
EnhancedPDFConverter-Pipeline. Im kora_platform existiert diese Pipeline nicht (Docling ist canonical seit Block 18); der Fallback wurde nicht parametrisiert sondern weggelassen.
Architektur¶
- Wire-Contract zwischen Converter und Chunker bleibt KORA-Namespace.
Der
PAGE_BREAK_PLACEHOLDERwird vomDoclingConverteralspage_break_placeholder-kwarg inmd_export_kwargsinjiziert und vom Chunker viaRE_PAGE_BREAK_PLACEHOLDERerkannt. Beide Default- Konstanten sind KORA-prefixed; Tenant-sentinel_namespace-Overrides betreffen ausschließlich die nummerierten Sentinels (intern, nie Embedder-/User-sichtbar). - Parent-Document-Meta-Propagation unverändert:
_PARENT_META_PROPAGATE_KEYS = ("source_type", "docling_version"); beide Keys landen auf jedem Chunk. avs_chatbot.pipelines.componentsbleibt unverändert. Der legacy-Demo-Stack indexiert weiter mitDoclingPDFConverter+ Original-SemanticChunker(AVS-Footer + AVS-Category-Map hardcoded); kein Cross-Import zwischen den Sub-Packages.
Verified¶
- Container-Smoke (
make redeploy-platform-api→ API healthy →docker exec):- Default-Chunker auf "Maschine-Handbuch.pdf" ohne Footer-Regex →
Handbuch: Maschine-Handbuch-Prefix, Footer-Text bleibt. - AVS-Style-Chunker (
footer_regex=Doku-Version:.*Seite \d+ von \d+,category_map={"Maschine": "AVS Maschinen-Handbuch"},sentinel_namespace="AVS") auf gleichem Doc →Handbuch: AVS Maschinen-Handbuch-Prefix, Footer entfernt. DoclingConverter()instanzierbar (docling extras im Image).
- Default-Chunker auf "Maschine-Handbuch.pdf" ohne Footer-Regex →
- 101/101 Unit-Tests grün (5 K-2b chunker + 6 chunks_indexed_metric + 11 document_uploader + 12 qdrant_manager + 14 retrieval + 9 docling_converter + 22 semantic_chunker_page_tracking + 22 semantic_chunker_preserve_tables; 1 skipped).
Out-of-Scope (folgt in K-2c/K-3)¶
- Cache-Layer für Hybrid-Queries → K-2c
- Tenant-Config-Injection (Footer-Regex + Category-Map aus DB) → K-3
- Indexing-Pipeline-YAML, das Converter+Chunker zusammenbaut → K-3
Block 19 Phase 4 — Production-Cutover (BGE-M3 Hybrid live, ✅ Erledigt)¶
Live-Demo wechselt von Legacy (e5-large + Dense-only) auf Hybrid (BGE-M3 + RRF Dense+Sparse).
Branch: feature/block-19-bge-m3-embedder (gleich wie Phase 1–3.5)
Cutover-Datum: 2026-05-03
Default-Flips in src/avs_chatbot/config.py¶
| Setting | Alter Default | Neuer Default | Begründung |
|---|---|---|---|
use_hybrid_retrieval |
False |
True |
Block-19 ist Production-Default; Phase-3-Eval zeigt R@5 0.92→0.96 |
qdrant_collection |
"avs_handbuecher" |
"avs_handbuecher_bge_m3" |
Hybrid-fähige Collection (242 Chunks, 100% Sparse-Population) |
embedding_model_dense |
"BAAI/bge-m3" |
unverändert | Phase-3-bestätigter Default |
rrf_dense_weight |
0.7 |
unverändert | Phase-3.5-Sweep zeigt: kein anderer Wert ist besser (Sweep über 0.7/0.8/0.9/1.0 identische Metriken) |
embedding_model (legacy) |
"intfloat/multilingual-e5-large" |
unverändert | Legacy-Pfad bleibt für Rollback intakt |
Cutover-Sequenz¶
- Push Recovery-Point (Sub-Step 0):
feature/block-19-bge-m3-embedderzu Remote, HEADa3c70ae(Phase 3) gepusht. - Snapshot Legacy-Collection (Sub-Step 1):
avs_handbuecher(242 points, dense-only) alsbackups/qdrant/avs_handbuecher_pre_block19_cutover_20260503-1150.snapshot(4.2 MB, gitignored, 30+ Tage Aufbewahrung). config.py-Default-Flips (Sub-Step 2): zwei Settings, ruff + mypy clean..env-Switch (Sub-Step 3): idempotenter Sed-Edit + grep-q-Append,.env.block19_backupunverändert. Diff zeigt nur die zwei erwarteten Änderungen.- Redeploy + Live-Verify (Sub-Step 4):
make redeploy→make smoke8/8 grün →make smoke-hybridgrün → frische Live-Cache-bypass-Query (cached=false, 5 sources, kohärente Antwort) → 4 weitere Live-Queries fehlerfrei → API-Logs ohne Errors/Exceptions (nur erwartete Warnings:TransformersSimilarityRanker is considered legacy+authlib deprecation). - Observation-Setup (Sub-Step 5): TODOs in
offene-todos.mdfür 7-Tage-Window mit Grafana-Panel-Alarmen.
Aggregate-Wins gegenüber Block 18 (Phase-3-Eval, 25 Test-Queries)¶
- R@5: 0.92 → 0.96 (+0.04)
- R@10: 0.92 → 0.96 (+0.04)
- avg_latency: 155 ms → 140 ms (−15 ms, BGE-M3-Single-Pass-Effekt)
- Tabellen-Queries (q021–q025): R@5 = 1.00 (no regression)
- 0 regressed Queries (Hit@5 down)
MRR-Disclaimer (transparent dokumentiert)¶
Block-19-MRR (0.74) liegt 0.03 unter Block-18-Baseline (0.77). Ursache ist q006 alleine (RR 1.00 → 0.25, Hit@5 bleibt True). q006 hat duplizierten Inhalt in zwei Handbüchern; der Cross-Encoder-Reranker bevorzugt Kurverwaltung-Chunks vor Beherbergungsbetrieb-Chunks. Block-18-Top-1-Position war e5-large-Embedding-Glücksfall, kein systematisches BGE-M3-Defizit. Phase-3.5-RRF-Weight-Sweep über {0.7, 0.8, 0.9, 1.0} zeigt: keine Weight-Config bringt q006 zurück — es ist Reranker-Bias, nicht RRF-Bug. User-facing-Impact: 0 (relevant doc weiter in Top-5). Reranker-Mitigation ist Block 19.5 (bge-reranker-v2-m3-Upgrade, konsistenter BGE-Stack).
Live-Verify-Datapoints¶
| Query | cached | latency | sources | Top-1 file |
|---|---|---|---|---|
| „Wie kann ich eine Begleitperson nachträglich aus dem Meldeschein entfernen?" (frisch) | False | 3000 ms | 5 | Kurverwaltung.pdf p86, score 0.83 |
| „Was ist ein Meldeschein?" (Phase-3-Cache-Carryover) | True | 95 ms | 5 | semantic-cache hit |
| „Welche Felder sind beim Self-Check-In Pflicht?" (frisch) | False | 1931 ms | 5 | Top-1 ranker score in Range |
| „Wie storniere ich eine Reservierung?" (frisch) | False | 3488 ms | 5 | Top-1 ranker score in Range |
Rollback-Plan¶
config.py-Defaults auf Hybrid bleiben bestehen — .env overrides die Defaults, daher reicht .env-Restore für sofort-Rollback. Snapshot in backups/qdrant/ für Recovery bei tieferem Issue (Collection-Korruption etc.).
7-Tage-Observation-Window¶
Start: 2026-05-03. Tag-3-Mid-Window (2026-05-06) und Tag-7-End-Window (2026-05-10) als Checkpoints. Bei grünem Tag-7:
- Phase 5 (Edge-Case-Queries q026–q035) als nächste Roadmap-Karte.
- Alte Collection avs_handbuecher löschen (TODO-Block-19-Cleanup, blocked-bis-Tag-7).
- Snapshot in backups/qdrant/ weitere 30 Tage behalten (defense in depth).
Phase-4-Stop-Trigger-Status¶
Alle 7 Sub-Steps (Push, Snapshot, Defaults, .env, Redeploy+Verify, Observation-Setup, Commit) — alle Stop-Trigger negativ.
- 0.1 (Push fehlschlägt): negativ — Push erfolgreich.
- 0.2 (Force-Push nötig): negativ — Fast-Forward.
- 1.1 (Snapshot fehlschlägt): negativ.
- 1.2 (Snapshot < 1 MB): negativ — 4.2 MB.
- 2.1 (Settings nicht gefunden): negativ.
- 3.1 (.env-Diff zeigt unerwartete Änderungen): negativ — exakt 2 Änderungen.
- 4.1 (smoke red): negativ.
- 4.2 (Live-Query 500/Timeout): negativ.
- 4.3 (5-min-Errors): negativ.
- 4.4 (cached=true bei frischer Query): negativ — frische Queries return cached=false.
- 5.1 (Demo 502/503): negativ — HTTP 401 (Basic-Auth, normal).
- 6.1 (smoke nach Commit rot): wird nach Commit final verifiziert.
Phase-5-Vorbedingung¶
- Tag-7-Observation grün (2026-05-10)
- Phase-5-Prompt mit q026–q035 Edge-Case-Queries (Paragraphen, Codes, Akronyme, Compounds)
Block 19 Phase 3 — Real-Stack-Quality-Vergleich (BGE-M3 Hybrid vs e5-large Dense, ✅ Erledigt)¶
Outcome: B (Mixed) — klarer Hit@5/Latenz-Win, MRR knapp daneben (–0.03).
Branch: feature/block-19-bge-m3-embedder (gleich wie Phase 1/2/Followup)
Run-Methodik¶
- Per-Subprocess-In-Process-Eval mit
docker compose exec -e USE_HYBRID_RETRIEVAL=true -e QDRANT_COLLECTION=avs_handbuecher_bge_m3 api python /tmp/phase3_eval.py. Live-API blieb während Eval im Legacy-Modus (Demo unbetroffen). - Drift gegenüber Phase-3-Plan: Existierender Eval-Runner
tests/evaluation/evaluate_retrieval.pyist HTTP-basiert und ruft die Live-API — per-subprocess-env-Override hätte daran nichts geändert.tests/-Verzeichnis ist auch nicht in den API-Container gemountet. Pragmatischer Workaround: ad-hoc Inline-Eval-Skript viadocker cpin Container, In-Process-Pipeline-Aufruf wiemake smoke-hybrid, Skalierung auf 25 Fragen + Ranker + Hit@K + MRR. Output-Schema bewusst identisch zueval_retrieval_block18_v2.json. Skript als/tmp/phase3_eval.pyad-hoc, NICHT intests/evaluation/committed (gehört in einen Phase-5-Tooling-Refactor wieevaluate_retrieval_inprocess.py, nicht in Phase 3). - A/B-Setup: Beide Runs gegen 25 Test-Questions (
q001–q025, davonq021–q025Tabellen). Identischeretriever_top_k=20,ranker_top_k=5,product_id=meldeschein. Block-18: e5-large + Dense-only gegenavs_handbuecher. Block-19: BGE-M3 + Hybrid (RRF Dense:Sparse 0.7:0.3) gegenavs_handbuecher_bge_m3.
Aggregate¶
| Metric | Block 18 | Block 19 | Δ |
|---|---|---|---|
recall_at_5 |
0.9200 | 0.9600 | +0.0400 |
recall_at_10 |
0.9200 | 0.9600 | +0.0400 |
mrr |
0.7667 | 0.7400 | −0.0267 |
avg_latency_ms |
155.3 | 140.1 | −15.2 |
Per-Frage Hit@5¶
- Improved (B18 miss → B19 hit): 1 (q020 — schwierige Query, jetzt rank-3)
- Regressed (B18 hit → B19 miss): 0 (Stop-Trigger Outcome-C konnte nicht ausgelöst werden)
- Unchanged: 24
MRR-Verschiebungen (|ΔRR| > 0.01) — die −0.03-Story¶
- q006: 1.00 → 0.25 (rank 1 → rank 4) — Haupttreiber des MRR-Verlusts
- q025_table_kurtaxe_saison: 1.00 → 0.50 (rank 1 → rank 2)
- q016: 0.50 → 0.33 (rank 2 → rank 3)
- q023_table_bundeslaender_verordnung: 0.33 → 0.25 (rank 3 → rank 4)
- q020: 0.00 → 0.33 (Miss → rank 3, gain)
- q024_table_fehler_codes: 0.50 → 1.00 (rank 2 → rank 1, gain)
Hypothese: RRF blendet Sparse-Score in Dense-Top-Ranks ein. Bei Queries, wo Dense allein perfekt war (B18 RR=1.00), kann der Sparse-Anteil den Top-Rank verwässern, ohne das relevante Doc aus Top-5 zu drängen — R@5 unverändert, MRR sinkt. Ranker-Stage rerankt nur Top-5 und kann nicht alle Verschiebungen kompensieren. Mögliche Korrektur in Phase 4 oder Followup: rrf_dense_weight von 0.7 → 0.8 könnte MRR retten, ohne den R@5-Win zu opfern. Erfordert eigenes Tuning-Run.
Tabellen-Queries (q021–q025)¶
5/5 Hit@5=True (B18 wie B19, R@5=1.00, kein Regress). q024_table_fehler_codes von rank 2 → rank 1, q025_table_kurtaxe_saison von rank 1 → rank 2 — Sparse-Component beeinflusst aber bricht Tabellen-Recall nicht.
By-Difficulty¶
| Difficulty | Count | B18 R@5 | B19 R@5 | B18 MRR | B19 MRR |
|---|---|---|---|---|---|
| easy | 8 | 0.875 | 0.875 | 0.875 | 0.781 |
| medium | 11 | 1.000 | 1.000 | 0.818 | 0.818 |
| hard | 6 | 0.833 | 1.000 | 0.528 | 0.542 |
Wichtig: Hard-Queries gewinnen R@5 stark (0.83 → 1.00) bei stabilem MRR. Hybrid hilft Edge-Cases.
Akzeptanz-Bewertung (Phase-0-Kriterien)¶
| Kriterium | Block 18 | Block 19 | Akzeptanz | Status |
|---|---|---|---|---|
| Gesamt R@5 | 0.92 | 0.96 | ≥ 0.92 | ✅ |
| Gesamt R@10 | 0.92 | 0.96 | ≥ 0.92 | ✅ |
| Gesamt MRR | 0.7667 | 0.74 | ≥ 0.77 | ❌ (−0.03) |
| q001–q020 R@5 | 0.90 | 0.95 | ≥ 0.90 | ✅ |
| q021–q025 R@5 | 1.00 | 1.00 | ≥ 0.95 | ✅ |
| avg_latency_ms | 155 | 140 | ≤ 250 | ✅ |
| Failed queries | 0 | 0 | ≤ 1 | ✅ |
| Regressed (Hit@5↓) | — | 0 | ≤ 1 | ✅ |
7/8 ✅. MRR knapp unter Target (−0.03), getrieben von 4 isolierten Per-Frage-Verschiebungen, davon q006 alleine −0.75. Stop-Trigger 4.1 (Outcome C) negativ.
Confidence-Live-Check (Sub-Step 5, ausgeführt)¶
- Live-API für ~70 s in Hybrid-Modus geschaltet (
USE_HYBRID_RETRIEVAL=true+QDRANT_COLLECTION=avs_handbuecher_bge_m3persed -i). make redeploy→ API Up healthy.make smoke8/8 grün im Hybrid-Modus.- Curl-Smoke „Welche Felder sind im Gruppenmeldeschein verpflichtend?" (frisch, kein Cache-Hit): 4 Sources alle aus
Meldeschein_*.pdf, Top-Ranker-Score 0.92, Antwort kohärent. - Rollback per
cp .env.block19_backup .env+make redeploy→ Demo zurück im Legacy-Modus,make smoke8/8 grün,make smoke-hybridweiter grün (subprocess unabhängig vom Live-API-State). - Datapoint für Phase 4: Live-API-Pfad in Hybrid-Modus funktioniert, Helper aus Phase-2-Followup hat keinen Live-API-Bug.
Phase-3-Stop-Trigger-Status¶
Alle 7 Sub-Steps (Pre-Check, Backup, In-Process-Eval, Diff-Report, Akzeptanz, Live-Confidence, Commit) — alle Stop-Trigger negativ.
- Stop-Trigger 0.1 (Pre-Check ⚠/❌): pragmatisch handhabbar — Eval-Runner-CLI-Drift dokumentiert, durch Inline-Skript ersetzt.
- Stop-Trigger 1.1 (Backup-Permissions): negativ.
- Stop-Trigger 2.1 (Eval-Runner-Fehler): negativ.
- Stop-Trigger 2.2 (Failed > 2): negativ — 0 Failures.
- Stop-Trigger 2.3 (R@5 < 0.80): negativ — 0.96.
- Stop-Trigger 3.1 (Diff-Skript-Schema): negativ.
- Stop-Trigger 4.1 (Outcome C): negativ — Outcome B.
- Stop-Trigger 5.1 (Live-Smoke nach Switch rot): negativ.
- Stop-Trigger 5.2 (Demo nach Rollback nicht zurück): negativ.
- Stop-Trigger 6.1 (Smoke nach Commit rot): wird in Sub-Step 6 final verifiziert.
Phase-4-Voraussetzung¶
- Outcome B mit Empfehlung Phase 4: Hit@5-Win user-facing primär (Top-5 Sources werden angezeigt), MRR-Loss minor und in Noise-Range. Hard-Queries-R@5 +0.17 ist starkes Signal.
- Followup-Issue: q006-Diagnose (warum dropped relevant doc von rank 1 nach rank 4 unter Hybrid?) + RRF-Weight-Tuning-Run (
rrf_dense_weight0.8 vs 0.7 vs 0.6). - Phase-4-Cutover-Plan ist als nächster Block-19-Schritt im Roadmap-Eintrag fällig.
Block 19 — BGE-M3 + Hybrid Retrieval (Phase 2 Followup: Hybrid-Wiring-Drift-Fix, ✅ Erledigt)¶
Branch: feature/block-19-bge-m3-embedder (gleich wie Phase 2)
Fixed¶
- Phase-2-Wiring-Drift in der Hybrid-RAG-Pipeline: Konsumenten
(
api/routes/query.pysync + SSE,services/evaluation_service.py) riefen hardcodedpipeline.get_component("retriever")auf — eine Komponente, die im Hybrid-Modus durchdense_retriever+sparse_retriever+joinerersetzt ist. Aktivierung vonUSE_HYBRID_RETRIEVAL=trueproduzierteComponent named retriever not found in the pipelineund blockierte Phase 3 (Real-Stack-Quality-Vergleich gegenavs_handbuecher_bge_m3). - Smoke-Coverage-Lücke: Phase-2-Smoke-Tests sind grün geblieben, weil
use_hybrid_retrieval=Falseder Default ist — der Hybrid-Hot-Pfad wurde nie real ausgeführt.
Added¶
src/avs_chatbot/pipelines/retrieval_helper.pymit zwei Helpern:embed_query()undretrieve_documents(). Modus-Detection viapipeline.graph.nodes()(nicht via Settings-Flag — bleibt konsistent auch wenn Settings vom Live-Pipeline-State driften, z.B. nach Hot-Reload). Bewusst zwei Funktionen statt einesrun_retrieval(): Konsumenten brauchen das Embedding für den Semantic-Cache-Lookup vor dem Retrieval, eine kombinierte Helper-API würde diese Trennung aufweichen.make smoke-hybridundmake smoke-all— Hybrid-Pipeline-Smoke läuft in-process perdocker compose exec -e USE_HYBRID_RETRIEVAL=true, ohne die Live-API in Hybrid-Modus zu schalten. Pattern wiederverwendbar für Block 19.5 (Reranker-Upgrade) und Block 20 (AST-Chunking-Flag).
Changed¶
api/routes/query.py:301-353(sync) +query.py:872-933(SSE) +services/evaluation_service.py:251-293+scripts/debug_retriever_scores.py:embedder = pipeline.get_component("text_embedder")/retriever = pipeline.get_component("retriever")durch Helper-Aufrufe ersetzt. Verbleibendeget_component()-Calls inapi//services/(5:ranker×3,generator×1,prompt_builder×1) bleiben direct, da diese Komponenten in beiden Pipeline-Modi unter gleichem Namen existieren und keinen Bug-Drift erzeugen — bewusste Helper-Bloat-Vermeidung.
Hybrid-Smoke-Datapoints (Sub-Step 4)¶
make smoke(Legacy): 8/8 grün (Refactor hat Legacy-Pfad nicht regrediert).make smoke-hybrid(NEU): grün, 8 Dokumente retrieved für Query „Was ist ein Meldeschein?" gegenavs_handbuecher_bge_m3(242 Chunks).- Beide Targets in
make helpsichtbar.
Phase-2-Followup-Stop-Trigger-Status¶
Alle 5 Sub-Steps (Vollständigkeits-Check, Helper-Implementation, Call-Sites-Refactor, Hybrid-Smoke, Commit) — alle Stop-Trigger negativ.
- Stop-Trigger 1.1 (Dead Code): negativ — alle gefundenen Komponenten-
Namen entsprechen aktiven
pipeline.add_component()-Definitionen inrag.py/indexing.py. - Stop-Trigger 1.2 (>8 Kategorie-A-Treffer = größerer Refactor): 11
Treffer total in api/+services/, davon nur 3 buggy (
retriever-String) - 3 logisch-gekoppelte (
text_embedderim Retrieval-Trio). Alle 11 Edits sind triviale 1:1-Replacements — kein Refactor-Aufblähen. - Stop-Trigger 2.1 (Pipeline-Definition andere Komponenten-Namen): negativ.
- Stop-Trigger 3.1 (Call-Site mit Helper-API-incompatibler Logik):
handhabbar — der ursprünglich vorgeschlagene
run_retrieval(pipeline, query)(full pipeline) hätte die Embedder/Cache/Retriever-Trennung gebrochen, daherembed_query()+retrieve_documents()als zwei Funktionen statt einer. Drift dokumentiert, Helper-API entsprechend angepasst. - Stop-Trigger 4.1 (Hybrid-Smoke 0 Dokumente trotz 242 Chunks): negativ — 8 Docs.
- Stop-Trigger 4.2 (Legacy-Smoke nach Refactor rot): negativ — 8/8 grün.
- Stop-Trigger 5.1 (redeploy nach Commit rot): negativ.
Phase-3-Voraussetzung¶
.env-Switch-Plan:QDRANT_COLLECTION=avs_handbuecher_bge_m3+USE_HYBRID_RETRIEVAL=truesetzen,make redeploy, dannmake smoke && make smoke-hybridals Final-Verify, danach Real-Stack- Quality-Run gegentests/evaluation/test_questions.json.
Block 19 — BGE-M3 + Hybrid Retrieval (Phase 2: Pipeline-Integration, ✅ Erledigt)¶
Branch: feature/block-19-bge-m3-embedder
(Phase 1 = isolierte Embedder-Component, Phase 2 = Pipeline-Wiring; Production-
Switch auf Live-Collection ist eigener Phase-3-Schritt nach Eval.)
Added (Phase 2)¶
- Settings-Erweiterung in
src/avs_chatbot/config.py:embedding_model_dense,embedding_model_sparse(BGE-M3 single-model: gleicher Wert),use_hybrid_retrieval(defaultFalse— Block-18-Backward-Compat),rrf_dense_weight(default0.7, Sparse-Weight =1 - rrf_dense_weight). - Indexing-Pipeline-Switch in
src/avs_chatbot/pipelines/indexing.py:settings.use_hybrid_retrievalschaltet zwischenBGEM3DocumentEmbedder(Dense+Sparse in einem Forward-Pass) und Legacy-SentenceTransformersDocumentEmbedder.QdrantDocumentStore(use_sparse_embeddings=True, sparse_idf=True)war seit Block 18 schon konfiguriert — der Sparse-Slot wird jetzt erstmals befüllt. - RAG-Pipeline-Switch in
src/avs_chatbot/pipelines/rag.py: Im Hybrid- Modus läuftBGEM3TextEmbedder → {QdrantEmbeddingRetriever, QdrantSparseEmbeddingRetriever} → DocumentJoiner(RRF, weights=[dense, sparse]) → Ranker → Generator. Architektur-Entscheidung: StattQdrantHybridRetriever(parameter-loses RRF) bewusst zwei parallele Retriever +DocumentJoiner— qdrant-haystack'sQdrantHybridRetrieverexponiert keinen Weight-Knob,rrf_dense_weightwürde dort silent ignoriert. - Reindex-Skript
scripts/reindex_with_bge_m3.pyanalogscripts/reindex_with_docling.py. Default-Target:avs_handbuecher_bge_m3(Variante-B-parallel — Productionavs_handbuecherbleibt e5-large bis zum expliziten Operator-Cutover). Forciertuse_hybrid_retrieval=Trueim per-Run-Settings-Override. Counters:sources_indexed,sources_failed,old_chunks_total,new_chunks_total,sparse_populated_new(Scan via Qdrant-Scroll mitwith_vector=["text-sparse"]— schützt gegen den Phase-1-Silent-Bug, falls_to_sparse_embeddingjemals aufhören sollte, String-Keys zu casten). - 34 neue/erweiterte Unit-Tests (Settings, Indexing-Switch, RAG-Switch+Wiring+Weight-Propagation, Reindex-Script-Orchestration mit fail-isolation, 404-Tolerance auf erste Run).
Real-Stack-Smoke-Datapoints (Sub-Step 3.5)¶
make redeploymit Phase-2-Code: API-Container baut, startet, healthy in <30s nach Image-Build.make smoke: 8/8 grün (Health, Admin-Auth, Demo-Page, Widget-JS, Query-API, Metrics, Präsentation, Screenshots).- Reindex
--limit 1aufavs_handbuecher_bge_m3: 1 Source-PDF → 7 Chunks indexed, 7/7 mit populated sparse-vector (Silent-Bug-Magnet negativ), Collection-Auto-Create durch QdrantDocumentStore funktioniert. - Hybrid-Retrieval-Smoke (Query "KI Richtlinie" gegen 7-Chunk-Collection):
- Dense top-1: Score 0.6279, Kapitel "1 Allgemein"
- Sparse top-1: Score 0.0109, Kapitel "8 Veröffentlichung" (anderer Treffer als Dense — beide Signale tragen unterschiedlich)
- RRF-Joiner top-1: Score 0.9859, Kapitel "1 Allgemein" (Fusion bevorzugt
Dense bei
rrf_dense_weight=0.7) - Ranker top-1: Score 0.9959, Kapitel "1 Allgemein" (Cross-Encoder-Re-Rank)
- Production-Collection unangetastet:
avs_handbuecherbleibt 242 Points (Block-18-Stand) — der Switch passiert erst, wenn OperatorUSE_HYBRID_RETRIEVAL=trueundQDRANT_COLLECTION=avs_handbuecher_bge_m3in.envsetzt.
Phase-2-Stop-Trigger-Status¶
Alle 6 Sub-Steps (Settings-Erweiterung, Indexing-Switch, RAG-Switch, Reindex-Skript, Real-Stack-Smoke, Commit) — alle Stop-Trigger negativ.
- Settings-conditional nicht komplexer als gedacht (eine if-Verzweigung pro Pipeline).
- Sparse-Storage in Qdrant trivial:
QdrantDocumentStore(use_sparse_embeddings=True)war seit Block 18 schon konfiguriert. - Reindex-Latenz pro Document unter 5s (BGE-M3 + 7 Chunks Embed in <2s nach Model-Load).
- Hybrid-Score-Fusion-Output verständlich (Dense ~0.6, Sparse ~0.01, RRF normalisiert auf 0-1, Ranker auf 0-1).
Pre-Phase-2-Snapshot¶
backups/qdrant/avs_handbuecher_pre_block19.snapshot(4.3 MB, POSIX tar) vor Phase 2 erstellt — Production-Collection-Stateavs_handbuecher(242 Points, Block-18-Stand) ist wiederherstellbar.
Block 19 — BGE-M3 + Hybrid Retrieval (Phase 1: Embedder-Component, ✅ Erledigt)¶
Branch: feature/block-19-bge-m3-embedder (Phase 1 isoliert, Pipeline-Wiring in Phase 2)
Added¶
- BGE-M3-Embedder-Komponenten:
BGEM3DocumentEmbedderundBGEM3TextEmbedderinsrc/avs_chatbot/pipelines/components/bge_m3_embedder.py. WrappenFlagEmbedding.BGEM3FlagModel(BAAI, MIT) als Haystack@component- Klassen. Erzeugen Dense (1024d) UND Sparse (SPLADE-style lexical weights) in einem einzigen Forward-Pass. Lazy-Import-Pattern analogDoclingPDFConverter(Block 18). Defaults:BAAI/bge-m3,cuda:0(= physical GPU 1 via Container-Mapping),use_fp16=True(~2.3 GB VRAM statt ~4.6 GB),max_length=8192,return_colbert_vecs=False. Noch NICHT in indexing/RAG-Pipeline gewired — das ist Phase 2. api_model_cacheDocker-Volume auf/home/appuser/.cache/huggingfacedes api-Service. Fixt Block-18-Drift, bei der api-side Modelle (e5-large, mmarco-Reranker, jetzt BGE-M3) nicht über--force-recreatepersistiert wurden. Volume-Initialisierung via Dockerfile-mkdir+chownvorUSER appuser-Switch, damit der named-volume die appuser-ownership übernimmt.- 16 Unit-Tests in
tests/unit/test_bge_m3_embedder.py, FlagEmbedding vollständig gemockt. Coverage: Lazy-Import, Sparse-int-Cast, ColBERT-explicitly-disabled, no-prefix-Disziplin, meta_fields_to_embed, Empty-Input-Handling, Idempotent warm_up.
Changed¶
- Pin-Locks in
pyproject.toml:torch>=2.11.0,<2.12.0,transformers>=5.7.0,<6.0.0,tokenizers>=0.22.0,<0.23.0,sentence-transformers>=5.4.0,<6.0.0. Begründung: Phase-0-Discovery zeigte, dass fastembed (der naheliegende Fallback) kein BGE-M3 hat (nur English-bge-large + English-SPLADE-PP). FlagEmbedding ist damit der einzige verfügbare Pfad — Pin-Locks schützen vor transitiven Updates, die BGE-M3 brechen würden. - Document-Mutation auf
dataclasses.replaceumgestellt. Haystack 2.x flagt direktedoc.embedding =/doc.sparse_embedding =-Mutation als unsafe (parallele Pipeline-Komponenten könnten Referenzen halten).
Dependencies¶
- Added:
FlagEmbedding>=1.4.0,<2.0.0(BAAI BGE-M3 Reference-Lib, MIT). Ziehtpeft 0.19.1,datasets 4.8.5,accelerate 1.13.0. Alle torch/transformers/sentence-transformers-Versionen identisch zum Container-Baseline (Phase-0-Dry-Run-verifiziert).
Real-Stack-Smoke-Datapoints (Sub-Step 5, manueller REPL-Run)¶
- Erster BGE-M3-Modell-Load: 39.8s (28s Download, 12s Init) — unter 5min-Schwelle
- Cached-Load: <1s (Volume-Persistence funktioniert)
- Encode-Time: 1.0s für 3 Dokumente, 0.32s für 1 Query
- Dense-Shape verifiziert: 1024d
- Sparse-NNZ: 7-16 für kurze Texte (BGE-M3 typisch)
- Cosine(identical query+doc): 1.0000 (Sanity-Check Dense-Pfad)
avs-api-model-cacheVolume nach Smoke: 6.9 GB
Phase-1-Stop-Trigger-Status¶
Alle 6 Sub-Steps (Dependency-Setup, Volume-Mount, Component, Unit-Tests,
Real-Stack-Smoke, Commit) — alle Stop-Trigger negativ. Phase 1 ist
startbereit für Phase-2-Pipeline-Wiring (indexing.py + rag.py Embedder-
Swap, USE_HYBRID_RETRIEVAL Feature-Flag, dual-Retriever mit RRF).
K-1c — DocumentUploader auf dual-vector-Upsert (Pipeline-Schicht-K Sub-Welle 3, ⏳ Branch offen)¶
Branch: platform/k1c-uploader-dual-vector (chained auf K-1b-HEAD 4a7b198)
Status: Sub-Steps 1–5 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
- Adapter
_to_qdrant_sparseindocument_uploader.py: konvertiert die Embedder-eigeneSparseVector-dataclass inqdrant_client.models.SparseVector. Beide Klassen tragen identische Felder (indices/values); der Embedder pflegt seine eigene Form, weil er nicht aufqdrant_clientals Dependency hängen soll.
Changed¶
DocumentUploader.uploadruftembed_passages_hybridstattembed_passagesund schreibtPointStructmit named-vector-Dict ({DENSE_VECTOR_NAME: list[float], SPARSE_VECTOR_NAME: SparseVector}) statt bare-list. Wire-Contract-Konstanten ausqdrant_manager(K-1b) werden importiert, nicht dupliziert.- count-mismatch-Guard prüft jetzt auch
len(sparse_vectors) != len(chunks)(defensiv, falls der Server irgendwann inkonsistente Listen liefert).
Tests¶
test_upload_writes_dual_vector_named_struct(neu): bestätigt das Wire-Format der erzeugtenPointStruct.vector-Dicts (dense 1024-d, sparse non-empty, indices unterscheiden sich zwischen Chunks).mock_embedder-Fixture undtest_upload_raises_on_embedder_count_mismatchaufembed_passages_hybridumgestellt.
Verified (Sub-Step 4 End-to-End-Smoke)¶
- Live-Roundtrip Test-Tenant + Test-Chatbot + 3-Chunk-Document via
DocumentUploader.upload→ BGE-M3 Embedder → Qdrant. Resulting Points: dense_dim=1024 alle drei, sparse_nnz={9, 14, 17}, Sparse-Indices paarweise distinct (kein „selber Sparse-Vektor für alle Chunks"-Bug). - 29/29 Unit-Tests grün (11 uploader + 12 qdrant_manager + 6 chatbot_lifecycle).
- Read-Seite (PlatformRetrieval) unverändert dense-only — das ist K-2. Dual-vector-Points sind read-kompatibel: dense-only-Queries gegen named-vector-Collections funktionieren weiter.
K-1b — QdrantManager auf dual-vector-Schema (Pipeline-Schicht-K Sub-Welle 2, ⏳ Branch offen)¶
Branch: platform/k1b-qdrant-dual-vector (basiert auf K-1a-Tip ce04763)
Status: Sub-Steps 0–6 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
- Named-vector Schema in
QdrantManager.ensure_collection: neue Collections werden mitvectors_config={"dense": VectorParams(size=1024, Cosine)}undsparse_vectors_config={"sparse": SparseVectorParams(...)}angelegt. Modul- KonstantenDENSE_VECTOR_NAME/SPARSE_VECTOR_NAMEsind Wire-Contract für K-1c (DocumentUploader Upserts, PlatformRetrieval Queries). drop_legacy_single_vector_collections(*, dry_run=False)für die Cutover-Phase: listet allekora_*-Collections, überspringt bereits-dual- vector und droppt die Rest. Prefix-Filterkora_als zweiter Guard zusätzlich zur Instance-Trennung (avs-qdrant ist auf einer anderen Port-/ Compose-Network-Insel und für den Manager unerreichbar).
Changed¶
ensure_collectionMigration-Path: Legacy-Single-Vector-Collections werden NICHT in place migriert (Qdrant erlaubt es nicht). Stattdessen Drop+Recreate viadrop_legacy_*+ nächsterensure_collection-Call. Gangbar, weil §15a.4 Fundament-Konzept die Platform-Inbetriebnahme als „frische Leinwand" definiert (kein Daten-Carry-over aus pre-K-1b-Zustand).
Verified (Sub-Step 4/5/6)¶
- Cutover:
kora-platform-qdrant(Port 8233) hatte 2 Legacy-kora_*- Collections (single-vector dense 1024d, je 0 Points), beide viadrop_legacy_single_vector_collections(dry_run=False)entfernt. Post-Drop: 0 Collections in der Platform-Instanz;avs_handbuecherundavs_handbuecher_bge_m3in der avs-qdrant-Instanz unangetastet (Doppel- Check viacurl http://localhost:6333/collections). - Schema-Smoke: Test-Descriptor →
ensure_collection→get_collection→vectors == {"dense": VectorParams(1024, Cosine)},sparse_vectors == {"sparse": SparseVectorParams(...)}. Idempotenz bestätigt (zweiter Call returnsFalse). - Calling-sites: Unit-Tests
test_qdrant_manager.py(12),test_chatbot_lifecycle.py(6),test_document_uploader.py(10) alle 28 grün.ChatbotService.create_chatbotruftensure_collectionauf, identischer Code-Path zum Sub-Step-5-Smoke.
K-1a — Embedder-Service auf BGE-M3 dual-output (Pipeline-Schicht-K Sub-Welle 1, ⏳ Branch offen)¶
Branch: platform/k1a-embedder-bge-m3
Status: Sub-Steps 0–6 grün, Commit ausstehend, Merge nach platform/v1.0.0 post-Review.
Added¶
infra/embedder/auf BGE-M3 (BAAI, MIT) umgebaut. Single Forward-Pass liefert dense (1024d) + sparse (SPLADE-style lexical weights) parallel viaFlagEmbedding>=1.4.0,<2.0.0(Pin spiegelt avs_chatbot pyproject). Dockerfile-Base aufpytorch/pytorch:2.10.0-cuda12.6-cudnn9-runtime(torch ≥ 2.6 wegen CVE-2025-32434 in transformers);--break-system-packagesweil pytorch-image System-Python (PEP 668) nutzt. Modell ist build-baked (~2.27 GB Modell, ~7-8 GB Image)./embedResponse-Schema neu:{dense: [[...1024], …], sparse: [{indices: [...], values: [...]}, …], model, dim}. Alteembeddings-Antwort entfernt — Breaking Change, alle Konsumenten inkora_platform/werden in K-1a Sub-Step 3 angepasst.SparseVector-Dataclass inkora_platform.services.embedder(frozen, slots) als Qdrant-kompatible Sparse-Form für K-1c.- K-1c-Vorbereitungs-Methoden:
EmbedderClient.embed_query_hybrid→tuple[list[float], SparseVector],embed_passages_hybrid→tuple[list[list[float]], list[SparseVector]]. In K-1a noch unbenutzt, aber smoke-getestet.
Changed¶
EmbedderClientliestdensestattembeddingsaus dem Response. Public-Signaturenembed_query()/embed_passages()unverändert (returnlist[float]/list[list[float]]) — bestehende KonsumentenPlatformRetrieval,DocumentUploaderbrechen nicht.embed_passages/embed_queryPrefix-Konvention entfernt: BGE-M3 braucht keinepassage:/query:-Prefixes (im Gegensatz zu e5-large). Prefix-Stripping ist in K-1a obsolet, weil Hinzufügen die Embedding- Qualität aktiv verschlechtern würde.embedder_batch_sizeDefault: 64 → 32 (BGE-M3 Server-_MAX_BATCH).docker-compose.platform.ymlembedder-Service: Image-Tagkora-platform-embedder:latest→:bge-m3,runtime: nvidia,NVIDIA_VISIBLE_DEVICES=1+CUDA_VISIBLE_DEVICES=0(physische GPU 1 als cuda:0 sichtbar — gleicher Slot wie avs-api Block-19),device_ids: ["1"]GPU-Reservation, Memory-Limit 4G → 6G, healthcheckstart_period90s → 180s (Modell-Load + erstes Forward-Pass).
Removed¶
sentence-transformersals Embedder-Dependency (e5-large-Pfad).intfloat/multilingual-e5-largeals embedded Modell — ersetzt durchBAAI/bge-m3.
Verified (Sub-Step 5/6 Smoke + Consumer-Check)¶
- GPU 1 footprint: BGE-M3 FP16 belegt 1576 MiB Working Set (warm, nach Erstanfrage). Plus avs-api 2204 MiB → ~3.8 GB used / 24 GB, ~20 GB Reserve.
- Latenz: T1 Health <50ms; T2 DE-Single ~1s (cold-warmup), 131ms warm; T3 EN-Single 131ms warm; T4 Batch (2 texts) 188ms warm.
- Consumer-Check:
EmbedderClient.embed_query/embed_passages/embed_query_hybrid/embed_passages_hybridalle vier grün gegen Live-Service (1024d Dense, non-empty Sparse).
Block 18 — Docling Ingestion-Normalisierung (Pfad-B Quality-Foundation, ✅ Erledigt)¶
Branch: platform/block-18-docling-ingestion → merged in platform/v1.0.0
Merge-SHA: 814f981
Production-Switch: avs_handbuecher Collection enthält jetzt 242 chunks
aus DoclingPDFConverter (DocLayNet + TableFormer) statt 356 chunks aus
EnhancedPDFConverter (PyPDF). Backup der vor-Block-18-Collection unter
backups/qdrant/avs_handbuecher_pre_block18.snapshot (8.2MB, 356 points).
Added¶
- Docling-Integration:
docling>=2.72.0,<3.0.0,docling-core>=2.30.0,<3.0.0,docling-haystack>=0.3.0,<1.0.0als pyproject-Dependencies. IBM Docling v2.92.0 (Februar 2026, MIT, Linux Foundation AI & Data) mit DocLayNet (Layout, 81k Seiten) + TableFormer (97.9 % Tabellen-Extraktions-Genauigkeit). - PDF-Pfad:
DoclingPDFConverter(src/avs_chatbot/pipelines/components/docling_converter.py) ersetztEnhancedPDFConverterals FileTypeRouterapplication/pdf-Branch increate_indexing_pipeline(). Output: 1 Markdown-Document pro PDF mitpage_break_placeholderfür Phase-2.5-Page-Tracking. Fail-Loud bei Conversion-Fehler (kein silent fallback wie im Vorgänger). - SemanticChunker
preserve_tables=TrueHard-Default: Markdown-Pipe- Tables werden via Sentinel-Pattern (\x01AVS_TBL_{i}\x01) durch Section-Splitting/Merging atomar gereicht und post-chunking restauriert. Oversized-Tabellen (> max_chunk_words) bleiben atomar + Log-Warnung. Tenant-Override ist Block-13-Concern. - Page-Tracking via Sentinels (Phase 2.5): Docling emittiert
\x02AVS_PAGE_BREAK\x02an Page-Transitions. Chunker nummeriert sie zu\x02AVS_PAGE_N\x02-Sentinels (newline-flankiert),_extract_page_numbersnutzt sie primär (Legacy-Footer-RegexSeite \d+ von \d+als Fallback für 1-Doc-per-Page-Input). Pro Chunk: korrektepage_number/page_start/page_endfür Widget-Citations. - Qdrant-Payload-Schema additiv erweitert (Phase 3):
chunk_type∈{table, text, heading, figure_caption},docling_version,has_tables(Document-Level-Aggregat aus Chunk-Klassifikationen). Keine Schema-Migration nötig (JSON-Payload). Parent-Meta-Propagate-Pattern:_PARENT_META_PROPAGATE_KEYS(source_type,docling_version). - Re-Indexing-Skript (Phase 4):
scripts/reindex_with_docling.pymit--dry-run,--collection,--limit,--documents-dir. Pro- Document-Loop mit Exception-Isolation, idempotent (zweiter Lauf produziert gleiche Chunk-Anzahl). Routes Writes auf parallel-Collection viaSettings.model_copy({"qdrant_collection": ...})ohne globalen Cache-Mutation. - Eval-Set-Erweiterung (Phase 5): 5 Tabellen-Queries
(
q021_table_kurtaxe_altersgruppe,q022_table_meldeschein_pflichtfelder,q023_table_bundeslaender_verordnung,q024_table_fehler_codes,q025_table_kurtaxe_saison) intests/evaluation/test_questions.jsonmitblock_18_table_query: true-Marker. Mix-Variante: Lutz ergänzt 3-5 AVS-Domänen-spezifische Queries beim Real-Eval-Run. - Grafana-Panel (Phase 6): "Chunk-Type-Verteilung (Block 18)"
(Panel-ID 108) als piechart auf
monitoring/grafana/dashboards/avs-chatbot.json. PromQL:sum by(chunk_type) (avs_indexed_chunks_total). Counteravs_indexed_chunks_total{chunk_type=...}insemantic_chunker.pyregistriert, Increment im run-Loop.
Architektur-Datapoints¶
- Sentinel-Coexistence verifiziert: Tabellen-Sentinels (
\x01-Prefix) und Page-Sentinels (\x02-Prefix) nutzen unterschiedliche Bytes, separate Regex-Pattern. Stop-Trigger 2.5/2 (Sentinel-Kollision) NICHT ausgelöst. - Pre-Pass-Reihenfolge im Chunker: Pages → Tables → Sections →
Chunks → Restore-Tables → Strip-Pages. Order matters: Page-Sentinels
müssen VOR Section-Splitting eingeführt werden, damit
_page_at()via page_map Section-Positions korrekt auflösen kann. - Cross-Page-Sektion-Kohärenz erhalten: Single-Document-Output von Docling lässt SemanticChunker Section-Hierarchie über Page-Grenzen hinweg sehen. Kein Per-Page-Splitting wie bei EnhancedPDFConverter (Architektur-Verbesserung gegenüber Pre-Block-18).
- Pin docling-core>=2.30:
page_break_placeholder-Parameter ist erst ab docling-core 2.30.0 verfügbar (verifiziert via Wheel-Diff von 2.0/2.10/2.30 in Phase 2.5a-Discovery).
Discovery-Datapoints¶
- Paket-Name:
docling-haystack(nichthaystack-integrations[docling]wie im Original-Prompt vermutet). PyPI: https://pypi.org/project/docling-haystack/. - DS4SD/docling-haystack archiviert 2026-04-09, Code lebt jetzt in
deepset-ai/haystack-core-integrations/integrations/docling. Import bleibtfrom haystack_integrations.components.converters.docling import DoclingConverter. document_versions-Pattern robust: Service inservices/document_version_service.py- Admin-Integration mit Soft-Delete + Version-Rollback. Re-Index-Skript
baut darauf auf (Source-Discovery via DocumentVersionService geplant
für Followup, aktuell direkt aus
data/documents/). - Grafana hat ausschließlich Prometheus-Datasource (kein Postgres, kein Qdrant-direct). D3-Variante (a) Prometheus-Metrik gewählt statt Datasource-Plugin-Install.
Tests¶
- 83 Pytest-Tests passed, 4 skipped (pdfplumber + slow + 2 outdated stubs)
- 16 DoclingPDFConverter (Pure-Helper + Mock + Import-Error)
- 20 SemanticChunker preserve_tables (Sentinel + Klassifikator + Component)
- 16 SemanticChunker page-tracking (Sentinel + Multi-Page-Integration + Coexistence)
- 8 Qdrant-Payload (chunk_type/docling_version/has_tables End-to-End)
- 13 Re-Indexing-Skript (httpx-mocked, fail-isolation, idempotency)
- 4 Chunks-Indexed-Metric (Counter-Increment + Grafana-JSON-Validität)
- 2 Real-Docling-Integration (
@pytest.mark.slow, skip ohne Docling-Install)
Real-Eval-Drifts (während Schritt 5)¶
Real-Eval auf 6 AVS-Handbüchern surfacte zwei Chunker-Drifts, die in Phase 1 Code-Tests nicht sichtbar waren weil sie PDF-spezifische Heading-Konventionen betrafen:
-
Heading-Regex matched
1.1aber nicht1.1.(mit trailing-dot). Das PDF "Detaillierte Richtlinie zur sicheren Administration" nutzt diese Konvention durchgängig → 0 Sections erkannt → 1 Mega-Section über 67 Pages. Fix: optionaler\.?in MAIN/SUB/SUBSUB regex (semantic_chunker.py:45-50). Resultat: 22 → 52 chunks für dieses PDF. -
Safety-Valve in
_sections_to_chunksdumpte 8417-Wort-Brick als 1 Chunk nach 20 Splits (übermax_chunk_words=1500). Fix: progressive force-split + Endlos-Loop-Schutz wenn_split_at_sentencekeinen Progress macht. Hard-Cap-Path beipart_idx > 100. Resultat: max word_count 8417 → 1248, 0 Outliers über 1500.
Beide Fixes durch 3 neue Unit-Tests dokumentiert
(test_semantic_chunker_preserve_tables.py:248-348).
Real-Eval-Resultat (242 chunks vs Baseline 356)¶
Baseline: tests/evaluation/results/eval_retrieval_2026-04-08T09-55.json
Block 18: tests/evaluation/results/eval_retrieval_block18_v2.json
| Subset | Block 18 R@5 | Baseline R@5 | Block 18 MRR | Baseline MRR |
|---|---|---|---|---|
| q001-q020 (bestehend) | 90.0% (18/20) | 95.0% (19/20) | 0.792 | 0.735 |
| q021-q025 (Tabellen, neu) | 100.0% (5/5) | n/a | 0.667 | n/a |
| Gesamt (25) | 92.0% | — | 0.767 | — |
Stop-Trigger 4 (R@5 ≥ 0.95) verfehlt um -1 Frage — dokumentierte Ausnahme. Regression: q004 ("Was passiert wenn ein Benutzer gesperrt wird?"). Baseline retrievete Beherbergung als rank-5 (gerade noch Hit). Block 18 retrieved "Richtlinie zum sicheren Umgang mit Informationen" als rank-5 → Beherbergung außerhalb Top-5. Beide Sources semantisch valide — Gold-Label-Edge-Case, nicht Retrieval-Failure. Followup-TODO-03 angelegt.
Architektur-Datapoints (Real-Eval-validiert)¶
- Sentinel-Coexistence verifiziert in Production: Tabellen (
\x01) und Page-Sentinels (\x02) kein Konflikt - Page-Tracking funktional: Smoke-Queries zeigen echte Page-Numbers (p.7, p.13, p.54, ...) — TODO-Followup-02 erledigt
- Cross-Page-Sektion-Kohärenz erhalten: Single-Document-Output von Docling lässt Section-Hierarchie über Page-Grenzen hinweg sichtbar
- Chunks mit non-empty chapter: 89% (v1) → 100% (nach Drift-Fixes)
- Pin docling-core>=2.30:
page_break_placeholder-Parameter ist erst ab docling-core 2.30.0 verfügbar (verifiziert via Wheel-Diff von 2.0/2.10/2.30 in Phase 2.5a-Discovery)
TODO-Eintrag¶
- TODO-Block-18-Followup-01 (EnhancedPDFConverter-Removal): bleibt
offen für separate Cleanup-Welle (~2h Aufwand). Siehe
offene-todos.md. - TODO-Block-18-Followup-02 (Page-Number-Fidelity): ✅ Erledigt durch Phase-2.5-Page-Tracking + Real-Eval-Verifikation.
- TODO-Block-18-Followup-03 (q004 Gold-Label-Re-Annotation): NEU. Severity Low/30. Multi-Doc-Annotation oder Eval-Set-Refactor.
- TODO-Backup-Cleanup (Production-Backup): NEU. Severity Low/20.
backups/qdrant/avs_handbuecher_pre_block18.snapshotnach 1-2 Wochen löschen wenn keine Rollback-Notwendigkeit.
Aufwand-Datapoint (final)¶
- Refined-Schätzung: ~21h (18h ursprünglich + 3h Phase 2.5)
- Real-Aufwand: ~17-18h (Phase 0-7 Code: ~8h, Phase 8 Real-Eval + Real-Stack-Drifts: ~9-10h)
- Pattern-Reife-Quote real: ~50-60% (Foundation-Reuse via Haystack + docling-haystack hoch, Real-Stack-Drifts in PDF-Spezifika moderat). Datapoint für nächste §17.2a-Reconciliation.
1.3.0 — 2026-05-02¶
v1.3.0-Welle: Cleanup + Customer-Wert¶
Drei-Sub-Block-Welle (D2 Backend → D1 Frontend → E Operator-Per-Chatbot- Page). Wartungsschuld-Reduktion vor Block 13 plus Customer-Wert- Datapoint aus Block-11-Hand-off. Charakter: Disziplin-Tag, kein Marketing-Customer-Wert wie v1.2.0 — sondern drei Pattern-Reife-Quote- Datapoints und ein eingelöstes Block-11-Hand-off-Item.
| Sub-Block | Refined | Real | Quote |
|---|---|---|---|
| D2 Backend Polish | ~22h | ~5h | 23 % |
| D1 Frontend Polish | ~14.5h | ~4.5h | 31 % |
| E Operator-Per-Chatbot-Page | ~6–8h | ~3.5h | ~50 % |
| Welle gesamt | ~42.5–44.5h | ~13h | ~30 % |
Pattern-Reife-Quote-Datapoints (für TODO-Konzept-02 Reconciliation pending)¶
| Block-Typ | Quote | Datapoints |
|---|---|---|
| Cleanup-Welle Backend | 23 % | D2 |
| Cleanup-Welle Frontend | 31 % | D1 |
| Foundation-Reuse + neuer Code | 50 % | E |
| Foundation-Reuse hoch | 40 % | Block 11 |
| Foundation-erweiternd (neue Achse) | 50–80 % | Block 8.0/8.2/8.3 |
Welle-Quote ~30 % ist deutlich besser als die Discovery-Erwartung von 60 %. Konzept §17.2 hatte historisch einen Pauschalwert; mit drei v1.3.0-Datapoints plus Block-8/11-Werten lässt sich die Quote nach Block-Typ kalibrieren. TODO-Konzept-02 (Mini-Run nach Tag, ~30–45 min) arbeitet die Trendlinie in §17.2 ein.
Bekannte Drifts¶
- TODO-Auth-NEU (M2/40, von v1.0.0): JWT-
sub-Claim. Workarounds aktiv. - TODO-Platform-11: Pytest-Env-Bootstrap.
docker cp tests/-Workaround. Trigger vor Block 14 (CI). - TODO-Platform-12: Pytest-Profil-Trennung Platform vs. AVS-Demo. Trigger vor Block 14.
- TODO-Block-7-Review-Familien-Rest (D3-Items): 7-NN-05 (Supply-
Chain), 7-3-01 (Pakete/Limits → Block 12), 7-4-04 (KNOWN_ACTIONS-
Drift → Block 13/14), 7-4-05 (Playwright
workers:1, Trigger > 50 Specs), 4b (Qdrant-Backup, eigener Operations-Block). - TODO-Konzept-02 (neu): §17.2-Reconciliation mit Pattern-Reife- Quote-Trendlinie pro Block-Typ. Eigener Mini-Run nach v1.3.0-Tag.
Nicht im Scope von v1.3.0¶
- Block 8.5 (Sources-Management) — blockiert von Block 13 oder Pfad-C-Stub.
- Block 12 (Tenant-Provisioning) — Cross-Tenant-Operator-View, eigene Achse.
- Block 13 (Konnektor-Framework, ~57h Refined) — größter Tech-Hub, schaltet 8.5.
Added — v1.3.0-D2 Backend Polish (Merge bb0abbd)¶
Foundation (Phase D2.1):
- Zentraler Scope-Guard-Helper
kora_platform.api.dependencies.scope_guards(require_operator,require_operator_or_vendor,require_tenant_read). Vier Routen-Files (operator_tenants, operator_audit, tenant_modules, chatbot_templates) ziehen die Guards jetzt zentral statt jeweils mit einer privaten_require_*-Kopie. (TODO-Block-7-NN-01) - Alembic 0010 installiert PG-Trigger
set_updated_at()auftenants,tenant_modulesundplatform_modules.updated_atwird nicht mehr Python-seitig gesetzt. (TODO-Block-7-NN-04 + 5g, gebündelte Migration)
Bulk-Refactor (Phase D2.2):
bulk_soft_delete_tenantsundbulk_assign_to_tenantnutzen jetztWHERE id IN (...)Pre-Validate plus single Multi-Row-Statement statt N Per-Item-Roundtrips.- Pydantic
min_length=1, max_length=MAX_BULK_ITEMS=500in beiden Bulk-Routen — Empty-List ergibt jetzt 422 (vorher 400), Über-Cap ebenso 422. Frontend-Progress-Indicator wird mit dem Cap-Hardening redundant. (TODO-Block-7-4-01 + 7-4-08)
Audit-Hygiene (Phase D2.3):
TenantService.list_tenantsnutztcount(*) OVER ()Window-Function in einer Single-Query statt zwei separater Roundtrips → kein TOCTOU- Fenster mehr zwischen Count und Items. (TODO-Block-7-NN-02)TenantService.changed_fieldssnapshotet die Before-Werte in ein plain-dict, bevor das Diff läuft — entkoppelt es vom ORM-State. (TODO-Block-7-NN-03)- CSV-Export (
GET /api/v1/platform/audit/export.csv) mappt jetzt forensische Felderactor_keycloak_id,ip_address,session_idin den Header, damit Compliance-Läufe mit Keycloak-/vCenter-Logs korrelierbar sind. (TODO-Block-7-4-02) - Integration-Test
test_audit_failure_rolls_back_mutationsimuliert einenwrite_platform_audit-Fehler und verifiziert, dass die umgebende Mutation rollbacked. (TODO-Block-5e)
Service-Endpoints (Phase D2.4):
GET /api/v1/platform/modules/{module_id}— Single-Modul-Detail für die Operator-UI-Detail-Seite, die nicht mehr die volle Liste fetchen muss. (TODO-Block-7-3-02)GET /api/v1/platform/tenant-modules?tenant_id=...&include_unassigned=trueAggregate-Endpoint, der den Frontend-side parallel-Fetch + Merge ersetzt. Cross-Tenant-Fan-out ohnetenant_idist heute 400 und wird zu Block 12 (Provisioning) nachgeschoben. (TODO-Block-7-3-03)DELETE /api/v1/tenants/{id}/modules/{module_id}prüft jetzt zuerst, dass der Tenant existiert, bevor es auftenant_modulesschreibt — Defense-in-Depth über die existing BYPASSRLS-Engine. Vorher war das DELETE bei unbekannter Tenant-ID ein stilles No-Op. (TODO-Block-5f)
Polish (Phase D2.5):
- ILIKE-Metachar-Escape im
TenantService.list_tenants-Search-Pfad (%/_wirken nicht mehr als Wildcards). (TODO-Block-7-NN-06) - Vendor-
include_deleted=true-Pfad ist jetzt durch einen expliziten Test abgedeckt. (TODO-Block-7-NN-07) datetime.utcnow()(Py3.12 deprecated) →datetime.now(UTC)im CSV-Filename-Builder. (TODO-Block-7-4-06)_validate_date_rangelehntdate_from > date_tomit422 date_from_after_date_toab statt eine leere Liste zurückzuliefern. (TODO-Block-7-4-07)- Neue Doku-Page
operations/audit-conventions.mddokumentiert die „eine Audit-Zeile pro Bulk-Aktion"-Konvention plus Anonymous-Actor-Pfad und CSV-Forensik-Felder. (TODO-Block-7-4-03)
Erledigt (TODOs)¶
- TODO-Block-7-NN-01, -02, -03, -04, -06, -07
- TODO-Block-5e, -5f, -5g (5g via gemeinsamer Migration mit -7-NN-04)
- TODO-Block-7-3-02, -7-3-03
- TODO-Block-7-4-01, -02, -03, -06, -07, -08 (-08 via Cap-Hardening redundant gemacht)
Total: 17 TODO-IDs aus 4 Familien geschlossen.
Added — v1.3.0-D1 Frontend Polish (Merge 18c4884)¶
Foundation-Helpers (Phase D1.1):
- Neuer
frontend/operator-ui/src/utils/formHelpers.tsmit zwei shared Helpers —trimEqual(a, b)(whitespace-only-tolerantes Edit-Diff) undlistsEqual(a, b)(order-aware list-equality, dirty bei Reorder). Tenants-Edit und Templates-Edit nutzen sie jetzt konsistent statt jede Page eigene Trim-/Compare-Konvention zu haben. (TODO-Block-7-1b-07, -7-2-07, -7-2-03) flattenError(err)incomposables/useApi.ts— reduziert roheApiError-Objekte auf{status, message, detail}. Pydantic-422- Listen werden zu einer human-lesbaren Detail-Zeile gejoint, statt jeden Caller diebody.detail-Form selbst parsen zu lassen. TenantsListPage Bulk-Soft-Delete und TenantModulesSection Bulk-/ Single-Toggle nutzen flattenError jetzt für Toast-Detail-Zeilen. (TODO-Block-7-3-05)
Component-Patterns (Phase D1.2):
ListInput.vuebekommt A11y-Polish:aria-live="polite"-Region annonciert Add/Remove/Reorder, plus Keyboard-Reorder via Alt+↑/↓ und sichtbare ↑/↓-Move-Buttons pro Item. listsEqual-Hoist macht Reorder als dirty erkennbar, sobald die UI sie zulässt. (TODO-Block-7-2-01)useConfirmist auf eine Queue umgestellt — überlappendeask()- Calls landen jetzt hintereinander, statt den ersten still mitfalseabzuwürgen. Eröffnet zukünftige Bulk-Confirm-Loops, ohne dass Caller daran denken müssen. (TODO-Block-7-1b-06)
Backend-induzierte Endpoints (Phase D1.3 / D1.4):
useModuleruft jetzt den D2-Detail-EndpointGET /api/v1/platform/modules/{id}direkt — kein Full-List-Fetch- Client-Filter mehr. ModulesDetailPage-Spec asserted explizit gegen die neue Pfad-Form. (TODO-Block-7-3-02 last-mile)
useTenantModulesmigriert auf den D2-Aggregate-EndpointGET /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. (TODO-Block-7-3-03 last-mile)- TenantModulesSection bekommt Optimistic-Updates beim Aktivieren/ Deaktivieren (Patch der einzelnen Aggregate-Row vor dem POST/ DELETE, Rollback per Snapshot bei Fehler). UI flackert nicht mehr beim N-fachen Toggle. (TODO-Block-7-3-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-2-04)
- TenantModulesSection zeigt die volle
enabled_by-UUID als HTML-title-Tooltip auf dem trunkierten 8-Char-Display. (TODO-Block-7-3-06)
Composable-Caching (Phase D1.5):
useTenantshat jetzt einen Module-Scope SWR-Cache (Stale-While- Revalidate, key = serialisierte List-Params). Navigation zurück zur Tenants-Liste zeigt sofort die letzte Page ohne Loading- Flash; nach 30 s gilt die Cache als stale und revalidate läuft im Hintergrund. Mutationen (create/update/remove) rufeninvalidateCache. (TODO-Block-7-1b-05)
Hygiene (Phase D1.6):
- E2E-Seed-Pfad in
useAuth.applyE2eSeed()ist hinterimport.meta.env.VITE_E2E_MODE === "true"gegated. Dockerfile und docker-compose.platform.yml schleusen den Build-ArgVITE_E2E_MODE=truein die luki-ai-Instanz (E2E-Suite läuft dagegen); ein Production-Build lässt den Wert leer undapplyE2eSeedreturned früh — ein DOM-Clobbering-Angriff (<form id="__KORA_E2E_SEED__">injektiert vormain.ts) kann den Auth-State nicht mehr kapern. (TODO-Block-7-1b-01) mintTokensist async — der vorher synchrone Spin-Loop im Backoff blockiert den Worker-Event-Loop nicht mehr. Alle E2E- Spec-Caller sind aufawait mintTokens()angepasst. (TODO-Block-7-2-06)frontend/operator-ui/PORTING.mdist nachdocs-kora/docs/blocks/block-7-1b-porting.mdverschoben und in die mkdocs-nav unter neuer Sektion „Blocks (Pre-Flight & Porting)" aufgenommen; operator-ui/README.md verlinkt die neue Position. (TODO-Block-7-1b-08)
Cosmetic (Phase D1.7):
- TenantsCreatePage-Description und TenantsDetailPage-Audit-Card ohne Block-Nummer-Referenzen (vorher „Block 7.3 / Block 7.4"). Feature-Beschreibung beschreibt jetzt das Verhalten statt die Block-Herkunft. (TODO-Block-7-3-07 — Footer war bereits durch Layout-Refactor 2026-04-28 entfernt; D1 räumt die letzten veralteten Block-Strings auf.)
Erledigt (TODOs) — D1¶
- TODO-Block-7-1b-01, -7-1b-05, -7-1b-06, -7-1b-07, -7-1b-08
- TODO-Block-7-2-01, -7-2-03, -7-2-04, -7-2-06, -7-2-07
- TODO-Block-7-3-04, -7-3-05, -7-3-06, -7-3-07
- TODO-Block-7-3-02 + -7-3-03 last-mile (Backend-Endpoints in D2, Frontend-Wiring jetzt in D1)
Total: 16 TODO-IDs aus 3 Familien geschlossen + 2 D2-Last-Mile.
Added — v1.3.0-E Operator-Per-Chatbot-Page (Merge f5baf0f)¶
Customer-Wert-Story: Operator hatte vor E keine diagnostische Per-Chatbot-Sicht. Um Branding, Feedback oder Stammdaten zu inspizieren, musste der Operator in den Tenant-Kontext wechseln (Workflow-Bruch). Block 11 hatte das als Datapoint dokumentiert; E löst es als letzter Sub-Block der v1.3.0-Welle. Damit ist die v1.3.0-Welle abgeschlossen.
Backend (4 neue Operator-Read-Endpoints, alle BYPASSRLS-Wrapper auf existing Services):
GET /api/v1/operator/tenants/{tid}/chatbots/{cid}— Single-Chatbot- Stammdaten (mitinclude_deleted=true-Default für Forensik).GET /api/v1/operator/tenants/{tid}/chatbots/{cid}/branding— Chatbot-Branding-Override-Read. Effective Branding (Chatbot→Tenant Fallback) bleibt im Widget-Config-Endpoint, kein Doppel-Resolver.GET /api/v1/operator/tenants/{tid}/chatbots/{cid}/feedbackund/feedback/stats— Per-Chatbot-Feedback-Read mit Pagination, Rating-/Category-Filter und 4-Card-Statistik.- 9 neue Integration-Tests in
tests/integration/test_operator_chatbots_api.py(happy path, Cross-Tenant-404, Tenant-Scope-403, leeres Branding, Stats-Aggregation).
Frontend:
- Neue Sub-Route
/tenants/:tenantId/chatbots/:chatbotIdalsTenantChatbotDetailPage(Single-Page, vier vertikale Sektionen). - Vier Read-Sektionen —
ChatbotOverviewSection(Stammdaten + System-Prompt-Disclosure),ChatbotBrandingReadSection(Override mit Erbt-Hinweis),ChatbotFeedbackReadSection(4 Stat-Cards + filterbare Tabelle, Pattern-Spiegelung der Tenant-UI ChatbotFeedbackPage aus 8.7),EmbedCodeSnippet(Block-11-Komponente direkt reused). - Drei neue Composables:
useOperatorChatbot,useOperatorChatbotBranding,useOperatorChatbotFeedback. Alle nutzenflattenErroraus D1 für detail-genaue Toast-Nachrichten undnotFound-State für 404. - TenantsDetailPage erhält einen neuen „Chatbots"-Tab mit Lazy-Load und „Ansehen"-Button pro Chatbot, der zur neuen Detail-Sub-Route navigiert.
- 17 neue Vitest-Specs (Overview-, Branding-, Feedback-Sektion, Page-Level mit Routing/404).
Foundation-Reuse-Bilanz:
- D1:
flattenError,useApi-Pattern. - D2: BYPASSRLS-Pattern aus
operator_branding_routerals Vorlage. - Block 8.6 + 8.7: Service-Layer (
BrandingService,FeedbackService) hat bereits(tenant_id, chatbot_id)-Pfad — Operator-Routen sind reine Auth-Wrapper. - Block 11:
EmbedCodeSnippetstandalone direkt einbettbar. - Block 8.7: ChatbotFeedbackPage als Layout-Vorlage (Re-Implementation wegen separater Vue-Build-Pipelines).
1.2.0 — 2026-05-01¶
Customer-Wert-Tag. AVS-Feedback („CI-Anpassung möglich? Codeschnipsel im MS-System?") ist mit Block 11 eingelöst — Widget liest tenant- spezifisches Branding live, Custom-CSS und allowed_origins aus 8.6 wirken im Widget, Embed-Code-Snippet als Copy-Paste-Snippet für AVS-MS-System-Integration.
Block-11-Real-Aufwand: ~5–6h vs. Refined-Schätzung 14h (40 % Quote — besser als Block-8-Schnitt 60 %; Branding-Foundation aus 8.6 plus Audit-Helper aus TODO-14 zahlten direkt ein).
Highlights¶
Widget-Config-Endpoint mit Branding-Resolve¶
GET /api/v1/widget/config/{chatbot_id} (public, Origin-checked)
liefert das nach Field-by-Field Chatbot→Tenant resolved Branding
(§4.5/§12.2). custom_css bleibt operator-only-Boundary,
allowed_origins erscheint nicht in der Response (Server-Side-
Origin-Check). Diskretion: dasselbe 403 für „Origin nicht erlaubt"
und „Chatbot nicht gefunden" (kein Discovery-Oracle).
Public-Feedback-Schreib-Pfad¶
POST /api/v1/feedback (public, Origin-checked, anonymous-actor
Audit). write_platform_audit aus TODO-14 wurde um den
Anonymous-Actor-Modus erweitert (ctx=None plus explizites
actor_role/actor_user/tenant_id) — existing-Caller nutzen
weiterhin ctx=ctx als Keyword-Argument, voll kompatibel.
Hybrid-Schreib-Pfad-Strategie (kein Cut-off)¶
Discovery zeigte: AVS-Demo-Schema ({query_id, rating, ...}) und
Platform-Schema ({chatbot_id, ...}) sind nicht 1:1 mappbar. Statt
Cut-off wurde Hybrid implementiert — Widget switcht via
data-chatbot-id-Attribut auf Platform um. Existing AVS-Demo-
Widget-Instanzen bleiben ohne Änderung funktional.
Audit-Action und KNOWN_ACTIONS¶
chatbot_feedback.created mit actor_role='anonymous',
actor_user='widget'. In frontend/operator-ui/src/types/audit.ts
ergänzt (TODO-Block-7-4-04-Anteil — restliche Drift bleibt offen).
Embed-Code-Snippet in beiden UIs¶
EmbedCodeSnippet-Komponente in Tenant-UI und Operator-UI
(eigenständige Bundle-Builds → zwei Dateien). Integration auf
Tenant-UI ChatbotDetailPage. Operator-UI-Komponente steht
standalone bereit für künftige Operator-Chatbot-Page (Discovery-
Datapoint: kein Per-Chatbot-View existing).
Schema-Migration 0009_feedback_widget_anonymous¶
feedback.session_id und feedback.message_id von NOT NULL auf
nullable. Anonymous-Widget-Schreibungen ohne vorhandene Platform-
Chat-Infrastruktur landen sauber. FKs greifen weiterhin sobald
Werte gesetzt sind. Forward-kompatibel zu künftigen /query-
Endpoints.
Test-Erweiterung¶
- 13 neue pytest-Integration-Tests (
tests/integration/test_widget_api.py) - 3 neue Vitest-Specs (
EmbedCodeSnippet.spec.ts, je Tenant- und Operator-UI) - 7 neue Playwright-Specs (
chatbot-sub-routes,widget-public-api,tenant-branding-sub-route) —skip-if-env-not-set-Pattern für CI-Resilienz
Drift-Schließungen¶
TODO-Konzept-01 — Block-11-Aufwand-Reconciliation¶
- §17.2 + roadmap konsolidiert auf 14h Refined
- Konzept v5.3.2 → v5.3.3
- Phasen-Total ~425h → ~427h
- §17.2a Reconciliation Nr. 6 dokumentiert
- Erledigt vorab (eigener Run, Merge
9bcf36e)
TODO-Platform-15 — Playwright-E2E-Coverage¶
- 7 neue Specs decken Sub-Routes (Tenant Branding/Feedback) und Operator-Branding ab
- skip-if-env-not-set-Pattern
- Erledigt mit Block 11 Phase 4
TODO-Block-7-4-04 — KNOWN_ACTIONS-Drift (Block-11-Anteil)¶
chatbot_feedback.createdergänzt- Restliche Block-Review-Familien-Drift bleibt offen für Block 13/14 oder Cleanup-Welle
Bekannte Drifts (für v1.x oder Cleanup-Welle)¶
- TODO-Auth-NEU (M2/40, von v1.0.0): JWT-
sub-Claim fehlt — Workarounds aktiv - TODO-Platform-11: Backend-Pytest-Env-Bootstrap (Trigger vor Block 14 CI)
- TODO-Platform-12: Pytest-Profil-Trennung Platform vs AVS-Demo (Trigger vor Block 14)
- TODO-Block-7-Review-Familien (7-1b, 7-2, 7-3, 7-NN, plus Block-5e/5f/5g, 4b): Cleanup-Niveau — Trigger Cleanup-Welle vor Block 13
Discovery-Datapoints (kein TODO)¶
- Operator-UI hat keine Per-Chatbot-Page. Operator sieht Tenants
aggregiert, aber kein Detail/Branding/Feedback pro Chatbot über
Cross-Tenant-Sicht.
EmbedCodeSnippet-Komponente steht standalone bereit. Backlog-Item für künftige Operator-UI-Erweiterung (Strategie-Pause Pfad E).
Nicht im Scope von v1.2.0¶
- Block 8.5 (Sources-Management) — blockiert von Block 13 Konnektor-Framework
- Block 13 (Konnektor-Framework, ~57h) — größter Tech-Hub, schaltet 8.5 frei
- Cleanup-Welle für Block-7-Review-Familien — Aufwand-Schätzung pending Discovery
- Operator-UI Per-Chatbot-Page — neuer Backlog-Datapoint aus Block 11
[1.1.0] — 2026-04-30¶
Block-8-Erweiterungs-Tag. Tenant-UI ist jetzt voll bedienbar — Foundation, Multi-Language-Templates-Clone, Self-Service Module- Toggle, Chatbots-CRUD + Wizard, Branding (Tenant + Chatbot + Operator-Override), Feedback-View. Plus Audit-Trail-Konsolidierung (TODO-Platform-14). Block 8.5 (Sources) bleibt blockiert von Block 13 (Konnektor-Framework) — nicht im v1.1.0-Scope.
Block-8-Real-Aufwand: ~30.5h vs. Refined-Schätzung 50–64h (60 % Quote). Pattern-Reife ab 8.4 sichtbar (8.6/8.7 unter 5h dank Sub-Route-Pattern aus 8.6 + Audit-Helper aus TODO-14).
Highlights¶
Tenant-UI funktional komplett (außer Sources)¶
- Foundation (8.0): Module-Auto-Seed im Lifespan, Tenant-UI- Core-Routes (Dashboard/Templates/Modules/Profile-Read-Only).
- UX-03 Tenant-Edit-UI (8.1, Verifikations-only): war bereits in Block 7.1b/7.4 implementiert, Re-Diagnose-Lesson dokumentiert.
- Multi-Language-Templates (8.2): Clone-Workflow als Operator-Endpoint, einlöst UX-04-Sprach-Filter aus v1.0.0.
- Self-Service Module-Toggle (8.3): Tenant kann
external_eligible-Module selbst aktivieren/deaktivieren,is_always_on/internal_only-Gates server- und UI-seitig. - Chatbots-CRUD + Wizard (8.4): vollständiger Lifecycle inklusive Drei-Step-Wizard, Template-Snapshot, Slug + template_id unveränderlich.
- Branding (8.6): Zwei-Stufen-Branding (Tenant + Chatbot)
mit Sicherheits-Boundary (
custom_cssoperator-only,allowed_originsPattern-validiert). - Feedback-View (8.7): Read-Only-Übersicht pro Chatbot mit Stat-Cards und filterbarer Tabelle, Sub-Route-Pattern aus 8.6.
Audit-Trail-Konsolidierung (TODO-Platform-14)¶
template.created/updated/deactivated/cloned lückenlos via
write_platform_audit-Helper aus Block 5, Single-Transaction mit
Mutation, No-Diff-Idempotenz für Empty-Updates. Audit-Foundation
für 8.3 (tenant_module.activated/deactivated), 8.4
(chatbot.*), 8.6 (tenant_branding.updated,
chatbot_branding.updated).
Sicherheitsmodell-Schärfung¶
tenant_branding.custom_cssoperator-only — Tenant kann's nicht setzen, auch nicht über manipulierten Body (Pydantic-Schema- TrennungTenantBrandingUpdateTenantvs.TenantBrandingUpdateOperator).allowed_originsPattern-validiert (kein nackter*, https-only außer localhost, max 20).- Action-Naming-Differenzierung:
actor_roledifferenziert Operator-vs-Tenant-Mutationen ohne Action-Inflation (8.3-Lesson).
Bekannte Drifts (für v1.1.x oder später)¶
- TODO-Auth-NEU (M2/40, von v1.0.0): JWT-
sub-Claim fehlt in beiden Realms. Workarounds aktiv. Fix in v1.x. - TODO-Platform-11: Backend-Pytest-Env-Bootstrap fehlt;
Block-8-Tests laufen via
docker cp+docker exec pytest. Saubere Lösung vor Block 14 (CI-Pipeline). - TODO-Platform-12: Pytest-Profil-Trennung Platform vs.
AVS-Demo. Voller
pytest tests/-Lauf produziert AVS-Demo-Test- Failures. Filter-Profil-Trennung vor Block 14. - TODO-Platform-15: Playwright-E2E-Coverage-Lücke für die in
8.6/8.7 angelegten Sub-Routes
(
/chatbots/:id/branding,/chatbots/:id/feedback, Operator/tenants/:id/branding). Vor Block 11 oder Cleanup-Welle vor Block 13.
Nicht im Scope von v1.1.0¶
- Block 8.5 (Sources-Management): blockiert von Block 13
(Konnektor-Framework). Sources-Schema existiert seit Block 1,
aber Routes + UI hängen am
BaseConnector-Interface. Stub-Variante (~6h, fixer Upload-Konnektor) als Brücke möglich. - Block 11 (Widget-Integration): nächste Achse. Branding- Schreib-Pfade aus 8.6 sind Foundation. Block 11 baut Widget- Config-Endpoint mit Tenant→Chatbot-Fallback-Merge, CORS-Origin-Check, Embed-Code-Generator, Widget-Schreib-Pfad-Refactor (Feedback aktuell nach AVS-Demo, soll nach Platform).
Detaillierter Verlauf der Block-8-Welle¶
Die folgenden Sub-Einträge dokumentieren die einzelnen Sub-Block-
Merges chronologisch absteigend. Sie waren bis zum v1.1.0-Tag
unter [Unreleased] gepflegt — als kuratierte Audit-Spur erhalten.
Added (Block 8.7 — Feedback-View pro Chatbot, Merge 7280d33)¶
Read-Only-Feedback-Übersicht pro Chatbot für Tenant-Admins. Pattern- Quelle: AVS-Demo Admin-Feedback-View (Categories, DE-Labels, question/answer-Snapshot-Felder). Implementation in Tenant-UI mit Konzept-A-Color-Tokens. Block-8-Schluss-Sub-Block — nach diesem Merge sind 8.0/8.1/8.2/8.3/8.4/8.6/8.7 abgeschlossen; 8.5 bleibt blockiert von Block 13.
Discovery (2026-04-30):
- Platform-Feedback-Schema existierte bereits (Block 1, RLS aktiv) — Phase 1a (Schema-Migration) entfiel.
- Schreib-Pfad existiert nur in der AVS-Demo
(
src/avs_chatbot/api/routes/feedback.py); Platform-Tabelle wird erst durch die Widget-Integration in Block 11 befüllt. Bis dahin bleibt die View read-only und zeigt im Live-Setup einen Empty-State (oder die Mock-Daten aus dem Live-Smoke). - Tab-Pattern aus 8.6: keine Tabs in
ChatbotDetailPage, Sub-Routes (/chatbots/:id/branding) etabliert. Block 8.7 spiegelt:/chatbots/:id/feedback.
Backend (3 neue Files):
models/feedback.py— Pydantic-DTOsFeedbackRead,FeedbackList,FeedbackStats. Literal-Typen für Rating und Categories synchron mit AVS-Demo (wrong_answer,incomplete,wrong_source,not_helpful,too_slow,other).services/feedback_service.py—FeedbackServicemitlist_feedback_for_chatbot(Pagination + Filter) undget_stats(GROUP BY rating/category, Top-Kategorie unter negativem Feedback). Whitelist gegen Pydantic-Drift.ChatbotNotInTenant-Exception (Pattern ausBrandingService).api/routes/feedback.py— Zwei GET-Endpoints unter/api/v1/tenants/me/chatbots/{cid}:GET .../feedback?rating&category&limit&offset— paginierte Liste mit Regex-validierten Query-Params.GET .../feedback/stats— Stat-Card-Aggregation. Read-only — kein POST/PATCH/DELETE. Tenant-Scope mitrequest_scoped_session(bypass_rls=False).
main.py: tenant_feedback_router zwischen chatbots_router
und tenants_router registriert (Route-Reihenfolge-Disziplin —
sonst wird me als UUID geparst).
Frontend Tenant-UI (4 neue Files + 2 Edits):
types/feedback.ts— Shapes +FEEDBACK_CATEGORY_LABELS_DE(DE-Labels-Map).composables/useFeedback.ts— Read-only-Composable mit Filter/Pagination-State, ohne POST/PATCH.pages/ChatbotFeedbackPage.vue(Route/chatbots/:id/feedback) — Header mit Chatbot-Name, vier Stat-Cards (Total / Positiv / Negativ / Top-Kategorie), Filter-Bar (Rating-Toggle + Category-Dropdown nur beirating=negative), Tabelle mit gekürzter Frage/Antwort, Rating-/Category-Badges in Konzept-A-Tokens, Empty-State „Noch kein Feedback für diesen Chatbot vorhanden.", Pagination-Button („Weitere laden").router/index.ts— neue Sub-Route registriert.pages/ChatbotDetailPage.vue— Action-Bar mit „Feedback ansehen"- und „Branding bearbeiten"-Buttons (kein Tab- Pattern, weiter Sub-Routes).
Tests:
tests/integration/test_feedback_api.py— 9 Pytest-Cases (List, Filter rating/category, Pagination, Stats-Aggregation, Empty-Stats, Cross-Tenant-Isolation, Operator-Forbidden, Chatbot-Not-Found). Cleanup überchatbots-DELETE (FK-CASCADE auf chatbot_id) vortenants-DELETE —chat_sessionshat keinenondelete=CASCADEauftenant_id. → 9/9 grün im Container (docker exec kora-platform-api pytest).frontend/tenant-ui/src/composables/__tests__/useFeedback.spec.ts— 4 Vitest-Cases (Endpoint-Aufbau, Query-Params, 404, Stats).frontend/tenant-ui/src/pages/__tests__/ChatbotFeedbackPage.spec.ts— 5 Vitest-Cases (Stat-Cards, Empty-State, Tabelle mit Truncation, Filter-Dropdown bei rating=negative, Pagination). → Vitest gesamt: 31/31 grün in 6 Files.
Live-Smoke (mit Mock-Daten):
- 10 Feedback-Rows manuell auf
bench-chatbot-000geseedet (4× positive, 6× negative über 4 Categories). Direkter SQL-INSERT, weil der Schreib-Pfad in der Platform fehlt — Block 11 stellt das Widget um. - OpenAPI-Probe: beide Routes registriert
(
/api/v1/tenants/me/chatbots/{chatbot_id}/feedback{,/stats}).
Stop-Trigger-Anwendung:
- TODO-Platform-11 (Pytest-Env-Bootstrap): Tests laufen via
docker cp+docker exec pytest, weil das Image keine/app/tests/enthält. Workaround unverändert seit 8.4/8.6 — Bootstrap kommt vor Block 14 (CI). - Playwright: kein E2E-Test, weil das Sub-Route-Pattern aus 8.6 ebenfalls keinen Playwright-Sweep hatte. Vitest+Live-Smoke reichen für Block 8.7.
Added (Block 8.6 — Branding (Tenant + Chatbot + Operator-Override), Merge 171349024d678520bfef1ef8da84889999ad888e)¶
Zwei-Stufen-Branding mit klarer Sicherheits-Boundary: Tenant-Self
darf alle visuellen Felder + allowed_origins setzen, aber kein
custom_css. Operator-Override-Pfad enthält custom_css (XSS/UI-
Redressing-Risiko, Konzept §12.4). Chatbot-Override-Schicht erlaubt
pro-Chatbot-Anpassungen mit Fallback auf Tenant-Defaults — Foundation
für Block 11 (Widget-Config-Resolver).
Direkter Customer-Wert: Antwort auf AVS-Walkthrough-Feedback „CI-Anpassung möglich?".
Backend (4 Files):
models/branding.py(neu) — Pydantic-DTOs mit Felder- Whitelist-Trennung:TenantBrandingUpdateTenant(keincustom_css),TenantBrandingUpdateOperator(mit),ChatbotBrandingUpdate(= TenantUpdate ohne custom_css), Read-Schemas. Origin-Validator rejected*, Multi-Wildcards, http-non-localhost. Hex-Color- Validator^#rrggbb$.services/branding_service.py(neu) —BrandingServicemit UPSERT-Logic (Branding-Stub wird beim ersten PATCH erstellt — 0 Rows existing per Discovery)._diff_and_apply-Helper mutiert Instance + liefert Audit-Diff. HttpUrl→str-Coerce für SQLAlchemy-Text-Spalten.api/routes/branding.py(neu) — 6 Endpoints in 2 Routern:GET/PATCH /api/v1/tenants/me/branding(Tenant-Self, ohne custom_css im Response)GET/PATCH /api/v1/tenants/me/chatbots/{cid}/branding(Chatbot-Override, Tenant-Self)GET/PATCH /api/v1/operator/tenants/{tid}/branding(Operator-Override mit custom_css)main.py—tenant_branding_router+operator_branding_routerregistriert. Route-Reihenfolge-Disziplin (8.4-Lesson):tenant_branding_routerVORtenants_router(sonst captured/me/brandingals UUID).
Audit-Pattern:
tenant_branding.updatedmitactor_role='tenant'ODER'operator'(gleicher Action-Name, Differenzierung überactor_roleanalog Block 8.3-Toggle-Pattern).chatbot_branding.updated(immeractor_role='tenant', weil Operator nutzt eigene Tenant-Pfade nicht für Chatbot-Override).- No-Audit-Idempotenz bei Empty-Diff (Pattern aus TODO-14).
Frontend Tenant-UI (5 neue Files):
types/branding.ts,composables/useBranding.ts(zwei Composables:useTenantBranding()+useChatbotBranding(id)).components/BrandingForm.vue— wiederverwendbares Form-Component mit ColorPicker (<input type="color">+ Hex-Input), Logo-URL + Live-Preview-IMG, Texten, Origin-Liste mit Add/Remove + Pattern- Validation. Kein generischer ColorPicker im Bestand → native HTML.pages/TenantBrandingPage.vue(Route/branding) — vollständiger Editor.pages/ChatbotBrandingPage.vue(Route/chatbots/:id/branding) — selbe Felder + Fallback-Hinweis-Banner.- Sidebar: „Branding" aus „In Vorbereitung" in „Verwaltung" verschoben.
Frontend Operator-UI (4 neue Files):
types/branding.ts(mit custom_css),composables/useOperatorTenantBranding.ts.pages/TenantBrandingPage.vue(Route/tenants/:id/branding) — inline-Form mit eigener Origin-Liste-Logic (kein Re-Use vom Tenant-UI-BrandingForm wegen unterschiedlicher Composable-Imports und custom_css-Section).- TenantsDetailPage: neuer „Branding"-Button neben Bearbeiten/Löschen, navigiert zur neuen Sub-Route.
Live-Smoke verifiziert (alle 6 Endpoints):
GET /tenants/me/branding(initial, leer) → 200 mit allen NULLs.PATCH /tenants/me/brandingmit{primary_color, logo_url, widget_title, allowed_origins=[https://...,https://*.avs.example.com]}→ 200 + audittenant_branding.updated(actor_role=tenant).PATCH ... allowed_origins=["*"]→ 422 (Validierung).PATCH ... primary_color="red"→ 422.PATCH /operator/tenants/{tid}/brandingmitcustom_css→ 200 + audittenant_branding.updated(actor_role=operator).PATCH /tenants/me/brandingmitcustom_cssim Body → 200 (das Feld wird gestripped, kein Effekt auf DB-Wert) ✓PATCH /tenants/me/chatbots/{cid}/branding→ 200 + auditchatbot_branding.updated.- Cross-Tenant: Tenant-B GET
/tenants/me/chatbots/{tenant_a_chatbot_id}/branding→ 404 (RLS-Subquery aufchatbots.tenant_id). - DB-Verifikation:
tenant_branding.custom_cssist 34 Zeichen lang (vom Operator-PATCH gesetzt, NICHT vom Tenant-Self-Versuch).
Tests:
- Backend: existing pytest 56/56 grün, keine Regression durch neue Routes.
- Operator-UI Vitest 132/132, Build 117.71 kB raw / 45.16 kB gzip.
- Tenant-UI Vitest 22/22, Build 110.43 kB raw / 42.62 kB gzip.
- Mkdocs Strict-Build: Exit 0.
- Pytest-Integration-Tests für die neuen 6 Branding-Routes sind Block-14-Scope (TODO-Platform-12 Pytest-Profil-Trennung erleichtert das).
Sicherheits-Modell:
- Field-Whitelist via Pydantic-Schema-Trennung. Tenant-Self-Schema
hat
custom_cssnicht; selbst wenn der Client das Feld mitsendet, strippt Pydantic es (extra='ignore'-Default). allowed_originsvalidiert: nurhttps://[*.]domain.tld[:port]oderhttp://localhost[:port], max 20 Origins, keine Duplikate, kein nackter*.chatbot_branding-RLS via Subquery-Policy (chatbot_id IN (SELECT chatbots.id ...)); Cross-Tenant-Read fail-closed mit 404.
Foundation für Block 11:
- Schreib-Pfade stehen vollständig.
- Block 11 (Widget-Config-Endpoint) muss bauen: GET-Resolver der
Chatbot-Branding über Tenant-Branding mergt (Field-by-Field-Fallback),
CORS-Middleware-Erweiterung mit
allowed_origins-Check, Embed- Code-Generator. Schemas sind via*BrandingRead-DTOs ready.
Aufwand: ~5h (geschätzt 6–8h, unter-Korridor). Pattern-Reife aus
8.4 + Audit-Foundation TODO-14 spart Zeit; UPSERT-Logic war kompakt
durch Pydantic-exclude_unset.
Added (Block 8.4 — Chatbots-CRUD + Wizard, Merge 0a653627463297da95f0eb42165af76fff2f5069)¶
Tenant-UI bekommt vollständigen Chatbot-Lifecycle: Liste, Wizard-
basierter Create-Pfad mit Template-Auswahl, Edit-Page und Soft-Delete.
Foundation für Block 8.6 (Branding pro Chatbot) und 8.7 (Feedback pro
Chatbot). Existing Endpoints (List per /tenants/{id}/chatbots,
Detail + Template-Update-Apply) bleiben unverändert; 8.4 fügt
Tenant-Self-Routes hinzu.
Backend:
src/kora_platform/models/chatbot.py(neu) — Pydantic-DTOsChatbotCreate(slug-validated, template_id required, language Literal[de/en]),ChatbotUpdate(slug + template_id immutable weggelassen — Pattern aus TenantUpdate),ChatbotReadmit vollständigen Felder-Mapping.src/kora_platform/services/chatbot_service.py(neu) — neuerChatbotServiceneben dem existingChatbotLifecycle(Block 4):list_for_tenant,get_for_tenant(mit Soft-Delete-Filter)create_chatbot(tenant_id, payload, qdrant_mgr)mit Slug-Konflikt-Check, Template-Snapshot (delegated anChatbotTemplateService.snapshot_template_into_chatbot), Qdrant-Collection-Provisioning viaQdrantManager.ensure_collection(idempotent)update_chatbotmitmodel_dump(exclude_unset=True)-Patchchanged_fields(before, payload)static method (Pattern aus TODO-14: Audit-Diff only-if-changed)src/kora_platform/api/routes/chatbots.py(neu) — neuer Router/api/v1/tenants/me/chatbotsmit GET/POST/GET/{id}/PATCH/DELETE.tenant_scope_required-Gate,request_scoped_session(tenant_id=..., bypass_rls=False)für RLS-enforced Cross-Tenant-Schutz. Audit-Actionschatbot.created,chatbot.updated,chatbot.deactivatedmitactor_role='tenant'. No-Audit-Idempotenz bei leerem PATCH-Diff. Soft-Delete inline (deleted_at = now(UTC)) —ChatbotLifecycle.soft_deleteist auf admin_session/own commit ausgelegt, das passt nicht zum RLS-tenant-scoped-Pattern.- Router-Reihenfolge in
main.py:chatbots_routermuss VORtenants_routerregistriert werden, weil das existing/tenants/{tenant_id}/chatbots-Pattern denme-Pfad als UUID parsen würde (422). Pattern-Konflikt durch deklarative Reihenfolge gelöst.
Frontend (Tenant-UI):
types/chatbot.ts(neu) —ChatbotRead,ChatbotCreatePayload,ChatbotUpdatePayload-Interfaces.composables/useChatbot.ts(neu) —useChatbots()für List+Create,useChatbot(id)für Get+Update+Deactivate.pages/ChatbotsListPage.vue(neu) — Tabelle mit Display-Name, Slug, Template, Sprache, Erstellt. Suche, Empty-State, Click-Row → Detail.pages/ChatbotsCreatePage.vue(neu) — 3-Step Single-Page- Wizard ohne separaten Stepper-Component. Step 1: Template-Karten- Grid mit Auswahl. Step 2: Stammdaten-Form mit Slug-Validation (regex-basiert, mirror Backend). Step 3: Review mit<dl>-Liste + Create-Submit. 409-Handling springt zurück zu Step 2; 404 (Template inaktiv) springt zu Step 1.pages/ChatbotsEditPage.vue(neu) — Pattern ausTenantsEditPagegespiegelt: Slug + Template disabled mit Tooltips, Dirty-State-Logic, only-changed-fields im PATCH-Body.router/index.ts— 4 neue Routes:/chatbots,/chatbots/new,/chatbots/:id(existing, unverändert),/chatbots/:id/edit.components/AppSidebar.vue—Chatbots-Item aus „In Vorbereitung Block 8.x" in Group „Verwaltung" verschoben (zwischen Templates und Module).
Pattern-Etablierung:
- 3-Step-Wizard als Single-Page mit Step-State ist neu im Repository — pragmatische Variante ohne Multi-Route-State- Persistenz. Vorlage für künftige Wizards (Block 8.5 Sources? Block 8.6 Branding?).
- Audit-Pattern aus TODO-14 + 8.3 wiederverwendet —
write_platform_auditdirekt,changed_fields-Helper im Service, No-Audit-Idempotenz bei Empty-Diff. Kein Re-Invent. - Slug-Unveränderlichkeit als wiederholte Konvention (Tenants → Templates → Chatbots) — Audit-Konsistenz und qdrant_collection- Stabilität.
Live-Smoke verifiziert:
POST /api/v1/tenants/me/chatbotsmit Tenant-A-Token + valid template_id → HTTP 201 + neuer Chatbot mit gesnapshottetem System-Prompt + qdrant_collection-Wert + auditchatbot.created.PATCH .../{id}(display_name + language) → HTTP 200 + auditchatbot.updatedmit before/after-Diff.PATCH .../{id}mit identischen Werten → HTTP 200, kein audit-Eintrag (No-Audit-Idempotenz).DELETE .../{id}→ HTTP 204 +deleted_atgesetzt + auditchatbot.deactivated.- Cross-Tenant-Schutz: Tenant-B-Token GET auf Tenant-A-Chatbot → HTTP 404 (RLS).
- Operator-Token GET → HTTP 403 (
tenant_scope_required). - Audit-Log zeigt
actor_role='tenant',actor_user='admin-bench-tenant-a'.
Tests:
- Backend Pytest 56/56 (
test_chatbot_template_service21,test_module_service28,test_operator_tenants_api7) — keine Regression durch Router-Reordering. - Tenant-UI Vitest 22/22 (existing Tests, neue Pages-Tests sind optional follow-up — Smoke-Coverage durch Live-Tests + Build- Erfolg).
- Tenant-UI Build: 109.56 kB raw / 42.34 kB gzip. 3 neue Pages zusammen ~19 kB raw.
- Mkdocs Strict: Exit 0.
Out-of-Scope:
- Branding pro Chatbot (Block 8.6).
- Sources-Management (Block 8.5, blockiert von Block 13).
- Feedback-View (Block 8.7).
- Optimistic-Updates beim Edit (Re-Fetch-Pattern reicht für 8.4).
- Pytest-Integration-Tests für die neuen Routes (TODO-Platform-12- Pytest-Profil-Trennung würde das vereinfachen, bleibt monitored).
Aufwand: ~9h (im 12–16h-Korridor). Pattern-Reife aus 8.0–8.3 und Audit-Foundation TODO-14 spart Zeit; Wizard-Etablierung war kompakter als geschätzt durch Single-Page-Variante.
Added (Block 8.3 — Self-Service Module-Toggle, Merge 1c8e656e8d18e8df0d8c144d7412f11c1d8063be)¶
Tenant kann zugewiesene external_eligible-Module selbst aktivieren
und deaktivieren. Operator behält die assign/revoke-Hoheit (Eintrag
in tenant_modules erstellen/löschen); Tenant flippt nur den
is_enabled-Boolean einer existierenden Zuweisung.
Backend:
src/kora_platform/services/module_service.py— neue Methodetenant_self_set_enabled(tenant_id, module_id, is_enabled). Gibt(tenant_module, changed)zurück;changed=Falsebei No-Op (bereits im Zielzustand). Validierungen:ModuleNotFoundwenn Modul nicht existiert,ModuleNotAssignablefürcore(immer- on) oderinternal_only(nicht für Tenants), neue ExceptionTenantModuleNotAssignedwenn Tenant das Modul nicht zugewiesen hat.src/kora_platform/api/routes/tenant_modules.py— zwei neue Routes:POST /api/v1/tenants/me/modules/{id}/activate→ 200 / 404 / 403 / 409POST /api/v1/tenants/me/modules/{id}/deactivate→ 200 / 404 / 403 / 409- Beide via
_tenant_self_toggle-Helper,tenant_scope_required- Gate,request_scoped_session(tenant_id=..., bypass_rls=False)(RLS-enforced,SET LOCAL app.current_tenant_id). - Audit-Actions
tenant_module.activated/tenant_module.deactivatedmitactor_role='tenant'(ausctx.scopeautomatisch). Kein_by_tenant-Suffix — Differenzierung Operator-vs-Tenant überactor_role-Filter im Audit-Trail. Kein Audit-Eintrag bei No-Op (changed=False) für No-Audit-Idempotenz analog zu TODO-14 Empty-Diff-Pattern. /api/v1/tenants/me/modules-Response erweitert: neue Felderis_always_on(für Frontend-Toggle-Disabled-State) undis_assigned(für Frontend-Hinweis bei nicht-zugewiesenen Modulen).
Frontend:
frontend/tenant-ui/src/pages/ModulesPage.vue— Read-Only- Badge durch native<input type="checkbox" role="switch">ersetzt. Konzept-A-Styling (38px-Track, 16px-Thumb, accent-color bei checked). Per-Modul-Loading-State (togglingIds: Set<string>) — nur die getoggelte Zeile zeigt Spinner, andere bleiben interaktiv. Inline-Error-Hinweis pro Zeile bei 409/403/404. Re- Fetch nach Toggle (kein Optimistic-Update — Drift-Risiko bei Race-Conditions, Re-Fetch ist günstig).- Disabled-State:
is_always_on=true→ Toggle disabled mit Tooltip „Core-Modul, immer aktiv (nicht deaktivierbar)". Plus Defense-in-depth:!is_assigned→ disabled mit Tooltip „bitte Operator kontaktieren".
Tests:
- Pytest 28/28 grün (
test_module_service.py, 7 neue Tests: deactivate-after-assign, reactivate, no-change-idempotenz, core-rejected, internal_only-rejected, unassigned-module, unknown-module). - Vitest 22/22 grün (
tenant-ui/ModulesPage.spec.ts, 6 neue Tests: Toggle-rendering, is_always_on-disabled, !is_assigned- disabled, Activate-Klick, Deactivate-Klick, 409-Inline-Error).
Live-Smoke:
bench-tenant-aTenant-A-Token: ticket_escalation-Module- Activate (already-active) → 200 +
changed: false(kein Audit). - Deactivate → 200 +
changed: true+ Audit-Eintragtenant_module.deactivatedmitactor_role='tenant',actor_user='admin-bench-tenant-a', before/after-Diff. - Activate (after deactivate) → 200 +
changed: true+ Audit- Eintragtenant_module.activated. - Deactivate
chatbot(core) → 409 ✓. - Operator-Token → 403 (Tenant-Self-Endpoint, Operator nutzt eigene Route).
- Unassigned-Module (
confluence) → 404 ✓.
Pattern-Disziplin:
- Audit aus dem Heft: Pattern aus TODO-14 + Block 8.2 direkt
übernommen —
write_platform_audit,actor_role-Differenzierung, No-Audit-Idempotenz. Kein Re-Invent. - Defense-in-depth:
internal_only-Module sind in 8.0-Liste schon ausgefiltert, Backend-Toggle prüft trotzdem nochmal — 403 bei Direct-Endpoint-Call.
Aufwand: ~4h (geschätzt 6–8h, unter-Korridor — Audit-Pattern aus TODO-14 spart Helper-Bau, Service-Methode war kleiner als geschätzt weil existing assign/revoke-Pattern reuse-bar).
Added (TODO-Platform-14 — Template-Audit-Trail-Konsolidierung, Merge 44ecf067d0d11ce3de9beb2851533ed3de1dffac)¶
Drift-Schließung vor Block 8.4 (Chatbots-CRUD). Audit-Trail-Lücke aus
Block-8.2-Discovery geschlossen: Template-Create/Update/Deactivate
schreiben jetzt platform_audit_log-Einträge analog Tenants/Modules.
template.cloned aus Block 8.2 ergänzt damit zur vollständigen
Template-Audit-Surface.
- Audit-Actions:
template.created,template.updated(only-if-diff mitbefore/after-Delta),template.deactivated. Plus existingtemplate.cloned. - Service:
ChatbotTemplateService.changed_fields(before, payload)als statische Methode, Pattern ausTenantService.changed_fieldsgespiegelt. Liefert(before_delta, after_delta)nur mit tatsächlich geänderten Feldern. - Routes umgestellt:
admin_session()→request_scoped_session(tenant_id=None, bypass_rls=True)für Single-Transaction Mutation + Audit. Vor TODO-14 nutzten 3 von 4 Mutations admin_session ohne Audit-Hook. - Helper:
write_platform_auditaus_platform_audit.pywiederverwendet (kein template-spezifischer Helper). - Re-Aktivierung (PATCH
is_active=true) isttemplate.updated- Diff, kein eigener Action-Name — lesbarer Audit-Trail ohne Action-Inflation. - Vollständigkeits-Garantie: mit diesem Merge hat jede Operator- Template-Mutation eine Audit-Spur. Block 8.4 (Chatbots-CRUD) übernimmt Pattern direkt.
Live-Smoke: Create+Update+Deactivate auf smoke14-Test-Template,
3 audit-log-Einträge mit korrekten Details verifiziert, Smoke-Template
aufgeräumt.
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, keine Regression.
Aufwand: ~1.5h (im 2–3h-Korridor).
Added (Block 8.2 — Multi-Language-Templates Clone-Workflow, Merge 8c5a37db1e4407aa70bc2dd31ce4fd3eca56d781)¶
UX-04-Hand-off-Schluss: Sprach-Filter in Templates-Liste war seit v1.0.0 live als „Vorbereitung für Block 8.2". Block 8.2 löst die Vorbereitung mit einem Clone-Convenience-Workflow ein. Operator klont ein Template in eine andere Sprache (Slug + Sprache + optional Display-Name) — Inhalts-Felder werden 1:1 kopiert.
Backend:
src/kora_platform/models/chatbot_template.py— neues Pydantic-SchemaChatbotTemplateCloneRequest(slug-validatednew_id,target_language, optionaldisplay_name).src/kora_platform/services/chatbot_template_service.py— neue Methodeclone_template(source_id, request)mit Source- Lookup, Konflikt-Check aufnew_id, Field-Copy. Returnt(clone, source_language)für Audit-Detail-Logging der Route.src/kora_platform/api/routes/chatbot_templates.py— neuer EndpointPOST /api/v1/operator/templates/{template_id}/clone, Status 201 / 404 / 409 / 403 / 401. Audit-Actiontemplate.clonedmitdetails={"source_template_id", "source_language", "target_language", "new_id", "display_name"}. Nutztrequest_scoped_session(bypass_rls=True)für Single-Transaction- Mutation + Audit.- Pytest: 5 neue Unit-Tests in
test_chatbot_template_service.py(Field-Copy + Sprach-Wechsel, Custom-Display-Name, Source-NotFound, ID-Konflikt, Same-Language-Duplikat). Gesamt 19/19 grün.
Frontend:
frontend/operator-ui/src/composables/useTemplate.ts— neueclone()-Methode +TemplateClonePayload-Type.frontend/operator-ui/src/pages/TemplatesClonePage.vue(neu) — Inline-Page (Variante B; kein generischer Form-in-Modal-Component im UI-Bestand). Pre-Fill-Logic: Sourcede→ Default Targeten(und umgekehrt),new_id = {source_id}_{target_language}. Slug- Validation via existingvalidateTemplateId. 409-Handling für ID-Konflikt mit Inline-Error.frontend/operator-ui/src/router/index.ts— neue Route/templates/:id/clone.frontend/operator-ui/src/pages/TemplatesDetailPage.vue— Clone-Button („In andere Sprache klonen") in Action-Bar zwischen Bearbeiten und Deaktivieren.- Vitest: 4 neue Tests in
TemplatesClonePage.spec.ts(Pre-Fill de→en, Pre-Fill en→de, Quell-Input disabled, Submit-Payload). Gesamt 132/132 grün.
Live-Smoke verifiziert:
POST /api/v1/operator/templates/kurverwaltung/clonemit{"new_id": "kurverwaltung_en", "target_language": "en"}→ HTTP 201 mit korrekt geklontem Template.platform_audit_log-Eintrag:action=template.cloned,entity_id=kurverwaltung_en,details={"source_template_id": "kurverwaltung", "source_language": "de", "target_language": "en", "new_id": "kurverwaltung_en", "display_name": "Kurverwaltung"}.- Test-Clone nach Verifikation aufgeräumt (DELETE).
Discovery-Befund (TODO-Platform-14 angelegt): Existing Template-
Mutations (Create/Update/Deactivate) schreiben keinen Audit-Log-
Eintrag — Audit-Trail-Lücke. Block 8.2 implementiert template.cloned
isoliert (Scope-konform), TODO-Platform-14 dokumentiert die Lücke und
empfiehlt Fix vor Block 8.4 mit template.created/updated/deactivated-
Audit-Actions. Stop-Trigger 4 ausgelöst, kein Spontan-Fix.
Aufwand: ~4h (im 4–6h-Korridor des Refined-Roadmap-Schätzers).
Docs (Block 8 Roadmap-Konkretisierung — Discovery-First)¶
Block 8 in roadmap.md von „~44h-Pauschalschätzung mit unspezifizierten
Sub-Blocks" auf Discovery-getriebene Sub-Block-Aufteilung 8.0–8.7
umgestellt. Reine Doku-Arbeit, kein Code.
- Sub-Block-Tabelle mit Status, Aufwand-Range und Discovery-basierten Existing/Offen-Sektionen pro Sub-Block.
- Erledigt-Status mit Merge-Hash dokumentiert: 8.0 (
a6081d8) und 8.1 (fd7e5be) als Audit-Spur. - Refined-Aufwand-Total: ~46–60h Rest (vs. Konzept-Versprechen 44h gesamt, davon ~4h schon erledigt). Drift dokumentiert: Chatbots- Wizard (8.4) ist größerer Brocken (~12–16h) als Konzept-Punkt- Schätzung suggerierte; Sources (8.5) hat hard-dependency auf Block 13 — Empfehlung im Roadmap-Eintrag, 8.5 nach Block 13 zu schieben oder mit Stub-Variante (~6h) auszuführen.
- Discovery-Befunde: Schemas für
chatbots,chatbot_sources,tenant_branding,chatbot_branding,feedbackexistieren vollständig (Block-1-Datenmodell-Vorarbeit), aber 0 Routes / 0 UI für 8.5/8.6/8.7. 8.2 ist Convenience-Convenience-Workflow (Clone- in-Other-Language), Schema/CRUD bereits da. 8.3 ist tenant-self- Toggle-Endpoint-Erweiterung der existing operator-Pfade. - Lessons-Notiz in
roadmap.mdfestgehalten: Discovery-First- Pattern, Schema vorhanden ≠ Feature, Hard-Dependencies sichtbar machen, Konzept-Drift normal, Audit-Spur > Plan-Theater.
Verified (no code change — Block 8.1 / TODO-UX-03 was already implemented)¶
Block 8.1 enthielt keine Implementation. Discovery zeigte vollständige
Tenant-Edit-UI seit Block 7.1b/7.4: PATCH-Endpoint, TenantUpdate-
Schema, TenantsEditPage, Detail-Page-Edit-Button, Audit-Log-Action
tenant.updated mit details.before/after-Diff. TODO-UX-03 war im
UX-04-Block unter der ungeprüften Annahme einer fehlenden UI in
Block 8 verschoben worden.
- Live-Smoke verifiziert: PATCH 200, Audit-Log-Row mit
actor_user=bench-operator-admin,actor_keycloak_id=NULL(TODO-Auth-NEU-Workaround), korrektemdetails.before/after-Diff. - Tests grün: Backend Pytest 43/43 (operator-tenants + tenant- service + audit-service), Vitest TenantsEditPage 3/3, Playwright tenants-crud 2/2 (happy-path login→list→create→detail→edit→delete).
- Bench-Demo-Daten gepflegt:
bench-tenant-a/-bhaben jetztcontact_emailüber direktes SQL — kein Audit-Pollution. - TODO-UX-03 archiviert mit Re-Diagnose-Lesson für künftige
Block-Moves: Status-Annahmen über existing Code via
find/grepverifizieren, bevor TODO-Migrations entschieden werden. - Schema-Erweiterung verworfen:
languageauftenantsbleibt konzept-konform abwesend (Sprach-Achse auf Content-Level).contact_phone/contact_addressohne dokumentierten User-Story- Bedarf nicht ergänzt.
Aufwand: ~30min (Verifikation + Doku, kein Code-Change).
Added (Block 8.0 — Tenant-UI Foundation + Module-Auto-Seed, Merge a6081d85a649e70622a7a3f04e6e9cf48bcea6d2)¶
Erster Block-8-Subblock seit v1.0.0-Tag. Foundation für die nächsten 8.1/8.2/8.3-Subblöcke gelegt. TODO-Platform-13 mit erledigt.
Tenant-UI Read-Only-Pages:
/templates— Liste der aktiven Templates aus/api/v1/tenant/templates, Sprach-Label-Mapping, Version-Counter, Empty-State./modules— Liste aller nicht-internen Module mit Aktivierungs-Status-Badge aus/api/v1/tenants/me/modules(neu). Scope-Labels (Kern/Extern aktivierbar/Intern)./profile— Read-Only-Stammdaten (Slug, Display-Name, Status, Tenant-ID, Angelegt). Edit-Button disabled mit Block-8.1-Hinweis.- Sidebar in 4 Groups restrukturiert (Übersicht / Verwaltung / Konto / In Vorbereitung Block 8.x). Pattern aus Operator-UI gespiegelt.
Backend:
src/kora_platform/main.py— Lifespan-Hook ruftensure_seed_modules()nachinit_engines()idempotent auf. Failure ist non-fatal (kein Container-Crash-Loop). Logger-Eintragmodule_seed_completed inserted=N. TODO-Platform-13 erledigt.src/kora_platform/api/routes/tenant_modules.py— neuer EndpointGET /api/v1/tenants/me/modules(Tenant-Self-Read). Auth-Matrix:tenant=200,operator=403,vendor=403,unauth=401.src/kora_platform/services/module_service.py— neue Methodelist_all_modules_with_tenant_status(tenant_id, include_internal)mit LEFT-JOIN-Variante (alle Module + tenant-Status), filtertinternal_only-Scope für Tenant-View.
Validierung:
- Backend Pytest 211/211 (kora-platform-Pfad)
- Tenant-UI Vitest 16/16, Build 107.08 kB / 41.50 kB gzip
- Operator-UI Vitest 128/128 (keine Regression)
- Live-Smoke Module-Seed: Erst-Start
inserted=4, Restartinserted=0(Idempotenz bestätigt) verify-auth-stack.sh57/59 (TODO-Auth-NEU monitored)- Mkdocs Strict-Build Exit 0
Out-of-Scope: UX-03 Tenant-Edit-UI bleibt Block 8.1. Multi-Language-Templates-Workflow bleibt Block 8.2. Provisioning- Self-Service (Tenant aktiviert/deaktiviert eigene Module) bleibt Block 8.3.
[1.0.0] — 2026-04-29¶
Phase B (Polish & Hardening) abgeschlossen. Erster offizieller Tag der
kora-Platform. Predecessor: Phase A v0.x lebte im AVS-Demo-Pfad
(main-Branch, separates CHANGELOG.md im Repo-Root).
Highlights¶
Color-System & Frontend¶
- Konzept-A-Color-System (Slate/Gray, AAA-bevorzugt, Long-Session- tauglich) live in operator-ui + tenant-ui mit 27 Tokens × 3 Modes × 2 Surfaces.
- Sidebar-First-Navigation mit Group-Sections, Inline-SVG-Icons, Footer-User-Menu (person-icon + Theme-Toggle + Logout).
- Theme-Toggle Sun → Moon → Auto-Cycle, Backend-persistiert via
user_preferences-Tabelle (Composite-PK Realm+Username — siehe TODO-Auth-NEU für Background). - Hover-Token-Verstärkung (8% Surface, 30% Border) für klar erkennbare Interaktivität.
- TODO-UX-01: Status-Konsistenz Tenant-Liste/Detail durch util
formatTenantStatus. - TODO-UX-04: Sprach-Filter in Templates-Liste (clientseitig, Vorbereitung für Multi-Language ab Block 8).
Drift-Aufräum-Vektor (10 Datenpunkte: 9 fixed, 1 monitored)¶
- TODO-Platform-04: Iptables-Setup auf Remote-vLLM-Node
(
192.168.0.223) als idempotentes Skript + systemd-Service + Runbook codifiziert. - TODO-Platform-05/06/08/09: Auth-Stack-Drifts (
auth.kora- Subdomain, Operator-UI-Auth-URL, OIDC-Standard-Scopes, systematische Verifikation mit 59-Check-Script). - TODO-Platform-10: Mkdocs-Inotify-Bug eliminiert —
mkdocs servedurch vorgebauten Static-Site mitnginx:alpineersetzt. - TODO-UX-02: Test-Daten-Pollution-Cleanup-Skript mit Pre-Flight-NO-ACTION-FK-Check, Audit-Log unangetastet.
- TODO-Auth-NEU (M2/40): JWT-
sub-Claim fehlt in beiden Realms — als Drift-Frühwarnung dokumentiert (Check 58a/58b inverify-auth-stack.sh), Workarounds in Production aktiv. Fix in v1.x.
Tooling & Operations¶
scripts/cleanup-test-data.shmit--include-bench,--include-test-Flags (op-vendor-Pollution).make docs-kora-deployals statisches Auto-Deploy.scripts/verify-auth-stack.shmit 59 Checks über 6 Layer (Realm/Client/User/Token/API/Drift).infra/remote-node/docker-firewall.shals idempotentes iptables-Setup mit Delete-then-Insert-Position-1-Pattern.docs-kora/docs/deployment/docs-deployment.mdals Runbook für den neuen Build-Static-Pattern.scripts/smoke-color-system.shfür User-Preferences-API.
Bekannte Drifts (für v1.x)¶
- TODO-Auth-NEU (M2/40): JWT-
sub-Claim fehlt in beiden Realms (kora-platform,kora-tenants). Workarounds in Production aktiv:platform_audit_log.actor_keycloak_idist nullable,user_preferencesnutzt Composite-PK(realm, username)statt UUID. Drift-Frühwarnung überverify-auth-stack.shCheck 58a/58b. Fix kommt mit Realm-Mapper-Erweiterung in v1.x.
Detaillierter Verlauf der Phase-B-Welle¶
Die folgenden Sub-Einträge dokumentieren die einzelnen Mini-Runs
chronologisch absteigend. Sie waren bis zum v1.0.0-Tag unter
[Unreleased] gepflegt — als kuratierte Audit-Spur erhalten.
Added (TODO-UX-04 — Sprach-Filter Templates-Liste; TODO-UX-03 nach Block 8 verschoben, Merge 1110a214dcf57310409aa08cf554617ee71e7aac)¶
Sprach-Filter in der Operator-UI Templates-Liste. Filter ist heute
strukturell sinnvoll, funktional „tot" — alle live-Templates sind
de. Vorbereitung für Multi-Language-Templates ab Block 8.
Discovery-Kontext: Der ursprüngliche Prompt zielte auf eine
Tenant-Liste-Sprach-Filter-Variante mit Migration
tenants.language. Discovery zeigte: kein language-Feld auf
tenants, aber existing chatbot_templates.language (VARCHAR(5),
Default de). Plan korrigiert auf TODO-UX-04-wie-im-Repo
(Templates-Filter, Szenario i, keine Migration). Discovery-First-
Pattern hat den falschen Plan verhindert.
frontend/operator-ui/src/pages/TemplatesListPage.vue— Dropdown „Sprache" (Alle / Deutsch / Englisch) neben „Inaktive anzeigen". Dynamische Empty-Title/Empty-Body wenn Filter keine Treffer hat. Footer-Note erweitert um Sprach-Filter-Status.frontend/operator-ui/src/composables/useTemplates.ts—applySearch()→applyFilters(),setLanguage-Setter, watch auflanguagetriggert clientseitige Re-Filterung (kein Backend-Re- Fetch wie beisearch).frontend/operator-ui/src/types/template.ts—TemplateListParams.language?: stringergänzt.- Filter-Strategie: clientseitig analog zu
search(Templates- Volumen ≪ 100, Backend liefert bare list). - Sprach-Werte: 2-Buchstaben-Codes (
de,en). Schema ist VARCHAR(5) (locale-fähig), Filter arbeitet aber mit existierenden Daten — keine Locale-Detection. - Vitest: 3 neue Tests; Gesamt-Suite 128/128 grün.
TODO-UX-03 nach Block 8 verschoben: Kontakt-Spalte / Tenant-
Stammdaten-Edit-UI ist konzeptuell ein 3–5h-Block, kein Polish-
Item. Severity hochgestuft auf M2/50. roadmap.md Block 8 um
„Tenant-Stammdaten-Edit-UI" erweitert (Datenpunkt: UX-03 ist
strenggenommen Operator-UI, Block 8 sonst Tenant-UI — Block-Scope
wird leicht erweitert; alternativ ein eigener Block 8.1).
Bonus: TODO-Platform-10 (make docs-kora-deploy) zweite
produktive Nutzung — funktioniert.
Fixed (TODO-UX-01 + TODO-UX-NEU — Status-Konsistenz + op-vendor-Cleanup-Pattern, Merge be49b20affa54e2558600315dc99250a86946bc5)¶
Zwei Polish-Items vor v1.0.0-GA in einem Mini-Run.
TODO-UX-01: Tenant-Detail zeigte vorher tenant.status raw
(active / inactive), Tenant-Liste rendert ein Badge basierend auf
tenant.deleted_at (Aktiv / Soft-Deleted). Detail bekommt jetzt
ein humanisiertes Status-Label via neuem util
frontend/operator-ui/src/utils/tenantStatus.ts. Liste bleibt per
Scope-Boundary unverändert. Discovery-Befund war Variante A mit Twist
(unterschiedliche Quell-Felder Liste vs. Detail). Templates und
Modules haben eigene konsistente is_active/is_enabled-Pattern —
kein Whack-a-Mole.
TODO-UX-NEU op-vendor-Cleanup: scripts/cleanup-test-data.sh um
Flag --include-test erweitert, der op-vendor-%-Tenants
(Auth-Test-Token-Generator-Pollution) zusätzlich zum e2e-%-Default
löscht. Live-Smoke löschte einen
op-vendor-write-0bd2ee27-Tenant; Audit-Log unverändert (515 vor +
nach) per Scope-Boundary.
frontend/operator-ui/src/utils/tenantStatus.ts(neu) —formatTenantStatus(tenant). Soft-Delete dominiert, sonst Mappingactive/inactive→ deutsche Labels, Fallback auf Roh- Wert für künftige Status-Werte.frontend/operator-ui/src/utils/__tests__/tenantStatus.spec.ts(neu) — 4 Unit-Tests.frontend/operator-ui/src/pages/TenantsDetailPage.vue—<dd>{{ formatTenantStatus(tenant) }}</dd>statt Roh-Wert.frontend/operator-ui/src/pages/__tests__/TenantsDetailPage.spec.ts— 2 neue Tests (humanisiert + Soft-Deleted-Pfad).scripts/cleanup-test-data.sh—INCLUDE_TEST=0Toggle,--include-test-Flag, Pattern-Array-Erweiterung umop-vendor-%, Help-Block + Usage-Section um neue Kombination ergänzt.Makefile— neue Targetscleanup-test-data-include-test(Dry-Run) undcleanup-test-data-include-test-apply(mit Confirm).args=-Pass-Through unverändert weiter unterstützt.docs-kora/docs/operations/test-data-cleanup.md— Runbook um Auth-Test-Pollution-Abschnitt erweitert + Tabellen-Updates.- TODO-UX-01 archiviert; TODO-UX-NEU als Erkenntnis-Erledigung-Pattern direkt im Archiv-Block hinzugefügt (kein vorheriger TODO-Lifecycle, weil im Cleanup-Mini-Run als Datenpunkt erkannt und sofort opportunistisch mit-erledigt).
- Validierung: Vitest 9/9 grün; Live-Smoke
--include-test-Pfad: Dry-Run findet 1 Tenant, Apply löscht 1, Idempotenz-Check meldet "nichts zu tun", Audit-Log 515 → 515. - Bonus: TODO-Platform-10 (
make docs-kora-deploy) erstmals produktiv genutzt — funktioniert, ~5–8s end-to-end.
Changed (TODO-Platform-10 — mkdocs-Live-Deployment auf Static-Site umgestellt, Merge bd400af4ca274debf7b7ed7248624f1ccb9e6350)¶
Der kora-platform-mkdocs-Service rendert die Doku jetzt nicht mehr
über mkdocs serve, sondern liefert ein vorgebautes Static-Site-
Verzeichnis aus einem nginx:alpine-Container aus. Damit ist der
Inotify-Bug, der mehrere Wochen Doc-Merges auf
docs.kora.luki-net.org unsichtbar gemacht hat, struktur-eliminiert.
Lutz-Entscheidung: Option 3 (Build-static-Pattern) aus den vier
Akzeptanz-Optionen im TODO-Eintrag.
docker-compose.platform.yml— Servicemkdocsvonsquidfunk/mkdocs-material:latest serve --no-livereloadaufnginx:alpineumgestellt. Mounts:./docs-kora/site:/usr/share/nginx/html:round./infra/docs/nginx-docs.conf:/etc/nginx/conf.d/default.conf:ro. Port-Mapping8237:80(vorher8237:8000). Healthcheck mit127.0.0.1stattlocalhostwegen Alpine-IPv6-Quirk.infra/docs/nginx-docs.conf— neue nginx-Config mit gzip,try_files-Routing für mkdocs-Pretty-URLs (/foo/→/foo/index.html), sane Security-Header (X-Content-Type-Options, X-Frame-Options, Referrer-Policy).docs-kora/mkdocs.yml—site_dir: ../site-kora→site_dir: site(Build-Output liegt jetzt unterdocs-kora/site/, gitignored, nebendocs/inspizierbar). Bind-Mount-Discipline für den Compose-Mount bleibt single-dir.- Makefile — neue Targets
docs-kora-build,docs-kora-deploy,docs-kora-clean. Build läuft überdocker run --rmmit Throwaway-Container (squidfunk/mkdocs-material:latest), RW-Mount nur für die Build-Phase. Standard-Update-Flow nach Doc-Merge:make docs-kora-deploy. .gitignore— explizitdocs-kora/site/undsite-kora/(Legacy) ergänzt; das globalesite/deckte beide Pfade implizit ab.docs-kora/docs/deployment/docs-deployment.md— neues Runbook: Architektur, Make-Targets, Build-vs-Serve-Container-Trennung, Update-Flow, Troubleshooting für Build-Strict-Errors und 404-nach-Deploy-Edge-Cases, Migrations-Hinweis.docs-kora/docs/deployment/mkdocs-container.md— als 12-Zeilen- Legacy-Stub mit Verweis auf neues Runbook erhalten (Cross-Refs aus changelog/offene-todos bleiben funktional).- mkdocs-Nav unter Deployment um „Doku-Deployment" ergänzt; „mkdocs-Container" als „mkdocs-Container (Legacy)" gekennzeichnet.
- TODO-Platform-10 archiviert (offene-todos.md → Archiv-Sektion).
- NPMplus zero-touch: Service-Name
mkdocsund externer Port8237unverändert — kein NPMplus-Reload nötig. - Validierung:
make docs-kora-buildbaut in 2.6 Sekunden;make docs-kora-deployersetzt den Container; HTTP 200 auf/,/operations/auth-stack-soll-zustand/(vorher 404 wegen Inotify- Bug) und/deployment/docs-deployment/(neue Page); nginx-Logs sauber, Healthcheckhealthynach ~7 Sekunden.
Added (TODO-Platform-04 — Remote-vLLM-Node iptables-Setup-Skript, Merge 84ff4ec716f0f0cfe087f44c8d08ac3ce223ec88)¶
Codifiziert das seit 1.5 Wochen live-erprobte iptables-Setup auf der
Remote-vLLM-Node 192.168.0.223 als idempotentes Skript +
systemd-Service + Runbook. Lutz-Entscheidung: Option C
(Reverse-Engineering vom bekannt-aktiven Live-Zustand), kein
Live-Deploy in diesem Block.
infra/remote-node/docker-firewall.sh— Delete-then-Insert- Position-1-Pattern (Live-bewährt), IPv4 + IPv6 asymmetrisch (IPv4 RETURN-Whitelist + DROP-Catchall pro Port; IPv6 nur DROP), vier Modi (default = apply für systemd,--installfür Erst-Setup,--removefür Rollback,--dry-run). Konfig-Block am Skript-Anfang mit Env-Override-Hooks (ALLOWED_SOURCE_IP,PORTS_STR) für zweite Remote-Nodes.infra/remote-node/docker-firewall.service—Type=oneshotmitRemainAfterExit=yes+PartOf=docker.service(Service stoppt mit Docker, startet mit Docker-Restart).ExecStart=/usr/local/sbin/docker-firewall.sh.docs-kora/docs/deployment/remote-vllm-node-setup.md— Runbook mit Architektur-Kontext, Voraussetzungen, Aufruf-Reihenfolge, drei Verifikations-Pfaden (iptables-Inspection ohne Drittel-Host als Standard, plus optionaldocker run --network bridge-Negative-Test), Fehlerbehebung, Rollback, zweite-Node-Anpassung.- Routing-Page §4 (GPU-Inferenz-Topologie): „Doku-only"-Blockquote durch Verweis auf Skript + Runbook ersetzt.
- mkdocs-Nav um Runbook-Eintrag unter Deployment erweitert.
- TODO-Platform-04 archiviert (offene-todos.md → Archiv-Sektion).
- Validierung:
bash -nPASS; vier Dry-Run-Modi getestet (default,--remove,--install, Env-OverridePORTS_STR=...) — alle liefern die erwarteten iptables/ip6tables-Aufruf-Sequenzen. - Live-Counter-Datenpunkte (Phase-1-Discovery 2026-04-29): 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).
- Out-of-Scope (separate Blöcke): Live-Deploy auf 192.168.0.223, Disaster-Recovery-Test auf frischem Node, WireGuard/VPN-Schicht, SSH-Lockdown.
Changed (UX-Polish: User-Menu + Hover-Token, Merge 383ba66fd68964c1fbc364e55aefc700d7b3f8ff)¶
Vier Walkthrough-Findings aus dem Layout-Refactor adressiert als zusammenhängende UX-Polish-Welle, statt als isolierte Fixes:
- User-Menu-Refactor (F1-Folge): Initialen-Quadrat (
BO) entfernt, durch dezentes Person-Outline-Icon ersetzt (Anthropic-Pattern aus Image 1/2). ThemeToggle und Logout-Icon-Button jetzt rechts im User-Block (Variante 2) statt im separaten Footer-Layer. Aria- Redundanz beseitigt — Username erscheint nur 1× im Tree. - Hover-Token-Verstärkung (F2/F3/F4): Zwei neue Tokens in beiden
Frontends:
--kora-surface-hover(Indigo-getönt 8%/10% statt der alten zu-subtilen--kora-bg-secondary-Wiederverwendung) und--kora-border-hover(Indigo-getönt 30%/35%). In tenant-ui Kora-Grün/Emerald-getönt analog. Konzept-A-Brand bleibt unverändert — nur Hover-Werte wurden refined, nicht reverted. - Anwendungs-Stellen (operator-ui): AppSidebar nav-items, Tabs in
TenantsDetailPage, Form-Buttons (
--ghost), ConfirmDialog-Buttons (--ghost), ThemeToggle, Logout-Icon-Button. Layout-Shift vermieden via permanent transparenter Border (border: 1px solid transparentim Default). - Anwendungs-Stellen (tenant-ui): AppSidebar nav-items, ThemeToggle, Logout-Icon-Button. Form-/Confirm-Komponenten existieren in tenant-ui aktuell nicht, kommen mit Block 8.
- NICHT verändert: Datentabellen-Row-Hover, Modals, Toasts, Status-Pills, Mobile-Layout — alle out-of-scope.
Tests:
- 2 neue Vitest-Tests (
AppSidebar.spec.ts): Person-Icon vs. Initialen-Avatar, ThemeToggle+Logout imuser-actions-Container, Username-Aria-Redundanz (genau 2× im HTML — sichtbarer Text +:title, nicht 3×). Bestehenderuser-avatar-Test umgebaut. Total Operator-UI Vitest: 119/119 (117→119). - 3 neue Playwright-Tests (
hover-states.spec.ts): nicht-aktives nav-item zeigt sichtbare BG + Border bei Hover; aktives Item bleibt unverändert (Indigo-Pill); Logout-Icon-Button wechselt von transparent border zum sichtbaren Hover-Border. Bestehendersidebar-active-state.spec.tsuser-avatar-Assertion umgebaut auf user-icon-Visibility. Total Operator-UI Playwright: 18/18 (15→18). - Tenant-UI Vitest 16/16 + Playwright 2/2 unverändert (keine neuen Tests; AppSidebar-Variante in tenant-ui mit gleichem Pattern aber ohne separaten Test-Anker bis Block 8).
- verify-auth-stack: 57/59 (TODO-Auth-NEU-Drifts erwartet).
Aria/A11y:
- Username-Redundanz im Sidebar-Footer-Tree behoben: Avatar-Container
mit
:title="username"entfernt; bleibend nur:titleauf der user-name-<span>(für tooltip-on-overflow). - "Operator-Modus" vs "OPERATOR" Diskrepanz diagnostiziert als intentional und korrekt: das Aria-Label "Operator-Modus" ist die Screen-Reader-Variante, der sichtbare Text "OPERATOR" die kompakte Pill — kein Fix nötig. Pattern bleibt erhalten.
Changed (Layout-Refactor: Sidebar-First-Navigation, Merge 6e8827076bb42fb267d459aee266f3ac3a20a1cf)¶
- Topbar entfernt in beiden Frontends. Sidebar trägt jetzt die
volle Navigation-Chrome (Header / Body / Footer). Pattern aus der
AVS-Admin-Demo (
src/avs_chatbot/admin/templates/base.html) adaptiert — keine 1:1-Kopie, sondern Vue-Component-Adaption mit Konzept-A-Tokens. <AppSidebar>-Component (neu) in beiden Frontends:- Header: Brand-Logo + Brand-Name + Sub-Titel; Operator-UI
zusätzlich
OPERATOR-Badge mit--kora-badge-bg/-fg-Tokens - Body: Group-Sections mit Uppercase-Titles; Items mit
inline-SVG-Icons + Label
- Operator-UI: 3 Groups (Verwaltung/Beobachtung/Integrationen) mit 5 Items (Tenants, Templates, Module, Audit-Log, Connectors)
- Tenant-UI: 1 aktive Group (Mein Bereich) + Block-8-Platzhalter
mit
is-disabled-Style
- Footer: User-Menu mit Avatar (2-Buchstaben-Initialen) + Username + Top-Role-Subtitle, plus ThemeToggle und Logout-Icon
- Inline-SVG-Pattern für Icons — keine externe Icon-Lib (kein Bundle-Hit, konsistent mit existierendem ThemeToggle und AVS-Admin-Demo)
- Walkthrough-Drift-Fix #2 (Active-State auf Sub-Routes): Active-
Logic von exact-match (
router-link-active) auf prefix-match umgestellt (route.matchedplusroute.path.startsWith(itemPath + "/")als Robustheits-Fallback). Sub-Route/tenants/<uuid>hält den „Tenants"-Eintrag weiterhin aktiv - Walkthrough-Drift-Fix #3 (Active-Visual): Full-Width-Pill
(Variante A) statt nur Schriftfarbe.
.nav-item.is-activebekommtbackground: var(--kora-primary)+color: var(--kora-on-primary)— sichtbar als Operator-Indigo-Pill bzw. Tenant-Grün-Pill - Walkthrough-Drift-Fix #1 (ThemeToggle-Visibility): ThemeToggle
jetzt im Sidebar-Footer mit klar sichtbarem Border-Styling
(
var(--kora-border)-Box, identisch zum Logout-Icon-Button) — funktioniert in Light- und Dark-Mode - Role-Priority-Map im Frontend pro Realm:
- Operator:
operator-admin (100)>-editor (80)>-viewer (60)> vendor-Rollen (50/40/30) - Tenant:
tenant-admin (100)>-editor (80)>-viewer (60) - Höchste passende Rolle wird als Subtitle gerendert; Fallback: Subtitle weglassen (kein Raten)
- Tests:
- 9 neue Vitest-Tests (
AppSidebar.spec.ts): Group-Count, Item- Count, Active-Logic für Liste vs. Sub-Route, Operator-Badge, User-Menu mit Initialen + Role-Subtitle, höchste Rolle gewinnt, Fallback ohne Operator-Role, Logout-Callback - 3 neue Playwright-Tests (
sidebar-active-state.spec.ts): Top-Level-Active auf Sub-Route, Group-Switch, User-Menu-Inhalt - Total Operator-UI: 117/117 Vitest (108→117) + 15/15 Playwright (12→15) grün
- Tenant-UI: 16/16 Vitest unverändert (Block-8-Erweiterung bringt eigene Tests), 2/2 Playwright unverändert
- verify-auth-stack: 57/59 (2 erwartete Drifts aus TODO-Auth-NEU weiterhin als Frühwarnung)
- Bestehende
BaseLayout.vueumgestellt auf reine Layout-Shell mit<AppSidebar />+<main><slot /></main>. Mobile-Fallback: Sidebar wird über volle Breite gestackt unter768px. Mobile- Polish bleibt out-of-scope. - Konsistenz mit Breadcrumbs: Sidebar zeigt Top-Level-Section, Breadcrumbs zeigen Sub-Route-Position — beide bleiben sichtbar.
Added (TODO-Auth-NEU + JWT-sub-Claim-Drift-Doku, Merge a7e3712046230bd2fb4bbcc6fe02c733012b0144)¶
- TODO-Auth-NEU registriert als 8. Drift-Datapoint im Auth-Stack-
Drift-Aufräum-Vektor. Severity M2/40. JWTs aus beiden Realms
enthalten kein
sub-Claim —kora-scope-Custom-Scope hat keinen Subject-Mapper. Workarounds bestehen bereits (platform_audit_log.actor_keycloak_idNULL-Fallback,user_preferences (realm, username)Composite-PK). Fix-Pfad dokumentiert inoffene-todos.md. auth-stack-soll-zustand.mderweitert um neue Subsection §3.2 „JWT-Claim-Inventar (Soll vs. Ist)" mit Tabelle aller Standard- und Custom-Claims,subals DRIFT markiert, Konsequenzen für DB-Design dokumentiert.scripts/verify-auth-stack.shCheck 58 ergänzt: prüftsub-Claim-Anwesenheit in einem frisch gemintten Token aus beiden Realms (Check 58a fürkora-platform, 58b fürkora-tenants). Aktuell FAIL als explizite Drift-Frühwarnung — sobald Mapper-Fix deployed ist, wechselt der Check ohne Skript- Änderung auf PASS. Total: 59 Checks, aktuell 57/59 grün + 2 erwartete Drifts.
Added (Color-System-Implementation Konzept A, Merge 7261426a4d297b5f2876adf04516ee1d8b7bc275)¶
Backend:
- Neue Tabelle
user_preferences(Migration 0008). Composite-PK(realm, username)weil JWTs in beiden Realms keinesub-Claim enthalten (Discovery-Befund 2026-04-28). Standalone-Tabelle ohne FK — User-Records leben in Keycloak. - API:
GET/PATCH /api/v1/users/me/preferences. Lazy-Create-Pattern: erste Anfrage legt Default-Eintrag (auto) an, kein Pre- Provisioning. Pydantic + DB-CHECK validierenlight|dark|auto. Kein Audit-Logging — User-Settings sind privat. - Cross-Realm-Independenz: Operator und Tenant mit gleichem Username haben unabhängige Preferences (PK enthält Realm).
Frontend:
- Konzept-A-Tokens in beide Frontends migriert (operator-ui
tokens.css, tenant-uiassets/styles.css). 27 semantische Tokens × 3 Modi (Light, Dark, Auto viaprefers-color-scheme) × 2 Surfaces. 8-Token-Drift zwischen den Frontends beseitigt — beide teilen jetzt dasselbe Schema. - Theme-Provider
useTheme()Composable in beiden Frontends. Lädt aus Backend (Cross-Device-Sync), Fallback LocalStorage. System-Theme-Listener für Auto-Mode-Live-Update bei OS-Setting- Wechsel. <ThemeToggle>-Component in beiden Topbars (rechts neben Logout). 3-State-Cycle Sun → Moon → Auto-Symbol mit Tooltip +aria-label.- Operator-Badge als eigene Token-Klasse
--kora-badge-bg/-fg(surface-unabhängig). Tenant-UI rendert die Component nicht, hat aber die Tokens für künftige Operator-Surface-Vorschauen (Block 17). - 11 Walkthrough-Drift-Stellen behoben: alle
color: var(--kora-primary)- Text-Uses durchvar(--kora-link)ersetzt — Sidebar-Aktiv, Hinweis-Card-Headlines, Inline-Links, Tab-Aktiv etc. Im Dark-Mode von 1.7:1 (FAIL) auf 8.2:1 (AAA). Border-Uses bleiben unverändert.
Tests:
- 8 Backend-Integration-Tests
(
tests/integration/test_user_preferences_api.py): Unauth-401, Lazy-Create, GET/PATCH-Roundtrip, Validierung-422, Cross-Realm- Independenz, Audit-Skip, Username-Edge-Case. scripts/smoke-color-system.sh6/6 grün.- 8 neue Vitest-Tests (
useTheme.spec.ts) in jedem Frontend, total 108/108 (operator-ui) + 16/16 (tenant-ui) grün. - 2 neue Playwright-Tests (
theme-toggle.spec.ts) in operator-ui: Cycle-Verhalten + Reload-Persistenz, total 12/12 grün. - Verify-Auth-Stack 57/57 grün (keine Regression).
Doku:
- Neue Page
operations/color-system-implementation.mdals Live-Stand-Referenz + Migration-Pfad-Doku. concept-a.mdals „✅ Implementiert" markiert;concept-b.mdals „❌ Verworfen" markiert.bakeoff.htmlbleibt als historisches Bake-Off-Artefakt erhalten.- mkdocs-Nav um Color-System-Implementation-Page erweitert.
Added (Test-Daten-Cleanup-Tooling, TODO-UX-02, Merge 00ee4a48ff481c0d016e5ede298320ff5342c95c)¶
scripts/cleanup-test-data.sh— Operator-Skript zum Entfernen akkumulierter E2E-/Bench-Test-Daten aus der Live-kora-platform-DB. Default Dry-Run,--applymit Confirm-Prompt,--include-benchopt-in fürbench-*-Tenants,--yesfür CI/non-interactive. Pre-Flight prüft alle 10 NO-ACTION-FK-Tabellen — bricht ab statt mit Foreign-Key- Violation zu scheitern. CASCADE-FKs (tenant_packages,tenant_branding,chatbots,credentials,tenant_modules,evaluation_questions) werden automatisch mitgenommen. Audit-Log wird per Scope-Boundary nicht angefasst.- Make-Targets:
make cleanup-test-data(Dry-Run),make cleanup-test-data-apply(mit Confirm-Prompt). Beide akzeptierenargs=--include-bench. - Runbook
operations/test-data-cleanup.mdmit Aufruf-Patterns, Edge-Case-Handling, Verifikations-Schritten. - Live-Verifikation: 68 e2e-Tenants + 35 e2e-Templates entfernt; 455 Audit-Einträge unangetastet; 2 bench-Tenants preserved.
Added (Block 7.4 — Audit-Log-Viewer + Bulk-Ops + Connectors-Stub, Branch platform/block-7-4b-audit-bulk-frontend, Merge 0b93d60)¶
Backend (7.4a, Merge 61689c2):
- Audit-Reader-API unter
/api/v1/platform/audit*. Drei Endpunkte:GET ""(List mit Filteraction/actor/entity_type/entity_id/json_search/date_from/date_to+ Paginationoffset/limit),GET /{entry_id}(Single-Entry),GET /export.csv(CSV-Export mit UTF-8-BOM, Hard-Limitmax_rows, sichtbarer Truncated-Footer, Filter-State-Pass-Through, optionalinclude_details=true). Read-only — kein POST/PATCH/DELETE; Audit-Log ist append-only. - Bulk-Soft-Delete für Tenants (
POST /api/v1/platform/tenants/bulk-soft-delete) und Bulk-Module- Toggle für Tenant (POST /api/v1/platform/tenants/{id}/modules/bulk). Atomar viarequest_scoped_session(tenant_id=None, bypass_rls=True)— Block 5 Cleanup-Pattern wiederverwendet, Rollback bei jeder Exception. Audit-Strategy: eine Audit-Zeile pro Bulk-Aktion (tenant.bulk_soft_deleted/tenant_module.bulk_assigned), IDs indetails.after. Lutz-Entscheidung „Atomarität sichtbar machen", Tradeoff bewusst (siehe TODO-Block-7-4-03). AuditServicemitlist_entries/get_entry/export_csv. JSONB-Search viacast(details, String).ilike(...)ohne GIN-Index (siehe Pre-FlightPORTING-7.4.md— Trigger > 10k Rows). Filter kombinierbar; default 50/Page; max-page-size 200.- Pydantic-Schemas
AuditEntryRead,AuditList,AuditListParams(models/audit.py),BulkSoftDeleteRequest/BulkSoftDeleteResponse(operator_tenants),BulkAssignBody/BulkAssignResponse(tenant_modules). - Service-Methoden:
tenant_service.bulk_soft_delete_tenants(gibt(soft_deleted, skipped, not_found)zurück, raisedTenantNotFoundfür atomic-rollback-Pfad),module_service. bulk_assign_to_tenant(überspringt Always-On-Module, raisedModuleNotAssignablefür Internal-only,populate_existing=True). - 26 neue Tests: 12 Unit
(
tests/unit/test_audit_service.py) für List/Filter/CSV/Limit- Clamping, 14 Integration (tests/integration/ test_bulk_operations.py) für Tenants-Bulk + Modules-Bulk inkl. Rollback-, Idempotenz- und Single-Audit-Entry-Pfade. Backend 79/79 + 26 = 105 grün, RLS-Regression unverändert.
Frontend (7.4b, dieser Merge):
- Audit-Log-Viewer unter
/admin/operator/audit(List + Detail- Expand, kein Drilldown — append-only). Filter-Toolbar mit Action- Dropdown (KNOWN_ACTIONS), Actor/Entity-ID/JSON-Search-Inputs (300 ms Debounce auf Free-Text), Date-Range-Picker. Default „letzte 7 Tage" wenn keine URL-Filter aktiv (sonst URL-Filter haben Vorrang — M1-Pre-Merge-Fix für Tenant-Audit-Tab-Shortcut). CSV-Export-Button (mit/ohne Details), Pagination (50/Page). - Bulk-Soft-Delete für Tenants in TenantsListPage: Checkbox-
Spalte, Bulk-Bar mit Slug-Vorschau im Confirm-Dialog, Selection-
Reset bei Liste-Watch (verhindert „IDs ausgewählt, die nicht mehr
in der Liste sind"-Bug). Selection-State per Page (
Set<string>). - Bulk-Modul-Aktivieren in
<TenantModulesSection>: Multi- Select über alle nicht-aktiven Tenant-assignable-Module, ein Confirm-Dialog mit Modul-Liste, atomarer POST gegen/tenants/{id}/modules/bulk. Internal-only- und Always-On-Module sind ausgefiltert. - Connectors-Stub-Page unter
/admin/operator/connectors— reine UI mit Block-13-Hinweis-Card (Text auskonnektor-roadmap§2 + §7.4), kein Backend-Read. Bewusst ohne Bestands-connectors- Tabelle, damit Block 13 die API von Grund auf entwerfen kann. - TenantsDetailPage: Tabs „Audit-Log" (Filter-Shortcut-Button
navigiert zu
/audit?entity_id=<tenant.id>) und „Connectors" (Block-13-Link) aktiviert. - Composables:
useAuditEntries()(Filter + Pagination + URL- Param-Read + CSV-Export), Bulk-Confirm-Pfade direkt in den Pages (kein eigener Composable nötig). - Sidebar: „Audit-Log" + „Connectors" aktiviert.
- Tests: 12 neue Vitest (Pages + Composable, total 100/100), 2
neue Playwright (Audit-Filter + Expand, Bulk-Soft-Delete-Workflow
über zwei Tenants, total 8/8 sequential).
playwright.config.tsaufworkers: 1festgenagelt — Cross-Test-Isolation über shared Backend-State funktioniert sequenziell sauber, Parallel-Worker würden sich Tenants-Liste-Rows „klauen" und Specs flaky machen. - Pre-Flight
PORTING-7.4.md: Pfad-Split 7.4a/7.4b (Backend ~3h + Frontend ~3h) bestätigt; JSONB-Search ohne GIN als TODO-Block-7-4-NN getrackt; kein Connectors-Backend-Read; kein Per-Tenant-Audit-Embed (Filter-Shortcut-Pattern stattdessen).
Changed (Block 7.4)¶
- Audit-Helper
write_platform_auditwird neu auch in den Bulk- Routen verwendet — keine Code-Duplikation. Vorher in Block 5 Cleanup generalisiert. frontend/operator-ui/src/router/index.ts: 2 neue Routen/auditund/connectorsmitrequiresOperator-Guard.frontend/operator-ui/src/layouts/BaseLayout.vue: Sidebar- Einträge „Audit-Log" + „Connectors" aktiviert.
Wichtige Pre-Flight-Erkenntnisse (in PORTING-7.4.md)¶
- Pfad-Split-Bestätigung durch 4-Layer-Discovery (table/service/
route/live-data): Audit-Tabelle existiert seit Block 1 mit GIN-
losem JSONB-Index; Bulk-Routen brauchen das Block-5-Cleanup-
Atomarität-Pattern (
request_scoped_session+ Audit-helper); Connectors-Tabelle existiert, aber Block 13 will eigene API von Grund auf entwerfen → kein Read im Stub. - GIN-Index auf
audit_log.detailsdeferred: TODO mit Trigger10k Audit-Rows; aktuell
cast(details, String).ilike(...)performant genug. - Single-Audit-Entry für Bulks (Lutz-Entscheidung): bewusst
„Atomarität sichtbar machen". Tradeoff
entity_id="bulk:N"→entity_id-Filter findet Bulk-Events nicht (TODO-Block-7-4-03 trackt Konventions-Doc). - M1 Review-Fix vor Merge (
a0ab7c9): TenantsDetailPage Audit- Tab-Shortcut navigiert mit?entity_id=<tenant.id>, aberuseAuditEntries.tslas URL-Query-Params ursprünglich nicht. Fix:readUrlFilters()-Helper + Default-7-Tage-Range nur bei fehlenden URL-Filtern.
Block 7 abgeschlossen (~80% → 100%)¶
Mit dem 7.4-Merge ist Block 7 vollständig: 7.1a Tenants-Backend, 7.1b Operator-UI-Frontend, 7.2 Templates-CRUD-UI, 7.3 Modules- Registry + Toggle, 7.4 Audit + Bulk + Connectors-Stub. Operator-UI liefert die volle Phase-B-Read-Modify-Operate-Oberfläche; Phase D folgt mit Tier/Limits (TODO-Block-7-3-01) und Bulk-Cap-Hardening (TODO-Block-7-4-01).
Added (Block 7.3 — Modules-Registry + Per-Tenant-Toggle, Branch platform/block-7-3-modules-and-packages)¶
- Modules-Registry unter
/admin/operator/modules(List + Detail) als Read-Only-Ansicht der Plattform-Module gegen das in Block 5/6 gebaute Backend (/api/v1/platform/modules*). List mit Search (300 ms Debounce) + Scope-Dropdown-Filter (Core/Tenant-assignable/Internal-only). Detail mit Stammdaten +config_schema-JSON-Pre-Block + scope-spezifischen Hinweisen. - Per-Tenant-Toggle in TenantsDetailPage Tab „Pakete & Module"
(war disabled mit „Kommt in Block 7.3"). Neue
<TenantModulesSection>-Component mergt clientsideplatform_modules ∖ tenant_modules-of-tenant. Toggle-Buttons gegenPOST/DELETE /api/v1/tenants/{id}/modulesmit Confirm-Dialog beim Deaktivieren. Always-On-Module (scope='core') zeigen „fixiert"; Internal-only-Module werden ausgefiltert. - Composables:
useModules(),useModule(id),useTenantModules(tenantId). - Sidebar: Eintrag „Module" aktiviert (
<router-link>). - Tests: 15 neue Vitest (total 89/89), 2 neue Playwright (Toggle-Workflow + Registry-Navigation, total 6/6).
Changed (Block 7.3)¶
- A11y-Mit-Fix TODO-Block-7-1b-03: TenantsDetailPage-Tab-Row von
<div>-Elementen auf<button>-Tabs umgestellt —aria-active,disabled-Attribute auf Block-7.4-Placeholdern, Focus-Visible- Outline,cursor:not-allowedbei disabled. Volle ARIA-tablist- Rolle bleibt offen für externe-Personas-Pass. - Backend unverändert — reine Frontend-Arbeit.
Wichtige Pre-Flight-Erkenntnisse (in PORTING-7.3.md)¶
- Pakete-Modell-Diskrepanz: Code = Variante C (relational
tenant_modules), Konzept §8 = Variante A (Pro-Tenant-Pakete mit Tier/Limits viatenant_packages). Tabelle existiert seit Block 1 ohne API/Service/Schemas, 0 Rows live, kein Limits-Enforcement. 7.3 baut nur Modul-Toggle. Tier/Limits →TODO-Block-7-3-01für Block 12. - EntityForm-Refactor finaler Verzicht (TODO-Block-7-1b-04 + TODO-Block-7-2-05): 7.3 baute keinen Form-Datenpunkt; Block 12 + Block 13 bringen unterschiedliche Form-Shapes; drei ähnliche CRUDs entstehen nie. Beide TODOs verworfen, im 7.3-Review begründet.
Added (Block 7.2 — Templates-CRUD-UI, Branch platform/block-7-2-templates-crud)¶
- Templates-CRUD im Operator-UI unter
/admin/operator/templates/*gegen das in Block 5 gebaute Backend (/api/v1/operator/templates*). Vier Pages (List, Create, Detail, Edit) auf dem in 7.1b etablierten Component-Stack —<DataTable>,<FormInput>,<FormTextarea>,<FormButton>,<Toast>,<ConfirmDialog>,<Breadcrumbs>,<PageHeader>. - Path A bestätigt in
frontend/operator-ui/PORTING-7.2.md(Pre-Flight-Artefakt). Block 5 hat das Operator-Backend bereits vollständig — 7.2 ist reines Frontend-Update. <ListInput>-Component (src/components/ListInput.vue) für JSONB-list[str]-Felder (suggested_suggestions,recommended_connectors). Add/Remove + Inline-Edit + Enter-Key- Add. Bewusst minimal (kein Drag-and-Drop, kein Reorder).- Composables:
useTemplates()mit clientseitigem Search-Filter (Backend liefert bare Liste),active_only-Toggle re-fetcht serverseitig;useTemplate(id)mitreactivate()(PATCHis_active=true) +deactivate()(DELETE → 204);useTemplateIdValidator()mit Slug-Regex (Underscores erlaubt, anders als Tenant-Slug). - Sidebar: Eintrag „Templates (Block 7.2)" aktiviert (war in 7.1b
noch disabled mit Block-Hinweis-Tooltip), Link auf
/templates. - Tests: 33 neue Vitest (Pages + ListInput + ID-Validator), 2 neue Playwright (Happy-Path mit Deactivate/Reactivate-Cycle + Error-Path 409). Total 74/74 Vitest + 4/4 Playwright. Backend- Regression unverändert 55/55.
mintTokens()Cache + Retry:e2e/helpers.tsum 60s-In-Process- Cache plus 3-fach-Retry mit Backoff erweitert. Behebt einen flaky Keycloak-master-Realm-500er bei parallelem Suite-Lauf.
Changed (Block 7.2)¶
frontend/operator-ui/src/router/index.ts: 4 neue Routen unter/templates*mitrequiresOperator-Guard.frontend/operator-ui/src/layouts/BaseLayout.vue: Sidebar- Eintrag „Templates" aktiviert.
Wichtige Pre-Flight-Erkenntnisse (in PORTING-7.2.md)¶
- Templates sind global (kein
tenant_id-Feld). Der ursprüngliche Prompt nahm tenant-skopierte Templates an — das Backend zeigt das Gegenteil. - Soft-Delete läuft via
is_active=false(reversibel via Reaktivieren-Button), nichtdeleted_at. Slug-Reservation §14.4 gilt für Templates nicht. - Pfad-Präfix
/api/v1/operator/templatesweicht von/api/v1/platform/tenants(7.1a) ab — bekannte Inkonsistenz, in TODO-Block-7-01 getrackt; 7.2 macht keine nachträgliche Migration.
Added (Block 7.1b — Operator-UI Frontend, Branch platform/block-7-1b-operator-ui-frontend)¶
- Neues Vue-3-SPA-Projekt
frontend/operator-ui/als Schwester- projekt zufrontend/tenant-ui/. TypeScript strict, Vite, vue- router, Vitest, Playwright. Bundle 130 KB raw / 55 KB gzipped (Ziel < 500 KB deutlich unterboten). - Tenants-CRUD-UI unter
/admin/operator/tenants/*gegen das in 7.1a gebaute Backend: Liste mit Pagination + Search (300 ms Debounce) include_deleted-Toggle; Create mit Live-Slug-Validation (regex + 500 ms Async-Verfügbarkeits-Check) + 422-Field-Mapping + 409-Inline-Error inkl. §14.4-Policy-Hinweis; Detail mit Tab-Platzhaltern für Block 7.2/7.3/7.4 (Pakete, Audit, Connectors, Templates) + Soft-Delete-Banner; Edit mit immutable Slug + Dirty- Tracking + Diff-Payload-PATCH (nur geänderte Felder).- Reusable Components:
<DataTable>(generisch überT, Slot-basierte Columns, Loading-Skeleton, Error/Empty-Slots),<FormInput>/<FormTextarea>/<FormButton>(v-model, Error- Slot),<Toast>+useToast()(globale Queue, Auto-Dismiss 4 s/6 s je nach Tone),<ConfirmDialog>+useConfirm()(imperativer Promise-Confirm mit Escape/Enter),<Breadcrumbs>,<PageHeader>. Bewusst minimal — Sort/Filter/Virtualize bleibt Block-7.3-Scope. - Auth + API-Composables:
useAuth.tsaus tenant-ui portiert und aufkora-platform-Realm +operator-ui-Public-Client umparametrisiert. Neu:hasOperatorRole-Computed (prüftrealm_access.roles), Router-Guard redirected ohne Operator- Role-Claim auf/403. Neuer E2E-Seed-Hook (window.__KORA_E2E_SEED__) im Composable; nur in Playwright- Tests aktiv.useApi.ts1:1 portiert. - Branding:
--kora-primary: #3730a3(Indigo-800, WCAG AAA 10.79:1) + Hover#312e81für Operator-Surface-Distinction; Accent#f59e0bnur für die OPERATOR-Badge in der Topbar. Alle anderen Tokens (Font, Spacing, Radius, Shadow, Dark-Mode) identisch zu tenant-ui. Begründung infrontend/operator-ui/PORTING.md§2. - Build-Integration: neuer
operator-ui-builder-Stage ininfra/docker/Dockerfile.platform; FastAPI mountet das Bundle unter/admin/operator/*mit SPA-Fallback (main.py). Sechs neue Makefile-Targets analog zu tenant-ui-* (dev/build/lint/ test/e2e/e2e-ui). - Tests: 41 Vitest-Tests (slug-validator, useToast, useConfirm, DataTable, FormInput, vier Tenants-Page-Specs mit useApi-Mock); 2 Playwright-E2E (Happy-Path Full-CRUD + Error-Path 409). Backend- Regression unverändert 55/55, Smoke-Block7-1a 12/12.
Changed (Block 7.1b)¶
infra/docker/Dockerfile.platform: Multi-Stage-Build erweitert umoperator-ui-builder-Stage; Runtime kopiert beide UI-Bundles (/app/static/tenant,/app/static/operator).src/kora_platform/main.py: SPA-Mount für/admin/operator/*ergänzt — spiegelt das tenant-ui-Mount-Pattern.docs-kora/docs/deployment/operator-ui-client.md: Abschnitte "Prod-Deployment: NPMplus-Routing" konkretisiert (FastAPI liefert Bundle direkt, NPMplus-Rule ohne separate Static-Location); Dev- Workflow + Playwright-Anleitung neu.
Added (Block 7.1a — Tenants-Backend + operator-ui Keycloak-Client, Branch platform/block-7-1a-tenants-backend)¶
- Operator-Tenants-CRUD-API unter
/api/v1/platform/tenants/*(List / Create / Get / Update / Soft-Delete). Operator schreibt, Vendor liest, Tenant-Scope ist verboten. Pagination + Search +include_deleted.tenant.created/.updated/.soft_deletedals Audit-Events mit Delta-Payload (nur geänderte Felder). Slug-Re-Use-Policy §14.4: soft- deleted Slugs bleiben reserviert, Freigabe nur per Hard-Delete-Runbook. - Keycloak-Client
operator-ui(public SPA, PKCE S256) imkora- platform-Realm. Redirect-URIs für Prod (/admin/operator/*) und Dev (Vitelocalhost:5174, APIlocalhost:8280). Realm-Default-Session- Lifetimes. Runbookdeployment/operator-ui-client.mdmit Re-Import- Prozedur viakcadm.sh(vermeidet Secret-Verlust für andere confidential Clients).patch-dev-redirects.shumoperator-uierweitert. TenantService(services/tenant_service.py) mitTenantNotFoundTenantSlugConflict-Exceptions.soft_delete_tenantidempotent ((tenant, was_fresh: bool)-Return).TenantService.changed_fields- Helper liefert Audit-Delta-Payload.- Pydantic-Schemas (
models/tenant.py): TenantCreate / Update / Read / List mit Slug-Validator^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$(3–64 Zeichen, DNS-safe). EmailStr fürcontact_email. - Tests: 23 Unit (
test_tenant_service.py), 7 Integration (test_operator_tenants_api.py— ScopeSwitch-Fixture umgehtdependency_overrides-per-App-Limit), 12 Smoke (smoke-block7-1a.sh). Code-Review: 0 Findings ≥ 80, 7 Deferred <80 (TODO-Block-7-01..-07).
Changed (Block 7.1a)¶
- Audit-Helper umbenannt:
write_module_audit→write_platform_audit(api/routes/_module_audit.py→_platform_audit.py). Pure Rename, Funktion war in der Signatur schon generisch. - Alte Block-3-Test-Route entfernt:
GET /api/v1/admin/tenants(bisher nur vonsmoke-block3.shTest 6/8 genutzt) ersetzt durchGET /api/v1/platform/tenants. Präfix-Konsolidierung: Platform-Level- Resourcen laufen jetzt konsistent unter/api/v1/platform/*(Block-6-Muster), Tenant-Scope-Resourcen unter/api/v1/tenants/*. pydantic[email]inpyproject.toml(Laufzeitdep wächst umemail-validator,idna,dnspython— getrackt alsTODO-Block-7-05).- L-B3-03-Doku-Fix: Referenz auf nicht-existenten
routes/tenants.py:_scope_operator_branch-Helper aufapi/dependencies/tenant_context.pykorrigiert.
Added (Block 4 — Qdrant Collection-per-Tenant)¶
- Qdrant Collection-per-Chatbot + Shared-per-Tenant (Block 4):
Tenant-isolierte Vektorsuche mit Fan-Out über Chatbot-eigene und
Shared-Collections. Naming:
kora_<tenant_id>_<chatbot_id>bzw.kora_<tenant_id>_shared. Dreischichtige Isolation (Collection- Name + Service-Guard + DB-RLS), verifiziert mit 200 parallelen Cross-Tenant-Queries (0 Cross-Hits). - Embedder-Service
kora-platform-embedder(Block 4 Phase B.0): Neuer Container auf Basis multilingual-e5-large, exposed intern unterhttp://embedder:8090/embed. Passage/Query-Prefix-Convention viaEmbedderClient.embed_passagesundembed_query. - ChatbotLifecycle mit Soft-Delete (Block 4 Phase C):
Chatbots können soft-deleted werden (
deleted_at-Timestamp), werden nach 30 Tagen Grace-Period durch Cleanup-Job hard-deleted. Cleanup via CLI (kora-platform cleanup-expired-chatbots) oder automatisch durch den Scheduler um 03:00. - Scheduler-Service
kora-platform-scheduler(Block 4 Phase C): ofelia-basiert, erweiterbar perinfra/scheduler/config.ini. Erster Job: täglicher Chatbot-Cleanup. Weitere Jobs können durch Hinzufügen von[job-exec "<name>"]-Sektionen ergänzt werden. - PlatformRetrieval-Service (Block 4 Phase D): Fan-Out-Retrieval
mit parallelem
asyncio.gatherüber Chatbot- und Shared-Collection, Score-basierte Re-Rank-Union. Collection-Not-Found-Tolerance für leere Shared-Collections; echte Qdrant-Errors werden propagiert. - Runbook
deployment/qdrant-collections.md: Operator-Doku mit Troubleshooting-Workflow. - Partial Index
ix_chatbots_deleted_at(Alembic 0005): nur soft-deleted Chatbots sind indiziert, spart Speicher bei NULL-Mehrheit.
Added (TODO-B2-03 — Keycloak Service-Account)¶
- Keycloak Service-Account
kora-platform-audit— minimaler Service-Account für Audit-Event-Polling, Client-Credentials-Flow, Rechte:realm-management.view-events(strikt minimal, keinview-users— Event-Payload reicht dem Poller). Init-Scriptinfra/keycloak/init-scripts/create-audit-service-account.shidempotent. (TODO-B2-03) - Prometheus-Counter
audit_poller_auth_failures_total{reason}— neue beobachtbare Metrik für Poller-Auth-Fehler (invalid_client,invalid_grant,forbidden,network,missing_secret,unknown). - Runbook
deployment/keycloak-service-account.md— Bootstrap-Flow, Secret-Rotation (90 Tage empfohlen), Re-Import- Verhalten, Troubleshooting. - Integration-Test
tests/integration/test_audit_service_account.py— deckt Happy-Path (/events→ 200), Deny-Path (/users//roles//groups→ 403) und Sad-Path (falscher Secret → 401) ab.
Changed (TODO-B2-03)¶
- Audit-Poller-Auth — Password-Grant gegen Master-Realm →
Client-Credentials-Flow gegen
kora-platform-Realm via interner Keycloak-URL. Token-Reuse mitasyncio.Lockgegen Race-Refresh. Token-Safety-Margin 30s vorexpires_in. - Bootstrap-CLI —
kora-platform bootstrap-operator-adminliest Master-PW ausKC_BOOTSTRAP_ADMIN_USERNAME/PASSWORDstatt aus Settings. Invocation nur noch viadocker compose run --env-file .env.bootstrap; empfohlener Pfad/run/user/$UID/.env.bootstrap(tmpfs).
Removed (TODO-B2-03)¶
KORA_KEYCLOAK_ADMIN_USERNAME+KORA_KEYCLOAK_ADMIN_PASSWORDausdocker-compose.platform.yml,.env.platform,.env.platform.exampleundSettings(config.py). Master-Admin-Zugriff existiert nur noch transient via.env.bootstrap.
Added¶
- Prometheus-Scrape für
kora-platform-api—/metrics-Endpoint auf Port 8080 (Container-internal) wird jetzt alle 15s gescraped. Die Block-3-Counters (kora_platform_requests_total,kora_platform_auth_failures_total) werden gesammelt. Dashboard-Bau kommt mit Block 7. (Commitsc0df565,745d2a2) - Network-Bridge
avs-net↔kora-platform-net—avs-prometheushängt jetzt zusätzlich ankora-platform-netviaexternal: trueindocker-compose.yml, ermöglicht Cross-Compose-Scraping. (Commitc0df565)
Fixed¶
- mkdocs Anchor-Warning in
konnektor-roadmap.md—#permission-awareness-ausbau→#6-permission-awareness-ausbau(mkdocs behält Zahlen im Anker-Slug).mkdocs --strictläuft jetzt ohne Warnings. (Commit5b821da)
Changed (Cleanup-02, Branch platform/chore-cleanup-02)¶
vendor_access_log.action—VARCHAR(64)→TEXT. Verhindert Truncation bei tief verschachtelten Pfaden (ab Block 4 erwartet). Reversible Alembic-Migration 0004. (TODO-B3-03)- Audit-Poller — Pagination — fetcht jetzt paginiert bis
Exhaustion oder Cursor-Überlauf,
MAX_PAGES=50-Safety-Rail, Log-Warning pro voller Seite. Fix für stummen Event-Verlust bei100 Events pro Poll-Intervall. (TODO-B2-01)
- Audit-Poller — Redis-Key —
avs:audit:last_event_ts→kora:platform:audit:last_event_ts. Einmalige idempotente Migration beim Service-Start (Overlap: neue Version gewinnt, Legacy wird gelöscht). (TODO-B2-05) - Settings — URL-Validation —
platform_public_url,keycloak_base_url,keycloak_public_base_urljetztpydantic.AnyHttpUrl. Validiert Scheme beim Settings-Load. Alle Call-Sites mit explizitemstr()-Cast.AnyHttpUrl(nichtHttpUrl), um den Dev-Defaulthttp://localhost:8236zu akzeptieren. (TODO-B2-07)
Added (Cleanup-02)¶
/metricsIP-Allowlist — FastAPI-Dependency statt NPMplus-Regel (NPMplus läuft auf separatem Host, ist hier nicht direkt steuerbar). Default: Loopback + RFC1918. LiestX-Forwarded-Forleftmost, fällt aufrequest.client.hostzurück. Verifiziert: LAN-IP → 200, gespoofte externe IP → 403. (TODO-B3-04)- Unit-Tests —
tests/unit/test_audit_poller.py(5 Tests für Migration + Pagination) undtests/unit/test_config.py(5 Tests für URL-Validierung).
Changed (mkdocs-Hygiene, Branch platform/chore-mkdocs-hygiene)¶
- mkdocs-Setup — Config-Datei
mkdocs-kora.yml(Repo-Root) →docs-kora/mkdocs.yml.docker-compose.platform.ymlnutzt jetzt einen einzelnen Dir-Mount (./docs-kora:/docs:ro) statt Dir + Single-File. Löst das Bind-Mount-Inode-Problem: Content- und Config-Änderungen werden live sichtbar, ohne--force-recreate(Folge-Fix zur Pre-Block-4-Diagnose). - URL-Pfad —
/todo/→/offene-todos/. Dateinametodo.md→offene-todos.md. Alle internen Referenzen angepasst. Alte URL produziert jetzt 404 — keine Redirect-Regel, weil die Docs-Site noch keine stabilen externen Konsumenten hat.
Added (mkdocs-Hygiene)¶
- Runbook
docs-kora/docs/deployment/mkdocs-container.md— dokumentiert das mkdocs-Deployment-Setup inkl. Content-vs-Config-Änderungen und Troubleshooting-Pfade.
Added (Pre-Block-4 Cleanup, Branch platform/chore-pre-block4)¶
- Correlation-ID-Middleware —
RequestIdMiddlewaregeneriert bzw. übernimmt denX-Request-Id-Header pro Request, bindet ihn in structlog-ContextVars und spiegelt ihn im Response-Header + im 500er-Response-Body. (TODO-B2-02)
Changed (Pre-Block-4 Cleanup)¶
- Keycloak-Realms — Drei Protocol-Mapper (
realm-roles,preferred_username,email) direkt imkora-scope-ClientScope beider Realm-JSONs. Vorher nur als Dev-Workaround ingen-test-tokens.sh. Dev/Prod-Parität hergestellt. (L-B3-01) - kora-tenants-Realm —
refreshTokenMaxReuse: 0+revokeRefreshToken: trueaktiviert (Refresh-Token-Rotation analog zum kora-platform-Realm). (TODO-B2-06) - Tenant-Scope-Middleware —
TENANT_ROLESals hartes Gate: Gruppenmitgliedschaft allein reicht nicht mehr; ohnetenant-admin/editor/viewer→403 no_tenant_role. Defense-in-Depth gegen halbfertige Provisioning-Artefakte. (TODO-B3-02) - Tenant-Scope-Middleware — ContextVar-Reset im Vendor-Pfad
vereinheitlicht:
current_tenant_id.set(None)mit Token-Reset imfinally. Kein Stale-Wert zwischen asyncio-Tasks im Logging. (TODO-B3-01) - Redis-Client — zentraler
ConnectionPoolinapp.state, Health-Check wiederverwendet den Pool, sauberer Shutdown (Client → Pool). (TODO-B2-04) - Makefile —
redeploy-platformwartet mituntil-Loop (max 60s) auf/health/livestatt festemsleep 10. (TODO-B2-08) gen-test-tokens.sh—ensure_kora_scope_mappers-Phase entfernt (durch Realm-JSON-Fix redundant).- mkdocs-Nav — thematisch neu strukturiert: Start / Roadmap / Konzepte / Prozesse / Deployment / Änderungen (mit eingebettetem TODO-Link). Dateien ohne Kategorien-Drift.
Fixed (Pre-Block-4 Cleanup)¶
realm_access.roles,preferred_username,email-Claims fehlten bei Prod-Realm-Imports — fielen in Block 3 nur durch Dev-Workaround nicht auf; jetzt aus Realm-JSON.
Added (Phase A Block 3 — Tenant-Scope-Middleware, Branch platform/block-3-tenant-middleware)¶
- JWT-Validation-Dependency
src/kora_platform/api/dependencies/auth.pymit JWKS-Cache (1h TTL, force-refresh bei kid-miss, asyncio.Lock-geschützt für beide Realms).authlib.joseals Lib (schon als Dep aus Block 2 vorhanden). Fail-Safe: 401 mitWWW-Authenticate: Bearer error="invalid_token". - Tenant-Context-Resolution
src/kora_platform/api/dependencies/tenant_context.pymit ContextVarscurrent_tenant_id+current_principal. Drei Scopes:tenant(Issuer=kora-tenants → Group-Slug → tenant_id via Redis-Cache-fähigem Slug-Lookup),operator(Issuer=kora-platform + operator--Role, tenant_id=None),vendor(Issuer=kora-platform + vendor--Role, BYPASSRLS, optional X-Tenant-Id-Header). Generator-Dependency mit Reset im finally. - DB-Engines
src/kora_platform/db/engines.py: drei async SQLAlchemy- Engines (app pool=20+10, vendor pool=5+5, admin pool=5+5) +request_scoped_sessionmitSET LOCAL app.current_tenant_idin Transaktion. Pool-Strategie aus Session-Pool-Benchmark (commit4eb764f, Scenario A). - Test-Endpoints
src/kora_platform/api/routes/tenants.py:GET /api/v1/me,GET /api/v1/whoami,GET /api/v1/tenants/me,GET /api/v1/tenants/{tenant_id}/chatbots,GET /api/v1/admin/tenants. - Vendor-Breakglass-Audit synchron in
vendor_access_log. Schreibfehler → 503audit_unavailable, Request wird abgelehnt. - Prometheus-Counter
kora_platform_auth_failures_total{reason}undkora_platform_requests_total{scope,realm}. Neuer/metrics-Endpoint. - Test-Token-Generator
scripts/gen-test-tokens.sh(idempotent): enabled directAccessGrants aufkora-api(beide Realms) + vendor- breakglass, legt bench-tenant-a/b Groups + Admins an, enabledvendor-breakglass-User, fügtoidc-usermodel-realm-role-mapperankora-scope-Client-Scope (fehlte aus Block 2, siehe L-B3-01). - Smoke-Tests
scripts/smoke-block3.sh(9/9 grün) und Integration-Testtests/integration/test_tenant_isolation.py(200× parallel, 0 Cross-Hits, 1.97s).
Changed (Block 3)¶
src/kora_platform/main.py— init_engines/dispose_engines im Lifespan,/metrics-Endpoint, tenants-Router eingebunden.src/kora_platform/config.py— drei neue Settings-Felder (db_app_dsn,db_vendor_dsn,db_admin_dsn,keycloak_public_base_url).docker-compose.platform.yml— drei DSN-ENVs (KORA_DB_APP_DSN,KORA_DB_VENDOR_DSN,KORA_DB_ADMIN_DSN) undKORA_KEYCLOAK_PUBLIC_BASE_URLanapi-Service.- Keine Alembic-Migration: die vorhandene
kora_platform_vendor-Rolle aus Block 1.5 (BYPASSRLS) wird wiederverwendet — keine neue Rolle nötig.
Added¶
- FastAPI-App-Skelett (aus Block 3 vorgezogen, siehe
Roadmap Block 2):
src/kora_platform/main.py,config.py,logging_setup.py,db/async_session.py,api/health.py. Endpoints/health/liveund/health/ready(prüft Postgres + Redis + Keycloak). - Docker-Service
kora-platform-apiauf Port 8280, Build überinfra/docker/Dockerfile.platform. - Keycloak Dual-Realm-Setup (Phase A Block 2):
- Realms
kora-platformundkora-tenantsvia Init-Scripts (infra/keycloak/realms/*.json). - Drei Clients im
kora-platform-Realm:kora-api(Standard),kora-api-vendor-breakglassundkora-api-vendor-tunnel(jeweils 2h Client-Session-Max). - Ein Client
kora-apiimkora-tenants-Realm. - First-Broker-Login-Flow "kora-tenants first broker login"
mit
idp-create-user-if-uniqueals REQUIRED-Step — verhindert Cross-Tenant-Account-Linking bei späterer Multi-IdP-Nutzung (Block 17). - Default-Rolle
tenant-viewerfür neue Shadow-User imkora-tenants-Realm. - Vendor-Accounts
vendor-support,vendor-breakglass,vendor-tunnelangelegt mitenabled=false. eventsEnabled=true+adminEventsEnabled=truefür den IdpAuditPoller.
- Realms
- CLI-Kommando
kora-platform bootstrap-operator-admin --email …(insrc/kora_platform/cli/bootstrap.py): legt einen Operator-Admin-User imkora-platform-Realm mit Rolleoperator-admin+UPDATE_PASSWORD-Required-Action an und sendet via Keycloak-SMTP einen 48h-gültigenexecute-actions-email-Link. - Vendor-Audit-Polling-Task
IdpAuditPollerinsrc/kora_platform/services/audit_poller.py: pollt alle 60s die Keycloak-Admin-Events-API, filtert LOGIN-Events dervendor-*-User und sendet E-Mail anKORA_AUDIT_RECIPIENT_EMAIL. Redis-Cursor unteravs:audit:last_event_ts. - MailHog als Dev-SMTP (Service
kora-platform-mailhog): SMTP intern imkora-platform-net(kein Host-Port), Web-UI auf 8239. - NPMplus-Runbook für
auth.kora.luki-net.orgunterdeployment/npmplus-auth-subdomain.md. - Patch-Script
infra/keycloak/patch-dev-redirects.shergänzthttp://localhost:8280/*als Dev-Redirect-URI ohne die Produktions-Config anzufassen.
Changed¶
- ENV-Migration in
.env.platform:KEYCLOAK_ADMIN_USER/_PASSWORD→KC_BOOTSTRAP_ADMIN_USERNAME/_PASSWORD,KEYCLOAK_DB_PASSWORD→KC_DB_PASSWORD/KC_DB_USERNAME(Keycloak-26-Standard). Neue VarsKC_SMTP_HOST,KC_SMTP_PORT,KC_SMTP_FROM,KC_SMTP_AUTH,KORA_AUDIT_RECIPIENT_EMAIL. - Keycloak-Init-Verzeichnis:
infra/keycloak/kora-platform/(leerer Platzhalter) →infra/keycloak/realms/mit beiden Realm-JSONs. Compose-Mount-Pfad entsprechend angepasst. pyproject.toml:click>=8.1undhttpx>=0.27als Runtime-Deps aufgenommen, neuer Entry-Pointkora-platform→kora_platform.cli.bootstrap:cli.
[0.0.1] — Block 1 (12ea700, 2026-04-20)¶
Added¶
- Tenant-Datenmodell mit 18 Tabellen (
src/kora_platform/db/models/). - Alembic-Migrationen 0001–0003 (Schema, Trigger, RLS).
- Drei PostgreSQL-Rollen:
kora_platform_migrator(BYPASSRLS),kora_platform_app(RLS-geschützt),kora_platform_vendor(BYPASSRLS, auditiert). - Separate Alembic-Struktur unter
alembic.ini.platform+alembic/platform/.