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:
-
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".
-
useModuleclientseitig (Composable): Bei aktuell 2 Modulen (<50realistisch ü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. mitdescriptionundusage_count), trivialer Swap aufapi.get(/platform/modules/${id}). → M1. -
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". -
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). -
/admin/operator/modules-Pfad: Konsistent mit/tenantsund/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.
L4 — Footer-Text "Block 7.1b" veraltet (Sev 30)¶
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:44filtertinternal_onlyclientseitig zusätzlich zum Backend- Filter. Korrekte Doppelsicherung — falls Vendor-Token mal versehentlich operator-route trifft. - Always-On-Logik:
is_always_onrendert "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 undaria-disabledfü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.