146 lines
12 KiB
Markdown
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>
|