Zum Inhalt

Code-Review — Block 7.1b (Operator-UI Frontend)

Branch: platform/block-7-1b-operator-ui-frontend gegen platform/v1.0.0. 5 Commits, 56 Dateien (+4578/-21), Frontend-only. Merge-Blocker-Regel: Findings ≥ 80 vor Merge fixen, < 80 deferred.

Summary

ID Sev Datei:Zeile Titel Entscheidung
Keine Merge-Blocker
M1 65 useAuth.ts:46–57 E2E-Seed ohne Build-Flag Deferred
M2 60 TenantsCreatePage.vue:76–88 Slug-Check via ILIKE-Substring Deferred
M3 55 TenantsDetailPage.vue:131–149 Tab-Row A11y (<div> statt <button>) Deferred
L1 45 Create/Edit-Pages ~50 % Form-Markup-Overlap Deferred
L2 40 useTenants.ts:26–127 Kein Stale-While-Revalidate Deferred
L3 35 useConfirm.ts:31–46 Singleton resolved Erstdialog mit false Deferred
L4 30 TenantsEditPage.vue:75–84 PATCH-Diff ohne Trim Deferred
L5 25 frontend/operator-ui/PORTING.md Pre-Flight-Artefakt im Repo Deferred

Verifications grün: Vitest 41/41, Playwright 2/2, Backend 55/55, Smoke 12/12, make redeploy-platform exit 0, /admin/operator/ 200, Bundle 130 KB raw / 55 KB gzip (Target < 500 KB). Plan-Alignment sauber: Scope-Boundaries (kein tenant-ui-Refactor, kein 7.2/7.3/7.4-Code, kein NPMplus-Rollout, kein eigenes Design-System) eingehalten.

Critical (≥80)

Keine. Die acht Auftraggeber-Fokus-Punkte:

  1. E2E-Seed (1): XSS-Vektor erweitert den Blast-Radius nicht (Angreifer hätte ohnehin Token-Zugriff). DOM-Clobbering ohne Script-Execution wäre theoretisch möglich, aber Operator-UI ist hinter Keycloak + NPMplus. → M1.
  2. Slug-TOCTOU (2): Backend 409 ist Wahrheit; Substring-Match wird client-seitig per t.slug === value gefiltert, aber limit=5 schneidet ggf. den Exact-Match weg. UX-Tropf, kein Datenrisiko. → M2.
  3. No-Cache (3): Operator-Liste klein, Re-Fetch < 200 ms warm. → L2.
  4. Form-Duplication (4): Create/Edit haben unterschiedliche Validierungs-Semantik (Slug-Async vs. Dirty-Tracking); DRY wäre verfrüht. → L1.
  5. useConfirm Singleton (5): Aktuell kein paralleler Caller. → L3.
  6. Tab-A11y (6): <div> mit cursor: help ist tastatur-blind. → M3.
  7. PATCH-Diff (7): Vergleich per !==, Trim nur im Payload-Build — Whitespace-Anhängen erzeugt {notes: null}-PATCH. → L4.
  8. PORTING.md (8): Pre-Flight-Rationale, Halbwertszeit gering. → L5.

Deferrable (<80)

M1 — E2E-Seed-Hook ohne Build-Flag (Sev 65)

Datei: useAuth.ts:46–57 Problem: applyE2eSeed() wird unconditionally beim Modul-Load ausgeführt; einzige Schutzschicht ist die Annahme, dass window.__KORA_E2E_SEED__ in Prod nie gesetzt wird. DOM-Clobbering (HTML-Injection ohne Script-Execution) könnte das Feld auf Browsern mit ID-as-Global-Property setzen. Fix: Hinter import.meta.env.VITE_E2E_MODE === "1" gaten — Tree-Shake eliminiert den Hook im Prod-Build. Playwright-Setup um VITE_E2E_MODE=1-Env erweitern. Rationale: Operator-UI hinter Keycloak + NPMplus, DOM-Clobbering setzt bereits kompromittierte HTML-Injection voraus. Trigger: Block 14 (NPMplus-Rollout) konfiguriert ohnehin den Prod-Build-Pipeline-Pass.

M2 — Slug-Check via ILIKE-Substring (Sev 60)

Datei: TenantsCreatePage.vue:76–88 Problem: ?search=acme matched per ILIKE auch acme-gmbh. Mit limit=5 kann der Exact-Match auf Page 2 landen → false-positive "verfügbar" → 409 beim Submit. Fix: Backend-Endpoint GET /platform/tenants/availability?slug= (Exact-Lookup, inkl. soft-deleted). Bis dahin clientseitig limit=64 oder strenge data.total === 1 && exact-Prüfung. Rationale: Backend-409 ist Wahrheit; UI-Indicator nur Hint. Trigger: Block 7.3 braucht ohnehin strukturierte Availability-Endpoints.

