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:
- 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.
- Slug-TOCTOU (2): Backend 409 ist Wahrheit; Substring-Match wird
client-seitig per
t.slug === valuegefiltert, aberlimit=5schneidet ggf. den Exact-Match weg. UX-Tropf, kein Datenrisiko. → M2. - No-Cache (3): Operator-Liste klein, Re-Fetch < 200 ms warm. → L2.
- Form-Duplication (4): Create/Edit haben unterschiedliche Validierungs-Semantik (Slug-Async vs. Dirty-Tracking); DRY wäre verfrüht. → L1.
useConfirmSingleton (5): Aktuell kein paralleler Caller. → L3.- Tab-A11y (6):
<div>mitcursor: helpist tastatur-blind. → M3. - PATCH-Diff (7): Vergleich per
!==, Trim nur im Payload-Build — Whitespace-Anhängen erzeugt{notes: null}-PATCH. → L4. - 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–202 ↔ TenantsEditPage.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. NeuerhasOperatorRole-Computed liestrealm_access.rolesdefensiv (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 ausmodels/tenant.pygespiegelt 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.