Files
tessera-ctl/.planning/quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/260922-m1h-PLAN.md
T
schalli 1315f370a9
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 1m15s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m7s
docs(quick-260922-m1h): Akte - Widget-Aufraeumen, Rundgang verhaltensneutral bestaetigt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:12:17 +02:00

146 lines
12 KiB
Markdown

---
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<Record<WidgetType, string>> = {};
```
`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
<tasks>
<task type="auto" tdd="true">
<name>Aufgabe 1: Typliste nach @tessera/shared, generische Anmeldung, Katalog aus der Registry</name>
<files>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</files>
<action>
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`
</action>
<verify>
<automated>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</automated>
</verify>
<done>`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.</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 2: Gesperrte Kachel erklaert sich, Uebersetzungen, Changelog, Entwicklerdoku</name>
<files>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</files>
<action>
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`
</action>
<verify>
<automated>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</automated>
</verify>
<done>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`.</done>
</task>
</tasks>
## 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) <noreply@anthropic.com>`
- `.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.
<threat_model>
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. |
</threat_model>