Files
tessera-ctl/.planning/quick/260929-9wc-eigene-module-admin-legt-seitenleisten-e/260929-9wc-SUMMARY.md
T
2026-09-29 07:35:42 +02:00

162 lines
12 KiB
Markdown

---
phase: quick-260929-9wc
plan: 01
quick_id: 260929-9wc
subsystem: api, web, prisma
tags: [custom-modules, sidebar, iframe, rls, admin]
status: complete
requires: []
provides:
- Tabelle CustomModule mit Zeilenschutz (Migration 20260929120000)
- API /custom-modules (GET fuer jeden Angemeldeten, POST/PATCH/DELETE nur Admin)
- Seitenleisten-Eintraege und Rahmen-Seite /modules/custom/[id]
- Verwaltungsseite Verwaltung > Eigene Module
- MODULE_CATEGORIES in @tessera/shared
affects: [sidebar, admin-sidebar, docs/mandantentrennung-zugriffsklassifikation.md]
key-files:
created:
- apps/api/prisma/migrations/20260929120000_custom_module/migration.sql
- apps/api/src/custom-modules/ (Controller, Dienst, Modul, DTO, je mit Spec)
- apps/web/src/lib/custom-modules-api.ts
- apps/web/src/components/modules/custom-module-view.tsx
- apps/web/src/app/(portal)/modules/custom/[id]/page.tsx
- apps/web/src/app/(portal)/admin/custom-modules/ (page, FormModal, DeleteDialog, Test)
- apps/web/src/messages/module-categories.spec.ts
modified:
- apps/api/prisma/schema.prisma
- apps/api/src/app.module.ts
- packages/shared/src/index.ts
- apps/web/src/components/layout/sidebar.tsx (+ Test)
- apps/web/src/components/admin/admin-sidebar.tsx
- apps/web/src/messages/de.json, en.json
- apps/web/src/app/(portal)/modules/module-layouts.test.tsx
- docs/mandantentrennung-zugriffsklassifikation.md
- CHANGELOG.md
decisions:
- "Gruppen-Einschraenkung (D-02) zurueckgestellt, siehe Bewusst offen"
- "Eigene Module haengen an keiner Modul-Aktivierung (kein @UseModule, kein ModuleAccessGate)"
- "Seitenleiste vereinheitlicht Module und eigene Module in einem internen Eintragstyp; eingebaute Module stehen je Kategorie vor eigenen"
- "https-Regel im Web bleibt EINE (isHttpsUrl aus xframe-config), Sandbox-Wert wird importiert, nicht kopiert"
duration: ca. 10 Minuten reine Ausfuehrung
completed: 2026-09-29
commits: 3
plan_head_before: 643b1a2caa01a506b0b7aec8c6236f98769e19f0
plan_head_after: e48c0de23816702b42a4fb265a22298c32769206
actuals:
tokens: 31000
tasks: 3
commits: 3
---
# Phase quick-260929-9wc Plan 01: Eigene Module Summary
Administratoren binden externe https-Seiten als Seitenleisten-Eintraege ein (Name, Adresse, Kategorie); alle Benutzer sehen sie unter der Kategorie, ein Klick zeigt die Seite in einem Rahmen mit dem XFrame-Sandbox-Wert und immer sichtbarem Knopf „In neuem Tab öffnen“. Daten liegen mandantengetrennt mit Zeilenschutz in der neuen Tabelle `CustomModule`.
## Was gebaut wurde
**Aufgabe 1 (Tracer), Commit b9d87be**
- `MODULE_CATEGORIES` (fuenf Kennungen) + Typ `ModuleCategory` in `packages/shared`; Gleichlauf-Spec gegen `moduleCategories` in de.json/en.json.
- Prisma-Modell `CustomModule` und handgeschriebene Migration `20260929120000_custom_module` (ENABLE/FORCE RLS, `tenant_isolation_policy` ohne Benutzerdimension, bewusst keine `system_read_policy`).
- API `/custom-modules`: DTO (Name 1 bis 100, Adresse nur https ohne Zugangsdaten, max 2048, Kategorie per `@IsIn`), Dienst (je Methode `const tenantPrisma = forTenant(this.prisma, tenantId)`, `row.tenantId`-Pruefung, 404 bei fremd/unbekannt), Controller (`list` vor `getOne`, schreibende Routen `@Roles(ADMIN, SUPER_ADMIN)`), Modul in `app.module.ts`.
- Zugriffsklassifikation nachgemessen fortgeschrieben (siehe Zahlen).
- Web: `custom-modules-api.ts` (inkl. `checkCustomModuleUrl`), Seitenleiste mit vereinigter Eintragsliste (Gruppierung, Suche, eingeklappte Kacheln, Auswahlmarke, Kopfzeilen-Titel ueber `useNavStore` mit slug = id), `CustomModuleView` + Seite `/modules/custom/[id]`, Texte `customModules` de/en.
- Lokal migriert (Container-IP, `prisma migrate deploy`), API neu gebaut; curl-Durchstich bestanden.
**Aufgabe 2, Commit e7fc4de**
- Verwaltungsseite `admin/custom-modules` (Liste, Leer-Zustand, Anlegen/Bearbeiten-Dialog mit Adresspruefung vor dem Senden, Loeschen mit Rueckfrage), Aufruf von `bumpSidebarRefresh` nach jedem erfolgreichen Speichern/Loeschen, Admin-Leisten-Eintrag hinter „Module“, Texte `admin.customModules` und `header.admin.customModules` de/en.
**Aufgabe 3, Commit e48c0de**
- CHANGELOG-Eintrag unter „Unveröffentlicht“ > „Neu“, alle Tore, Stack neu gebaut.
## Tore (gemessen)
| Tor | Ergebnis |
|-----|----------|
| Web-Tests vollstaendig | 102 Dateien, 992 Tests, alle gruen |
| API-Tests vollstaendig | 88 Dateien, 1495 Tests, alle gruen |
| `pnpm turbo run type-check lint` | 9/9 Aufgaben erfolgreich |
| Biome-Warnungen Web | 55 (Grundlinie 55) |
| Biome-Warnungen API | 82 (Grundlinie 82) |
| rls-coverage.spec / rls-access-inventory.spec | gruen (30 Zusicherungen im Inventar) |
| `prisma migrate deploy` (zweiter Lauf) | „No pending migrations to apply.“ |
| `prisma migrate diff` | enthaelt „CustomModule“ nicht |
| curl-Durchstich | Anlegen, Liste, Einzelabruf ok; http 400; anonym 401; Loeschen; danach 404 |
| Stack | web :3000/login 200, api /health ok, `GET /custom-modules` anonym 401 |
| Nicht gepusht | `git branch -r --contains HEAD` leer |
**Zugriffsklassifikation nachgemessen (Gate-Schleife, nur .ts ohne spec):**
- Summe vorher gemessen 61/217/6 (Dokument nannte 61/216/6); Drift in `user`: gemessen 18 gebunden statt 17 (aus quick-260928-ujj), korrigiert.
- `custom-modules`: 0/7/0 (list 1, getOne 1, create 1, update 2, remove 2).
- Neue Summe: 61/224/6.
- Klassen-Verteilung: Ueberschrift/Tabelle nannten 77 Paare/40 muss, Bestandsaufnahme hatte schon 78/41 (`grep -cE '^\| apps/api/src/'`); nach neuem Eintrag 79 Paare, davon 42 muss, 21 keine-mandantengebundene-tabelle, 14 beides, 2 bewusst-uebergreifend. Nachtrag-Absatz „quick-260929-9wc“ ergaenzt.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Endlosschleife beim Laden der Verwaltungsseite**
- **Found during:** Aufgabe 2 (Test zaehlte 5 statt 2 Listenabrufe)
- **Issue:** `fetchModules` hing per `useCallback` an `t` (Uebersetzungsfunktion); ein Mock liefert je Render eine neue Funktion, der Effekt lief erneut. Auch mit echtem next-intl fragil.
- **Fix:** Fehler als Boolean `loadFailed` gefuehrt, Text erst im JSX uebersetzt; `fetchModules` ohne Abhaengigkeit.
- **Files modified:** `apps/web/src/app/(portal)/admin/custom-modules/page.tsx`
- **Commit:** e7fc4de
**2. [Rule 3 - Blocking] Layout-Waechter der Modulordner**
- **Found during:** Aufgabe 3 (voller Web-Testlauf)
- **Issue:** `module-layouts.test.tsx` (T-e8k-04) verlangt in jedem nicht-dynamischen Ordner unter `modules/` eine `layout.tsx` mit ModuleAccessGate; der neue Ordner `custom/` ist bewusst fuer alle sichtbar (D-01) und hat keine Schranke.
- **Fix:** Explizite, begruendete Ausnahmeliste `DIRS_WITHOUT_GATE = ['custom']` im Test, statt eine wirkungslose Durchreich-Layout-Datei anzulegen.
- **Files modified:** `apps/web/src/app/(portal)/modules/module-layouts.test.tsx`
- **Commit:** e48c0de
**3. Plan-Feinheit (kein Regelfall):** Die Seitenleisten-Fehlerbehandlung fuer `listCustomModules` laesst bei Fehler den bisherigen Stand stehen (leer beim ersten Laden), wie der Modulabruf, statt aktiv zu leeren; Ergebnis beim ersten Laden identisch mit „leere Liste“.
## Bewusst offen
- **Gruppen-Einschraenkung fuer eigene Module (D-02) zurueckgestellt.** `ModuleGrant.moduleId` ist ein Pflicht-Fremdschluessel auf `Module` (`onDelete: Cascade`); eigene Module sind keine `Module`-Zeilen. Eine Einschraenkung braeuchte eine neue Freigabetabelle oder einen Umbau von `ModuleGrant` samt `module-access.service.ts` und der Admin-Freigabeoberflaeche, also nicht „sehr wenig Aufwand“. Im Code nichts dafuer gebaut.
## Browser-Pruefung offen (Orchestrator)
Playwright MCP steht in diesem Ausfuehrungskontext nicht zur Verfuegung. Der Orchestrator fuehrt die Pruefung durch: web :3000, Anmeldung admin / admin123, echte Navigation (`browser_navigate`), nie per `fetch()` aus der Seite messen, zuerst ueber den Theme-Knopf der Kopfzeile auf dunkel schalten. Stack ist neu gebaut und laeuft.
1. Verwaltung > „Eigene Module“ (neuer Eintrag in der Admin-Leiste hinter „Module“, „Module“ dabei nicht markiert): Leer-Zustand mit Knopf „Eigenes Modul anlegen“.
2. Anlegen mit Name „Beispielseite“, Adresse `http://example.com` -> Meldung, nichts gespeichert; dann `https://user:pw@example.com` -> Meldung; dann `https://example.com`, Kategorie „Infrastruktur“ -> gespeichert, Tabelle zeigt den Eintrag, die Seitenleiste zeigt „Beispielseite“ unter „Infrastruktur“ OHNE Neuladen.
3. Zweiter Eintrag „GitHub“, `https://github.com`, Kategorie „Sicherheit“ -> erscheint unter „Sicherheit“.
4. Klick auf „Beispielseite“: `/modules/custom/<id>`, Kopfzeilen-Titel „Beispielseite“, Auswahlmarke am Eintrag, Rahmen fuellt den Inhaltsbereich ohne doppelten Rollbalken, „In neuem Tab öffnen“ sichtbar; iframe traegt den Sandbox-Wert `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox` und `referrerpolicy="no-referrer"`; der Link oeffnet einen neuen Tab mit example.com.
5. Klick auf „GitHub“: Rahmen zeigt die Einbettungssperre des Browsers, „In neuem Tab öffnen“ ist trotzdem sichtbar und funktioniert.
6. Seitenleiste eingeklappt: beide Eintraege als Kachel mit Namen im Tooltip; Suche „Beisp“ findet den Eintrag.
7. Bearbeiten: „Beispielseite“ in „Beispiel“ umbenennen -> Seitenleiste zieht sofort nach. Loeschen mit Rueckfrage -> Eintrag verschwindet aus Tabelle und Seitenleiste; alte Adresse `/modules/custom/<id>` zeigt „Dieses Modul gibt es nicht mehr.“
8. Sprache auf Englisch: keine rohen Uebersetzungsschluessel auf Verwaltungsseite und Rahmen-Seite.
9. Screenshots (dunkel) von Verwaltungsseite, Seitenleiste mit Eintraegen und Rahmen-Seite ablegen; danach die Testeintraege loeschen, damit die lokale Datenbank sauber bleibt.
Hinweis: Der curl-Durchstich hat seinen Testeintrag bereits geloescht; die lokale Datenbank enthaelt keine eigenen Module.
## Known Stubs
Keine.
## Threat Flags
Keine neue Angriffsflaeche ausserhalb des Plan-Bedrohungsmodells (T-9WC-01 bis 07 umgesetzt: Rollen-Metadaten per Spec geprueft, tenantId nur aus `req.tenantId`, https-Regel in API und Web, exakter Sandbox-Wert, Referrer/`rel`, MaxLength, `whitelist: true`).
## Nichts gepusht
Drei lokale Commits (b9d87be, e7fc4de, e48c0de), kein `git push`; die vorgemerkten Loeschungen von `.planning/.continue-here.md` und `.planning/HANDOFF.json` blieben unangetastet im Index.
## Self-Check: PASSED
- Dateien vorhanden: Migration, `custom-modules.service.ts`/`controller.ts`/`module.ts`/`dto`, `custom-modules-api.ts`, `custom-module-view.tsx`, `modules/custom/[id]/page.tsx`, `admin/custom-modules/page.tsx` mit Komponenten (alle im Commit-Stat sichtbar).
- Commits vorhanden: b9d87be, e7fc4de, e48c0de (`git log`), `commits: 3` gemessen ueber `rev-list` vom Ledger.
## Browser-Pruefung (Orchestrator, 29.09., dunkel)
Durchgefuehrt per Playwright MCP auf :3000, Theme per Kopfzeilen-Knopf auf „Dunkel“:
1. Verwaltung > „Eigene Module“: Eintrag in der Admin-Leiste, Leer-Zustand korrekt.
2. http://example.com -> „Bitte geben Sie eine Adresse ein, die mit https:// beginnt.“; https://user:pw@example.com -> „Die Adresse darf keinen Benutzernamen und kein Kennwort enthalten.“; https://example.com / Infrastruktur -> gespeichert, Seitenleiste zeigt „Beispielseite“ ohne Neuladen.
3. „GitHub“ / Sicherheit erscheint unter „Sicherheit“.
4. Rahmen-Seite: Kopfzeilen-Titel, Auswahlmarke, kein doppelter Rollbalken; sandbox = `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox`, referrerpolicy = no-referrer; Link target=_blank rel="noopener noreferrer".
5. GitHub: Einbettung per frame-ancestors blockiert, „In neuem Tab öffnen“ oeffnet github.com im neuen Tab.
6. Eingeklappt: Eintraege als Symbole; Suche „Beisp“ findet den Eintrag.
7. Umbenennen zieht Seitenleiste sofort nach; Loeschen mit Rueckfrage; alte Adresse zeigt „Dieses Modul gibt es nicht mehr.“
8. Englisch: nicht per Oberflaeche umgeschaltet; stattdessen Schluessel-Paritaet de/en geprueft (keine fehlenden Schluessel, alle Texte ueber t()).
9. Testeintraege geloescht, lokale DB ohne eigene Module.