Zum Inhalt

Code-Review — Block 7.3 (Modules-Registry + Per-Tenant-Toggle)

Branch: platform/block-7-3-modules-and-packages gegen platform/v1.0.0. 3 Commits, 15 Dateien (+1718/-17), Frontend-only. Backend (Block 5/6 Module-Routes) unverändert; Pre-Flight (Path A, Variante C/A-Hybrid) in PORTING-7.3.md dokumentiert.

Verifications: Vitest 89/89, Playwright 6/6, Backend 55/55, smoke-block7-1a 12/12, npm run build 64 KB gzip (+8 KB), npm run type-check exit 0, make redeploy-platform grün.

Summary

ID Sev Datei:Zeile Titel Entscheidung
Keine Merge-Blocker
M1 60 useModule.ts:18–35 Detail-Lookup lädt komplette Liste Deferred
M2 55 TenantModulesSection.vue:36–53 Clientseitiger Set-Diff mit zwei API-Calls Deferred
M3 50 TenantModulesSection.vue:107–119 assign() + tenant.load() ohne Optimistic-Update Deferred
L1 45 useTenantModules.ts:34–43 Mutation-Calls ignorieren Errors auf Composable-Ebene Deferred
L2 40 Tenants-/Templates-/Modules-Pages EntityForm-Refactor — finaler Verzicht Deferred (final)
L3 35 TenantModulesSection.vue:198 enabled_by-Truncation auf 8 Char ohne Tooltip Deferred
L4 30 BaseLayout.vue:63 Footer hartkodiert auf "Block 7.1b" Deferred

Plan-Alignment sauber: Scope-Boundaries (kein Tier/Limits-Editor, kein EntityForm-Refactor, kein Module-CRUD, kein Cross-Tenant-Aggregat-View, kein RLS-Update) gehalten. Pre-Flight-Discoveries (Variante C im Code vs. A im Konzept, tenant_packages ohne Service/API/Enforcement, bare list-Antwort, kein Detail-Endpoint) klar dokumentiert; Tier/Limits korrekt nach Block 12 vertagt. ID/Scope-Routing konsistent mit Tenants/Templates (/admin/operator/{tenants,templates,modules}).

Critical (≥80)

