docs(quick-261008-who): Eigene Module vorladen; Auftrag Cert-Manager-Umbau
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||
Plan: 6 of 6
|
||||
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
||||
Last activity: 2026-10-08 - Quick 261008-w5w Modul-Changelog
|
||||
Last activity: 2026-10-09 - Quick 261008-who Eigene Module vorladen
|
||||
|
||||
Progress: [██████████] 99%
|
||||
|
||||
@@ -502,6 +502,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 261008-j9f | Nextcloud-Status: http-Logo-Adresse wird einmalig abgeholt (nur Internet, SSRF-Schutz), Formular nur deutsche Meldungen | 2026-10-08 | 166a6fc | [261008-j9f-nextcloud-status-logo-per-http-adresse-h](.planning/quick/261008-j9f-nextcloud-status-logo-per-http-adresse-h/) |
|
||||
| 261008-mzu | Neues Modul „Dateien“ (Nextcloud-Client): Admin setzt Adresse, Login Passwort→App-Passwort bzw. Login Flow v2 (2FA), Dateien/Raster/Vorschau/Upload in 8-MiB-Stücken/Download+ZIP/Ordner-Ops, Sperr- und Brute-Force-Schutz, Design „Mosaik-Ablage“ — Review 18 Funde behoben; Needs Review (echte Umgebung) | 2026-10-08 | HEAD | [261008-mzu-modul-nextcloud-dateien-eigenstaendiger-](.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/) |
|
||||
| 261008-w5w | Modul-Changelog je Modul (Version = neuester Eintrag, Wächter-Test), Marktplatz-Abschnitt „Änderungen“, Historie aller 10 Module nachgetragen | 2026-10-08 | 4c14ec8 | [261008-w5w-modul-changelog-und-modulversionen-nacht](.planning/quick/261008-w5w-modul-changelog-und-modulversionen-nacht/) |
|
||||
| 261008-who | Eigene Module beim Start vorladen (je Benutzer, Schalter in Modulansicht + Einstellungen), Keep-alive-Grenze 5→8, vorgeladene nie verworfen | 2026-10-09 | 86495ae | [261008-who-eigene-module-beim-start-vorladen](.planning/quick/261008-who-eigene-module-beim-start-vorladen/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
phase: 261008-who-eigene-module-beim-start-vorladen
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/prisma/migrations/20261008210000_user_custom_module_preload/migration.sql
|
||||
- packages/shared/src/index.ts
|
||||
- apps/api/src/custom-modules/custom-modules.service.ts
|
||||
- apps/api/src/custom-modules/custom-modules.service.spec.ts
|
||||
- apps/api/src/custom-modules/custom-modules.controller.ts
|
||||
- apps/api/src/custom-modules/custom-modules.controller.spec.ts
|
||||
- apps/api/src/custom-modules/dto/custom-module.dto.ts
|
||||
- apps/api/src/custom-modules/dto/custom-module.dto.spec.ts
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- apps/web/src/lib/custom-modules-api.ts
|
||||
- apps/web/src/lib/custom-modules-api.test.ts
|
||||
- apps/web/src/lib/custom-module-preload.ts
|
||||
- apps/web/src/lib/custom-module-preload.test.ts
|
||||
- apps/web/src/lib/stores/custom-module-cache-store.ts
|
||||
- apps/web/src/lib/stores/custom-module-cache-store.test.ts
|
||||
- apps/web/src/components/modules/custom-module-preloader.tsx
|
||||
- apps/web/src/components/modules/custom-module-preloader.test.tsx
|
||||
- apps/web/src/components/modules/custom-module-frame-host.tsx
|
||||
- apps/web/src/components/modules/custom-module-frame-host.test.tsx
|
||||
- apps/web/src/components/modules/custom-module-view.tsx
|
||||
- apps/web/src/components/modules/custom-module-view.test.tsx
|
||||
- apps/web/src/components/layout/app-shell.tsx
|
||||
- apps/web/src/components/custom-modules/custom-module-preload-settings.tsx
|
||||
- apps/web/src/components/custom-modules/custom-module-preload-settings.test.tsx
|
||||
- apps/web/src/app/(portal)/settings/custom-modules/page.tsx
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- .planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh
|
||||
- docs/anleitung-anwender.md
|
||||
- CHANGELOG.md
|
||||
autonomous: true
|
||||
requirements: [QUICK-261008-WHO]
|
||||
|
||||
estimate:
|
||||
tokens: 160000
|
||||
raw_tokens: 160000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Jeder Benutzer schaltet je eigenem Modul (persönlich UND gemeinsam, soweit sichtbar) „Beim Start vorladen“ ein und aus — in der App-Leiste der Modulansicht und in Einstellungen → Eigene Module; die Wahl liegt serverseitig pro Benutzer und gilt damit in Browser und Desktop-App gleich (D-01)"
|
||||
- "Die Wahl eines Benutzers ist für keinen anderen Benutzer sichtbar und wirkt nicht auf ihn; eine fremde persönliche Modul-Kennung ergibt beim Schalten 404 ohne Schreibzugriff (D-01)"
|
||||
- "Kurz NACH dem Laden von Tessera (Anmeldung oder Desktop-Start mit bestehender Sitzung) entstehen die Rahmen der vorgeladenen Module unsichtbar und nacheinander (erster frühestens ca. 2 s nach dem Start, danach je ca. 1,5 s Abstand, im Leerlauf des Browsers), mit derselben Sandbox und nur für https-Adressen ohne Zugangsdaten (D-03)"
|
||||
- "Der erste Klick auf ein vorgeladenes Modul zeigt den bereits geladenen Rahmen ohne Neuladen: derselbe DOM-Knoten, kein erneutes load-Ereignis, kein zweiter Rahmen (D-03)"
|
||||
- "Bis zu acht eigene Module bleiben offen; vorgeladene werden nie per LRU verworfen; Vorladen ist serverseitig auf acht begrenzt, der neunte Versuch zeigt einen verständlichen Hinweis (D-02)"
|
||||
- "Abmelden oder Benutzerwechsel verwirft alle Rahmen und bricht ausstehendes Vorladen ab; gelöschte oder nicht mehr sichtbare Module werden nie vorgeladen (D-04)"
|
||||
- "Anwenderanleitung und CHANGELOG „Unveröffentlicht“ beschreiben Offenhalten, Vorladen und die Grenze acht (D-05)"
|
||||
artifacts:
|
||||
- path: "apps/api/prisma/migrations/20261008210000_user_custom_module_preload/migration.sql"
|
||||
provides: "Spalte User.customModulePreloadIds (TEXT[], NOT NULL, Standard leer)"
|
||||
- path: "apps/api/src/custom-modules/custom-modules.service.ts"
|
||||
provides: "setPreload (prüft Sichtbarkeit, Grenze 8, bereinigt), list/getOne liefern preload je Aufrufer"
|
||||
- path: "apps/api/src/custom-modules/custom-modules.controller.ts"
|
||||
provides: "PUT /custom-modules/:id/preload"
|
||||
- path: "apps/web/src/lib/custom-module-preload.ts"
|
||||
provides: "Vorlade-Planer: Startverzögerung, Leerlauf, Staffelung, Abbruch"
|
||||
- path: "apps/web/src/components/modules/custom-module-preloader.tsx"
|
||||
provides: "Startet das Vorladen nach Anmeldung, bricht bei Abmelden/Benutzerwechsel ab"
|
||||
- path: "apps/web/src/lib/stores/custom-module-cache-store.ts"
|
||||
provides: "CUSTOM_MODULE_KEEP_ALIVE_MAX = 8, pinned, preload(), setPinned(), LRU ohne Verwerfen gepinnter Module"
|
||||
- path: "apps/web/src/components/modules/custom-module-frame-host.tsx"
|
||||
provides: "Ersatzgröße aus dem Inhaltsbereich, bevor ein Platzhalter gemessen wurde"
|
||||
- path: "apps/web/src/components/custom-modules/custom-module-preload-settings.tsx"
|
||||
provides: "Karte „Beim Start vorladen“ in Einstellungen → Eigene Module mit Zähler x von 8"
|
||||
key_links:
|
||||
- from: "apps/web/src/components/layout/app-shell.tsx"
|
||||
to: "apps/web/src/components/modules/custom-module-preloader.tsx"
|
||||
via: "<CustomModulePreloader /> neben <CustomModuleFrameHost />"
|
||||
pattern: "CustomModulePreloader"
|
||||
- from: "apps/web/src/components/modules/custom-module-preloader.tsx"
|
||||
to: "apps/web/src/lib/stores/custom-module-cache-store.ts"
|
||||
via: "listCustomModules → Filter preload + checkCustomModuleUrl → preload(mod) gestaffelt"
|
||||
pattern: "preload\\("
|
||||
- from: "apps/web/src/components/modules/custom-module-frame-host.tsx"
|
||||
to: "apps/web/src/lib/stores/custom-module-cache-store.ts"
|
||||
via: "openIds (stabile Reihenfolge, key=id) — vorgeladener Rahmen wird bei activate übernommen, nicht neu eingehängt"
|
||||
pattern: "openIds"
|
||||
- from: "apps/api/src/custom-modules/custom-modules.service.ts"
|
||||
to: "User.customModulePreloadIds"
|
||||
via: "forTenant(this.prisma, tenantId).user — nur die eigene Zeile (where id = caller.id)"
|
||||
pattern: "customModulePreloadIds"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Eigene Module (iframe-basiert) können pro Benutzer „Beim Start vorladen“ bekommen: Tessera lädt sie kurz nach dem Start unsichtbar und gestaffelt im bestehenden Rahmen-Behälter, sodass der erste Klick sofort die fertige Seite zeigt — ohne Neuladen. Die Offenhalten-Grenze steigt auf acht, vorgeladene Module werden nie verworfen.
|
||||
|
||||
Purpose: Nutzerwunsch — eigene Module sind beim ersten Aufruf langsam; das Offenhalten (seit 30.09.) hilft erst ab dem zweiten Aufruf.
|
||||
Output: Benutzer-Einstellung serverseitig (Spalte + PUT-Route), Vorlade-Planer und -Komponente, erweiterter Speicher/Behälter, Schalter in Modulansicht und Einstellungen, Tests, Anleitung, CHANGELOG, Browser-Nachweis.
|
||||
|
||||
Gesperrte Nutzerentscheidungen (aus dem Auftrag, hier D-01 bis D-05):
|
||||
- D-01: Jeder Benutzer entscheidet selbst je Modul (persönlich und gemeinsam, soweit sichtbar); Speicherung serverseitig pro Benutzer; keine Begriffe der Mehrfirmen-Trennung in der Oberfläche.
|
||||
- D-02: CUSTOM_MODULE_KEEP_ALIVE_MAX = 8; vorgeladene Module nie per LRU verwerfen; Verhalten bei mehr als acht dokumentieren.
|
||||
- D-03: Vorladen kurz NACH dem Start, gestaffelt, im Leerlauf, unsichtbar, gleiche Sandbox/Attribute, nur checkCustomModuleUrl === 'ok'; vorgeladener Rahmen wird beim ersten Öffnen ohne Neuladen und ohne DOM-Verschieben übernommen; Größe vor dem ersten Platzhalter sinnvoll setzen.
|
||||
- D-04: Abmelden/Benutzerwechsel verwirft alles; gelöschte/entzogene Module nicht vorladen.
|
||||
- D-05: Doku in docs/anleitung-anwender.md, docs/anleitung-entwicklung.md nur falls Architektur dort beschrieben, CHANGELOG „Unveröffentlicht“; Modul-Changelog von 261008-w5w prüfen.
|
||||
|
||||
Claude's Discretion (hier entschieden und zu dokumentieren):
|
||||
- Speicherort (D-01): Spalte `User.customModulePreloadIds` (TEXT[]) nach dem Muster `dashboardBackground` (Benutzerzeile, schon unter Zeilenschutz) statt eigener Tabelle — keine neue RLS-Regel nötig. Gelöschte Module bleiben als tote Kennung stehen, werden aber beim Lesen gegen die sichtbaren Module geschnitten und beim nächsten Schreiben entfernt.
|
||||
- Ort des Schalters (D-01): BEIDE Orte. (a) App-Leiste der Modulansicht neben „In neuem Tab öffnen“ — gilt dort für persönliche UND gemeinsame Module; (b) Karte „Beim Start vorladen“ in Einstellungen → Eigene Module mit ALLEN sichtbaren Modulen und Zähler „x von 8“ — nötig, weil die bestehende Liste dort nur persönliche Einträge zeigt und man beim Erreichen der Grenze sehen muss, welche Module schon vorgeladen werden.
|
||||
- Grenze (D-02): Gesamtgrenze acht offene Rahmen. Vorgeladene zählen mit, werden aber nie verworfen; die übrigen teilen sich den Rest (mindestens das gerade geöffnete). Vorladen ist serverseitig auf acht begrenzt (HTTP 409, Hinweistext). Extremfall: acht vorgeladen plus ein gerade geöffnetes, nicht vorgeladenes = neun Rahmen; öffnet man ein weiteres nicht vorgeladenes, fällt das vorige nicht vorgeladene heraus.
|
||||
- Gemeinsame Konstante `CUSTOM_MODULE_PRELOAD_MAX = 8` in `@tessera/shared` (Quelltext-Paket, API und Web importieren es bereits).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@CLAUDE.md
|
||||
@apps/web/src/components/modules/custom-module-frame-host.tsx
|
||||
@apps/web/src/lib/stores/custom-module-cache-store.ts
|
||||
@apps/web/src/components/modules/custom-module-view.tsx
|
||||
@apps/web/src/components/layout/app-shell.tsx
|
||||
@apps/web/src/lib/custom-modules-api.ts
|
||||
@apps/api/src/custom-modules/custom-modules.service.ts
|
||||
@apps/api/src/custom-modules/custom-modules.controller.ts
|
||||
|
||||
Projektnotizen (verbindlich):
|
||||
- Diese Aufgabe läuft NACH Quick 261008-w5w (Modul-Changelog). Nur auf das bauen, was davon committet ist. w5w liefert für eigene Module bewusst eine leere Liste (eigene Module stehen nicht im Modul-Register) — für dieses Vorhaben also NUR der Gesamt-CHANGELOG, kein Modul-Changelog-Eintrag (D-05).
|
||||
- Nie `.env` lesen. Nie etwas in `gespraech-2026-11/` anfassen oder stagen — immer explizite Dateipfade stagen.
|
||||
- NestJS: statische GET-Routen vor `@Get(':id')`.
|
||||
- Lokaler Stack: `docker compose up -d --build api web` (ein schlichtes `up` baut nicht neu); die API führt Migrationen beim Start aus (`apps/api/scripts/migrate-and-start.sh`). Anmeldung lokal admin / admin123 (Standardwerte aus docker-compose.yml). API http://localhost:3001, Web http://localhost:3000.
|
||||
- App-Texte siezen. Nicht pushen (gebündelt pushen macht der Nutzer).
|
||||
- Commits enden mit: `Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>`
|
||||
|
||||
Bestehende Bausteine (aus der Analyse, nicht erneut suchen):
|
||||
- Speicher `useCustomModuleCacheStore`: `modules`, `openIds` (stabile Einhängereihenfolge, key=id — ein umsortiertes oder verschobenes iframe lädt neu), `recent` (LRU), `active` {id, slot}; Aktionen remember/forget/activate/deactivate/clear; reine Funktion `touchLru(recent, id, max)`.
|
||||
- Behälter `CustomModuleFrameHost`: rendert `openIds.map` im selben Elternknoten, `position: fixed`, versteckt mit `visibility: hidden` + `pointer-events: none` + `aria-hidden`, Größe aus dem gemessenen Platzhalter (`rect`, anfangs null → heute 0×0). Leert bei Benutzerwechsel selbst (`clear()` bei geänderter `useAuthStore` user.id); das Abmelden leert in `header.tsx` (Zeile ~246).
|
||||
- `CustomModuleView`: lädt `getCustomModule(id)` im Hintergrund, `remember()`, meldet Platzhalter per `activate(id, slot)` an; App-Leisten-Knopf per Portal in `HEADER_ACTIONS_SLOT_ID` (Klasse `btn btn-appbar`).
|
||||
- `useAuthStore`: `user` (null nach `clearUser()`), `user.id`.
|
||||
- `useMarketplaceStore`: `sidebarRefreshKey` / `bumpSidebarRefresh()` — die Verwaltungsliste ruft `bumpSidebarRefresh()` nach Anlegen/Ändern/Löschen.
|
||||
- API: `CustomModulesService.loadVisible(...)` liefert 404 für fremde/fehlende Kennungen; Benutzerzeile schreiben nach Muster `PATCH /users/me/dashboard-background` (`forTenant(this.prisma, tenantId).user.update({ where: { id: caller.id } })`).
|
||||
- `rls-access-inventory.spec.ts` vergleicht Paare (Datei, Prisma-Modell) gegen `docs/mandantentrennung-zugriffsklassifikation.md` — ein neuer Zugriff `…user` in `custom-modules.service.ts` braucht dort eine Zeile.
|
||||
- Bestehender Test „haelt hoechstens fuenf Module offen …“ in `custom-module-view.test.tsx` muss auf acht umgestellt werden.
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Aufgabe 1 (Durchstich): Einstellung speichern → nach dem Start unsichtbar vorladen → erster Klick übernimmt den Rahmen</name>
|
||||
<files>apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20261008210000_user_custom_module_preload/migration.sql, packages/shared/src/index.ts, apps/api/src/custom-modules/custom-modules.service.ts, apps/api/src/custom-modules/custom-modules.service.spec.ts, apps/api/src/custom-modules/custom-modules.controller.ts, apps/api/src/custom-modules/custom-modules.controller.spec.ts, apps/api/src/custom-modules/dto/custom-module.dto.ts, apps/api/src/custom-modules/dto/custom-module.dto.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, apps/web/src/lib/custom-modules-api.ts, apps/web/src/lib/custom-modules-api.test.ts, apps/web/src/lib/custom-module-preload.ts, apps/web/src/lib/custom-module-preload.test.ts, apps/web/src/lib/stores/custom-module-cache-store.ts, apps/web/src/lib/stores/custom-module-cache-store.test.ts, apps/web/src/components/modules/custom-module-preloader.tsx, apps/web/src/components/modules/custom-module-preloader.test.tsx, apps/web/src/components/modules/custom-module-frame-host.tsx, apps/web/src/components/modules/custom-module-frame-host.test.tsx, apps/web/src/components/layout/app-shell.tsx, .planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh</files>
|
||||
<behavior>
|
||||
API (Vitest, Prisma gemockt wie in custom-modules.service.spec.ts):
|
||||
- list: `preload: true` nur für Module, deren Kennung in der Benutzerzeile DES AUFRUFERS steht; Benutzer B (eigene Zeile leer) bekommt für dasselbe gemeinsame Modul `preload: false` (Benutzertrennung). Die Benutzerzeile wird mit `where: { id: caller.id }` gelesen.
|
||||
- getOne liefert zusätzlich `preload` (boolesch) für den Aufrufer.
|
||||
- setPreload(true): schreibt NUR `where: { id: caller.id }`; doppelte Kennung wird nicht doppelt gespeichert; tote Kennungen (nicht mehr sichtbar/gelöscht) werden beim Schreiben entfernt; Antwort `{ id, preload, preloadCount }`.
|
||||
- setPreload(true) bei schon acht sichtbaren vorgeladenen Modulen → ConflictException, KEIN Schreibzugriff; tote Kennungen zählen dabei nicht mit.
|
||||
- setPreload(false) entfernt die Kennung.
|
||||
- fremde persönliche oder unbekannte Kennung → NotFoundException ohne Schreibzugriff.
|
||||
- Controller: Route PUT `:id/preload` (Metadaten geprüft), reicht `req.tenantId` und Benutzer durch; DTO lehnt fehlendes oder nicht-boolesches `preload` ab.
|
||||
Web:
|
||||
- custom-module-preload.test.ts (Fake-Timer): vor Ablauf der Startverzögerung (2000 ms) kein Aufruf; danach Ziele laden und das erste vorladen (über requestIdleCallback, wenn vorhanden — gestubbt; sonst setTimeout-Ersatz); jedes weitere erst nach gapMs (1500 ms); Reihenfolge bleibt; `cancel()` vor dem Start, zwischen zwei Schritten und während eines offenen Leerlauf-Rückrufs verhindert jeden weiteren Aufruf (cancelIdleCallback/clearTimeout).
|
||||
- custom-module-preloader.test.tsx: mit angemeldetem Benutzer und Liste [A preload https, B ohne preload, C preload mit http-Adresse] steht nach den Timern genau A in `openIds` und `pinned`, B und C nie; wird der Benutzer vor Ablauf null, wird nichts vorgeladen; Benutzerwechsel A→B wendet die Liste des alten Benutzers nicht mehr an.
|
||||
- custom-module-frame-host.test.tsx: nach `preload(mod)` genau EIN iframe, versteckt (visibility hidden, pointer-events none, aria-hidden), mit XFRAME_SANDBOX, `allow=""`, referrerPolicy no-referrer und Größe ungleich 0 aus dem Inhaltsbereich (getBoundingClientRect des Elements mit `data-app-shell-main` gemockt); danach CustomModuleView für dieselbe id gerendert (getCustomModule gemockt, gleiche Adresse) → weiterhin genau ein iframe, DERSELBE Knoten (`toBe`), `src` unverändert, sichtbar.
|
||||
- Speicher: `preload(mod)` merkt Metadaten, hängt die id hinten an `openIds` an (falls noch nicht offen), nimmt sie in `pinned` auf, setzt `active` NICHT; zweimal aufgerufen ändert nichts; mehr als CUSTOM_MODULE_PRELOAD_MAX gepinnte werden ignoriert; `forget`/`clear` entfernen auch aus `pinned`.
|
||||
</behavior>
|
||||
<action>
|
||||
API-Schicht (D-01, D-02, D-04):
|
||||
1. In `packages/shared/src/index.ts` `export const CUSTOM_MODULE_PRELOAD_MAX = 8;` mit kurzem Kommentar (eigene Module, die ein Benutzer beim Start vorladen lässt; gleich der Offenhalten-Grenze im Web).
|
||||
2. `apps/api/prisma/schema.prisma`, Modell User: Spalte `customModulePreloadIds String[] @default([])` mit Kommentar im Stil der Nachbarspalten (quick-261008-who, Kennungen eigener Module, die dieser Benutzer beim Start vorladen lässt; tote Kennungen werden beim Lesen geschnitten). Migration von Hand anlegen: Ordner mit Zeitstempel SPÄTER als der neueste unter `apps/api/prisma/migrations/` (bei Planung 20261008183000; liegt inzwischen ein späterer, Zeitstempel entsprechend erhöhen und `files_modified` im SUMMARY nennen), Name `_user_custom_module_preload`, Inhalt: `ALTER TABLE "User" ADD COLUMN "customModulePreloadIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];`. Danach `pnpm --filter @tessera/api exec prisma generate`. Prüfen, dass keine Antwort mit vollständiger Benutzerzeile die neue Spalte nach außen gibt (auth.service nutzt `select`; in user.service/user.controller Listen mit `findMany` ohne `select` ggf. wie `dashboardBackground` ausschließen).
|
||||
3. `custom-modules.service.ts`: privater Helfer zum Lesen der eigenen Kennungen über einen eigenen Klienten `const userPrisma = forTenant(this.prisma, tenantId)` (Muster dashboard-background, OHNE Benutzer-Argument, `where: { id: caller.id }`, `select: { customModulePreloadIds: true }`). `list` liefert je Zeile zusätzlich `preload: boolean` (Schnitt mit den sichtbaren Zeilen — so werden gelöschte/entzogene Module nie als vorgeladen gemeldet, D-04). `getOne` liefert ebenfalls `preload`. Neue Methode `setPreload(tenantId, caller, id, preload)`: Sichtbarkeit über `loadVisible` (fremd/fehlend → 404), sichtbare Kennungen des Aufrufers ermitteln (gemeinsame + eigene, nur `select: { id: true }`), aktuelle Liste = gespeicherte ∩ sichtbare (ohne Doppelte), Grenze `CUSTOM_MODULE_PRELOAD_MAX` aus `@tessera/shared` → `ConflictException` mit deutscher Meldung, sonst eigene Zeile mit der bereinigten Liste schreiben und `{ id, preload, preloadCount }` zurückgeben. Kopfkommentar der Klasse um einen Absatz zum Vorladen ergänzen (Speicherort, Bereinigung, Grenze, nur eigene Zeile).
|
||||
4. DTO `SetCustomModulePreloadDto` mit `@IsBoolean() preload!: boolean` in `dto/custom-module.dto.ts`; Controller-Route `@Put(':id/preload')` direkt unter `getOne` (PUT, zwei Segmente — kein Verschatten; Kommentar dazu), `tenantId` nur aus `requireTenantId(req)`, kein `@Roles` (jeder angemeldete Benutzer, wie die übrigen Routen).
|
||||
5. `docs/mandantentrennung-zugriffsklassifikation.md`: Zeile für das neue Paar `apps/api/src/custom-modules/custom-modules.service.ts | user` (Klasse und Stand nach dem Muster vorhandener `user`-Zeilen, gebunden über `forTenant(this.prisma, tenantId)`, nur eigene Zeile) ergänzen und die Bereichszeile `custom-modules` mit nachgemessenen Rohtreffern fortschreiben. `rls-access-inventory` und `rls-coverage` müssen grün sein.
|
||||
Web-Schicht (D-03, D-04):
|
||||
6. `custom-modules-api.ts`: `CustomModule` um optionales `preload?: boolean`; neue Funktion `setCustomModulePreload(id, preload)` → PUT `/custom-modules/{id}/preload`, `credentials: 'include'`, JSON-Körper `{ preload }`, Fehler über `failure(res)` (409 bleibt als `CustomModuleRequestError` mit Status 409 erkennbar). Test im bestehenden `custom-modules-api.test.ts`.
|
||||
7. Neues `apps/web/src/lib/custom-module-preload.ts`: reiner Planer `startCustomModulePreload<T>({ loadTargets, preloadOne, initialDelayMs = 2000, gapMs = 1500, idleTimeoutMs = 3000 })` → gibt `cancel()` zurück. Ablauf: setTimeout(initialDelayMs) → Leerlauf abwarten (`window.requestIdleCallback` mit `{ timeout: idleTimeoutMs }`, sonst setTimeout 0) → `loadTargets()` → für jedes Ziel nacheinander: Leerlauf abwarten → `preloadOne(ziel)` → gapMs warten. Nach `cancel()` keine Aufrufe mehr (auch nicht nach Auflösen eines laufenden `loadTargets`). Kopfkommentar: warum verzögert und gestaffelt (Start nicht verlangsamen).
|
||||
8. Speicher `custom-module-cache-store.ts`: Zustand `pinned: string[]` (Invariante im Kommentar: jede gepinnte id ist auch in `openIds`) und Aktion `preload(mod)` laut behavior; `forget`/`clear` leeren auch `pinned`. Die Grenzen-Logik (acht, LRU ohne Gepinnte) kommt in Aufgabe 2 — hier nur so, dass `activate` gepinnte Module nicht aus `pinned` entfernt.
|
||||
9. Neue Komponente `apps/web/src/components/modules/custom-module-preloader.tsx` (`'use client'`, rendert null): Effekt auf `useAuthStore` user.id; ohne Benutzer nichts. Mit Benutzer: `startCustomModulePreload` mit `loadTargets = listCustomModules()` gefiltert auf `preload === true` UND `checkCustomModuleUrl(url) === 'ok'`, auf `CUSTOM_MODULE_PRELOAD_MAX` gekürzt; `preloadOne = (m) => useCustomModuleCacheStore.getState().preload({ id, name, url })`; Fehler beim Listen still verschlucken (Vorladen ist Komfort). Aufräumen des Effekts ruft `cancel()` — deckt Abmelden (user null) und Benutzerwechsel ab (D-04); das Leeren der Rahmen bleibt beim Behälter/Kopfzeile wie bisher.
|
||||
10. Behälter `custom-module-frame-host.tsx` (D-03): solange noch kein Platzhalter gemessen wurde (`rect === null`) und Rahmen offen sind, Ersatzgröße aus dem Inhaltsbereich messen: Element mit Attribut `data-app-shell-main` (in app-shell.tsx am `<main>` setzen), `getBoundingClientRect` minus berechnete Innenabstände (`getComputedStyle`), Höhe = `window.innerHeight − top − Innenabstand unten`, mindestens 320; bei `resize` nachmessen, solange nur die Ersatzgröße gilt. Sobald ein Platzhalter aktiv ist, gilt wie bisher dessen Messung (Größenänderung eines iframes lädt nicht neu). Sichtbarkeitsregel, Sandbox, Attribute, Elternknoten und `key={id}` UNVERÄNDERT lassen. Kopfkommentar um einen Absatz „Vorladen“ ergänzen (Ersatzgröße, Übernahme ohne Neuladen über stabile openIds).
|
||||
11. `app-shell.tsx`: `<main>` bekommt `data-app-shell-main`; `<CustomModulePreloader />` direkt vor `<CustomModuleFrameHost />` mit Kurzkommentar (quick-261008-who).
|
||||
12. E2E-Skript `.planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh` (bash, `set -euo pipefail`, Hilfsdateien per `mktemp`, am Ende aufräumen per trap): wartet bis zu 180 s auf `curl -sf http://localhost:3001/health`; meldet admin/admin123 an (Cookie-Datei); holt einen Kategorieschlüssel aus `GET /module-categories` (erstes Element, Feld `key`); legt neun persönliche Probe-Module „Probe who 1..9“ mit `https://example.com/?who=N` an; prüft: PUT preload true auf Nr. 1–8 → 200 mit `"preload":true`; Nr. 9 → 409; `GET /custom-modules` zeigt Nr. 1 mit `"preload":true` und Nr. 9 mit `"preload":false`; PUT preload false auf Nr. 1 → 200, danach Nr. 9 → 200; PUT mit `{"preload":"ja"}` → 400; PUT auf eine zufällige UUID → 404; löscht danach alle Probe-Module und prüft, dass keines mehr in der Liste steht; gibt am Ende `e2e preload api ok` aus. Vor dem Ausführen den Stack neu bauen: `docker compose up -d --build api web`.
|
||||
Tests laut behavior schreiben (rot), dann umsetzen (grün). Commit: `feat(quick-261008-who): eigene Module beim Start vorladen — Durchstich` (explizite Pfade stagen).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api exec prisma generate && pnpm --filter @tessera/api exec vitest run src/custom-modules rls-coverage rls-access-inventory && pnpm --filter @tessera/web exec vitest run src/lib/custom-modules-api src/lib/custom-module-preload src/lib/stores/custom-module-cache-store src/components/modules && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && grep -q "customModulePreloadIds" apps/api/prisma/schema.prisma && ls apps/api/prisma/migrations/*_user_custom_module_preload/migration.sql >/dev/null && grep -q "CustomModulePreloader" apps/web/src/components/layout/app-shell.tsx && grep -q "data-app-shell-main" apps/web/src/components/layout/app-shell.tsx && grep -q "CUSTOM_MODULE_PRELOAD_MAX = 8" packages/shared/src/index.ts && bash .planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh && echo "tracer ok"</automated>
|
||||
</verify>
|
||||
<done>Die Einstellung wird pro Benutzer gespeichert und in list/getOne geliefert (Grenze 8, 404 für Fremdes, bereinigte Liste); nach dem Start lädt Tessera die markierten Module verzögert, gestaffelt und unsichtbar in den Behälter; ein vorgeladener Rahmen wird beim ersten Öffnen als derselbe Knoten ohne Neuladen übernommen; Abmelden/Benutzerwechsel bricht ausstehendes Vorladen ab; alle genannten Tests und das E2E-Skript sind grün und committet.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Aufgabe 2: Grenze acht mit gepinnten Modulen, Schalter in der Modulansicht und Karte in den Einstellungen</name>
|
||||
<files>apps/web/src/lib/stores/custom-module-cache-store.ts, apps/web/src/lib/stores/custom-module-cache-store.test.ts, apps/web/src/components/modules/custom-module-view.tsx, apps/web/src/components/modules/custom-module-view.test.tsx, apps/web/src/components/custom-modules/custom-module-preload-settings.tsx, apps/web/src/components/custom-modules/custom-module-preload-settings.test.tsx, apps/web/src/app/(portal)/settings/custom-modules/page.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
|
||||
<behavior>
|
||||
Speicher (reine Funktion + Aktionen):
|
||||
- `CUSTOM_MODULE_KEEP_ALIVE_MAX === 8` und `CUSTOM_MODULE_PRELOAD_MAX <= CUSTOM_MODULE_KEEP_ALIVE_MAX`.
|
||||
- Ohne Gepinnte: das neunte verschiedene Modul verwirft das am längsten unbenutzte.
|
||||
- 3 gepinnt + 5 übrige offen: ein sechstes übriges verwirft das älteste ÜBRIGE, nie ein gepinntes.
|
||||
- 8 gepinnt: ein übriges X bleibt offen, solange es das aktive ist (9 Rahmen); ein weiteres übriges Y verwirft X; alle gepinnten bleiben.
|
||||
- Ein gepinntes Modul aktivieren lässt es gepinnt und zählt es nicht unter den übrigen.
|
||||
- `setPinned(id, false)` macht das Modul wieder verwerfbar (beim nächsten Aktivieren gilt die Grenze); `setPinned(id, true)` über der Grenze wird ignoriert; `openIds`-Reihenfolge bleibt in allen Fällen stabil (nur Herausfallen, kein Umsortieren).
|
||||
Modulansicht:
|
||||
- Der Schalter „Beim Start vorladen“ steht in der App-Leiste (Portal in HEADER_ACTIONS_SLOT_ID) neben „In neuem Tab öffnen“, mit `aria-pressed` gemäß `preload` aus getCustomModule; Klick ruft `setCustomModulePreload(id, !aktuell)`, setzt danach `aria-pressed` um und ruft `setPinned`.
|
||||
- Antwort 409 → Hinweis `role="status"` mit dem Grenz-Text, `aria-pressed` bleibt false; anderer Fehler → Speicherfehler-Text, Zustand bleibt.
|
||||
- Der bestehende Test zur Offenhalten-Grenze prüft jetzt acht statt fünf.
|
||||
Einstellungen-Karte:
|
||||
- Listet ALLE sichtbaren Module (gemeinsame und persönliche, Kennzeichnung „Für alle“ / „Persönlich“) mit Kontrollkästchen, Zähler „x von 8“; bei acht gewählten sind die übrigen Kästchen deaktiviert und ein Hinweis steht da; Umschalten ruft die API und aktualisiert Liste und Speicher (an → `preload(mod)`, aus → `setPinned(id, false)`); 409/Fehler zeigen den passenden Text und setzen das Kästchen zurück; zieht nach `bumpSidebarRefresh` (sidebarRefreshKey) neu.
|
||||
</behavior>
|
||||
<action>
|
||||
1. Speicher (D-02): `CUSTOM_MODULE_KEEP_ALIVE_MAX = 8` (Kommentar zur Begründung von 5 auf 8 nachziehen: Nutzerentscheidung 08.10.2026). `touchLru` erhält einen vierten Parameter `pinned: readonly string[] = []`: neue Reihenfolge wie bisher; verworfen werden nur NICHT gepinnte Kennungen jenseits von `max(1, max − Anzahl gepinnter)`, das gerade berührte Modul nie; `recent` enthält danach alle nicht verworfenen. `activate` und `preload` (Aufgabe 1) wenden dieselbe Regel an (preload ohne `active` zu verändern; das aktive Modul wird nie verworfen). Neue Aktion `setPinned(id, on)`: an nur, wenn das Modul offen ist und die Grenze `CUSTOM_MODULE_PRELOAD_MAX` nicht überschritten wird; aus entfernt aus `pinned`, ohne den Rahmen zu schließen. Kopfkommentar der Datei um „Vorladen/Pinnen“ ergänzen und dort das Verhalten bei mehr als acht festhalten (Serverbegrenzung 409 mit Hinweis; Extremfall acht gepinnt + ein aktives = neun Rahmen) — per D-02.
|
||||
2. Modulansicht `custom-module-view.tsx` (D-01): lokaler Zustand `preload` (Startwert `pinned.includes(id)`, danach aus `loaded.preload === true` der Hintergrund-Antwort; dabei `setPinned(id, loaded.preload === true)`). Knopf `type="button"`, Klasse wie der Link (`btn btn-appbar` bzw. Ersatz `btn btn-secondary` ohne App-Leiste), `aria-pressed`, schlichtes Inline-SVG (Stil wie das vorhandene Pfeil-Symbol, gefüllt bei an), Text `t('preload.toggle')`; während des Speicherns deaktiviert. Beide Elemente gemeinsam in einem Fragment ins Portal. Hinweiszeile `role="status"` (klein, gedämpft bzw. destructive bei Fehler) oberhalb des Platzhalters, bis zum nächsten Klick. Kopfkommentar ergänzen. Bestehenden Grenz-Test auf acht umstellen, neue Tests laut behavior.
|
||||
3. Neue Karte `apps/web/src/components/custom-modules/custom-module-preload-settings.tsx` mit `SettingsSection` (Titel/Beschreibung aus `customModules.preload.settingsTitle`/`settingsDescription`, Zähler `count` mit Platzhaltern `{count}`/`{max}`), Liste über `listCustomModules()`, Neuladen bei `sidebarRefreshKey`. In `settings/custom-modules/page.tsx` unter dem `CustomModuleManager` einhängen (Abstand wie andere Karten, z. B. `mt-6`); bestehenden Seitentest nur anpassen, falls er durch den zusätzlichen Listenaufruf bricht.
|
||||
4. Texte (Sie-Form) unter `customModules.preload` in de.json UND en.json, gleiche Schlüssel: `toggle` („Beim Start vorladen“ / „Preload at startup“), `limitReached` (de sinngemäß: „Es können höchstens {max} eigene Module beim Start vorgeladen werden. Schalten Sie das Vorladen zuerst bei einem anderen Modul aus.“), `saveError` („Die Einstellung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.“), `settingsTitle` („Beim Start vorladen“), `settingsDescription` (de sinngemäß: ausgewählte Module lädt Tessera kurz nach dem Start unsichtbar im Hintergrund, damit sie beim ersten Öffnen sofort da sind; höchstens {max}; gilt im Browser und in der Desktop-App), `count` („{count} von {max} ausgewählt“), `shared` („Für alle“), `personal` („Persönlich“), `empty` („Es gibt noch keine eigenen Module.“). Keine Begriffe der Mehrfirmen-Trennung oder Freischaltung (D-01; das Prüfskript in verify schlägt sonst an). Typografisch korrekte Anführungszeichen, keine Ersatzschreibungen für Umlaute.
|
||||
Tests zuerst (rot), dann umsetzen. Commit: `feat(quick-261008-who): Vorladen schalten, Grenze acht mit gepinnten Modulen`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web exec vitest run src/lib/stores/custom-module-cache-store src/components/modules src/components/custom-modules settings/custom-modules src/messages && pnpm --filter @tessera/web exec tsc --noEmit && grep -q "CUSTOM_MODULE_KEEP_ALIVE_MAX = 8" apps/web/src/lib/stores/custom-module-cache-store.ts && grep -q "CustomModulePreloadSettings" "apps/web/src/app/(portal)/settings/custom-modules/page.tsx" && node -e 'const de=require("./apps/web/src/messages/de.json"),en=require("./apps/web/src/messages/en.json");const w=(o,p,r)=>{for(const[k,v]of Object.entries(o||{})){const q=p+"."+k;if(v&&typeof v==="object")w(v,q,r);else r[q]=v}return r};const a=w(de.customModules&&de.customModules.preload,"p",{}),b=w(en.customModules&&en.customModules.preload,"p",{});if(!Object.keys(a).length||Object.keys(a).sort().join()!==Object.keys(b).sort().join()){console.error("key mismatch");process.exit(1)}for(const k of ["toggle","limitReached","saveError","settingsTitle","settingsDescription","count","shared","personal","empty"])if(!a["p."+k]){console.error("missing",k);process.exit(1)}if(a["p.toggle"]!=="Beim Start vorladen"){console.error("label");process.exit(1)}for(const v of [...Object.values(a),...Object.values(b)])if(/mandant|tenant|lizenz|licens/i.test(String(v))){console.error("bad text",v);process.exit(1)}console.log("i18n ok")' && echo "task2 ok"</automated>
|
||||
</verify>
|
||||
<done>Offenhalten-Grenze acht, gepinnte Module werden nie verworfen, Vorladen auf acht begrenzt mit Hinweis; Schalter in der App-Leiste der Modulansicht und Karte „Beim Start vorladen“ in Einstellungen → Eigene Module funktionieren für persönliche und gemeinsame Module; Texte de/en vollständig; Tests grün und committet.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Aufgabe 3: Anleitung, CHANGELOG, Gesamtprüfung und Browser-Nachweis</name>
|
||||
<files>docs/anleitung-anwender.md, CHANGELOG.md</files>
|
||||
<action>
|
||||
1. `docs/anleitung-anwender.md` (D-05): direkt nach dem Absatz „**Eigene Module:**“ im Einstellungen-Abschnitt (bei Planung Zeile ~319) einen neuen Absatz „**Offen halten und beim Start vorladen:**“ in ganzen Sätzen, Sie-Form: einmal geöffnete eigene Module bleiben geöffnet, bis Sie die Seite neu laden oder die Desktop-App neu starten (Scrollstand, Anmeldung auf der Seite und Eingaben bleiben erhalten); bis zu acht gleichzeitig, danach schließt Tessera das am längsten nicht benutzte; „Beim Start vorladen“ schalten Sie in der Leiste oben in der Modulansicht oder in Einstellungen → Eigene Module in der Karte „Beim Start vorladen“ — auch für Einträge, die der Administrator für alle angelegt hat; die Wahl gilt nur für Sie, im Browser und in der Desktop-App gleich; Tessera startet zuerst vollständig und lädt die gewählten Module kurz danach nacheinander unsichtbar, ein Klick zeigt sie dann sofort; höchstens acht Module, beim neunten erscheint ein Hinweis; vorgeladene Module schließt Tessera nie von selbst; beim Abmelden werden alle geschlossen; Seiten, die das Einbetten verbieten, bleiben auch vorgeladen leer. Den bestehenden Satz zu den Einträgen des Administrators nicht widersprüchlich lassen (dort bleibt: hier nicht änderbar — das Vorladen ist davon ausgenommen).
|
||||
2. `docs/anleitung-entwicklung.md` (D-05): nur ergänzen, falls dort die Architektur der eigenen Module / des Rahmen-Behälters beschrieben ist (bei Planung per grep: NICHT der Fall). Bei Ausführung erneut prüfen (`grep -n -i "custom-module\|eigene Module\|iframe" docs/anleitung-entwicklung.md`); ohne Fundstelle die Datei unverändert lassen und das im SUMMARY festhalten.
|
||||
3. `CHANGELOG.md`, Abschnitt „## Unveröffentlicht“ (D-05): unter „### Neu“ ein Punkt „Eigene Module beim Start vorladen: …“ (Schalter je Modul in der Modulansicht und in den Einstellungen, pro Benutzer gespeichert, gilt in Browser und Desktop-App, lädt kurz nach dem Start unsichtbar nacheinander, höchstens acht); unter „### Geändert“ ein Punkt: Tessera hält jetzt bis zu acht eigene Module offen (vorher fünf), vorgeladene werden nie geschlossen. Vorhandene Unterüberschriften nutzen, nichts Bestehendes umformulieren. Modul-Changelog (261008-w5w): eigene Module stehen nicht im Modul-Register, w5w liefert für sie eine leere Liste — daher KEIN Modul-Changelog-Eintrag; im SUMMARY vermerken.
|
||||
4. Gesamtprüfung: vollständige Testläufe API und Web, beide `tsc --noEmit`, `pnpm exec biome lint` auf alle in diesem Plan geänderten bzw. neuen .ts/.tsx-Dateien (Fehler beheben).
|
||||
5. Browser-Nachweis mit den Playwright-MCP-Werkzeugen, Dunkelmodus (über den Theme-Knopf umschalten), Ablage unter `.playwright-mcp/custom-module-preload/` (nicht committen). Vorher `docker compose up -d --build api web` und auf die API warten. Vorbereitung per curl (admin/admin123, Kategorieschlüssel aus GET /module-categories): zwei persönliche Module „Probe who A“ (`https://example.com/?who=a`) und „Probe who B“ (`https://example.com/?who=b`) mit preload true, ein drittes „Probe who C“ ohne. Ablauf: (a) http://localhost:3000 anmelden, Dashboard öffnen; (b) Seite neu laden (entspricht Start mit bestehender Sitzung) und SOFORT per `browser_evaluate` eine Messung starten, die 10 s lang alle 100 ms die Knoten `[data-testid="custom-module-frame"]` erfasst und je `data-custom-module-id` den Zeitpunkt des ersten Auftauchens (`performance.now()`), deren `style.visibility` und Breite festhält; zusätzlich `performance.getEntriesByType('navigation')[0].loadEventEnd` und die Einträge `performance.getEntriesByType('resource')` mit `initiatorType === 'iframe'` (Startzeiten) — NICHT per fetch aus der Seite messen; (c) vor dem Klick Rahmen A in `window.__probe` merken, `window.__loads = 0` und einen `load`-Zuhörer an Rahmen A hängen, der hochzählt; dann in der Seitenleiste „Probe who A“ anklicken, 2 s warten, prüfen: Anzahl Rahmen 2, `document.querySelector('[data-custom-module-id="<A>"]') === window.__probe`, `window.__loads === 0`, Rahmen A sichtbar; Bildschirmfoto `t3-dark-preloaded-open.png`; (d) Messwerte als JSON nach `.playwright-mcp/custom-module-preload/t3-measure.json` schreiben mit den Feldern `frameCountAtStart`, `allHiddenAtStart`, `allWidthPositiveAtStart`, `firstFrameAppearMs`, `gapBetweenFramesMs`, `loadEventEndMs`, `sameNodeAfterClick`, `loadsAfterClick`, `visibleAfterClick`, `notPreloadedFramePresent` (C darf beim Start NICHT auftauchen); (e) abmelden, prüfen, dass nach erneuter Anmeldung nur A und B wieder vorgeladen werden; (f) Probe-Module per curl löschen. Weicht ein Wert ab (z. B. Rahmen vor dem Ende des Seitenladens, Neuladen beim Klick), Ursache beheben statt Schwelle anpassen.
|
||||
Commit: `docs(quick-261008-who): Vorladen eigener Module in Anleitung und CHANGELOG` (explizite Pfade; nichts aus `gespraech-2026-11/` oder `.playwright-mcp/`).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api test && pnpm --filter @tessera/web test && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && sed -n '/^## Unveröffentlicht/,/^## [0-9]/p' CHANGELOG.md | grep -qi "vorladen" && sed -n '/^## Unveröffentlicht/,/^## [0-9]/p' CHANGELOG.md | grep -q "acht" && grep -q "Beim Start vorladen" docs/anleitung-anwender.md && grep -q "Offen halten und beim Start vorladen" docs/anleitung-anwender.md && test -f .playwright-mcp/custom-module-preload/t3-dark-preloaded-open.png && node -e 'const m=require("./.playwright-mcp/custom-module-preload/t3-measure.json");const ok=m.frameCountAtStart===2&&m.allHiddenAtStart===true&&m.allWidthPositiveAtStart===true&&m.firstFrameAppearMs>=1500&&m.firstFrameAppearMs>m.loadEventEndMs&&m.gapBetweenFramesMs>=1000&&m.sameNodeAfterClick===true&&m.loadsAfterClick===0&&m.visibleAfterClick===true&&m.notPreloadedFramePresent===false;if(!ok){console.error(JSON.stringify(m));process.exit(1)}console.log("browser ok")' && echo "task3 ok"</automated>
|
||||
</verify>
|
||||
<done>Anwenderanleitung und CHANGELOG beschreiben Offenhalten, Vorladen und die Grenze acht; Entwicklungsanleitung geprüft (nur bei vorhandener Architekturbeschreibung ergänzt); alle Tests und Typprüfungen grün; der Browser-Nachweis belegt verzögertes, gestaffeltes, unsichtbares Vorladen und die Übernahme beim ersten Klick ohne Neuladen; Doku-Commit liegt vor, nichts gepusht.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → API (PUT /custom-modules/:id/preload) | Kennung und Körper vom Client, nicht vertrauenswürdig |
|
||||
| Datenbank → Web (Adresse eigener Module) | Adresse wird als iframe-src im Portal gerendert, auch ohne Klick des Benutzers |
|
||||
| Benutzer A → Benutzer B (selbes Gerät/Fenster) | Abmelden/Benutzerwechsel ohne Neuladen der Seite |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-WHO-01 | Elevation of Privilege | CustomModulesService.setPreload | high | mitigate | Sichtbarkeit über `loadVisible` (fremde persönliche/fehlende Kennung → 404 ohne Schreiben); geschrieben wird ausschließlich die eigene Benutzerzeile `where: { id: caller.id }` über `forTenant(this.prisma, tenantId)`; kein Benutzer-Kennungsparameter; Tests in custom-modules.service.spec.ts |
|
||||
| T-WHO-02 | Information Disclosure | list/getOne, Benutzerantworten | medium | mitigate | `preload` wird je Aufrufer aus der eigenen Zeile berechnet; neue Spalte nicht in /auth/me oder Benutzerlisten ausgegeben (Prüfung in Aufgabe 1, Schritt 2); Test Benutzer A/B |
|
||||
| T-WHO-03 | Denial of Service | Benutzerzeile / Browser | medium | mitigate | Serverseitige Grenze `CUSTOM_MODULE_PRELOAD_MAX` (409), Bereinigung auf sichtbare Kennungen bei jedem Schreiben (Liste wächst nicht); Web kürzt auf acht und staffelt im Leerlauf, Offenhalten-Gesamtgrenze acht |
|
||||
| T-WHO-04 | Tampering | Vorlade-Komponente / Behälter (iframe-src) | high | mitigate | Vorladen nur bei `checkCustomModuleUrl(url) === 'ok'` (Komponente UND Behälter wie T-9WC-03), unveränderte Sandbox `XFRAME_SANDBOX`, `allow=""`, `referrerPolicy="no-referrer"`; Test mit http-Adresse wird nie vorgeladen |
|
||||
| T-WHO-05 | Information Disclosure | Abmelden/Benutzerwechsel | high | mitigate | Effekt-Aufräumen bricht ausstehendes Vorladen ab, bestehendes `clear()` in Kopfzeile/Behälter verwirft alle Rahmen und `pinned`; Tests in custom-module-preloader.test.tsx und Browser-Schritt (e) |
|
||||
| T-WHO-06 | Information Disclosure | Drittseiten erhalten Aufrufe beim Start | low | accept | Der Benutzer schaltet das Vorladen je Modul bewusst ein; kein Referrer, gleiche Sandbox wie beim manuellen Öffnen |
|
||||
| T-WHO-SC | Tampering | npm/pip/cargo installs | high | accept | Dieser Plan installiert keine Pakete; `@tessera/shared` ist ein vorhandenes Workspace-Paket |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Aufgabe 1: API-Tests (inkl. rls-access-inventory/rls-coverage), Web-Tests für Planer, Vorlade-Komponente, Behälter-Übernahme, Speicher; E2E-Skript gegen den neu gebauten Stack.
|
||||
- Aufgabe 2: Speicher-LRU mit Gepinnten und Grenze acht, Schalter in Modulansicht und Einstellungen, i18n-Prüfskript.
|
||||
- Aufgabe 3: Vollständige Testläufe, Typprüfungen, Lint der geänderten Dateien, Doku-Greps, Browser-Messwerte aus t3-measure.json.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Ein Benutzer markiert bis zu acht eigene Module (persönlich oder gemeinsam) für das Vorladen; ein anderer Benutzer bleibt unberührt.
|
||||
- Nach Anmeldung bzw. Neuladen erscheinen die Rahmen der markierten Module erst nach dem Seitenladen (frühestens ca. 1,5–2 s), nacheinander (Abstand ≥ 1 s), unsichtbar und mit sinnvoller Größe.
|
||||
- Der erste Klick zeigt den vorgeladenen Rahmen ohne Neuladen (selber Knoten, 0 load-Ereignisse).
|
||||
- Grenze acht offen; vorgeladene werden nie verworfen; neunter Vorladeversuch → Hinweis.
|
||||
- Abmelden verwirft alles; nicht markierte, gelöschte oder fremde Module werden nie vorgeladen.
|
||||
- Anwenderanleitung und CHANGELOG „Unveröffentlicht“ aktualisiert; kein Modul-Changelog-Eintrag (eigene Module stehen nicht im Register).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-SUMMARY.md` when done (mit: gewählter Speicherort und Begründung, tatsächlicher Migrations-Zeitstempel, Verhalten bei mehr als acht, Ergebnis der Prüfung docs/anleitung-entwicklung.md, Modul-Changelog-Entscheidung, Browser-Messwerte aus t3-measure.json)
|
||||
</output>
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
phase: 261008-who-eigene-module-beim-start-vorladen
|
||||
plan: 01
|
||||
subsystem: custom-modules
|
||||
tags: [preload, iframe, prisma, nestjs, nextjs, zustand]
|
||||
requires: [261008-w5w]
|
||||
provides:
|
||||
- "Benutzer-Einstellung 'Beim Start vorladen' je eigenem Modul (User.customModulePreloadIds, PUT /custom-modules/:id/preload)"
|
||||
- "Vorlade-Planer + Preloader-Komponente (verzoegert, gestaffelt, unsichtbar)"
|
||||
- "Offenhalten-Grenze 8, gepinnte Module nie per LRU verworfen"
|
||||
affects: [custom-modules, app-shell, settings/custom-modules]
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/prisma/migrations/20261008210000_user_custom_module_preload/migration.sql
|
||||
- apps/web/src/lib/custom-module-preload.ts
|
||||
- apps/web/src/components/modules/custom-module-preloader.tsx
|
||||
- apps/web/src/components/custom-modules/custom-module-preload-settings.tsx
|
||||
- .planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh
|
||||
modified:
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/src/custom-modules/*
|
||||
- apps/api/src/user/user.controller.ts
|
||||
- apps/web/src/lib/stores/custom-module-cache-store.ts
|
||||
- apps/web/src/components/modules/custom-module-frame-host.tsx
|
||||
- apps/web/src/components/modules/custom-module-view.tsx
|
||||
- apps/web/src/components/layout/app-shell.tsx
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- docs/anleitung-anwender.md
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- CHANGELOG.md
|
||||
- packages/shared/src/index.ts
|
||||
decisions:
|
||||
- "Speicherort: Spalte User.customModulePreloadIds (TEXT[]) statt eigener Tabelle - Benutzerzeile steht schon unter Zeilenschutz, keine neue RLS-Regel; tote Kennungen werden beim Lesen geschnitten, beim Schreiben entfernt"
|
||||
- "Schalter an zwei Orten: App-Leiste der Modulansicht und Karte in Einstellungen -> Eigene Module (alle sichtbaren Module, Zaehler x von 8)"
|
||||
- "Grenze acht gesamt; gepinnte zaehlen mit, werden nie verworfen; Server begrenzt Vorladen auf acht (409)"
|
||||
metrics:
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 1bb869a7b5245029ef28be844944e9b2914d2088
|
||||
plan_head_after: 86495ae71be6fcb64bf2559a28ed8e376c91a925
|
||||
status: complete
|
||||
actuals:
|
||||
tokens: 60000
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 1bb869a7b5245029ef28be844944e9b2914d2088
|
||||
plan_head_after: 86495ae71be6fcb64bf2559a28ed8e376c91a925
|
||||
commits: 3
|
||||
---
|
||||
|
||||
# Phase 261008-who Plan 01: Eigene Module beim Start vorladen Summary
|
||||
|
||||
Pro Benutzer serverseitig gespeicherte Wahl "Beim Start vorladen" fuer eigene Module; Tessera laedt die gewaehlten Module 2 s nach dem Start gestaffelt (1,5 s Abstand) unsichtbar im bestehenden Rahmen-Behaelter, der erste Klick uebernimmt denselben iframe-Knoten ohne Neuladen; Offenhalten-Grenze von fuenf auf acht.
|
||||
|
||||
## Commits
|
||||
|
||||
- ac10449 feat: Durchstich (Migration, API, Planer, Preloader, Speicher, Behaelter, E2E-Skript)
|
||||
- c7b6a4b feat: Schalter, Grenze acht mit gepinnten Modulen, Einstellungen-Karte, Texte de/en
|
||||
- 86495ae docs: Anleitung und CHANGELOG
|
||||
|
||||
## Wichtige Angaben
|
||||
|
||||
- **Migrations-Zeitstempel:** 20261008210000 (`_user_custom_module_preload`), auf dem lokalen Stand angewendet (Spalte vorhanden, `prisma migrate status` sauber).
|
||||
- **Verhalten bei mehr als acht:** Server liefert 409 beim neunten Vorladen (tote Kennungen zaehlen nicht mit), Oberflaeche zeigt Hinweis, Kontrollkaestchen der Karte werden bei acht gesperrt. Extremfall: acht gepinnt plus ein aktives, nicht gepinntes = neun Rahmen; ein weiteres nicht gepinntes verdraengt das vorige.
|
||||
- **docs/anleitung-entwicklung.md:** per grep geprueft, keine Beschreibung der Architektur eigener Module/des Rahmen-Behaelters -> unveraendert.
|
||||
- **Modul-Changelog (261008-w5w):** kein Eintrag; eigene Module stehen nicht im Modul-Register (leere Liste). Nur Gesamt-CHANGELOG ("Unveroeffentlicht": Neu + Geaendert). Der alte Punkt "fuenf zuletzt benutzten" in einer freigegebenen Version bleibt als Historie stehen.
|
||||
- **Antworten mit Benutzerzeile:** `user.controller.ts` entfernt `customModulePreloadIds` aus den drei Antworten, die die ganze Zeile zurueckgaben (T-WHO-02); Listen nutzen `select`.
|
||||
|
||||
## Tests
|
||||
|
||||
- API: 152 Dateien / 3049 Tests gruen (inkl. rls-coverage, rls-access-inventory); `tsc --noEmit` sauber.
|
||||
- Web: 148 Dateien / 1695 Tests gruen; `tsc --noEmit` sauber.
|
||||
- E2E `e2e-preload-api.sh` gegen neu gebauten Stack: `e2e preload api ok` (1-8 ok, 9 -> 409, "ja" -> 400, Zufalls-UUID -> 404, Aufraeumen).
|
||||
- Biome lint auf den geaenderten Dateien ohne neue Befunde (vorhandene Warnung in module-tile.tsx, Format-Abweichungen in user.controller.ts/shared index.ts bestanden schon).
|
||||
|
||||
## Browser-Messwerte (Chromium, Dunkelmodus, Neuladen mit bestehender Sitzung; `.playwright-mcp/custom-module-preload/t3-measure.json`)
|
||||
|
||||
| Messwert | Ergebnis |
|
||||
|---|---|
|
||||
| frameCountAtStart | 2 (A, B; C nie) |
|
||||
| allHiddenAtStart / allWidthPositiveAtStart | true / true (Breite 1136 px) |
|
||||
| firstFrameAppearMs | 2539 (loadEventEnd 339) |
|
||||
| gapBetweenFramesMs | 1500 |
|
||||
| sameNodeAfterClick | true (2 Rahmen, derselbe Knoten) |
|
||||
| loadsAfterClick | 0 |
|
||||
| visibleAfterClick | true (1136 x 828) |
|
||||
| notPreloadedFramePresent | false |
|
||||
| Abmelden | 0 Rahmen; erneute Anmeldung: nur A und B |
|
||||
|
||||
Bildschirmfotos: `t3-dark-preloaded-open.png`, `t3-dark-settings.png` (Karte "Beim Start vorladen", Zaehler 1 von 8). Probe-Module per curl geloescht.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] setPinned ignorierte die Serverwahl bei noch nicht offenem Rahmen**
|
||||
- **Found during:** Aufgabe 2 (Test Modulansicht)
|
||||
- **Issue:** `setPinned(id, true)` verlangt einen offenen Rahmen; beim ersten Laden der Modulansicht ist der Rahmen noch nicht in `openIds`, die Serverwahl wurde nicht gepinnt.
|
||||
- **Fix:** Die Ansicht und die Einstellungen-Karte rufen fuer "an" `preload(mod)` (nur bei gueltiger https-Adresse), fuer "aus" `setPinned(id, false)`.
|
||||
- **Files modified:** custom-module-view.tsx, custom-module-preload-settings.tsx
|
||||
- **Commit:** c7b6a4b
|
||||
|
||||
**2. [Rule 3 - Blocking] Seitentest der Einstellungen zaehlte Listenaufrufe**
|
||||
- **Issue:** Die neue Karte ruft ebenfalls `listCustomModules`, der bestehende Test erwartet zwei Aufrufe.
|
||||
- **Fix:** Karte im Seitentest per `vi.mock` ausgeblendet (eigener Test fuer die Karte).
|
||||
- **Commit:** c7b6a4b
|
||||
|
||||
**3. Fortsetzung nach abgebrochenem Lauf:** Aufgabe 1 war als unversionierter Arbeitsstand vorhanden; nach Pruefung (Tests, tsc, Migration angewendet, E2E) unveraendert uebernommen und committet.
|
||||
|
||||
Sonst: Plan wie geschrieben.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None - neue Route ist im Bedrohungsmodell (T-WHO-01..05) abgedeckt.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Tote Kennungen geloeschter Module bleiben in der Benutzerzeile, bis der Benutzer das naechste Mal schreibt (Entwurfsentscheidung).
|
||||
- Nicht gepusht.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Commits ac10449, c7b6a4b, 86495ae vorhanden; SUMMARY, Migration, Planer, Preloader, Karte, E2E-Skript vorhanden.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
created: 2026-10-09
|
||||
title: Cert Manager umbauen — mehrere Zertifikate zusammenführen, ZIP-Upload, „Fullchain erzeugen“
|
||||
area: modules/cert-manager
|
||||
severity: bug + enhancement
|
||||
trigger: NÄCHSTER BAU-KANDIDAT nach Abschluss von Quick 261008-who (Vorladen eigener Module).
|
||||
User hat das Modul am 09.10.2026 getestet und den Umbau ausdrücklich beauftragt
|
||||
(„muss umgebaut werden“), Reihenfolge: erst offene Schritte, dann dieser Punkt.
|
||||
---
|
||||
|
||||
## Befund des Users (Test 09.10.2026)
|
||||
|
||||
1. **Fehler:** Beim Zusammenführen lässt sich nur EIN Zertifikat hochladen. Wählt man ein
|
||||
zweites aus, wird das erste überschrieben.
|
||||
2. **Wunsch:** Auch eine **ZIP-Datei** hochladen können (z. B. wie sie vom Zertifikatsanbieter
|
||||
kommt) — Tessera entpackt und analysiert die enthaltenen Zertifikate/Schlüssel.
|
||||
3. **Wunsch:** Die Analyse funktioniert bereits richtig. Zusätzlich soll man auswählen können,
|
||||
was man haben möchte, z. B. **„Fullchain“** — Tessera ordnet dann selbst die richtigen
|
||||
Zertifikate (Server-Zertifikat → Zwischenzertifikate → ggf. Root) in der korrekten
|
||||
Reihenfolge zusammen und stellt das Ergebnis zum Download bereit.
|
||||
|
||||
## Hinweise für die Planung
|
||||
|
||||
- Mehrfachauswahl + Drag&Drop mehrerer Dateien, Liste der hochgeladenen Dateien mit
|
||||
Entfernen-Knopf; ZIP serverseitig mit Größen-/Anzahl-/Zip-Bomb-Grenzen entpacken.
|
||||
- Kettenbildung über Issuer/Subject + Authority/Subject Key Identifier, Lücken klar melden
|
||||
(„Zwischenzertifikat fehlt“), privater Schlüssel passend zum Zertifikat prüfen.
|
||||
- **User-Entscheidung 09.10.: „Es sollen immer alle Formate unterstützt sein.“** — als
|
||||
Eingabe UND als Ausgabe alle gängigen Formate: PEM/CRT/CER (Base64), DER, PKCS#7 (.p7b/.p7c),
|
||||
PKCS#12 (.pfx/.p12, mit Passwort), private Schlüssel (PKCS#1, PKCS#8, verschlüsselt/
|
||||
unverschlüsselt, RSA + EC), CSR, sowie Zusammenstellungen: Fullchain, nur Kette
|
||||
(Zwischenzertifikate), Zertifikat einzeln, Zertifikat + Schlüssel (PEM-Bundle), PFX mit
|
||||
wählbarem Passwort. Bestehende Konvertierungsfunktionen des Moduls prüfen und Lücken schließen.
|
||||
- Modulversion + Modul-Changelog (cert-manager steht auf 1.1.0) und Anwender-/Admin-Doku
|
||||
pflegen ([[feedback-modulversion-changelog]]).
|
||||
Reference in New Issue
Block a user