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)
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)
// 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-gridProp: In v2 entfernt. Layout-Items nur ueberlayoutsProp 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
-
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.
-
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.
-
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-02apps/web/src/components/dashboard/widgets/clock-widget.test.tsx-- covers DASH-03apps/web/src/components/dashboard/widgets/search-widget.test.tsx-- covers DASH-04apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx-- covers DASH-05apps/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 Modelsapps/web/src/lib/stores/sidebar-store.ts-- bestehendes Zustand Store Patternapps/web/src/components/layout/header.tsx-- bestehender Header, Integrationspunkt fuer Settings-Linkapps/web/src/app/(portal)/page.tsx-- bestehender Dashboard-PlatzhalterCLAUDE.md-- Technology Stack Definitionen (react-grid-layout 2.2.x, Zustand 5, next-intl, shadcn/ui)
Secondary (MEDIUM confidence)
- react-grid-layout GitHub README -- v2 API, Props, Hooks
- @uiw/react-md-editor GitHub -- API, Commands, Dark Mode
- tsdav GitHub -- CalDAV Client API
- node-ical npm -- 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)