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

12 KiB

phase, plan, type, autonomous, subsystem, requirements
phase plan type autonomous subsystem requirements
quick-260922-m1h 01 refactor true apps/web/src/components/dashboard

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:
    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

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) <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>