M3 — Tab-Row A11y (Sev 55)

Datei: TenantsDetailPage.vue:131–149 Problem: Disabled-Tabs als <div class="tab--disabled"> ohne role/aria-disabled, nicht fokussierbar, Tooltip mausabhängig. Fix: <button type="button" disabled :aria-disabled="true">, aktiver Tab :aria-current="page". CSS bleibt. Rationale: Phase-A nur GTS intern. Trigger: A11y-Pass im Phase-A-Abschluss vor v1.0.0-RC.

L1 — Form-Markup-Duplication (Sev 45)

Datei: TenantsCreatePage.vue:143–202TenantsEditPage.vue:150–198 Problem: ~120 LOC strukturelle Überlappung. Fix: <TenantForm mode="create|edit">-Abstraktion erst nach Block 7.3, wenn Modul-/Paket-Forms ähnliches Muster brauchen. Rationale: Premature DRY. Validierungs-Semantik unterschiedlich.

L2 — Kein Stale-While-Revalidate (Sev 40)

Datei: useTenants.ts:26–127 Problem: Jede List-Navigation re-fetched komplett (Skeleton statt Stale-Render). Fix: Module-Level-Cache mit lastLoadedAt, oder Tanstack-Query in 7.3. Rationale: Liste klein. Trigger: Block 7.3 Modul-/Paket-Listen.

L3 — useConfirm Auto-Reject (Sev 35)

Datei: useConfirm.ts:31–46 Problem: Zweiter ask() resolved den ersten mit false — Aufrufer sieht "User abgelehnt" obwohl es UI-State-Bug war. Fix: Zweiten ask() mit Promise.reject abweisen, oder Dialog-Stack. Rationale: Aktuell kein paralleler Caller. Trigger: Block 7.4 Bulk-Actions + Row-Confirm.

L4 — PATCH-Diff ohne Trim (Sev 30)

Datei: TenantsEditPage.vue:75–84 Problem: Diff per !== ohne Trim, Payload mit .trim() || null — Whitespace-Anhängen erzeugt {notes: null} und löscht den Originalwert. Fix: Diff auf getrimmten Werten: form.value.notes.trim() !== (initial.value.notes ?? "").trim(). Rationale: Edge-Case. Trigger: nächster Block, der Edit-Page anfasst.

L5 — PORTING.md im Repo (Sev 25)

Datei: frontend/operator-ui/PORTING.md Problem: 124 Z. Pre-Flight-Rationale, Halbwertszeit gering. Fix: Nach docs-kora/docs/blocks/block-7-1b-vorbereitung.md oder Inhalt in README.md konsolidieren. Rationale: Kosmetik. Trigger: Phase-A-Abschluss-Doku-Pass.

Looks good

  • Auth-Port (useAuth.ts): saubere Spiegelung tenant-ui, Memory-Only Token, PKCE, State-Validation. Neuer hasOperatorRole-Computed liest realm_access.roles defensiv (Array.isArray, drei Role-Aliases).
  • Router-Guard (router/index.ts:70–80): zwei-stufige Kette (requiresAuth → login, requiresOperator → /403) ist die richtige UX-Trennung; Backend bleibt Source-of-Truth.
  • 422-Detail-Mapping (Create/Edit): Pydantic-detail[].loc[-1] → field-errors, sauber.
  • Slug-Validator-Quelle (useSlugValidator.ts:1–2): Regex aus models/tenant.py gespiegelt mit Top-of-File-Comment — best practice für Frontend-Backend-Kontrakt-Doku.
  • DataTable-Generic (DataTable.vue): <T extends { id }>, Slot-basiert, explizite Verzicht-Doku auf Sort/Filter/Virtualize spart 200+ LOC Premature-Generality.
  • Build-Integration (Dockerfile.platform, main.py:180–196): exakte Replikation tenant-ui-Pattern, optionale Mounts (skip wenn Bundle fehlt) halten Pytest-Bootstrap funktionsfähig.
  • Bundle-Disziplin: 130 KB / 55 KB gzip bei vollem CRUD + Auth + Toast + Confirm + DataTable. Verzicht auf Pinia zahlt sich aus.
  • Test-Ratio: 4 Page-Specs mit gemockter useApi + 2 Playwright-Specs gegen Backend-Mount — angemessener Vitest:Playwright-Cut für SPA-Scope.

Deferred-Items nach offene-todos.md: TODO-B7-1b-01 (M1) … TODO-B7-1b-08 (L5), Schema analog Block-7-1a.