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