Keine. Die fünf Auftraggeber-Fokus-Punkte:

  1. EntityForm-Refactor-Final-Decision: 7.3 baut keine Form, sondern einen Toggle. Der "dritte Datenpunkt" ist damit kein Form-Datenpunkt und triggert den Refactor nicht. Empfehlung: jetzt final verwerfen — nicht weiter aufschieben. Begründung: Tenants und Templates sind die einzigen Form-CRUDs auf Sicht; Module-Toggle, geplante Tier/Limits (Block 12) und künftige Connector-Configs (Block 13) haben jeweils ein eigenes Form-Shape (Sliders, JSON-Schema-Driven, OAuth-Flows). Drei sehr ähnliche Forms entstehen damit nie. → L2 markiert als finales Defer mit Trigger "wird nicht mehr aufgegriffen".

  2. useModule clientseitig (Composable): Bei aktuell 2 Modulen (<50 realistisch über Sicht von Block 12) ist ein zweiter Roundtrip für /modules/{id} nicht gerechtfertigt — der Listen-Call ist eine bare List ohne Pagination und cached günstig. Wenn das Backend einen Detail-Endpoint bekommt (z. B. mit description und usage_count), trivialer Swap auf api.get(/platform/modules/${id}). → M1.

  3. TenantModulesSection clientseitiger Merge: Berechtigt, weil Backend zwei orthogonale Sichten exposed (Plattform-Registry + Tenant- Scope) und keinen Aggregat-Endpoint hat. Bei < 50 Modulen pro Tenant ist Map-Lookup O(n) trivial. Risiko bei n > 200: zwei sequenzielle Calls + Re-Render. → M2 mit Trigger "Module > 50".

  4. Tab-A11y-Side-Reward (<div><button> in TenantsDetailPage): Kein Scope-Bruch — die Tab-Aktivierung war Pflichtteil von 7.3, und der A11y-Fix ist eine 5-Zeilen-Änderung mit :focus-visible-Style, die direkt im selben Diff sinnvoll war. Willkommenes Side-Reward; TODO-Block-7-1b-03 darf jetzt closed werden (im offene-todos.md abhaken, nicht still überschreiben).

  5. /admin/operator/modules-Pfad: Konsistent mit /tenants und /templates, alle drei sind Operator-Top-Level-Resources. Kein Anlass zur Hinterfragung — /admin/operator/* ist die Vue-Router- Base und reflektiert die Keycloak-Role-Boundary. Ein zusätzlicher /platform/-Präfix wäre Doppelung (Backend hat ihn bereits; UI darf einfacher sein). Kein Finding.

Deferrable (<80)

M1 — useModule lädt komplette Liste statt Detail-Endpoint (Sev 60)

Datei: frontend/operator-ui/src/composables/useModule.ts:18–35 Problem: load() ruft GET /platform/modules und filtert mit data.find(m => m.id === id). Bei wachsender Modul-Anzahl wird der Detail-View teurer als nötig (kompletter List-Body über die Leitung). Die not-found-Logik versteckt 200 OK + leeres Array genauso wie 200 OK mit Treffer — kein Cache-Sharing mit useModules. Fix: Wenn Backend einen GET /platform/modules/{id}-Endpoint bekommt, auf den umstellen. Alternativ: einen useModulesCache-Singleton einführen, den useModule und useModules teilen (TTL ~5 min). Rationale: Heute 2 Module live, < 50 erwartet bis Block 12. Die Pre-Flight-Begründung ("billiger als ein neuer Backend-Endpoint") ist zutreffend. Trigger: Modul-Anzahl > 50 oder Backend bekommt Detail- Endpoint mit zusätzlichen Feldern (z. B. usage_count).

M2 — Clientseitiger Set-Diff platform_modules ∖ tenant_modules (Sev 55)

Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:36–53 Problem: Zwei sequenzielle Roundtrips (/platform/modules + /tenants/{id}/modules) plus clientseitiger Map-Merge. Bei N Tenants in einer Operator-Session werden die Plattform-Module N mal neu geladen (kein Composable-Cache). Wachstum: N · M Anfragen statt N + 1. Fix: Kurzfristig — Plattform-Module beim Operator-Login einmal in einen pinia-Store oder Singleton laden, in useModules und TenantModulesSection daraus lesen. Mittelfristig — Backend-Endpoint GET /tenants/{id}/modules?include_unassigned=true hinzufügen, der das Joining serverseitig macht (Konzept §8.3 würde davon profitieren). Rationale: Bei < 50 Modulen und < 100 Tenants sub-second-performant. Trigger: Operator-Audit-Session mit > 20 Tenant-Wechseln in 5 Minuten.

M3 — Toggle ohne Optimistic-Update + Re-Fetch (Sev 50)

Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:107–119, 121–140 Problem: assign()await tenant.load() re-fetcht die komplette Tenant-Module-Liste nach jeder Mutation. UX-Lag ~150–400 ms; bei schwacher Latenz sieht der Operator zuerst den ungeänderten Status. Kein is_optimistic-Flag, keine inflight-Markierung außer togglingId. Fix: assign()-Response enthält is_enabled + enabled_by — direkt in tenant.items.value mergen, statt re-laden. Re-Load nur auf Error-Recovery. Rationale: API-Response liefert genug Daten für lokale State-Update; Re-Fetch ist defensiv aber unnötig. Kein Datenverlustrisiko, nur UX. Trigger: Operator-User-Feedback "Toggle fühlt sich träge an".

L1 — Mutation-Methods schlucken kein API-Detail (Sev 45)

Datei: frontend/operator-ui/src/composables/useTenantModules.ts:34–43 Problem: assign() und revoke() reichen Fehler ungewrapt nach oben durch (Component fängt mit (err as Error).message). Bei 422 mit Pydantic-Loc-Detail (z. B. module_id ist internal_only) wird der Toast generisch "API-Fehler" zeigen, weil useApi.post den Body nicht parst. Fix: Im Composable try/catch mit 422-Field-Extraction analog TenantsCreatePage (entry.loc[entry.loc.length - 1]). Rationale: Backend liefert bei scope='core'/internal_only einen 400, kein 422 (laut PORTING-7.3.md §"Backend-Vertragspunkte"), und die UI filtert internal_only ohnehin bevor der Toggle erscheint. Trigger: Wenn Backend Validierungs-Pfad differenziert (Block 12).

L2 — <EntityForm>-Refactor: Final verwerfen (Sev 40)

Datei: Tenants-Create+Edit, Templates-Create+Edit, Modules (kein Form) Problem: Tenants ~120 LOC, Templates ~200 LOC. 7.3 trägt keine weitere Form bei (Module-Toggle ist Click). Block 12 (Tenant- Provisioning) wird vermutlich einen User-Form bringen, aber mit anderem Shape (Email-Validation, Role-Picker, optional Init-Password). Block 13 (Connectors) bringt Schema-Driven-Forms. Empfehlung: Endgültig verwerfen. Die Hypothese "drei sehr ähnliche CRUDs" trägt nicht — jede künftige Form hat eigenes Shape. <ListInput> und <FormInput> als geteilte Primitives reichen aus. TODO-Block-7-1b-04 und TODO-Block-7-2-05 in offene-todos.md als "abgeschlossen — Refactor verworfen" markieren, nicht mehr offen halten. Rationale: Premature DRY ist teurer als zwei (oder drei) klar lesbare Edit-Pages. Trigger: Keiner mehr — das Refactor-Fenster ist geschlossen.

L3 — UUID-Truncation auf 8 Chars ohne Tooltip (Sev 35)

Datei: frontend/operator-ui/src/components/TenantModulesSection.vue:198 Problem: enabled_by.slice(0, 8) zeigt 11111111… ohne title- Attribut. Operator kann nicht erkennen, welcher User das Modul aktiviert hat — die UUID-Präfixe sind in Test-Setups absichtlich identisch. Fix: <span :title="row.tenant_state.enabled_by">…</span> für Hover-Detail. Mittelfristig: Backend liefert enabled_by_email oder enabled_by_username zusätzlich. Rationale: Cosmetic; in Produktion sind UUIDs eindeutig genug. Trigger: Audit-Use-Case in Block 7.4.

Datei: frontend/operator-ui/src/layouts/BaseLayout.vue:63 Problem: Operator-UI (Block 7.1b) — seit 7.2 und jetzt 7.3 nicht mit-aktualisiert. Suggestion an Operator, der UI wäre auf 7.1b-Stand. Fix: Entweder dynamisch aus package.json-Version oder hartkodiert auf Block 7.3. Saubere Variante: Build-Zeit-Inject mit vite define. Rationale: Reine Cosmetics, kein Operator-Bug. Trigger: Phase-A- Abschluss-Refactor (Block 16 Docs).

Looks good

  • PORTING-7.3.md: Pre-Flight-Discovery in der gleichen Tiefe wie 7.1b/7.2 — tenant_packages-Lücke (Code/Vision-Drift) sauber herausgearbeitet, Variante-C-Wahl mit Konzept-Querverweis begründet, Tier/Limits ehrlich nach Block 12 verschoben statt halb-implementiert.
  • Defense-in-depth-Filter: TenantModulesSection.vue:44 filtert internal_only clientseitig zusätzlich zum Backend- Filter. Korrekte Doppelsicherung — falls Vendor-Token mal versehentlich operator-route trifft.
  • Always-On-Logik: is_always_on rendert "fixiert" + Always-On- Badge, Buttons werden gar nicht erst gezeigt — kein Risiko, dass Operator versehentlich ein Core-Modul abschalten kann.
  • activeTab-State (TenantsDetailPage): <button>-Tabs mit :focus-visible-Outline und aria-disabled für die noch leeren Tabs (Audit, Connectors). Side-Reward für TODO-Block-7-1b-03.
  • Vitest-Coverage: 5+5+5 mit klarer Trennung — List-Page (Filter, Empty, Error, Badges), Detail-Page (Found/NotFound/Schema/Hint), TenantModulesSection (Render, Filter, Always-On, Mutation, Footer). Kein Test-Smell.
  • Playwright module toggle: navigate → activate → deactivate: Voller Zyklus mit frischem Tenant, Confirm-Dialog, Status-Badge- Wechsel — eine seltene Coverage-Tiefe für Toggle-E2E.
  • Bundle-Δ: +8 KB gzip für drei neue lazy chunks (List, Detail, TenantModulesSection) ist im Rahmen.

Empfehlung: Merge-ready. Sieben Deferred-Items unter Score 80, alle mit konkretem Trigger (außer L2, das final geschlossen wird). In offene-todos.md als TODO-Block-7-3-02 bis TODO-Block-7-3-08 aufnehmen (TODO-Block-7-3-01 ist bereits für Tier/Limits/Block 12 reserviert). TODO-Block-7-1b-03 (Tab-A11y) und TODO-Block-7-1b-04 + TODO-Block-7-2-05 (EntityForm) als abgeschlossen markieren.