Files
tessera-ctl/.planning/phases/05-dashboard-calendar/05-RESEARCH.md
T

39 KiB

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>

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 </user_constraints>

<phase_requirements>

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
</phase_requirements>

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

pnpm add react-grid-layout @uiw/react-md-editor

Installation (Backend - apps/api):

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

// 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])
}
// 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<void>;   // PUT /api/dashboard/layout
  loadLayout: () => Promise<void>;   // 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

// 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<CalendarEvent[]>;
  testConnection(source: CalendarSource): Promise<boolean>;
}

Pattern 3: Widget Component Contract

What: Standardisiertes Interface fuer alle Widget-Komponenten When to use: Jedes Widget implementiert dieses Interface

// 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<WidgetProps>;
}

interface WidgetProps {
  instanceId: string;
  config: Record<string, unknown>;
  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

// 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 (
    <div className="flex h-full">
      <SettingsSidebar />
      <div className="flex-1 overflow-y-auto p-6">
        {children}
      </div>
    </div>
  );
}

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:

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

// 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<HTMLDivElement>(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 (
    <div ref={containerRef}>
      <Responsive
        width={width}
        breakpoints={BREAKPOINTS}
        cols={COLS}
        layouts={layouts}
        rowHeight={80}
        isDraggable={isEditMode}
        isResizable={isEditMode}
        onLayoutChange={(_, allLayouts) => onLayoutChange(allLayouts)}
        draggableHandle=".widget-drag-handle"
      >
        {widgets.map((widget) => (
          <div key={widget.id} data-grid={{
            ...layouts.lg?.find(l => l.i === widget.id),
            minW: WIDGET_CONSTRAINTS[widget.widgetType].minW,
            minH: WIDGET_CONSTRAINTS[widget.widgetType].minH,
          }}>
            <WidgetWrapper
              widget={widget}
              isEditMode={isEditMode}
            />
          </div>
        ))}
      </Responsive>
    </div>
  );
}

NestJS Dashboard Controller

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

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

// 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<NodeJS.Timeout>();
  const abortRef = useRef<AbortController>();

  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 (
    <div data-color-mode="auto" className="h-full">
      <MDEditor
        value={value}
        onChange={handleChange}
        commands={NOTE_COMMANDS}
        preview="edit"
        height="100%"
        visibleDragbar={false}
      />
    </div>
  );
}

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)

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)