diff --git a/.planning/STATE.md b/.planning/STATE.md
index 3945cd1..e2035b0 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -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
diff --git a/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-PLAN.md b/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-PLAN.md
new file mode 100644
index 0000000..8938a62
--- /dev/null
+++ b/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-PLAN.md
@@ -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: " neben "
+ 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"
+---
+
+
+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).
+
+
+
+@~/.claude/gsd-core/workflows/execute-plan.md
+@~/.claude/gsd-core/templates/summary.md
+
+
+
+@.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) `
+
+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.
+
+
+
+
+
+ Aufgabe 1 (Durchstich): Einstellung speichern → nach dem Start unsichtbar vorladen → erster Klick übernimmt den Rahmen
+ 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
+
+ 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`.
+
+
+ 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({ 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 `` 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`: `` bekommt `data-app-shell-main`; `` direkt vor `` 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).
+
+
+ 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"
+
+ 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.
+
+
+
+ Aufgabe 2: Grenze acht mit gepinnten Modulen, Schalter in der Modulansicht und Karte in den Einstellungen
+ 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
+
+ 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.
+
+
+ 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`.
+
+
+ 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"
+
+ 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.
+
+
+
+ Aufgabe 3: Anleitung, CHANGELOG, Gesamtprüfung und Browser-Nachweis
+ docs/anleitung-anwender.md, CHANGELOG.md
+
+ 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=""]') === 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/`).
+
+
+ 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"
+
+ 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.
+
+
+
+
+
+## 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 |
+
+
+
+- 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.
+
+
+
+- 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).
+
+
+
diff --git a/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-SUMMARY.md b/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-SUMMARY.md
new file mode 100644
index 0000000..a620cd3
--- /dev/null
+++ b/.planning/quick/261008-who-eigene-module-beim-start-vorladen/261008-who-SUMMARY.md
@@ -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.
diff --git a/.planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md b/.planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md
new file mode 100644
index 0000000..6a48a93
--- /dev/null
+++ b/.planning/todos/pending/2026-10-09-cert-manager-mehrere-dateien-zip-fullchain.md
@@ -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]]).