docs(quick-260922-m1h): Akte - Widget-Aufraeumen, Rundgang verhaltensneutral bestaetigt
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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 16:12:17 +02:00
parent 8be0725577
commit 1315f370a9
3 changed files with 363 additions and 2 deletions
+3 -2
View File
@@ -5,9 +5,9 @@ current_phase: 18
current_phase_name: desktop-client-fertigstellen
status: verified
stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren."
last_updated: "2026-09-22T13:40:00.000Z"
last_updated: "2026-09-22T14:15:00.000Z"
last_activity: 2026-09-21
last_activity_desc: Quick 260922-hk4 — Bilderrahmen-Bilder in den Dateibereich umgezogen (automatisch beim Start, Selbstheilung aus der alten Spalte), im Browser nachgewiesen; davor Freigabe 1.3.0
last_activity_desc: Quick 260922-m1h — Widget-Typen an einer Stelle, Katalog filtert nach Modulzugriff, Kachel kennt ihr Modul (Vorarbeit Proxmox); im Browser als verhaltensneutral bestaetigt
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
progress:
total_phases: 18
@@ -462,6 +462,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| fast-260922-b | **Desktop-App: Download-Knoepfe in der App ohne Funktion (fast, 747a4d4).** Befund des Nutzers: „Herunterladen“ unter Einstellungen → Desktop-App tut in der App nichts (Windows und Linux). Ursache: die Webansicht hatte keinen Download-Handler — webkit2gtk verwirft Downloads dann still, WebView2 zeigte ebenfalls nichts. Fix: Hauptfenster entsteht im Code (`app.windows` in tauri.conf.json leer), weil nur `WebviewWindowBuilder` `on_download` annimmt; der Handler bricht den Download in der App ab und oeffnet die Adresse per Opener im System-Browser (Fortschritt, Speicherort, Passwortfenster fuer den Proxy). Capability `main` unveraendert. cargo fmt/clippy/test gruen. Nicht am laufenden Client geprueft (kein Display auf dem Dev-Host) — CI baut, Nachweis beim Nutzer oder auf der Windows-VM. | 2026-09-22 | 747a4d4 | — |
| 260922-ge2 | **XFrame: Ausschnitt der Seite waehlen und einpassen, Zoom, „Nur anzeigen“.** Wunsch des Nutzers: nur einen bestimmten Ausschnitt der eingebetteten Seite zeigen, und die Groesse soll skalieren. Config: `crop {x,y,w,h}` in Seitenpixeln bei fester Layoutbreite 1280 (`XFRAME_PAGE_WIDTH`, keine UI), Klemmung ueber EINE Funktion `clampXframeCrop` (x+w ≤ 1280 verschiebt x; w ≥ 100, h ≥ 60, y+h ≤ 4000); `zoom` (50…150 %, nur Ganzseiten-Modus); `readOnly` (transparente Flaeche ueber dem Rahmen im Ansichtsmodus). Kachel: `computeCropLayout` (contain + Zentrierung, Massstab darf > 1 sein), der `<iframe>` wird selbst verschoben und skaliert (cross-origin — die Seite laesst sich von aussen nicht scrollen), Kachelmass per ResizeObserver. Einstellungen: Vorschau der Seite bei 1280 px (Stage 3000 Seitenpixel hoch, eigener Bildlauf), Rahmen als `<fieldset>` (Biome `useSemanticElements`) mit vier Eckgriffen, Ziehen per Pointer-Events mit lokalem Entwurf und genau einem PATCH beim Loslassen, Zahlenfelder als Tastaturweg; Zoom-Auswahl nur ohne Ausschnitt; Aktivieren setzt `readOnly` mit. **Befund im Browser-Rundgang, behoben (cf70a19):** Kachel und Vorschau hatten verschiedene Rahmenhoehen (max(y+h,720) vs. 3000) — bei vh-relativen Seiten (example.com `margin: 15vh`) lag derselbe Inhalt an verschiedenen Stellen, der gewaehlte Ausschnitt haette in der Kachel daneben gelegen; jetzt dieselbe Layouthoehe. Neun Pruefpunkte bestanden (Verschieben, Ecken mit fester Gegenecke und Mindestbreite, Klemmung der Zahlenfelder, Einpassen und Mitskalieren bei Kachelgroesse, Nur-anzeigen, Zoom 60 %, verweigernde Seite). Playwright kann in einem per `transform` skalierten iframe nicht selbst klicken — per `elementFromPoint` + `mouse.click` umgangen, ist eine Werkzeuggrenze. Test-Helfer `src/test/fake-resize-observer.ts`. **Zahlen:** web 604 → 640, api 1175, type-check 4/4, lint 5/5 (web 53 Warnungen unveraendert), `as unknown as` 27/6, Umlaut-Allowlist + „Ausschnitt“. | 2026-09-22 | 445b1d3,30fdd99,cf70a19 | [260922-ge2-xframe-widget-ausschnitt-der-eingebettet](./quick/260922-ge2-xframe-widget-ausschnitt-der-eingebettet/) |
| 260922-hk4 | **Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Datenbank.** Frage des Nutzers nach der Freigabe 1.3.0, ob `bytea` auf Dauer sinnvoll ist. Befund: Geschwindigkeit ist NICHT das Argument (ein Bild wird je Browser einmal taeglich geladen), die SICHERUNG ist es — gesichert wird von Hand per `pg_dump`, und 30 Bilder à 5 MiB je Benutzer waeren im Extremfall 150 MB pro Benutzer in jedem Abzug (alpha-DB heute 18 MB). Dazu Einheitlichkeit: Profilbilder (`user-files/avatars`, `User.avatarPath`) und DKV-Exporte liegen laengst im Volume. Umsetzung: Spalte `storagePath`, Ablage `user-files/dashboard-images/<userId>/<uuid>.<ext>` — Dateiname IMMER vom Server (UUID + Endung aus dem erkannten Mime-Typ), `originalName` nie im Pfad; ein eigener Ordner je Benutzer ist ausdruecklich KEIN Schutz, es entscheidet weiterhin die Besitzpruefung im Dienst. Umzug laeuft automatisch beim Start (`onApplicationBootstrap` ueber `forSystem()`), idempotent; die Spalte `data` bleibt bewusst vorerst stehen (Todo fuer den DROP, erst wenn alpha und live einmal gelaufen sind). **Befund im Rundgang, eigener Commit:** eine Zeile zeigte auf eine fehlende Datei (lokal Host vs. Container-Volume; im Betrieb: alter `pg_dump` + leeres Volume) — `getBytes` stellt die Datei jetzt aus der noch vorhandenen Spalte `data` wieder her, statt 404 zu melden. **Zahlen:** api 1175 → 1188, web 640, type-check 4/4, lint 5/5, RLS-Waechter 78/78. | 2026-09-22 | 9039cea,8cbfb8b,82472ee | [260922-hk4-bilderrahmen-bilder-auf-die-festplatte](./quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/) |
| 260922-m1h | **Ein Modul bringt seine Dashboard-Kachel jetzt selbst mit (Vorarbeit fuer Proxmox).** Bestandsaufnahme (lesend) hatte ergeben: ein neuer Widget-Typ war an SIEBEN Stellen hartkodiert (Union-Typ, Constraints, Registry, eigene `wireXWidget()` je Typ, Aufruf in page.tsx, zweite Liste im Katalogfenster, `@IsIn` im API-DTO); die Verbindung Kachel↔Modul existierte als `WIDGET_MODULE_MAP` in `dashboard.service.ts` (filtert fail-closed), war aber nie befuellt; der Katalog zeigte jedem alle Kacheln, auch die gesperrter Module. Umbau: `WIDGET_TYPES`/`WidgetType`/`WIDGET_MODULE_SLUGS` in `packages/shared` als EINE Quelle (API validiert per `@IsIn` gegen genau sie), ein generisches `registerWidget()` statt neun Funktionen, Katalog leitet seine Liste aus der Registry ab und filtert ueber `/modules/active` (fail-closed bei Fehler, reine Funktion `visibleWidgetTypes`), nicht verfuegbare Kachel zeigt `widgets.unavailable` statt leer zu bleiben. Deckungsgleichheits-Test faengt kuenftig jede vergessene Stelle. **Befund des Executors, geprueft statt vermutet:** `apps/web` hatte KEINE Abhaengigkeit auf `@tessera/shared` (frueher bewusst) — vor der Umsetzung nachgemessen, dass Bau und Produktions-Abbild das tragen (node:24-alpine strippt die Typen nativ); Folgeregel „nur loeschbare Syntax in shared“ steht als Warnung in der Datei. Verhalten der neun Kacheln unveraendert, im Browser bestaetigt (Reihenfolge, Anlegen, Entfernen, keine rohen Schluessel). Bewusst offen: der Einstellungs-Zweig je Typ in `widget-settings-panel.tsx` und die Live-Aktualisierung des Katalogs. **Zahlen:** api 1188 → 1202, web 640 → 659, type-check 4/4, lint 5/5 (74/53 wie Basis). | 2026-09-22 | 56c07c3,8be0725 | [260922-m1h-dashboard-widgets-ein-modul-bringt-seine](./quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/) |
## Deferred Items
@@ -0,0 +1,145 @@
---
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>
@@ -0,0 +1,215 @@
---
phase: quick-260922-m1h
plan: 01
subsystem: apps/web/src/components/dashboard
tags: [refactor, dashboard, widgets, module-access]
status: complete
requires: []
provides:
- "WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS als geteilte Quelle in packages/shared"
- "registerWidget() als einzige Anmeldestelle je Kachel"
- "visibleWidgetTypes() — Katalogfilter nach Modulzugriff, fail-closed"
affects:
- apps/api/src/dashboard
- apps/web/src/app/(portal)/page.tsx
tech-stack:
added:
- "apps/web haengt jetzt auf @tessera/shared (workspace:*)"
patterns:
- "erster Laufzeit-Import aus @tessera/shared (bisher nur import type)"
key-files:
created:
- apps/api/src/dashboard/widget-module-map.spec.ts
modified:
- packages/shared/src/index.ts
- apps/web/src/components/dashboard/widget-registry.tsx
- apps/web/src/components/dashboard/widget-catalog-modal.tsx
- apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
- apps/web/src/app/(portal)/page.tsx
- apps/api/src/dashboard/dto/create-widget.dto.ts
- apps/api/src/dashboard/widget-module-map.ts
decisions:
- "Typliste als Laufzeit-Konstante in packages/shared statt gespiegelter Kopien — traegt, weil Node 24 rohes TypeScript per Type-Stripping laedt"
- "apps/web bekommt die Abhaengigkeit auf @tessera/shared; die frueher dokumentierte Gegenbegruendung war ueberholt"
- "Katalogfilter als reine Funktion visibleWidgetTypes(registry, slugs|null) statt Logik im Dialog"
metrics:
duration: "~70 min"
completed: 2026-09-22
actuals:
tokens: 21000
tasks: 2
commits: 2
plan_head_before: ee2b025
---
# Quick-Aufgabe 260922-m1h: Ein Modul bringt seine Dashboard-Kachel selbst mit — Zusammenfassung
Die Kachel-Typliste stand an sieben Stellen; sie steht jetzt an einer. Der
Katalog fuehrt keine zweite Liste mehr und blendet Kacheln gesperrter Module
aus, eine Kachel ohne Bauteil erklaert sich mit einem Satz statt leer zu
bleiben. Die neun vorhandenen Kacheln verhalten sich unveraendert.
## So fuegt man kuenftig eine Modul-Kachel hinzu
Vorher sieben Stellen, jetzt drei (plus das Uebliche an Text und Maßen):
1. **Kachel-Komponente schreiben** — `apps/web/src/components/dashboard/widgets/<name>-widget.tsx`,
nimmt `WidgetProps` (`instanceId`, `config`, `isEditMode`).
2. **Typ eintragen** — in `WIDGET_TYPES` in `packages/shared/src/index.ts`. Gehoert die Kachel zu
einem Modul, zusaetzlich `WIDGET_MODULE_SLUGS['<typ>'] = '<modul-slug>'` in derselben Datei.
Das ist die einzige Liste — die API validiert per `@IsIn` gegen genau sie.
3. **Anmelden** — `registerWidget('<typ>', <Name>Widget)` in `apps/web/src/app/(portal)/page.tsx`.
Dazu wie bei jeder Oberflaeche: Uebersetzungsschluessel `<typ>.name` und `<typ>.description` unter
`widgets` in **de.json und en.json**, ein Inline-SVG-Symbol und die Groessenvorgaben in
`WIDGET_CONSTRAINTS` — Symbol und Maße in `widget-registry.tsx`.
Eine Kachel mit `moduleSlug` verschwindet danach **von selbst** aus Katalog und Dashboard, wenn der
Benutzer das Modul nicht nutzen darf. Vergisst man eine der drei Stellen, schlaegt der
Deckungsgleichheits-Test in `widget-registry.test.tsx` fehl, statt dass die Kachel im Katalog fehlt
oder die API mit 400 antwortet.
Dieselbe Liste steht als Abschnitt „Eine Kachel zum Modul" in
`docs/anleitung-entwicklung.md`.
## Was gebaut wurde
**Aufgabe 1 — `56c07c3`** (`refactor`)
- `packages/shared/src/index.ts`: `WIDGET_TYPES`, `WidgetType`, `WIDGET_MODULE_SLUGS`.
- `create-widget.dto.ts`: `@IsIn([...WIDGET_TYPES])` statt handgepflegter Liste.
- `widget-module-map.ts`: liest `WIDGET_MODULE_SLUGS` statt einer eigenen Kopie; die oeffentliche
Funktion `getModuleSlugForWidgetType()` ist unveraendert, damit `dashboard.service.spec.ts`
sie weiter mocken kann.
- `widget-registry.tsx`: neun `wireXWidget()` → ein `registerWidget()` (idempotent; unbekannter Typ
wirft in der Entwicklung, wird in der Produktion ignoriert). `WidgetDefinition` traegt
`moduleSlug?`. Neue reine Funktion `visibleWidgetTypes(registry, slugs|null)`.
- `widget-catalog-modal.tsx`: Liste kommt aus der Registry (Reihenfolge erhalten, Test darauf),
gefiltert nach Modulzugriff; neue Prop `accessibleModuleSlugs`.
- `(portal)/page.tsx`: neun `registerWidget`-Aufrufe; holt `/modules/active` im Muster der
Seitenleiste (`credentials: 'include'`, Fehler still) und reicht die Slugs an den Katalog durch.
**Aufgabe 2 — `8be0725`** (`docs`)
- `widget-wrapper.tsx`: Kachel ohne Bauteil zeigt `widgets.unavailable` zentriert und grau statt des
rohen Typnamens; Schluessel in de.json und en.json.
- Changelog-Stichpunkt unter „Unveroeffentlicht → Geaendert"; Entwicklerdoku-Abschnitt.
## Abweichungen vom Plan
**1. [Rule 3 — blockierend] Die Planannahme „apps/web importiert @tessera/shared bereits" war falsch**
- **Gefunden bei:** Aufgabe 1, vor der ersten Zeile Code.
- **Befund:** `apps/web` hatte **keine** Abhaengigkeit auf `@tessera/shared`. Zwei Kommentare
(`lib/app-version.ts`, `lib/desktop.ts`) dokumentierten das sogar ausdruecklich als Absicht und
begruendeten damit gespiegelte Typen. Ohne Abhaengigkeit ist Entscheidung 1 des Plans nicht
umsetzbar. Zudem waren **alle** bisherigen `@tessera/shared`-Importe in `apps/api` reine
`import type` — die Typliste ist aber ein Laufzeitwert.
- **Geprueft statt vermutet:**
- `nest build` mit einem Laufzeit-Import: laeuft; das Ergebnis laedt `@tessera/shared` im
fertigen `dist` tatsaechlich (nachgestellt, 9 Typen).
- `packages/shared` liefert rohes TypeScript ohne Bauschritt — in `node:24-alpine` direkt
geprueft: Node 24 laedt es per nativem Type-Stripping (`OK [ 'clock', 'xframe' ] {}`).
- Die alte Gegenbegruendung ist ueberholt: der Web-Dockerfile kopiert `packages/shared` in
deps- **und** builder-Stufe bereits. Es aendert sich nur das Lockfile (3 Zeilen).
- `pnpm --filter @tessera/web build` laeuft durch — ohne `transpilePackages`.
- **Umsetzung:** `@tessera/shared: workspace:*` in `apps/web/package.json`. Die beiden Kommentare,
deren Begruendung dadurch unwahr wurde, sagen jetzt den aktuellen Stand; die Typ-Spiegel selbst
blieben bewusst unangetastet (nicht Teil dieser Aufgabe).
- **Nebenwirkung fuer die Zukunft:** `packages/shared/src/index.ts` darf nur noch loeschbare Syntax
enthalten — kein `enum`, kein `namespace`, keine Parameter-Eigenschaften. Steht als Warnung in
der Datei.
**2. [Abweichung vom Auftrag des Orchestrators] Keine gemeinsame Hilfsfunktion fuer `/modules/active`**
Der Auftrag nannte „dieselbe Hilfsfunktion wie die Seitenleiste". Eine solche gibt es nicht: die
Seitenleiste hat einen eingebauten `fetch`, und `lib/api.ts#getActiveModules` ist serverseitig
(Cookie-Header, kein `credentials`). Die Dashboard-Seite benutzt daher dasselbe **Muster** wie die
Seitenleiste. Eine Hilfsfunktion herauszuloesen haette `sidebar.tsx` angefasst — ausserhalb dieser
Aufgabe.
## Bewusst offen gelassen
- **`widget-settings-panel.tsx`** — der Einstellungs-Zweig je Typ bleibt wie er war. Den generisch
zu machen ist ein eigener Umbau (so im Plan festgelegt). Die Datei wurde nicht angefasst.
- **Die Typ-Spiegel** in `lib/app-version.ts` und `lib/desktop.ts` koennten jetzt echte Importe
werden. Nicht gemacht, nur die Kommentare richtiggestellt.
- **Katalog aktualisiert sich nicht live**, wenn im Marketplace gerade ein Modul freigeschaltet
wird — die Seitenleiste tut das ueber `sidebarRefreshKey`, die Dashboard-Seite holt die Liste nur
beim Aufbau. Heute ohne Wirkung (keine Kachel hat einen `moduleSlug`); mit Proxmox reicht ein
Neuladen der Seite. Bewusst so, weil der Auffrisch-Ausloeser einen `biome-ignore` erzwungen
haette, den die Qualitaetsregeln dieser Aufgabe ausschliessen.
## Keine Stubs
Es wurden keine Platzhalter, leeren Rueckgaben oder „coming soon"-Texte eingebaut.
`WIDGET_MODULE_SLUGS` ist leer — das ist kein Stub, sondern der korrekte Zustand: alle neun Kacheln
sind Plattform-Kacheln. Die erste Modul-Kachel (Proxmox) traegt sich dort ein.
## Bedrohungsmodell
| ID | Stand |
|---|---|
| T-M1H-01 | Akzeptiert wie geplant. Der Katalogfilter ist Komfort; im Code an drei Stellen so kommentiert. Durchsetzung bleibt `DashboardService.getWidgets` (unveraendert, 85 Tests gruen). |
| T-M1H-02 | Unveraendert — diese Aufgabe schwaecht nichts ab. Fuer Proxmox bleibt `@UseModule(slug)` verbindlich. |
| T-M1H-03 | Mitigiert. Reine Konstantenliste, keine Laufzeitdaten; die API validiert weiterhin `@IsIn` gegen genau diese Liste — jetzt nachweislich (Test validiert alle neun Typen und lehnt einen unbekannten ab). |
| T-M1H-04 | Mitigiert. `accessibleModuleSlugs === null` blendet Kacheln MIT `moduleSlug` aus; Test im Katalog und in `visibleWidgetTypes`. |
## Zahlen
| | |
|---|---|
| Commits | 2 (`56c07c3`, `8be0725`), Basis `ee2b025` |
| Dateien geaendert | 20 (1 neu) |
| Zeilen | +610 / −150 (gemessen: `git diff --shortstat ee2b025 HEAD`) |
| Neue Tests | 29 (Registry 9, Katalog 3, Seite 2, `widget-module-map.spec.ts` 14, Wrapper 1) |
| API-Tests | 1202 gruen (76 Dateien) |
| Web-Tests | 659 gruen (81 Dateien) |
| type-check | sauber (4 Pakete) |
| lint | api 74 / web 53 Warnungen — **unveraendert** zur Basis |
| `as unknown as` | api 27 / web 6 — **unveraendert** |
| neue `any` / `!` / `biome-ignore` | 0 / 0 / 0 |
| Next.js-Produktionsbau | laeuft |
| `nest build` | laeuft |
## Browser-Pruefliste
Der Umbau ist verhaltensneutral — die Pruefung soll vor allem bestaetigen, dass **nichts** anders
aussieht. Lokalen Stack neu bauen (`--build`), dann im Portal:
1. **Dashboard oeffnen.** Alle bisherigen Kacheln stehen an ihrem Platz und funktionieren wie
vorher (Uhr laeuft, Kalender zeigt Termine, Bilderrahmen wechselt, XFrame laedt).
2. **Stift → „Widget hinzufuegen".** Der Katalog zeigt **neun** Kacheln in genau dieser Reihenfolge:
Uhr, Suchleiste, Kalender, Notiz, Taschenrechner, Favoriten, Stoppuhr, Bilderrahmen, XFrame.
Namen und Beschreibungen unveraendert.
3. **Eine Kachel anlegen** (z. B. Stoppuhr) — sie erscheint, laesst sich ziehen, vergroessern und
wieder entfernen. Kein 400-Fehler.
4. **Groessen pruefen:** eine frisch angelegte Kachel hat dieselbe Startgroesse wie frueher, und
sie laesst sich nicht kleiner ziehen als bisher.
5. **Einstellungen → Dashboard:** die Einstellungen je Kachel sind unveraendert da (dieser Bereich
wurde bewusst nicht angefasst).
6. **Sprache auf Englisch umstellen** — der Katalog bleibt vollstaendig, keine rohen Schluessel wie
`clock.name` sichtbar.
7. *(optional, zeigt das Neue)* Der Hinweis bei einer nicht verfuegbaren Kachel laesst sich heute
nur kuenstlich ausloesen — er greift erst mit der ersten Modul-Kachel. Wer ihn sehen will: in der
Datenbank den `widgetType` einer vorhandenen Kachel auf `proxmox` setzen und die Seite neu laden;
die Kachel zeigt dann „Diese Kachel steht nicht zur Verfuegung — das zugehoerige Modul ist nicht
freigegeben." statt leer zu bleiben. Danach zuruecksetzen.
## Self-Check: PASSED
- `apps/api/src/dashboard/widget-module-map.spec.ts` vorhanden.
- Commits `56c07c3` und `8be0725` in `git log` gefunden.
- `git diff --diff-filter=D ee2b025..HEAD` — keine geloeschten Dateien.
- `git rev-list --count ee2b025..HEAD` = 2, gemessen.
- `.planning/**` nicht committet.
## Rundgang durch den Orchestrator (22.09.2026, lokaler Stack aus 8be0725)
Bestanden, keine Abweichung zum Stand vorher:
- Dashboard zeigt die bestehenden Kacheln (Kalender, Notizen, Favoriten, Bilderrahmen, XFrame) unveraendert, keine Konsolenfehler.
- Katalog zeigt **neun** Kacheln in der alten Reihenfolge: Uhr, Suchleiste, Kalender, Notizen, Taschenrechner, Favoriten, Stoppuhr, Bilderrahmen, XFrame; Namen und Beschreibungen unveraendert, keine rohen Schluessel.
- Stoppuhr angelegt → erscheint (396x160 px), wird gespeichert (`stopwatch` in `GET /dashboard/widgets`), kein 400; danach wieder entfernt, Liste sauber.
- `/modules/active` wird beim Seitenaufbau abgerufen (4x 200) — der Katalogfilter hat seine Datenquelle.
- Der Hinweis bei nicht verfuegbarer Kachel liess sich nicht echt ausloesen (es gibt noch keine Modul-Kachel); er ist durch den Test in `widget-wrapper.test.tsx` gedeckt und greift mit Proxmox.