--- phase: quick-260922-m1h plan: 01 type: refactor autonomous: true subsystem: apps/web/src/components/dashboard requirements: [] --- # Quick-Aufgabe 260922-m1h: Ein Modul bringt seine Dashboard-Kachel selbst mit ## Warum (Auftrag des Nutzers, 22.09.2026) Als Naechstes kommt ein Proxmox-Modul (PVE/PBS/PMG), das zusaetzlich als kompakte Kachel auf dem Dashboard erscheinen soll — und kuenftig sollen weitere Module dasselbe tun (PBS: Sicherungsstatus, PMG: Mail-Zahlen). Eine Bestandsaufnahme (lesend, 22.09.) hat ergeben: - **Ein neuer Widget-Typ ist heute an SIEBEN Stellen hartkodiert**: `WidgetType` (Union), `WIDGET_CONSTRAINTS`, `WIDGET_REGISTRY`, eine eigene `wireXWidget()` je Typ, der Aufruf in `(portal)/page.tsx`, die ZWEITE Liste `WIDGET_TYPES` in `widget-catalog-modal.tsx` und die `@IsIn`-Whitelist in `apps/api/src/dashboard/dto/create-widget.dto.ts`. Vergisst man eine, fehlt die Kachel im Katalog oder die API lehnt sie mit 400 ab. - **Die Verbindung Kachel↔Modul existiert schon, ist aber leer:** `apps/api/src/dashboard/widget-module-map.ts` (`WIDGET_MODULE_MAP = {}`), gelesen von `dashboard.service.ts` — `getWidgets()` filtert Kacheln aus, deren Modul der Benutzer nicht hat (fail-closed, Zeile ~164-205). Das funktioniert, wurde nur nie benutzt. - **Zwei Luecken:** (a) der Katalog („Widget hinzufuegen") zeigt JEDEM alle Kacheln, auch die gesperrter Module — anlegen geht, danach verschwindet die Kachel kommentarlos; (b) eine Kachel mit unbekanntem Typ rendert leer, ohne Erklaerung. Diese Aufgabe raeumt das auf, BEVOR Proxmox kommt. Kein neues Modul, keine neue Kachel — reiner Umbau mit unveraendertem Verhalten fuer die neun vorhandenen Kacheln. ## Gebundene Entscheidungen (Orchestrator) 1. **Eine Quelle fuer die Typliste, geteilt zwischen Web und API.** In `packages/shared/src/index.ts` (wird von beiden Apps bereits importiert, z. B. `desktop.service.ts`, `apps/web/src/lib/app-version.ts`) kommt: ```ts export const WIDGET_TYPES = ['clock','search','calendar','note','calculator','favorites','stopwatch','picture-frame','xframe'] as const; export type WidgetType = (typeof WIDGET_TYPES)[number]; /** Kachel → Modul-Slug; eine Kachel ohne Eintrag ist immer sichtbar. */ export const WIDGET_MODULE_SLUGS: Partial> = {}; ``` `create-widget.dto.ts` validiert mit `@IsIn([...WIDGET_TYPES])`, das Frontend leitet `WidgetType` von dort ab. `widget-module-map.ts` behaelt seine oeffentliche Funktion `getModuleSlugForWidgetType()`, liest aber `WIDGET_MODULE_SLUGS` aus `@tessera/shared` statt einer eigenen Kopie (Kommentar: eine Tabelle fuer beide Seiten, damit Katalogfilter und Server-Filter nicht auseinanderlaufen). 2. **Eine Anmeldestelle je Kachel.** Statt neun `wireXWidget()`-Funktionen mit je eigenem Bool-Flag ein generisches `registerWidget(type, component)` in `widget-registry.tsx`; `(portal)/page.tsx` ruft es je Kachel einmal auf (die Datei bleibt die Stelle, an der die Komponenten importiert werden — der Zirkelimport-Grund aus dem Bestandskommentar gilt weiter, also NICHT die Komponenten direkt in der Registry importieren). Mehrfachanmeldung desselben Typs ist ein No-Op (wie die bisherigen Flags); Anmeldung eines unbekannten Typs wirft in der Entwicklung und wird in der Produktion ignoriert. 3. **`WIDGET_REGISTRY` bekommt `moduleSlug?: string`** je Eintrag, befuellt aus `WIDGET_MODULE_SLUGS`. Heute bleibt es fuer alle neun Kacheln leer. 4. **Der Katalog leitet seine Liste aus der Registry ab** (`Object.keys` in der Reihenfolge der Registry-Definition, die heutige Reihenfolge bleibt erhalten — Test darauf) und **filtert nach Modulzugriff**: `widget-catalog-modal.tsx` bekommt eine Liste der zugaenglichen Modul-Slugs als Prop von der Seite, die sie ueber den vorhandenen Weg `/modules/active` holt (Muster `apps/web/src/components/layout/sidebar.tsx` — dort wird genau dieser Endpunkt schon gefetcht; dieselbe Hilfsfunktion nutzen, nicht neu bauen). Eine Kachel ohne `moduleSlug` ist immer sichtbar; eine mit `moduleSlug` nur, wenn der Slug in der Liste steht. Schlaegt der Abruf fehl, werden Kacheln MIT `moduleSlug` ausgeblendet (fail-closed, wie serverseitig). 5. **Gesperrte/unbekannte Kachel erklaert sich.** `widget-wrapper.tsx` rendert heute nichts, wenn `definition?.component` fehlt. Neu: ein zentrierter grauer Hinweistext `widgets.unavailable` („Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.") in de und en. Der Fall tritt erst mit Proxmox real auf, ist aber ab jetzt abgedeckt. 6. **Verhalten der neun vorhandenen Kacheln aendert sich NICHT.** Gleiche Namen, gleiche Reihenfolge im Katalog, gleiche Groessenvorgaben, gleiche Einstellungen. Der Einstellungs-Zweig je Typ in `widget-settings-panel.tsx` bleibt wie er ist — den generisch zu machen waere ein eigener Umbau und gehoert NICHT in diese Aufgabe (im SUMMARY als bewusst offen gelassen nennen). 7. Keine neuen Abhaengigkeiten. Keine Aenderung an der Datenbank. ## Aufgaben Aufgabe 1: Typliste nach @tessera/shared, generische Anmeldung, Katalog aus der Registry packages/shared/src/index.ts, apps/api/src/dashboard/dto/create-widget.dto.ts, apps/api/src/dashboard/widget-module-map.ts, apps/api/src/dashboard/widget-module-map.spec.ts, apps/web/src/components/dashboard/widget-registry.tsx, apps/web/src/components/dashboard/widget-registry.test.tsx, apps/web/src/app/(portal)/page.tsx, apps/web/src/app/(portal)/page.test.tsx, apps/web/src/components/dashboard/widget-catalog-modal.tsx, apps/web/src/components/dashboard/widget-catalog-modal.test.tsx Entscheidungen 1-4 umsetzen. Reihenfolge: shared zuerst (beide Apps bauen dagegen), dann API-DTO und `widget-module-map.ts`, dann Registry + `registerWidget`, dann `page.tsx`, zuletzt der Katalog. Tests zuerst anpassen/ergaenzen, wo sie die alten Namen festhalten (`widget-registry.test.tsx` prueft heute die Typliste und die Constraints-Tabelle; `widget-catalog-modal.test.tsx` die Eintraege). Neu mindestens: Katalogreihenfolge entspricht der Registry-Reihenfolge; eine Kachel mit `moduleSlug` fehlt im Katalog, wenn der Slug nicht in den zugaenglichen Modulen steht, und erscheint, wenn doch; fehlgeschlagener Modulabruf blendet Kacheln mit `moduleSlug` aus; `registerWidget` ist idempotent; `WIDGET_TYPES` aus shared und die Registry-Schluessel sind deckungsgleich (ein Test, der kuenftig jede vergessene Stelle faengt). Fuer den Katalog-Test eine Kachel mit `moduleSlug` brauchen, ohne eine echte zu erfinden: die Registry im Test per Hilfsfunktion um einen Testeintrag erweitern ODER den Filter als reine Funktion `visibleWidgetTypes(registry, accessibleSlugs | null)` auslagern und diese direkt testen — die reine Funktion ist vorzuziehen (Muster `picture-frame-config.ts`). Commit: `refactor(quick-260922-m1h): Widget-Typen an einer Stelle, Katalog aus der Registry, Kachel kennt ihr Modul` cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard "src/app/(portal)/page.test.tsx" && pnpm --filter @tessera/api exec vitest run src/dashboard && pnpm type-check && pnpm lint `WIDGET_TYPES`/`WidgetType`/`WIDGET_MODULE_SLUGS` stehen in `packages/shared`; API-DTO und Web leiten davon ab; genau EINE `registerWidget`-Funktion (kein `wireXWidget` mehr); Katalogliste kommt aus der Registry (keine zweite Liste); Deckungsgleichheits-Test vorhanden und gruen. Alle bestehenden Tests gruen, Reihenfolge und Namen der neun Kacheln unveraendert. Aufgabe 2: Gesperrte Kachel erklaert sich, Uebersetzungen, Changelog, Entwicklerdoku apps/web/src/components/dashboard/widgets/widget-wrapper.tsx, apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, CHANGELOG.md, docs/anleitung-entwicklung.md Entscheidung 5 umsetzen (Hinweistext statt leerer Kachel, Test dafuer), Schluessel `widgets.unavailable` in beiden Sprachdateien. `docs/anleitung-entwicklung.md`: den vorhandenen Modul-Walkthrough (Abschnitt um Zeile 372-400) um einen kurzen Abschnitt „Eine Kachel zum Modul" ergaenzen — welche drei Stellen es NACH diesem Umbau noch sind (Komponente schreiben, `registerWidget` in `page.tsx`, Eintrag in `WIDGET_TYPES` + optional `WIDGET_MODULE_SLUGS` in `packages/shared`, plus Uebersetzungen und Groessenvorgaben) und dass eine Kachel mit `moduleSlug` automatisch aus Katalog und Dashboard verschwindet, wenn das Modul fehlt. CHANGELOG unter „Unveröffentlicht → Geändert": „Dashboard: Kacheln, die zu einem Modul gehören, erscheinen nur noch für Benutzer, die dieses Modul nutzen dürfen; eine nicht mehr freigegebene Kachel erklärt das jetzt, statt leer zu bleiben" (Stichpunkt, kein Fliesstext). Volle Tore am Ende. Commit: `docs(quick-260922-m1h): Hinweis bei gesperrter Kachel, Changelog und Entwicklerdoku` cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard src/messages && pnpm type-check && pnpm lint && pnpm --filter @tessera/api test && pnpm --filter @tessera/web test Unbekannter/gesperrter Typ zeigt den Hinweistext (Test); beide Sprachdateien tragen den Schluessel; Changelog-Zeile steht; Entwicklerdoku nennt die verbliebenen Schritte; alle Tore gruen; genau zwei Commits mit Scope `quick-260922-m1h`. ## Hinweise fuer den Executor - HEAD ist `ee2b025`, Arbeitsbaum sauber, Zweig `main`. Version 1.3.0 wurde heute freigegeben; dieser Umbau geht in die naechste Freigabe. Zweig `live` und Tags NICHT anfassen. - `packages/shared` wird von beiden Apps importiert (`@tessera/shared`); pruefen, ob ein Build-Schritt noetig ist (`pnpm --filter @tessera/shared build`?) — turbo erledigt das ueblicherweise, im Zweifel `pnpm build` fuer shared vor dem Typecheck. - Qualitaetsregeln: keine neue `any`, `as unknown as` api 27 / web 6 unveraendert, keine `!`, kein `biome-ignore`, web-Warnungen bleiben 53, api 74. - Commits: Conventional Commits, Scope `quick-260922-m1h`, deutscher Betreff im Stil von `git log --oneline -15`, Commit-Body endet mit `Co-Authored-By: Claude Opus 5 (1M context) ` - `.planning/**` NICHT committen. - Testserver nicht anfassen. Lokaler Docker-Stack laeuft, nicht noetig fuer diese Aufgabe. - SUMMARY nach `/home/vicolab/projects/tessera-ctl/.planning/quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/260922-m1h-SUMMARY.md` (`status: complete`), mit: was jetzt noch zu tun ist, um eine Modul-Kachel hinzuzufuegen (die kurze Liste), Abweichungen, Zahlen, und einer kurzen Browser-Pruefliste fuer mich. ASVS 1, block on high. | ID | Bedrohung | Schwere | Disposition | |---|---|---|---| | T-M1H-01 | Katalogfilter clientseitig = Umgehung moeglich (Kachel per API trotzdem anlegen) | medium | Der Katalogfilter ist Komfort, die Durchsetzung bleibt serverseitig in `dashboard.service.ts` (`getWidgets()` filtert fail-closed) und im Modul-Guard der jeweiligen Daten-Endpunkte. Im Code so kommentieren. Akzeptiert. | | T-M1H-02 | Kachel eines gesperrten Moduls zeigt weiter Daten | high | Daten holt jede Kachel ueber ihre eigenen Modul-Endpunkte, die `@UseModule(slug)` tragen muessen — fuer Proxmox in der naechsten Aufgabe verbindlich. Diese Aufgabe aendert daran nichts und schwaecht nichts ab. | | T-M1H-03 | Typliste in `packages/shared` als neue Vertrauensgrenze | low | Reine Konstantenliste, keine Laufzeitdaten; die API validiert weiterhin mit `@IsIn` gegen genau diese Liste. Mitigiert. | | T-M1H-04 | Fehlender Modulabruf oeffnet den Katalog | medium | Fail-closed: bei Fehler werden Kacheln MIT `moduleSlug` ausgeblendet (Entscheidung 4), Test dafuer. Mitigiert. |