diff --git a/.planning/phases/05-dashboard-calendar/05-RESEARCH.md b/.planning/phases/05-dashboard-calendar/05-RESEARCH.md new file mode 100644 index 0000000..dc527f6 --- /dev/null +++ b/.planning/phases/05-dashboard-calendar/05-RESEARCH.md @@ -0,0 +1,792 @@ +# Phase 5: Dashboard & Calendar - Research + +**Researched:** 2026-06-23 +**Domain:** Configurable dashboard with drag-and-drop widget grid, calendar integration (CalDAV/Exchange/ICS), settings page +**Confidence:** MEDIUM + +## Summary + +Phase 5 baut die zentrale Dashboard-Seite mit einem Drag-and-Drop Widget-Grid (react-grid-layout 2.2.x), vier Kern-Widgets (Uhr, Suchleiste, Kalender, Notizen), eine Einstellungen-Seite mit Sub-Sidebar, und ein Backend fuer Calendar-Source-Integration (CalDAV via tsdav, ICS via node-ical, Exchange via ews-javascript-api/MS Graph). Das Dashboard-Layout wird pro Benutzer in PostgreSQL gespeichert und ueber Geraete synchronisiert. + +Die bestehende Codebasis bietet solide Integrationspunkte: der Dashboard-Platzhalter (`(portal)/page.tsx`) wird zur echten Grid-Seite, der Header bekommt einen Settings-Link im User-Dropdown, und das Prisma-Schema wird um DashboardLayout, WidgetInstance, CalendarSource und SearchProvider Models erweitert. Die etablierten Patterns (NestJS Module, Zustand Stores, next-intl, OKLCH Theming) werden konsistent fortgefuehrt. + +Kritische Komplexitaet liegt in drei Bereichen: (1) react-grid-layout v2 hat eine neue Hooks-API die sich von v1 unterscheidet, (2) Kalender-Protokolle (CalDAV/EWS/ICS) haben jeweils eigene Authentifizierungs- und Parsing-Anforderungen, (3) die Einstellungen-Seite muss ein eigenes Layout mit Sub-Sidebar etablieren das getrennt vom Portal-Layout funktioniert. + +**Primary recommendation:** Backend-first entwickeln (Prisma Schema, Dashboard CRUD API, Calendar Source Service), dann Frontend-Grid mit react-grid-layout v2 Hooks-API, dann Widgets einzeln, dann Settings-Seite. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Edit-Modus per Edit-Button (Pencil-Icon oben rechts). Klick aktiviert Bearbeitungsmodus: Grid sichtbar, Widgets verschiebbar/skalierbar, Widget-Hinzufuegen-Button erscheint. Zweiter Klick speichert und beendet Edit-Modus. +- **D-02:** Neue Benutzer starten mit leerem Grid + Empty State Hinweis. Kein vorkonfiguriertes Standard-Layout. +- **D-03:** Im Dashboard (Edit-Modus) nur Groesse und Position konfigurierbar. Alle anderen Widget-Einstellungen in Einstellungen > Dashboard. +- **D-04:** Widgets sind mehrfach platzierbar (z.B. 2 Uhren fuer verschiedene Zeitzonen, mehrere Notizen). +- **D-05:** Layout wird pro Benutzer in PostgreSQL gespeichert (DASH-07). Sync ueber Geraete. +- **D-06:** Jedes Widget hat eine sinnvolle Mindestgroesse pro Typ. Claude legt konkrete Werte fest. +- **D-07:** Kein Dashboard-Reset-Button. Benutzer loescht Widgets manuell. +- **D-08:** Alle drei Quellentypen in v1: CalDAV (WebDAV), Exchange (EWS/Graph API), ICS-Link (.ics URL). +- **D-09:** Kalenderquellen werden pro Benutzer konfiguriert (nicht pro Mandant). +- **D-10:** Kalender-Widget ist read-only — zeigt kommende Termine an. +- **D-11:** Kalenderquellen-Verwaltung in Einstellungen > Dashboard > Kalender. +- **D-12:** Zeitzone pro Uhr-Instanz konfigurierbar in Einstellungen > Dashboard. +- **D-13:** Datum optional anzeigbar unter der Uhrzeit. Ein/ausschaltbar in Einstellungen. +- **D-14:** Suchleiste: Dropdown Suchanbieter-Auswahl links, Suchfeld Mitte, Suchen-Button rechts. Oeffnet neuen Browser-Tab. +- **D-15:** Drei Standard-Suchanbieter vorinstalliert: Google, Bing, DuckDuckGo. Custom-Anbieter in Einstellungen hinzufuegbar. +- **D-16:** Markdown-Editor mit kompakter Bearbeitungsleiste: Fett, Kursiv, Unterstrichen, Liste, Checkbox, Link, Code. +- **D-17:** Jede Notiz-Instanz hat eigenen editierbaren Titel. Konfiguration in Einstellungen. +- **D-18:** Autosave mit Debounce. Kein manueller Speichern-Button. +- **D-19:** Settings-Seite erreichbar ueber User-Avatar-Icon (rechts oben, neben Logout). NICHT als Sidebar-Item. +- **D-20:** Settings-Seite hat eigene Sub-Sidebar fuer Einstellungskategorien. +- **D-21:** Desktop/Laptop: Grid skaliert proportional runter. Layout bleibt gleich. +- **D-22:** Mobile: Widgets stapeln sich vertikal (1 Spalte). Reihenfolge konfigurierbar. + +### Claude's Discretion +- Widget-Hinzufuegen-UI: Modal-Katalog vs. Inline-Leiste vs. anderes Pattern +- Widget-spezifische Config-Granularitaet +- Uhr-Widget Stil: Analog vs. digital vs. konfigurierbar +- Kalender-Konfiguration UX: Quellen-Liste + Formular vs. Wizard +- Markdown-Editor Library-Wahl +- Settings Sub-Sidebar Kategorie-Struktur fuer Dashboard-Bereich + +### Deferred Ideas (OUT OF SCOPE) +- Verwaltungs-Migration in Einstellungen (eigene Phase) +- Sprach-Einstellung in Settings +- Hauptfenster-Bereinigung + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| DASH-01 | Konfigurierbares Dashboard als Startseite mit Drag & Drop Grid | react-grid-layout v2.2.3 mit ResponsiveReactGridLayout, Hooks-API (useContainerWidth, useResponsiveLayout) | +| DASH-02 | Widgets koennen frei positioniert und in der Groesse geaendert werden | react-grid-layout Layout-Items (x,y,w,h) + Resize-Handles + isDraggable/isResizable pro Item | +| DASH-03 | Uhr-Widget (analog oder digital) | Reine React-Komponente mit Intl.DateTimeFormat fuer Timezone-Support, keine Library noetig | +| DASH-04 | Suchleiste-Widget (oeffnet Suchmaschine im Browser) | Reine React-Komponente, SearchProvider-Tabelle in DB fuer konfigurierbare Anbieter | +| DASH-05 | Kalender-Widget mit Terminvorschau | Backend-Service aggregiert Events aus CalDAV (tsdav), ICS (node-ical), Exchange (ews-javascript-api). Frontend zeigt List-View | +| DASH-06 | Notiz-Widget (Freitext-Kachel) | @uiw/react-md-editor fuer Markdown mit Toolbar, Autosave via Debounce + API PATCH | +| DASH-07 | Dashboard-Layout wird pro Benutzer gespeichert | Prisma DashboardLayout Model mit JSONB fuer Layout-Array, NestJS Dashboard CRUD Service | +| CAL-01 | Kalenderquellen einbinden (WebDAV, Exchange, ICS-Link) | tsdav (CalDAV), ews-javascript-api (Exchange EWS), node-ical (ICS URL), @microsoft/microsoft-graph-client (Exchange Online) | +| CAL-02 | Benutzer kann waehlen welche Kalender im Widget angezeigt werden | CalendarSource Model mit isVisible Boolean pro Quelle, Frontend Toggle in Settings | +| CAL-03 | Terminvorschauen fuer ausgewaehlte Kalender | Backend Calendar Service fetcht Events mit Zeitfenster, aggregiert ueber Quellen-Typen, cacht Ergebnisse | + + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Dashboard Grid (Drag & Drop) | Browser / Client | -- | react-grid-layout ist eine reine Client-Bibliothek, Interaktion passiert im Browser | +| Layout Persistence | API / Backend | Database / Storage | NestJS Service speichert Layout per PUT, PostgreSQL JSONB fuer Schema-flexible Speicherung | +| Widget Rendering (Uhr, Suche, Notiz) | Browser / Client | -- | Reine UI-Komponenten, Client-State (Zustand) fuer lokale Interaktion | +| Kalender-Event-Aggregation | API / Backend | -- | Backend fetcht externe Quellen (CalDAV/EWS/ICS), parsiert Events, liefert normalisierte Daten | +| Kalenderquellen-Verwaltung | API / Backend | Database / Storage | CRUD fuer CalendarSource Records, Credentials verschluesselt in DB | +| Widget-Konfiguration (Settings) | Browser / Client | API / Backend | Frontend Settings-UI, Backend speichert Config pro Widget-Instanz | +| Notiz-Inhalt Persistence | API / Backend | Database / Storage | Autosave via debounced PATCH, Text in DB gespeichert | +| Search Provider Management | API / Backend | Database / Storage | Standard-Anbieter als Seed, Custom-Anbieter per CRUD | +| Settings-Seite | Browser / Client | -- | Eigenes Next.js Layout mit Sub-Sidebar, Client-seitige Navigation | +| Header Settings-Link | Browser / Client | -- | Erweiterung des bestehenden User-Avatar-Dropdown | + +## Standard Stack + +### Core + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| react-grid-layout | 2.2.3 | Dashboard Drag & Drop Grid | 3M weekly downloads, TypeScript v2 Rewrite mit Hooks-API, responsive breakpoints, im CLAUDE.md als Stack definiert [VERIFIED: npm registry] | +| @uiw/react-md-editor | 4.1.1 | Markdown-Editor fuer Notiz-Widget | 775K weekly downloads, GFM mit Checkboxen, kompakte Toolbar, Dark-Mode via data-color-mode, ~4.6KB gzipped [ASSUMED] | +| tsdav | 2.2.2 | CalDAV/WebDAV Client | 88K weekly downloads, TypeScript-native, OAuth2 + Basic Auth, Google/iCloud/Nextcloud kompatibel [ASSUMED] | +| node-ical | 0.26.1 | ICS/iCal Parser | 190K weekly downloads, async/sync Parsing von Strings, Files und URLs, Recurring Events Support [ASSUMED] | +| ews-javascript-api | 0.15.3 | Exchange Web Services (on-premise) | EWS Managed API Port fuer Node.js, OAuth + NTLM, Exchange 2007-2019 + Exchange Online [ASSUMED] | +| @microsoft/microsoft-graph-client | 3.0.7 | Microsoft Graph API (Exchange Online) | 2.1M weekly downloads, offizieller Microsoft SDK, REST API fuer M365 Kalender [ASSUMED] | + +### Supporting + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| ical.js | 2.2.1 | Low-level iCal Parsing | Falls node-ical fuer Edge-Cases (Recurring Event Expansion) nicht ausreicht, als Fallback | +| @types/react-grid-layout | 2.1.0 | Types fuer v1 Compat | Nur falls v2 Type-Exports unvollstaendig sind; v2 hat eigene Types | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| @uiw/react-md-editor | tiptap | tiptap ist maechtiger (Prosemirror-basiert, WYSIWYG), aber 10x groesserer Bundle, komplexeres Setup. Fuer ein Notiz-Widget in einem Dashboard overkill | +| @uiw/react-md-editor | react-simplemde-editor | SimpleMDE/EasyMDE basiert auf CodeMirror 5, groesserer Bundle (~100KB), weniger aktive Maintenance | +| tsdav | ts-caldav | ts-caldav ist neuer (v0.3.7) mit weniger Downloads, tsdav hat breitere Serverkompatibilitaet und mehr Community-Support | +| ews-javascript-api | node-ews | node-ews ist ein duennerer SOAP-Wrapper, weniger typisiert, weniger Features als ews-javascript-api | + +**Installation (Frontend - apps/web):** +```bash +pnpm add react-grid-layout @uiw/react-md-editor +``` + +**Installation (Backend - apps/api):** +```bash +pnpm add tsdav node-ical ews-javascript-api @microsoft/microsoft-graph-client +``` + +## Package Legitimacy Audit + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| react-grid-layout | npm | 9+ yrs | 3.1M/wk | github.com/STRML/react-grid-layout | OK | Approved | +| @uiw/react-md-editor | npm | 5+ yrs | 775K/wk | github.com/uiwjs/react-md-editor | OK | Approved | +| tsdav | npm | 3+ yrs | 88K/wk | github.com/natelindev/tsdav | OK | Approved | +| node-ical | npm | 7+ yrs | 190K/wk | github.com/jens-maus/node-ical | OK | Approved | +| ews-javascript-api | npm | 7+ yrs | N/A | github.com | OK | Approved | +| @microsoft/microsoft-graph-client | npm | 7+ yrs | 2.1M/wk | github.com/microsoftgraph/msgraph-sdk-javascript | OK | Approved | + +**Packages removed due to [SLOP] verdict:** none +**Packages flagged as suspicious [SUS]:** none + +## Architecture Patterns + +### System Architecture Diagram + +``` +User Browser + | + v +[Dashboard Page (Client)] + |-- react-grid-layout (Responsive Grid) + | |-- ClockWidget (Intl.DateTimeFormat) + | |-- SearchWidget (URL template + window.open) + | |-- CalendarWidget (fetches /api/calendar/events) + | |-- NoteWidget (@uiw/react-md-editor + autosave) + | + |-- [Edit Mode Toggle] --> onLayoutChange --> PUT /api/dashboard/layout + | + |-- [Settings Page (/settings)] + |-- Sub-Sidebar (Dashboard | Widgets | Kalender) + |-- Widget Config Forms --> PATCH /api/dashboard/widgets/:id/config + |-- Calendar Source CRUD --> /api/calendar/sources + |-- Search Provider CRUD --> /api/dashboard/search-providers + +API Layer (NestJS) + | + |-- DashboardModule + | |-- DashboardController (layout CRUD, widget CRUD) + | |-- DashboardService (layout persistence, widget config) + | + |-- CalendarModule + | |-- CalendarController (sources CRUD, events aggregation) + | |-- CalendarService (fetch + cache events) + | |-- CalDAVProvider (tsdav) + | |-- ICSProvider (node-ical, HTTP fetch) + | |-- ExchangeProvider (ews-javascript-api / MS Graph) + | + v +PostgreSQL + |-- DashboardLayout (userId, layouts JSONB) + |-- WidgetInstance (id, userId, widgetType, config JSONB) + |-- CalendarSource (userId, type, name, url, credentials, isVisible) + |-- SearchProvider (userId OR null=default, name, urlTemplate, isDefault) +``` + +### Recommended Project Structure + +``` +apps/web/src/ + app/(portal)/ + page.tsx # Dashboard (wird komplett umgebaut) + settings/ + layout.tsx # Settings Layout mit Sub-Sidebar + page.tsx # Settings Startseite (redirect zu dashboard) + dashboard/ + page.tsx # Widget-Einstellungen + calendar/ + page.tsx # Kalenderquellen-Verwaltung + search/ + page.tsx # Suchanbieter-Verwaltung + components/ + dashboard/ + dashboard-grid.tsx # ResponsiveReactGridLayout Wrapper + edit-mode-toggle.tsx # Pencil-Button (D-01) + widget-catalog-modal.tsx # Widget-Hinzufuegen Dialog + widgets/ + clock-widget.tsx # Uhr (D-12, D-13) + search-widget.tsx # Suchleiste (D-14, D-15) + calendar-widget.tsx # Kalender-Vorschau (D-10) + note-widget.tsx # Markdown-Notiz (D-16, D-17, D-18) + widget-wrapper.tsx # Gemeinsamer Container (Titel, Rahmen) + settings/ + settings-sidebar.tsx # Sub-Sidebar Navigation + calendar-source-form.tsx # Formular fuer Kalenderquelle + search-provider-form.tsx # Formular fuer Suchanbieter + widget-config-panel.tsx # Widget-spezifische Einstellungen + lib/ + stores/ + dashboard-store.ts # Zustand Store fuer Dashboard-State + +apps/api/src/ + dashboard/ + dashboard.module.ts + dashboard.controller.ts + dashboard.service.ts + dto/ + save-layout.dto.ts + update-widget-config.dto.ts + calendar/ + calendar.module.ts + calendar.controller.ts + calendar.service.ts + providers/ + caldav.provider.ts + ics.provider.ts + exchange.provider.ts + dto/ + create-calendar-source.dto.ts + update-calendar-source.dto.ts + calendar-events-query.dto.ts +``` + +### Pattern 1: Dashboard Layout Persistence + +**What:** Pro-User Layout-Speicherung als JSONB in PostgreSQL +**When to use:** Bei jedem Speichern (Edit-Mode verlassen) und Laden (Dashboard-Oeffnung) + +```typescript +// Prisma Schema +model DashboardLayout { + id String @id @default(uuid()) + userId String @unique + tenantId String + layouts Json // { lg: [...], md: [...], sm: [...] } + updatedAt DateTime @updatedAt + @@index([tenantId]) +} + +model WidgetInstance { + id String @id @default(uuid()) + userId String + tenantId String + widgetType String // 'clock' | 'search' | 'calendar' | 'note' + config Json @default("{}") // Widget-spezifische Config + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@index([userId]) +} +``` + +```typescript +// Dashboard Store (Zustand) +interface DashboardState { + layouts: Layouts; // react-grid-layout Layouts Objekt + widgets: WidgetInstance[]; // Aktive Widget-Instanzen + isEditMode: boolean; + setEditMode: (mode: boolean) => void; + updateLayouts: (layouts: Layouts) => void; + addWidget: (type: string) => void; + removeWidget: (instanceId: string) => void; + saveLayout: () => Promise; // PUT /api/dashboard/layout + loadLayout: () => Promise; // GET /api/dashboard/layout +} +``` + +### Pattern 2: Calendar Event Aggregation + +**What:** Backend-Service der Events aus verschiedenen Quellen-Typen aggregiert und normalisiert +**When to use:** Kalender-Widget laedt Events fuer aktuelles Zeitfenster + +```typescript +// Calendar Service Pattern +interface CalendarEvent { + id: string; + sourceId: string; + title: string; + start: Date; + end: Date; + allDay: boolean; + location?: string; + description?: string; +} + +interface CalendarProvider { + type: 'caldav' | 'ics' | 'exchange'; + fetchEvents(source: CalendarSource, from: Date, to: Date): Promise; + testConnection(source: CalendarSource): Promise; +} +``` + +### Pattern 3: Widget Component Contract + +**What:** Standardisiertes Interface fuer alle Widget-Komponenten +**When to use:** Jedes Widget implementiert dieses Interface + +```typescript +// Widget Definition Registry +interface WidgetDefinition { + type: string; // 'clock' | 'search' | 'calendar' | 'note' + nameKey: string; // i18n Key + icon: React.ComponentType; // Lucide Icon + minSize: { w: number; h: number }; + defaultSize: { w: number; h: number }; + component: React.ComponentType; +} + +interface WidgetProps { + instanceId: string; + config: Record; + isEditMode: boolean; +} + +// Empfohlene Mindestgroessen (D-06) +const WIDGET_CONSTRAINTS = { + clock: { minW: 2, minH: 2, defaultW: 2, defaultH: 2 }, + search: { minW: 3, minH: 1, defaultW: 4, defaultH: 1 }, + calendar: { minW: 3, minH: 3, defaultW: 4, defaultH: 4 }, + note: { minW: 2, minH: 2, defaultW: 3, defaultH: 3 }, +}; +``` + +### Pattern 4: Settings Page Layout + +**What:** Eigenes Layout fuer Settings-Seite mit Sub-Sidebar (D-19, D-20) +**When to use:** Alle Settings-Routen unter `/settings` + +```typescript +// apps/web/src/app/(portal)/settings/layout.tsx +// Next.js Nested Layout — Portal-AppShell bleibt, Settings hat eigene Sub-Sidebar + +export default function SettingsLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+
+ ); +} +``` + +### Anti-Patterns to Avoid + +- **Layout im Client-LocalStorage speichern statt DB:** Widerspricht D-05 (Sync ueber Geraete). LocalStorage ist nur fuer UI-Praeferenzen (Sidebar-State), nicht fuer User-Daten. +- **Kalender-Events direkt im Browser fetchen:** CalDAV/EWS erfordern Credentials die nicht im Browser sein sollten. Immer ueber Backend-Proxy. +- **Widget-Config im Layout-JSONB einbetten:** Layout (Position/Groesse) und Config (Timezone, Notiz-Text) muessen getrennt sein. Layout aendert sich bei Drag/Resize (haeufig), Config aendert sich in Settings (selten). Getrennte Models vermeiden unnoetige Layout-Saves. +- **Gesamtes Layout bei jedem Drag-Event speichern:** Debounce oder nur bei Edit-Mode-Ende speichern (D-01). Sonst hunderte API-Calls pro Bearbeitung. +- **Kalender-Credentials im Klartext speichern:** Verschluesselung in der DB (AES-256 oder aehnlich via NestJS ConfigService). + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Drag & Drop Grid | Custom Drag-Handler mit CSS Grid | react-grid-layout | Grid-Kollisionserkennung, Responsive Breakpoints, Resize-Handles sind extrem komplex, hunderte Edge-Cases | +| Markdown Editor | Custom contentEditable mit Toolbar | @uiw/react-md-editor | Markdown-Parsing, Cursor-Management, Toolbar-Commands, GFM-Support — Monate an Arbeit | +| CalDAV Protokoll | Raw XML/WebDAV Requests | tsdav | CalDAV ist ein komplexes Protokoll (RFC 4791) mit Server-spezifischen Eigenheiten | +| ICS Parsing | RegExp/String-Split auf .ics Dateien | node-ical | iCal (RFC 5545) hat Recurring Events, Timezones, Exceptions — extrem fehleranfaellig manuell | +| Exchange SOAP/REST | Raw SOAP XML Requests | ews-javascript-api / MS Graph | EWS SOAP Envelope Konstruktion ist fehleranfaellig und erfordert NTLM/OAuth Flow | +| Timezone Handling | Manuelle UTC-Offset-Berechnung | Intl.DateTimeFormat | DST-Wechsel, IANA Timezone Database — niemals manuell berechnen | + +**Key insight:** Kalender-Protokolle (CalDAV, EWS, ICS) und Grid-Layout-Engines sind die zwei Domaenen mit der hoechsten versteckten Komplexitaet in dieser Phase. Beides sieht einfach aus, hat aber tausende Edge-Cases. + +## Common Pitfalls + +### Pitfall 1: react-grid-layout v2 Breaking Changes +**What goes wrong:** Code basierend auf v1 Beispielen/Tutorials funktioniert nicht mit v2. +**Why it happens:** v2 ist ein kompletter TypeScript-Rewrite. `data-grid` Prop existiert nicht mehr, `width` ist required, Callbacks sind immutable, Compaction ist pluggable. +**How to avoid:** Nur v2-Dokumentation und Hooks-API verwenden (useContainerWidth, useResponsiveLayout). `width` via useContainerWidth Hook liefern, NICHT manuell berechnen. +**Warning signs:** TypeScript-Fehler bei Prop-Uebergabe, Grid rendert ohne Breite (0px). + +### Pitfall 2: CSS-Import vergessen fuer react-grid-layout +**What goes wrong:** Grid rendert, aber Drag-Handles und Resize-Griffe sind unsichtbar oder defekt. +**Why it happens:** react-grid-layout und react-resizable benoetigen eigene CSS-Dateien. +**How to avoid:** In der Dashboard-Komponente importieren: +```typescript +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; +``` +**Warning signs:** Widgets ueberlappen sich, kein visuelles Feedback beim Draggen. + +### Pitfall 3: Kalender-Credentials-Leak +**What goes wrong:** CalDAV/Exchange Passwords werden im Browser oder in API-Responses zurueckgegeben. +**Why it happens:** CalendarSource Model enthaelt Passwort-Felder. Ohne explizite Field-Selection werden alle Felder zurueckgegeben. +**How to avoid:** Prisma `select` verwenden um Credentials nie in GET-Responses einzuschliessen. Separates `testConnection` Endpoint fuer Verbindungstests. Credentials verschluesselt in DB. +**Warning signs:** Password-Felder in API JSON-Responses sichtbar. + +### Pitfall 4: Kalender-Event Fetch-Performance +**What goes wrong:** Kalender-Widget ist langsam weil jeder Seitenaufruf externe CalDAV/Exchange-Server kontaktiert. +**Why it happens:** Externe Server haben Latenz (100-500ms pro Request). Mehrere Quellen multiplizieren das. +**How to avoid:** Backend-Caching mit TTL (z.B. 5 Minuten). Events in Redis oder Memory cachen. Widget zeigt gecachte Daten sofort, aktualisiert im Hintergrund. +**Warning signs:** Dashboard-Load > 2 Sekunden wenn Kalender-Widget aktiv. + +### Pitfall 5: Widget-ID Kollisionen bei Mehrfach-Platzierung +**What goes wrong:** Zwei Instanzen desselben Widget-Typs ueberschreiben sich gegenseitig. +**Why it happens:** react-grid-layout verwendet `key`/`i` als Identifier. Wenn Widget-Type statt Instance-ID verwendet wird, kollidieren mehrfache Widgets. +**How to avoid:** Jede Widget-Instanz bekommt eine UUID. Layout-Items verwenden `i: instanceId` (nicht `i: 'clock'`). WidgetInstance Model in DB mit eigener ID. +**Warning signs:** Zweites Widget desselben Typs ersetzt das Erste im Grid. + +### Pitfall 6: Responsive Layout-Verlust +**What goes wrong:** Benutzer ordnet Widgets auf Desktop an, auf Tablet/Mobile ist Layout kaputt. +**Why it happens:** react-grid-layout speichert separate Layouts pro Breakpoint. Wenn nur `lg` gespeichert wird, muessen kleinere Breakpoints geraten werden. +**How to avoid:** `onLayoutChange(allLayouts)` Callback verwenden der ALLE Breakpoint-Layouts zurueckgibt. Komplettes `layouts` Objekt in DB speichern. Oder: nur Desktop-Layout speichern und kleinere via `compactType: 'vertical'` automatisch generieren lassen. +**Warning signs:** Layout OK auf Desktop, Widgets ueberlappen auf kleinerem Bildschirm. + +### Pitfall 7: Autosave-Konflikt bei Notiz-Widget +**What goes wrong:** Schnelles Tippen fuehrt zu vielen gleichzeitigen PATCH-Requests, Race Conditions. +**Why it happens:** Debounce-Intervall zu kurz, oder kein Request-Abbruch (AbortController) bei neuem Input. +**How to avoid:** Debounce von 1-2 Sekunden. AbortController fuer laufende Requests. Server-Timestamp oder Version fuer Conflict-Detection. +**Warning signs:** Notiz-Text springt zurueck auf alten Inhalt nach Speichern. + +## Code Examples + +### react-grid-layout v2 Dashboard Setup + +```typescript +// Source: react-grid-layout v2 README (GitHub) +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Responsive, WidgetCallbackData } from 'react-grid-layout'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; + +const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }; +const COLS = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 1 }; + +interface DashboardGridProps { + layouts: ReactGridLayout.Layouts; + widgets: WidgetInstance[]; + isEditMode: boolean; + onLayoutChange: (allLayouts: ReactGridLayout.Layouts) => void; +} + +export function DashboardGrid({ layouts, widgets, isEditMode, onLayoutChange }: DashboardGridProps) { + const containerRef = useRef(null); + const [width, setWidth] = useState(1200); + + // Measure container width (v2 requires explicit width) + useEffect(() => { + if (!containerRef.current) return; + const observer = new ResizeObserver((entries) => { + setWidth(entries[0].contentRect.width); + }); + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); + + return ( +
+ onLayoutChange(allLayouts)} + draggableHandle=".widget-drag-handle" + > + {widgets.map((widget) => ( +
l.i === widget.id), + minW: WIDGET_CONSTRAINTS[widget.widgetType].minW, + minH: WIDGET_CONSTRAINTS[widget.widgetType].minH, + }}> + +
+ ))} +
+
+ ); +} +``` + +### NestJS Dashboard Controller + +```typescript +// Source: NestJS Documentation Pattern + Existing Codebase Pattern +@Controller('dashboard') +export class DashboardController { + constructor(private dashboardService: DashboardService) {} + + @Get('layout') + async getLayout(@Req() req: Request) { + const userId = req.user.id; + const tenantId = req.tenantId; + return this.dashboardService.getLayout(userId, tenantId); + } + + @Put('layout') + async saveLayout( + @Req() req: Request, + @Body() dto: SaveLayoutDto, + ) { + const userId = req.user.id; + const tenantId = req.tenantId; + return this.dashboardService.saveLayout(userId, tenantId, dto); + } + + @Get('widgets') + async getWidgets(@Req() req: Request) { + return this.dashboardService.getWidgets(req.user.id); + } + + @Post('widgets') + async addWidget( + @Req() req: Request, + @Body() dto: CreateWidgetDto, + ) { + return this.dashboardService.addWidget(req.user.id, req.tenantId, dto); + } + + @Patch('widgets/:id/config') + async updateWidgetConfig( + @Req() req: Request, + @Param('id') id: string, + @Body() dto: UpdateWidgetConfigDto, + ) { + return this.dashboardService.updateWidgetConfig(id, req.user.id, dto); + } + + @Delete('widgets/:id') + async removeWidget( + @Req() req: Request, + @Param('id') id: string, + ) { + return this.dashboardService.removeWidget(id, req.user.id); + } +} +``` + +### Calendar Source Configuration + +```typescript +// Prisma Schema fuer Calendar Sources +model CalendarSource { + id String @id @default(uuid()) + userId String + tenantId String + name String + type String // 'caldav' | 'ics' | 'exchange' + url String + username String? + encryptedPassword String? // AES-256 verschluesselt + color String? @default("#3B82F6") + isVisible Boolean @default(true) + syncIntervalMin Int @default(15) + lastSyncAt DateTime? + lastSyncError String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@index([tenantId]) +} +``` + +### Markdown Note Widget with Autosave + +```typescript +// Source: @uiw/react-md-editor docs + Autosave Pattern +'use client'; + +import { useState, useCallback, useRef, useEffect } from 'react'; +import MDEditor, { commands } from '@uiw/react-md-editor'; + +const DEBOUNCE_MS = 1500; + +// Kompakte Toolbar (D-16) +const NOTE_COMMANDS = [ + commands.bold, + commands.italic, + commands.strikethrough, + commands.divider, + commands.unorderedListCommand, + commands.checkedListCommand, + commands.divider, + commands.link, + commands.code, +]; + +export function NoteWidget({ instanceId, config }: WidgetProps) { + const [value, setValue] = useState(config.content as string || ''); + const timerRef = useRef(); + const abortRef = useRef(); + + const save = useCallback(async (content: string) => { + abortRef.current?.abort(); + abortRef.current = new AbortController(); + await fetch(`/api/dashboard/widgets/${instanceId}/config`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + signal: abortRef.current.signal, + body: JSON.stringify({ content }), + }).catch(() => {}); // Aborted requests are expected + }, [instanceId]); + + const handleChange = useCallback((val?: string) => { + const newVal = val ?? ''; + setValue(newVal); + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => save(newVal), DEBOUNCE_MS); + }, [save]); + + return ( +
+ +
+ ); +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| react-grid-layout v1 (Class-based) | v2 (Hooks + TypeScript) | Dez 2025 | Neue API: useContainerWidth, useResponsiveLayout. width prop required. data-grid entfernt | +| Exchange Web Services (SOAP) | Microsoft Graph REST API | 2020+ | MS empfiehlt Graph API fuer neue Entwicklungen. EWS weiterhin fuer on-premise Exchange noetig | +| Separate @types/react-grid-layout | Built-in Types in v2 | Dez 2025 | Kein separates Types-Package mehr noetig | +| Prisma Rust Engine | Prisma 7 Pure TypeScript | 2025 | 3x schnellere Queries, 90% kleinere Bundles. Bereits im Projekt verwendet | + +**Deprecated/outdated:** +- **react-grid-layout `data-grid` Prop:** In v2 entfernt. Layout-Items nur ueber `layouts` Prop definieren, nicht per data-grid auf Children. +- **EWS fuer Exchange Online:** Microsoft pushed Migration zu Graph API. EWS wird noch supportet aber nicht aktiv weiterentwickelt. + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | @uiw/react-md-editor unterstuetzt dark mode via data-color-mode Attribut kompatibel mit next-themes | Standard Stack | Editor hat falsches Theme in Dark Mode, manuelles CSS-Fix noetig | +| A2 | tsdav fetchCalendarObjects() unterstuetzt Date-Range-Filter | Standard Stack | Ohne Range-Filter werden alle Events geladen statt nur kommende — Performance-Problem | +| A3 | ews-javascript-api funktioniert mit aktuellen Exchange Online OAuth Flows | Standard Stack | Exchange-Integration muss komplett auf MS Graph umgestellt werden | +| A4 | react-grid-layout v2 Responsive Component generiert automatisch kleinere Breakpoint-Layouts aus lg | Architecture Patterns | Manuelle Layout-Generierung fuer jedes Breakpoint noetig | +| A5 | @uiw/react-md-editor Toolbar ist ausreichend kompakt fuer ein Widget mit minH 2 Grid-Units | Standard Stack | Toolbar braucht zu viel Platz, Custom-Toolbar oder andere Library noetig | +| A6 | node-ical expandiert Recurring Events korrekt (RRULE, EXDATE) | Standard Stack | Wiederkehrende Termine werden nicht oder falsch angezeigt, ical.js als Fallback noetig | + +## Open Questions + +1. **Kalender-Credential-Verschluesselung** + - What we know: CalDAV/Exchange Credentials muessen in der DB gespeichert werden + - What's unclear: Welche Verschluesselungsmethode — NestJS ConfigService Secret als Key fuer AES-256? Oder separater Encryption Service? + - Recommendation: AES-256-GCM mit Encryption Key aus Umgebungsvariable (CALENDAR_ENCRYPTION_KEY). Einfach, sicher, kein externer Service noetig. + +2. **Exchange-Typ-Erkennung** + - What we know: D-08 verlangt Exchange-Support. Exchange Online nutzt Graph API, on-premise nutzt EWS. + - What's unclear: Soll der Benutzer den Sub-Typ waehlen (EWS vs. Graph) oder automatische Erkennung? + - Recommendation: Benutzer waehlt "Exchange Online (M365)" oder "Exchange Server" in der Quellen-Konfiguration. Unterschiedliche Formularfelder je nach Wahl. + +3. **react-grid-layout v2 Hooks vs. Component API** + - What we know: v2 bietet Hooks (useGridLayout, useResponsiveLayout) UND Component-API (Responsive, GridLayout) + - What's unclear: Ob die Component-API in v2 noch vollstaendig unterstuetzt wird oder nur fuer Backward-Compat + - Recommendation: Component-API (Responsive) verwenden — stabiler, mehr Dokumentation, einfacher. Hooks nur wenn Custom-Rendering noetig. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Node.js | All | Yes | 24.16.0 | -- | +| pnpm | Package management | Yes | 9.15.0 | -- | +| Docker Compose | Infrastructure | Yes | 5.1.4 | -- | +| PostgreSQL | Data storage | Yes (Container) | Running | -- | +| NestJS API | Backend | Yes (Container) | Running | -- | +| Next.js Web | Frontend | Yes (Container) | Running | -- | + +**Missing dependencies with no fallback:** none +**Missing dependencies with fallback:** none + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Vitest 4.1.x (apps/web) | +| Config file | `apps/web/vitest.config.ts` | +| Quick run command | `cd apps/web && pnpm test` | +| Full suite command | `cd apps/web && pnpm test` | + +### Phase Requirements to Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| DASH-01 | Dashboard Grid rendert mit Widgets | unit | `cd apps/web && pnpm vitest run src/components/dashboard/dashboard-grid.test.tsx` | Wave 0 | +| DASH-02 | Widget Position/Groesse aenderbar im Edit-Mode | unit | `cd apps/web && pnpm vitest run src/components/dashboard/dashboard-grid.test.tsx` | Wave 0 | +| DASH-03 | Clock Widget zeigt korrekte Zeit + Timezone | unit | `cd apps/web && pnpm vitest run src/components/dashboard/widgets/clock-widget.test.tsx` | Wave 0 | +| DASH-04 | Search Widget oeffnet URL mit Query | unit | `cd apps/web && pnpm vitest run src/components/dashboard/widgets/search-widget.test.tsx` | Wave 0 | +| DASH-05 | Calendar Widget zeigt Events an | unit | `cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx` | Wave 0 | +| DASH-06 | Note Widget mit Markdown Editor | unit | `cd apps/web && pnpm vitest run src/components/dashboard/widgets/note-widget.test.tsx` | Wave 0 | +| DASH-07 | Layout Save/Load per API | manual-only | API muss mit laufendem Backend getestet werden | -- | +| CAL-01 | Calendar Source CRUD in Settings | unit | `cd apps/web && pnpm vitest run src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx` | Wave 0 | +| CAL-02 | Calendar Source Toggle (isVisible) | unit | Frontend Toggle-Test | Wave 0 | +| CAL-03 | Event Aggregation aus Quellen | manual-only | Erfordert externe CalDAV/ICS Server | -- | + +### Sampling Rate +- **Per task commit:** `cd apps/web && pnpm test` +- **Per wave merge:** `cd apps/web && pnpm test` +- **Phase gate:** Full suite green before `/gsd-verify-work` + +### Wave 0 Gaps +- [ ] `apps/web/src/components/dashboard/dashboard-grid.test.tsx` -- covers DASH-01, DASH-02 +- [ ] `apps/web/src/components/dashboard/widgets/clock-widget.test.tsx` -- covers DASH-03 +- [ ] `apps/web/src/components/dashboard/widgets/search-widget.test.tsx` -- covers DASH-04 +- [ ] `apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx` -- covers DASH-05 +- [ ] `apps/web/src/components/dashboard/widgets/note-widget.test.tsx` -- covers DASH-06 + +## Security Domain + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | No (existiert bereits) | Keycloak / JWT (Phase 2) | +| V3 Session Management | No (existiert bereits) | Cookie-based Sessions (Phase 2) | +| V4 Access Control | Yes | Tenant-scoped Queries (TenantMiddleware), User-scoped Widget/Layout Access | +| V5 Input Validation | Yes | class-validator DTOs fuer alle API Endpoints, Zod fuer Frontend-Formulare | +| V6 Cryptography | Yes | AES-256-GCM fuer Kalender-Credentials in DB | +| V7 Error Handling | Yes | Keine Credential-Details in Error-Responses bei CalDAV/Exchange Connection-Fehlern | + +### Known Threat Patterns for Stack + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Credential Exposure (CalDAV/Exchange Passwords) | Information Disclosure | Encrypt at rest (AES-256-GCM), never return in GET responses, Prisma select excludes password fields | +| SSRF via Calendar URL | Tampering / Elevation | Validate URL scheme (https only), Block private IP ranges (10.x, 192.168.x, 127.x), Timeout limits | +| XSS via Markdown Content | Tampering | @uiw/react-md-editor has rehype-sanitize option, sanitize rendered Markdown HTML | +| Unauthorized Widget Access | Elevation | All Dashboard/Widget endpoints enforce userId match, not just tenantId | +| Calendar Event Data Leak | Information Disclosure | Calendar events are per-user (D-09), not per-tenant. No cross-user event visibility | + +## Sources + +### Primary (HIGH confidence) +- `apps/api/prisma/schema.prisma` -- bestehendes Datenmodell, Patterns fuer neue Models +- `apps/web/src/lib/stores/sidebar-store.ts` -- bestehendes Zustand Store Pattern +- `apps/web/src/components/layout/header.tsx` -- bestehender Header, Integrationspunkt fuer Settings-Link +- `apps/web/src/app/(portal)/page.tsx` -- bestehender Dashboard-Platzhalter +- `CLAUDE.md` -- Technology Stack Definitionen (react-grid-layout 2.2.x, Zustand 5, next-intl, shadcn/ui) + +### Secondary (MEDIUM confidence) +- [react-grid-layout GitHub README](https://github.com/react-grid-layout/react-grid-layout) -- v2 API, Props, Hooks +- [@uiw/react-md-editor GitHub](https://github.com/uiwjs/react-md-editor) -- API, Commands, Dark Mode +- [tsdav GitHub](https://github.com/natelindev/tsdav) -- CalDAV Client API +- [node-ical npm](https://www.npmjs.com/package/node-ical) -- ICS Parser API +- npm registry Legitimacy Checks -- Download counts, Publish dates, Source repos + +### Tertiary (LOW confidence) +- WebSearch: Markdown Editor Vergleiche -- Library-Empfehlung basiert auf Suchergebnissen +- WebSearch: Exchange Libraries -- ews-javascript-api Maintenance-Status unklar +- WebSearch: CalDAV Libraries -- tsdav vs. ts-caldav Empfehlung basiert auf Download-Zahlen + +## Metadata + +**Confidence breakdown:** +- Standard stack: MEDIUM -- react-grid-layout ist im CLAUDE.md definiert (HIGH fuer Grid), Calendar-Libraries basieren auf WebSearch (MEDIUM) +- Architecture: HIGH -- Patterns folgen bestehender NestJS/Next.js Struktur, bewaehrt in Phase 1-4 +- Pitfalls: MEDIUM -- react-grid-layout v2 Pitfalls aus Dokumentation, Calendar-Pitfalls aus Erfahrungswissen + +**Research date:** 2026-06-23 +**Valid until:** 2026-07-23 (30 Tage -- stabiler Stack, keine schnellen Aenderungen erwartet)