Code-Review — Block 7.2 (Templates-CRUD-UI)¶
Branch: platform/block-7-2-templates-crud gegen platform/v1.0.0.
3 Commits, 20 Dateien (+2515/-3), Frontend-only. Backend (Block 5
/api/v1/operator/templates*) unverändert; Pre-Flight (Path A) in
PORTING-7.2.md dokumentiert.
Verifications: Vitest 74/74, Playwright 4/4, Backend 55/55, smoke-block7-1a
12/12, npm run build 130 KB raw / 56 KB gzip, npm run type-check exit 0.
Summary¶
| ID | Sev | Datei:Zeile | Titel | Entscheidung |
|---|---|---|---|---|
| — | — | — | Keine Merge-Blocker | — |
| M1 | 60 | ListInput.vue:80–98 |
A11y — kein Keyboard-Reorder, kein Live-Region | Deferred |
| M2 | 55 | useTemplates.ts:39–54 |
Clientseitige Suche skaliert nicht > ~200 Items | Deferred |
| M3 | 55 | TemplatesEditPage.vue:88–91, 128–134 |
listsEqual ignoriert Reorder |
Deferred |
| L1 | 45 | TemplatesCreatePage.vue:78–87 |
[] immer im POST, nie weggelassen |
Deferred |
| L2 | 40 | Templates-/Tenants-Pages | EntityForm-Refactor — Trigger jetzt sinnvoll | Deferred |
| L3 | 35 | helpers.ts:65–70 |
Busy-Wait-Sleep im Token-Mint-Retry | Deferred |
| L4 | 30 | TemplatesEditPage.vue:98, 102 |
Trim-Diff bei description/shared_collection_name inkonsistent zur Tenants-Edit |
Deferred |
Plan-Alignment sauber: Scope-Boundaries (kein Code-Editor, kein
EntityForm-Refactor, kein RLS-Update, kein tenant-ui-Refactor) gehalten.
Pre-Flight-Discoveries (global statt tenant-scoped, is_active=false
statt deleted_at, Operator-Prefix statt Platform-Prefix, bare
list[]-Antwort) sauber dokumentiert; alle Backend-Annahmen aus dem
Prompt im Commit-1 korrigiert. ID-Regex (useTemplateIdValidator)
spiegelt Backend exakt inkl. Underscore-Erlaubnis und 1-Char-Edge-Case.
Critical (≥80)¶
Keine. Die fünf Auftraggeber-Fokus-Punkte:
<ListInput>A11y (1): Tab durch Items + Buttons funktioniert, Enter im Draft-Input addiert, Buttons habenaria-label="Eintrag N entfernen". Was fehlt: einaria-live-Region für Add/Remove-Feedback und Keyboard-Reorder (Shift-Arrow). Templates haben < 10 Items, daher Reorder-UX kein Block. → M1.- Clientseitige Suche (2):
toLowerCase().includes()über die bereits geladene Liste; bei 40 Items unter 1 ms, bei 200 noch < 5 ms, ab ~500 spürbar. Kein akutes Risiko, weil das Backend kein?search=-Param ableitet. → M2. - PATCH-Diff Reorder (3):
listsEqual([a,b], [b,a])→ true → PATCH-Payload enthält das Feld nicht → Reorder schlägt nicht durch. Snapshot-relevante Felder (suggested_suggestions) sind betroffen, aber Reorder-UI fehlt ohnehin (kein Drag-and-Drop in 7.2). → M3. - Token-Cache (4): Module-State in einem Helper, der pro Playwright- Worker als eigener Node-Prozess läuft → kein Cross-Worker-Sharing, und genau das ist gewollt. Saubere 60-s-TTL, 3-fach-Retry mit linearem Backoff. Korrekt — kein Finding.
- Empty
suggested_suggestions(5):TemplatesCreatePage.vue:83schickt das Feld immer mit (form.value.suggested_suggestions, default[]). Backend-Default ist[], also semantisch identisch zum Weglassen, aber nicht "omit". Verbose, nicht falsch. → L1.
Deferrable (<80)¶
M1 — <ListInput> ohne Live-Region und Keyboard-Reorder (Sev 60)¶
Datei: frontend/operator-ui/src/components/ListInput.vue:80–98
Problem: Add/Remove signalisiert keine Statusänderung an Screen-Reader;
Tab landet nach Remove auf dem Draft-Input (Standard-Verhalten der Browser),
das ist akzeptabel, aber kein expliziter Focus-Move. Kein Keyboard-Reorder
(Shift-Arrow), Reorder per Drag-and-Drop bewusst ausgespart.
Fix: <div role="status" aria-live="polite"> neben der Liste,
nach add() "Eintrag hinzugefügt" und nach removeAt() "Eintrag
entfernt" setzen. Reorder mit Shift-Arrow-Up/Down auf jedem
list-item-input.
Rationale: A11y-Audit ist Block 18-Scope (geplant nach Phase D);
Templates-Listen haben < 10 Items, Reorder-Use-Case ist hypothetisch.
M2 — Clientseitige Suche skaliert nicht (Sev 55)¶
Datei: frontend/operator-ui/src/composables/useTemplates.ts:39–54
Problem: applySearch() filtert allItems mit String.includes(). Bei
~200 aktiven Templates wird das Tippen spürbar. Backend exposed kein
?search=, aber Operator könnte über Reactivate-Flow inaktive Templates sammeln, die mitgeladen werden, sobaldactive_only=false. Fix: Threshold beiallItems.value.length > 200einen Toast oder Hinweis zeigen, alternativ Backend-Search als Block-7.4-Folge-Item einplanen. Im Backend ein?search=mitILIKEaufidunddisplay_namemitLIMIT 100ist trivial. Rationale: Templates-Bestand wächst eher in Jahren als in Wochen. Trigger: erstes Tenant-Onboarding mit > 50 Templates.
M3 — listsEqual ignoriert Reordering (Sev 55)¶
Datei: frontend/operator-ui/src/pages/TemplatesEditPage.vue:88–91, 128–134
Problem: listsEqual([a,b], [b,a]) → true. Wenn die UI später Reorder
erlaubt (Block 7.4 oder Drag-and-Drop in <ListInput>), kommt die
Reihenfolge im PATCH-Body nicht an, der version_counter bumpt nicht,
Tenants sehen den Update-Hinweis nicht. Aktuell kein Schaden, weil
<ListInput> Reorder nicht erlaubt — aber das Komponenten-Contract
ist falsch.
Fix: dirty-Vergleich per JSON.stringify(a) !== JSON.stringify(b)
oder Index-vergleich aufgeben und das Feld immer ins PATCH-Payload
aufnehmen, wenn der Nutzer in <ListInput> interagiert hat.
Rationale: Reorder-UX ist nicht in 7.2-Scope. Trigger: Erste
Reorder-Funktion (Block 7.4 oder Connector-Reordering).
L1 — Empty Arrays werden immer mitgeschickt (Sev 45)¶
Datei: frontend/operator-ui/src/pages/TemplatesCreatePage.vue:78–87
Problem: Auch wenn der Nutzer keine Suggestion und keinen Connector
eintippt, schickt der POST suggested_suggestions: [] und
recommended_connectors: []. Das Backend-TemplateCreate-Pydantic-Schema
akzeptiert das (Default ist ohnehin []), aber semantisch wäre "Feld
omitten" sauberer.
Fix: Vor dem POST if (form.value.suggested_suggestions.length === 0)
delete payload.suggested_suggestions; (analog für Connectors).
Rationale: Verbose, nicht falsch. Keine Audit-Log-Differenz,
keine Migration-Fallstricke.
L2 — <EntityForm>-Refactor — Trigger-Bewertung (Sev 40)¶
Datei: Tenants-Create+Edit, Templates-Create+Edit
Problem: Form-Markup-Overlap: Tenants ~120 LOC, Templates ~200 LOC
(8 vs. 4 Felder). <ListInput> ist bereits ein erster Refactor-Schritt
(Liste-Edit war doppelt vorhanden). PATCH-Diff-Logik (isDirty,
listsEqual, Field-Level-Diff) ist in beiden Edit-Pages dupliziert.
Empfehlung: Deferred bis nach 7.3 Modules-CRUD. Begründung:
Module-Forms haben eine andere Schema-Form (config-JSON-Editor,
enabled-Toggle), das wären drei verschiedene Form-Shapes statt drei
gleicher. Der EntityForm-Refactor wird wertvoll, sobald drei sehr
ähnliche CRUDs existieren — Modules wird das wahrscheinlich nicht sein.
Nach 7.3 entscheiden, ob Refactor sich lohnt oder ob <ListInput> als
einziges geteiltes Primitive ausreicht. → TODO-B7-2-04.
Rationale: Premature DRY ist teurer als zwei Edit-Pages. Trigger:
Vier identische Form-Shapes (vermutlich erst Block 8 Connector-Configs).
L3 — Busy-Wait-Sleep im Token-Mint-Retry (Sev 35)¶
Datei: frontend/operator-ui/e2e/helpers.ts:65–70
Problem: while (Date.now() - start < 800 * attempt) { /* spin */ }
blockiert den Event-Loop für 800–1600 ms. Im Worker-Prozess unkritisch,
aber CPU-Verschwendung.
Fix: Sync-Sleep durch Atomics.wait(new Int32Array(new
SharedArrayBuffer(4)), 0, 0, 800) ersetzen oder den Helper auf async
umstellen (alle Caller sind ohnehin in test.beforeEach).
Rationale: E2E-Helper, kein Hot-Path. Trigger: Wenn der Helper aus
dem Operator-UI noch in andere UIs portiert wird.
L4 — Trim-Diff inkonsistent (Sev 30)¶
Datei: frontend/operator-ui/src/pages/TemplatesEditPage.vue:98, 102
Problem: description.trim() !== i.description.trim() und
shared_collection_name.trim() — Trim auf beiden Seiten des Vergleichs.
In TenantsEditPage.vue:62 wird notes ohne Trim verglichen (siehe
TODO-B7-1b-04 / L4 dort). Die Templates-Variante ist robuster,
aber inkonsistent.
Fix: TenantsEditPage an Templates-Pattern angleichen (Trim auf
beiden Seiten + im Payload-Build).
Rationale: Verbessertes Pattern hier, Tenants-Edit hatte bereits
ein Defer-Item — gleiches Defer-Item, gleicher Trigger.
Looks good¶
- Backend-Treue:
template.tsspiegeltchatbot_template.py1:1 (alle Felder, Optionalität, Defaults). Kein Drift, kein Schein-Type. useTemplate.reactivate():PATCH is_active=trueist die einzig korrekte Inversion zuDELETE(das Backend erwartet exakt das); die Detail-Page-Buttons toggeln idempotent.- 422-Field-Mapping:
entry.loc[entry.loc.length - 1]extrahiert das letzte Pfadsegment — robust gegen Pydantic-V2-Loc-Strukturen wie["body", "language"]. Identisches Pattern wieTenantsCreatePage. - 409-Inline: Statt Toast wird die Server-Error-Map gefüllt, der ID-Computed-Errror zeigt "ID bereits vergeben — andere wählen oder reaktivieren". Operator-Hint exakt richtig (Reactivate ist erlaubt, weil Slug-Reservierung nicht greift).
- Deactivate-Reactivate-Cycle (E2E): Voller Zyklus durchgespielt, inkl. Banner "Inaktiv" + Reactivate-Button. Eine seltene Coverage- Tiefe für CRUD-E2E.
- Sidebar-Aktivierung:
BaseLayout.vuekorrekt von Tooltipdisabledauf<router-link to="/templates">umgestellt; kein Layout-Sprung, kein neues Icon. - Bundle-Δ: +0.8 KB gzipped für vier neue lazy chunks ist im Rahmen.
Empfehlung: Merge-ready. Sieben Deferred-Items unter Score 80, alle
mit konkretem Trigger; in offene-todos.md als TODO-B7-2-01 bis
TODO-B7-2-07 aufnehmen. EntityForm-Refactor explizit auf Block 8 (oder
später) verschieben — 7.3 Modules wird die Frage klären, dann
Re-Evaluierung.