diff --git a/packages/crm-service/Summarize.md b/packages/crm-service/Summarize.md
index 1c8b55f..8c6f86e 100644
--- a/packages/crm-service/Summarize.md
+++ b/packages/crm-service/Summarize.md
@@ -59,8 +59,9 @@ packages/crm-service/
- `docker-compose.crm.yml` im Projekt-Root
- Port: 3100
- Netzwerke: insight-web, insight-db, insight-cache
-- Traefik-Route: /api/v1/crm/*
-- JWT Public Key als Read-Only Volume
+- Traefik-Route: `Host(172.20.10.59) && PathPrefix(/api/v1/crm)` mit Priority 100
+- JWT Public Key als Read-Only Volume (.keys/jwt-public.pem)
+- Direkte PostgreSQL-Verbindung (PgBouncer unterstuetzt kein search_path fuer Schema-Auswahl)
### Sicherheit
@@ -70,10 +71,44 @@ packages/crm-service/
- TenantGuard sichert mandantenbezogenen Zugriff
- Globaler ValidationPipe (whitelist + forbidNonWhitelisted)
- Strict TypeScript, kein `any`
+- 401 bei fehlendem/ungueltigem Token
+
+### Deployment-Status
+
+**Erfolgreich deployed auf insight-dev-01 (172.20.10.59) am 2026-03-10**
+
+- Container: insight-crm (Development-Mode)
+- Prisma Migration `20260310163211_init` angewendet
+- Alle API-Endpunkte getestet und funktionsfaehig
+- Traefik-Routing aktiv: http://172.20.10.59/api/v1/crm/*
+- Swagger-Docs: nicht ueber Traefik erreichbar (nur Container-intern)
+
+### Getestete Endpunkte
+
+| Test | Ergebnis |
+|------|----------|
+| GET /contacts (leere Liste) | 200 OK, pagination korrekt |
+| POST /contacts (Kontakt erstellen) | 201 Created, UUID generiert |
+| GET /contacts/:id | 200 OK, Detail korrekt |
+| PATCH /contacts/:id | 200 OK, Update + Tags |
+| GET /contacts?search=Muster | 200 OK, Suche funktioniert |
+| POST /activities (Notiz) | 201 Created, contactId verknuepft |
+| POST /pipelines (mit 4 Stages) | 201 Created, Stages korrekt |
+| POST /deals | 201 Created, Pipeline/Stage/Contact verknuepft |
+| PATCH /deals/:id (WON) | 200 OK, closedAt automatisch gesetzt |
+| GET /deals (Liste) | 200 OK, pagination korrekt |
+| GET /contacts ohne Token | 401 Unauthorized |
+| Validierung (falsche Felder) | 400 Bad Request, Details korrekt |
+
+### Bekannte Einschraenkungen
+
+- PgBouncer kann nicht genutzt werden (search_path nicht kompatibel mit transaction pooling)
+- Swagger-Docs nur Container-intern erreichbar (kein Traefik-Route fuer /api/v1/crm/docs)
### Naechste Schritte
-1. `npm install` in packages/crm-service/
-2. Prisma Migration: `npx prisma migrate dev --schema=prisma/crm.schema.prisma --name init`
-3. Docker Build testen
-4. Integration mit laufender Plattform testen
+1. DELETE-Endpunkte testen (Kontakte, Deals, Pipelines)
+2. Swagger-Docs ueber Traefik erreichbar machen (optional)
+3. Integration mit Frontend (CRM-Modul im Admin-Bereich)
+4. E2E-Tests schreiben
+5. Production-Build testen (multi-stage Dockerfile)
diff --git a/packages/frontend/src/components/WeatherWidget.module.css b/packages/frontend/src/components/WeatherWidget.module.css
new file mode 100644
index 0000000..78fe711
--- /dev/null
+++ b/packages/frontend/src/components/WeatherWidget.module.css
@@ -0,0 +1,51 @@
+/* ============================================================
+ WeatherWidget - Kompakte Wetter-Anzeige
+ ============================================================ */
+
+.weather {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.375rem 0.75rem;
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ font-size: 0.875rem;
+ color: var(--color-text);
+ white-space: nowrap;
+}
+
+.weatherIcon {
+ font-size: 1.25rem;
+ line-height: 1;
+}
+
+.weatherTemp {
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.weatherLabel {
+ color: var(--color-text-secondary);
+ font-size: 0.8125rem;
+}
+
+.weatherCity {
+ color: var(--color-text-muted);
+ font-size: 0.75rem;
+}
+
+.hint {
+ font-size: 0.8125rem;
+ color: var(--color-text-muted);
+ text-decoration: none;
+ padding: 0.375rem 0.75rem;
+ border-radius: var(--radius-md);
+ transition: color 0.15s;
+}
+
+.hint:hover {
+ color: var(--color-primary);
+ text-decoration: underline;
+}
diff --git a/packages/frontend/src/components/WeatherWidget.tsx b/packages/frontend/src/components/WeatherWidget.tsx
new file mode 100644
index 0000000..175ed6a
--- /dev/null
+++ b/packages/frontend/src/components/WeatherWidget.tsx
@@ -0,0 +1,45 @@
+import { Link } from 'react-router-dom';
+import { useWeather } from '../hooks/useWeather';
+import styles from './WeatherWidget.module.css';
+
+interface WeatherWidgetProps {
+ city: string | null | undefined;
+}
+
+export function WeatherWidget({ city }: WeatherWidgetProps) {
+ const { data, isLoading, isError } = useWeather(city);
+
+ // Kein Ort im Profil -> Hinweis-Link
+ if (!city || city.trim().length < 2) {
+ return (
+
+ Ort im Profil hinterlegen
+
+ );
+ }
+
+ // Laden
+ if (isLoading) {
+ return (
+
+ Wetter wird geladen...
+
+ );
+ }
+
+ // Fehler -> nichts anzeigen (graceful degradation)
+ if (isError || !data) {
+ return null;
+ }
+
+ return (
+
+ {data.icon}
+
+ {Math.round(data.temperature)}°C
+
+ {data.label}
+ {data.cityName}
+
+ );
+}
diff --git a/packages/frontend/src/hooks/useWeather.ts b/packages/frontend/src/hooks/useWeather.ts
new file mode 100644
index 0000000..b2bc393
--- /dev/null
+++ b/packages/frontend/src/hooks/useWeather.ts
@@ -0,0 +1,126 @@
+import { useQuery } from '@tanstack/react-query';
+
+// ============================================================
+// Open-Meteo API Types
+// ============================================================
+
+interface GeocodingResponse {
+ results?: Array<{
+ latitude: number;
+ longitude: number;
+ name: string;
+ country: string;
+ }>;
+}
+
+interface WeatherResponse {
+ current: {
+ temperature_2m: number;
+ weather_code: number;
+ is_day: number;
+ };
+}
+
+// ============================================================
+// Public Types
+// ============================================================
+
+export interface WeatherData {
+ temperature: number;
+ weatherCode: number;
+ isDay: boolean;
+ cityName: string;
+ icon: string;
+ label: string;
+}
+
+// ============================================================
+// WMO Weather Code Mapping (deutsch)
+// ============================================================
+
+export function getWeatherInfo(
+ code: number,
+ isDay: boolean,
+): { icon: string; label: string } {
+ if (code === 0)
+ return { icon: isDay ? '\u2600\uFE0F' : '\uD83C\uDF19', label: 'Klar' };
+ if (code <= 3)
+ return {
+ icon: isDay ? '\u26C5' : '\uD83C\uDF19',
+ label: 'Teilweise bew\u00F6lkt',
+ };
+ if (code <= 49)
+ return { icon: '\u2601\uFE0F', label: 'Bew\u00F6lkt' };
+ if (code <= 59)
+ return { icon: '\uD83C\uDF27\uFE0F', label: 'Nieselregen' };
+ if (code <= 69)
+ return { icon: '\uD83C\uDF27\uFE0F', label: 'Regen' };
+ if (code <= 79)
+ return { icon: '\uD83C\uDF28\uFE0F', label: 'Schnee' };
+ if (code <= 84)
+ return { icon: '\uD83C\uDF27\uFE0F', label: 'Regenschauer' };
+ if (code <= 94)
+ return { icon: '\uD83C\uDF28\uFE0F', label: 'Schneeschauer' };
+ if (code <= 99)
+ return { icon: '\u26C8\uFE0F', label: 'Gewitter' };
+ return { icon: '\u2601\uFE0F', label: 'Bew\u00F6lkt' };
+}
+
+// ============================================================
+// Hook
+// ============================================================
+
+export function useWeather(city: string | null | undefined) {
+ // Schritt 1: City -> Koordinaten (Geocoding)
+ const geocoding = useQuery({
+ queryKey: ['geocoding', city],
+ queryFn: async () => {
+ const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city!)}&count=1&language=de&format=json`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error('Geocoding fehlgeschlagen');
+ return res.json() as Promise;
+ },
+ enabled: !!city && city.trim().length >= 2,
+ staleTime: 24 * 60 * 60 * 1000, // 24 Stunden
+ retry: 1,
+ });
+
+ const geoResult = geocoding.data?.results?.[0];
+
+ // Schritt 2: Koordinaten -> Aktuelles Wetter
+ const weather = useQuery({
+ queryKey: ['weather', geoResult?.latitude, geoResult?.longitude],
+ queryFn: async () => {
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${geoResult!.latitude}&longitude=${geoResult!.longitude}¤t=temperature_2m,weather_code,is_day&timezone=auto`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error('Wetter-Abfrage fehlgeschlagen');
+ return res.json() as Promise;
+ },
+ enabled: !!geoResult,
+ staleTime: 15 * 60 * 1000, // 15 Minuten
+ retry: 1,
+ });
+
+ // Ergebnis zusammenbauen
+ const data: WeatherData | undefined =
+ weather.data && geoResult
+ ? (() => {
+ const { temperature_2m, weather_code, is_day } = weather.data.current;
+ const info = getWeatherInfo(weather_code, is_day === 1);
+ return {
+ temperature: temperature_2m,
+ weatherCode: weather_code,
+ isDay: is_day === 1,
+ cityName: geoResult.name,
+ icon: info.icon,
+ label: info.label,
+ };
+ })()
+ : undefined;
+
+ return {
+ data,
+ isLoading: geocoding.isLoading || weather.isLoading,
+ isError: geocoding.isError || weather.isError,
+ };
+}
diff --git a/packages/frontend/src/shell/DashboardPage.module.css b/packages/frontend/src/shell/DashboardPage.module.css
new file mode 100644
index 0000000..869b171
--- /dev/null
+++ b/packages/frontend/src/shell/DashboardPage.module.css
@@ -0,0 +1,19 @@
+/* ============================================================
+ DashboardPage - Header Layout
+ ============================================================ */
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1.5rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0;
+ color: var(--color-text);
+}
diff --git a/packages/frontend/src/shell/DashboardPage.tsx b/packages/frontend/src/shell/DashboardPage.tsx
index b6df335..c6de3cc 100644
--- a/packages/frontend/src/shell/DashboardPage.tsx
+++ b/packages/frontend/src/shell/DashboardPage.tsx
@@ -1,13 +1,19 @@
import { useAuth } from '../auth/AuthContext';
+import { WeatherWidget } from '../components/WeatherWidget';
+import styles from './DashboardPage.module.css';
export function DashboardPage() {
const { user } = useAuth();
return (
-
- Dashboard
-
+ {/* Header: Titel links, Wetter rechts */}
+
+
+ Willkommen, {user?.firstName} {user?.lastName}
+
+
+
-
- Willkommen, {user?.firstName}!
-
INSIGHT Platform - Sprint 1 Alpha