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). + + + +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) + 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]]).