Code-Review — Block 7.4 (Audit-Reader + Bulk-Ops + Connectors-Stub)¶
Branch: platform/block-7-4b-audit-bulk-frontend gegen platform/v1.0.0,
inkl. Backend-Merge 61689c2 (7.4a). Zwei Sub-Commits, 23 Dateien
(+2270 / -13). Tests: Backend 26 neu (12 unit + 14 integration), Frontend
12 neu Vitest + 2 neu Playwright. Regression Block 5–7 grün (79/79
Backend, 100/100 Vitest, 8/8 Playwright sequential).
Summary¶
| ID | Sev | Datei:Zeile | Titel | Entscheidung |
|---|---|---|---|---|
| — | — | — | Keine Merge-Blocker | — |
| M1 | 75 | AuditLogPage.vue (gesamt) |
Filter-Shortcut aus TenantsDetailPage wirkt nicht — ?entity_id=… aus Route wird nicht gelesen |
Deferred (UX-Fix Block 7.5 oder fast-follow) |
| M2 | 70 | module_service.py:213–268, tenant_service.py:213–223 |
Bulk-Loop ohne Max-Cap und ohne Batch-Hydration (N round-trips) | Deferred (Limit-Hardening Phase D) |
| M3 | 65 | audit_service.py:96–168 |
CSV-Export mappt actor_keycloak_id / ip_address / session_id nicht in den Header |
Deferred |
| M4 | 60 | operator_tenants.py:329, tenant_modules.py:312 |
Bulk-Audit-Auffindbarkeit: entity_id="bulk:N" schließt sich gegenseitig aus mit entity_id=<tenant.id>-Filter |
Deferred (Konvention dokumentieren) |
| M5 | 55 | types/audit.ts:42–52 |
KNOWN_ACTIONS ist kuratierte Liste, hat keine Connector/Template/License-Actions |
Deferred (Block 13/14 erweitern) |
| L1 | 40 | playwright.config.ts:19 |
workers: 1 als Pragma |
Deferred (akzeptiert) |
| L2 | 35 | operator_audit.py:131 |
datetime.utcnow() (Py3.12 deprecated) |
Deferred |
| L3 | 30 | operator_audit.py Filter |
Keine Validierung date_from <= date_to |
Deferred |
| L4 | 25 | TenantsListPage.vue / TenantModulesSection.vue |
Bulk-UI ohne Progress-Indicator über Toast-Latenz | Deferred (akzeptiert für v1.0.0) |
Plan-Alignment sauber: Pfad-Split 7.4a/7.4b wie in PORTING-7.4.md,
JSONB-Search ohne GIN als TODO mit Trigger > 10k, kein
Connectors-Backend, kein Per-Tenant-Audit-Embed, kein Templates-Bulk.
Atomarität via request_scoped_session + Single-Audit-Entry konsistent
zur Block-5-Cleanup-Konvention.
Critical (≥80)¶
Keine. Atomarität-Pfad gut: TenantNotFound und ModuleNotAssignable
rollen den ganzen Block via Context-Manager zurück (verifiziert über
test_bulk_*_rollback-Pfade). Race-Sicherheit über ON CONFLICT DO
UPDATE aus Block-5-Cleanup wiederverwendet. Scope-Guards
(_require_operator für Tenants-Bulk, _require_operator_or_vendor für
Modules-Bulk + Audit-Reader) konsistent. RLS-Bypass nur, wo der Operator
fremde Tenants legitim sehen muss; expliziter tenant_id-WHERE bleibt
für Defense-in-Depth bestehen.
Decision-Points¶
-
Bulk-Audit Single-Entry vs. N-Entries. Die Lutz-Entscheidung ist für Compliance-Lesbarkeit richtig: ein Reviewer sieht „der Operator hat 17 Tenants in einer Aktion soft-gelöscht" als ein Event mit voller ID-Liste in
details.after.soft_deleted_ids. Der Tradeoff ist die Auffindbarkeit (M4): wer den Audit-Log mitentity_id=<tenant.id>filtert, findet dietenant.bulk_soft_deleted- Zeile nicht, weilentity_id="bulk:17"ist. Empfehlung: Single- Entry behalten, aber im JSON-Search-Filter einen UI-Hint ergänzen („für Bulk-Events nach Tenant-UUID in JSON-Suche fragen, nicht im Entity-ID-Feld") und in der Doku verankern. Skalierungsobergrenze:details.after-Liste wird bei sehr großen Bulks (mehrere tausend IDs) groß — bei aktuellem 50/Page-Limit der Audit-Liste irrelevant, aber CSV-Export mitinclude_details=truekann eine einzelne Zelle in den dreistelligen KB drücken. Akzeptabel für v1.0.0; bei Bedarf einebulk_summary-Hilfstabelle in einem späteren Block. -
Audit-Tab im TenantsDetailPage als Filter-Shortcut. Vertretbar für v1.0.0. Aber M1 ist real: der Button navigiert zu
/audit?entity_id=<tenant.id>, und die Audit-Page liest den Query-Parameter nirgends —useAuditEntries.ts:42initialisiertentity_id: null, keinuseRoute().query.entity_id-Hookup. Damit ist der Shortcut heute ein leerer Klick. Fix ist trivial (eineuseRoute()-Auswertung inAuditLogPage.vueonMounted, plus Aufruf vonsetFilter("entity_id", route.query.entity_id)). Score 75 statt 80 nur, weil der User über das Filter-Feld den Wert manuell einfügen kann — die UX-Erwartung „Klick öffnet bereits gefiltert" ist aber gebrochen. Empfehlung: vor Phase D fixen, nicht nach Block 7-Abschluss vergessen. -
Bulk ohne Progress-Indicator. Akzeptabel. 100 Tenants × ein
session.get+ ein UPDATE liegen unter 500 ms gegen lokales Postgres. Bei 1000+ ändert sich die Wahrnehmung. Da es keinen Max-Cap gibt (M2), könnte ein Operator theoretisch 10000 IDs submitten und die UI hängt für Sekunden. Empfehlung: weiches Cap im Backend (z. B.max_items=500pro Request) parallel zur N+1-Optimierung in Phase D, plus optional<FormButton :loading>reicht visuell. Kein Blocker für v1.0.0. -
workers: 1in Playwright. Pragma, kein Smell. Cross-Test-Isolation per „lege Tenant pro Test, suche per Slug, delete am Ende" funktioniert sequenziell sauber. Parallel-Worker würde dieselbe Tenants-Liste lesen, gegenseitig Rows soft-deleten und die Specs flaky machen — das wäre der eigentliche Smell. Die Test-Suite ist in 8 Specs sequenziell unter ~30 s; der Lauf-Zeit- Overhead ist akzeptabel. Wenn Test-Volumen wächst, ist die richtige Antwortworkers: N+ per-Worker-DB-Schemas, nicht „Tenant-Slugs uniquen und hoffen". Heute korrekt. -
Connectors-Stub liest nichts vom Backend. Genau richtig. Block 13 wird die Operator-API designen; ein zwischenzeitliches Read-Only auf der Bestands-Tabelle würde Felder wie
last_sync_at,error_state,permission_mode, die Block 13 entscheidet, in der UI festklopfen. Hint-Card mit Konnektor-Roadmap-Auszug ist die sauberste Lösung. Defensiv, ja — aber Block 13 ist ein Mehrjahres- Konnektor-Subsystem (Welle 1 v1.0.0 → MediaWiki v1.1.0 → Teams/Slack v1.2.x), und genau diese Defensive verhindert, dass eine Stub-API zur impliziten Spezifikation wird.
Deferrable (<80)¶
- M1 (75) —
?entity_id-Routing-Hookup fehlt. UX-Bug. Fix < 10 LOC. In Block 7.5 oder vor Block 8. - M2 (70) — Bulk-N+1.
module_service.bulk_assign_to_tenantmachtsession.get+INSERT...ON CONFLICT...RETURNINGpro Item. Bei 100 Items sind das 200 Round-Trips.tenant_service.bulk_soft_deletehat dieselbe Form. Refactor: Pre-Validate perWHERE id IN (...)- Bulk-Select, dann ein einzelnesINSERT ... VALUES (...), (...) ON CONFLICT DO UPDATEbzw. einUPDATE tenants SET deleted_at = now() WHERE id IN (...) AND deleted_at IS NULL RETURNING id. Phase D. - M3 (65) — CSV-Export-Felder.
actor_keycloak_id,ip_address,session_idsind in DB + Pydantic + TS-Type, aber der CSV-Header inaudit_service.export_csvlistet sie nicht. Forensisch ärgerlich. Spalten ergänzen, Default-Header-Kompatibilität egal — Excel verkraftet zusätzliche Spalten. - M4 (60) — Bulk-Audit-Auffindbarkeit. Single-Entry-Konvention
dokumentieren (vermutlich in
docs-kora/docs/konzepte/audit.mdoder Runbook), damit der Operator beim Suchen weiß, dass Bulk-Events nicht überentity_idauffindbar sind, sondern überaction=*.bulk_*oder JSON-Search. - M5 (55) —
KNOWN_ACTIONS-Drift. Bei Block 13/14 (Connectors, Lizenz-Audit) als Erweiterungspunkt mitnehmen. - L1 (40) —
workers: 1. Akzeptiert. - L2 (35) —
datetime.utcnow(). Aufdatetime.now(UTC)umstellen, sobald irgendein anderer Bereich des Backends das Warning trifft. - L3 (30) — Date-Range-Validation.
date_from > date_toführt heute zu leerer Result-Liste, kein 422. Pydantic-Validator inAuditListParamseinbauen oder in der Route prüfen. - L4 (25) — Bulk-UI-Progress. Toast-Latenz ist heute nicht spürbar; bei Cap-Hardening (M2) nochmal anschauen.
Looks good¶
- Pfad-Split 7.4a/7.4b sauber durchgehalten — Backend ist unabhängig mergebar, Frontend verlinkt nur auf existierende Routes.
- Atomarität-Pattern aus Block 5 Cleanup (
request_scoped_session+populate_existing=True) konsistent in den neuen Bulk-Pfaden. - Default-Date-Range „letzte 7 Tage" im Composable verhindert
Initial-Full-Scan;
defaultLast7DaysIso()ist UTC-stabil. - CSV-Export mit UTF-8-BOM + Hard-Limit + truncated-Footer ist Excel-tauglich und trunziert sichtbar.
- Selection-Reset bei
state.items-Watch inTenantsListPage.vuesowie beitenantId-Watch inTenantModulesSection.vueschließt die klassische „IDs ausgewählt, die nicht mehr in der Liste sind"-Falle. - Filter-Toolbar mit 300-ms-Debounce auf Free-Text-Inputs hält den Server-Hit-Druck niedrig.
- Connectors-Stub mit Roadmap-Verlinkung ist die richtige Defensive gegen Block-13-Präjudiz.
- Pre-Flight-Discovery-Pflicht (
PORTING-7.4.mdmit Pfad- Entscheidung + Performance-Hinweisen) konsequent eingehalten und ist inzwischen Standard-Praxis im Platform-Branch.
Empfehlung¶
Merge nach platform/v1.0.0 mit Commit-Message-Footer „Code review:
9 issues found, 0 fixed, 9 deferred (<80)". M1 als Fast-Follow (10 LOC
in AuditLogPage.vue onMounted) idealerweise vor dem Block-7-
Abschluss-Tag, um den Audit-Tab-Filter-Shortcut funktional zu machen
— sonst verspricht die UI etwas, was sie heute nicht liefert.