# Payload CMS - Detaillierte Subsystem-Referenz

> Diese Datei enthält detaillierte Dokumentation zu den Subsystemen des Projekts.
> Kurzübersicht und Schlüsseldateien: siehe `CLAUDE.md`.

## E-Mail-System

Multi-Tenant E-Mail-System mit tenant-spezifischer SMTP-Konfiguration.

**Architektur:**
- Globaler SMTP als Fallback (via .env)
- Tenant-spezifische SMTP in Tenants Collection
- Transporter-Caching mit automatischer Invalidierung
- EmailLogs Collection für Audit-Trail

**Tenant E-Mail-Konfiguration:**
```
Tenants → email → fromAddress, fromName, replyTo
       → email → useCustomSmtp (Checkbox)
       → email → smtp → host, port, secure, user, pass
```

**API-Endpoint `/api/send-email`:**
```bash
curl -X POST https://pl.porwoll.tech/api/send-email \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{
    "to": "empfaenger@example.com",
    "subject": "Betreff",
    "html": "<p>Inhalt</p>",
    "tenantId": 1
  }'
```

**Sicherheit:**
- Authentifizierung erforderlich
- Tenant-Zugriffskontrolle (User muss Tenant-Mitglied sein)
- Rate-Limiting: 10 E-Mails/Minute pro User
- SMTP-Passwort nie in API-Responses

**Dateien:**
- `src/lib/email/tenant-email-service.ts` - Haupt-Service
- `src/lib/email/payload-email-adapter.ts` - Payload-Integration
- `src/hooks/invalidateEmailCache.ts` - Cache-Invalidierung

### Troubleshooting: Tenant SMTP Save hängt im Admin

Stand: 17. Februar 2026

**Symptom:**
- Beim Speichern eines Tenants mit aktivem `email.useCustomSmtp` bleibt das Admin Panel bei "wird aktualisiert" hängen.
- SMTP-Werte werden nicht zuverlässig in `tenants` persistiert.

**Ursachen (behoben):**
- Die SMTP-Felder lagen in einer conditional Group (`email.smtp`), während `required: true` auf Unterfeldern (`host`, `user`) serverseitig weiterhin greifen konnte.
- Feld-Validierungen nutzten teilweise `siblingData` mit falschem Scope in der verschachtelten Struktur.
- `afterChange` im Tenant-Audit-Log wartete synchron auf `payload.create()` für `audit-logs` und konnte die Admin-Response blockieren.

**Implementierter Fix:**
- Konditionale Pflichtvalidierung auf Gruppenebene `email.smtp.validate` verschoben (Prüfung gegen `email.useCustomSmtp`).
- `required: true` von `email.smtp.host` und `email.smtp.user` entfernt; stattdessen explizite Group-Validierung.
- Tenant-Audit-Hooks auf Fire-and-Forget umgestellt (`.catch(...)` statt `await`), analog zu User-Audit-Hooks.

**Relevante Dateien:**
- `src/collections/Tenants.ts`
- `src/hooks/auditTenantChanges.ts`

## Newsletter (Double Opt-In)

DSGVO-konformes Newsletter-System mit Double Opt-In.

**Flow:**
1. User meldet sich an → Status: `pending`, Token wird generiert
2. Double Opt-In E-Mail wird automatisch gesendet
3. User klickt Bestätigungs-Link → Status: `confirmed`
4. Willkommens-E-Mail wird gesendet
5. Abmeldung jederzeit über Link in E-Mails möglich

**API-Endpoints:**
```bash
# Anmeldung
POST /api/newsletter/subscribe
{"email": "...", "firstName": "...", "tenantId": 1, "source": "footer"}

# Bestätigung (via Link aus E-Mail)
GET /api/newsletter/confirm?token=<uuid>

# Abmeldung (via Link aus E-Mail)
GET /api/newsletter/unsubscribe?token=<uuid>
```

**Features:**
- Token-Ablauf nach 48 Stunden
- Rate-Limiting: 5 Anmeldungen/10 Minuten pro IP
- Erneute Anmeldung nach Abmeldung möglich

**Dateien:**
- `src/lib/email/newsletter-service.ts` - Service-Logik
- `src/lib/email/newsletter-templates.ts` - E-Mail-Templates
- `src/hooks/sendNewsletterConfirmation.ts` - Hook

## BullMQ Job Queue

Asynchrone Job-Verarbeitung mit Redis als Backend.

**Queue-Worker:** Läuft als separater PM2-Prozess (`pm2 logs queue-worker`).

### Email Queue

```typescript
import { queueEmail } from '@/lib/queue'

await queueEmail({
  tenantId: 1,
  to: 'empfaenger@example.com',
  subject: 'Betreff',
  html: '<p>Inhalt</p>',
  source: 'form-submission'
})
```

### PDF Queue

```bash
# PDF aus HTML generieren
POST /api/generate-pdf
{"source": "html", "html": "<h1>Test</h1>", "filename": "test.pdf"}

# Job-Status abfragen
GET /api/generate-pdf?jobId=abc123
```

```typescript
import { queuePdfFromHtml, queuePdfFromUrl, getPdfJobStatus } from '@/lib/queue'

const job = await queuePdfFromHtml('<h1>Test</h1>', { filename: 'test.pdf' })
const job2 = await queuePdfFromUrl('https://example.com', { format: 'A4' })
const status = await getPdfJobStatus(job.id)
```

### Worker-Konfiguration

Über `ecosystem.config.cjs`:
- `QUEUE_EMAIL_CONCURRENCY`: Parallele E-Mail-Jobs (default: 3)
- `QUEUE_PDF_CONCURRENCY`: Parallele PDF-Jobs (default: 2)
- `QUEUE_RETENTION_CONCURRENCY`: Parallele Retention-Jobs (default: 1)
- `QUEUE_DEFAULT_RETRY`: Retry-Versuche (default: 3)
- `QUEUE_REDIS_DB`: Redis-Datenbank für Queue (default: 1)
- `REDIS_PASSWORD`: Redis-Authentifizierung (Pflicht)

**Dateien:**
- `src/lib/queue/queue-service.ts` - Zentrale Queue-Verwaltung
- `src/lib/queue/jobs/email-job.ts` - E-Mail-Job
- `src/lib/queue/jobs/pdf-job.ts` - PDF-Job
- `src/lib/queue/workers/email-worker.ts` - E-Mail-Worker
- `src/lib/queue/workers/pdf-worker.ts` - PDF-Worker
- `scripts/run-queue-worker.ts` - Worker-Starter

## Data Retention

Automatische Datenbereinigung für DSGVO-Compliance und Speicheroptimierung.

### Retention Policies

| Collection | Retention | Umgebungsvariable |
|------------|-----------|-------------------|
| email-logs | 90 Tage | `RETENTION_EMAIL_LOGS_DAYS` |
| audit-logs | 90 Tage | `RETENTION_AUDIT_LOGS_DAYS` |
| consent-logs | 3 Jahre | `RETENTION_CONSENT_LOGS_DAYS` |
| media (orphans) | 30 Tage | `RETENTION_MEDIA_ORPHAN_MIN_AGE_DAYS` |

Scheduler: Täglich 03:00 Uhr (`RETENTION_CRON_SCHEDULE`).

### API-Endpoint `/api/retention`

```bash
# Konfiguration abrufen
GET /api/retention

# Job-Status
GET /api/retention?jobId=abc123

# Manueller Job
POST /api/retention
{"type": "full"}                                    # Alle Policies
{"type": "collection", "collection": "email-logs"}  # Einzelne Collection
{"type": "media-orphans"}                           # Nur Media-Orphans
```

**Architektur:**
```
Scheduler (Cron) → Retention Queue (BullMQ) → Retention Worker
  → Email-Logs (createdAt) + Audit-Logs (createdAt) + Consent-Logs (expiresAt)
  → Media-Orphan-Cleanup
```

**Dateien:**
- `src/lib/retention/retention-config.ts` - Zentrale Konfiguration
- `src/lib/retention/cleanup-service.ts` - Lösch-Logik
- `src/lib/queue/jobs/retention-job.ts` - Job-Definition
- `src/lib/queue/workers/retention-worker.ts` - Worker
- `src/app/(payload)/api/retention/route.ts` - API-Endpoint

## Redis Caching

Redis erfordert Authentifizierung (`REDIS_PASSWORD`). Eviction-Policy: `noeviction` (BullMQ-Anforderung — verhindert Datenverlust bei Queue-Jobs).

```typescript
import { redis } from '@/lib/redis'

await redis.set('key', JSON.stringify(data), 'EX', 60)  // TTL in Sekunden
const cached = await redis.get('key')
await redis.keys('posts:*').then(keys => keys.length && redis.del(...keys))  // Pattern-Invalidierung
```

## FormSubmissions CRM

Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert.

**Multi-Tenant Kontaktformular-System:**
- `forms` und `form-submissions` sind tenant-scoped (via `multiTenantPlugin` mit `customTenantField: true`)
- Formulare werden in der CMS Admin-UI pro Tenant erstellt (Forms Collection)
- E-Mail-Empfänger werden pro Formular konfiguriert (Built-in Plugin Feature)
- `ContactFormBlock` referenziert ein Formular aus der Forms Collection (statt hardcoded E-Mail)
- Frontends posten an `/api/form-submissions` mit der `formId` aus dem Block

**Hooks:**
- `setSubmissionTenant` (beforeChange): Kopiert den Tenant automatisch vom Formular auf die Einsendung
- `sendFormNotification` (afterChange): Sendet E-Mail-Benachrichtigungen via Tenant-SMTP an konfigurierte Empfänger
- `formSubmissionBeforeChange`: Auto-Markierung als gelesen, Auto-Autor bei Notizen

**Status-Workflow:** Neu → Gelesen → In Bearbeitung → Warten → Erledigt → Archiviert

**Features:**
- Priorität (Hoch/Normal/Niedrig)
- Zuständigkeits-Zuweisung an User
- Interne Notizen mit Auto-Autor und Zeitstempel
- Antwort-Tracking (Methode, Zeitstempel, Zusammenfassung)
- Tags zur Kategorisierung
- Auto-Markierung als gelesen beim ersten Öffnen

**Dateien:**
- `src/collections/FormSubmissionsOverrides.ts` - Feld-Definitionen + Tenant-Feld
- `src/hooks/setSubmissionTenant.ts` - Auto-Tenant-Zuweisung von Form → Submission
- `src/hooks/sendFormNotification.ts` - E-Mail-Benachrichtigung via Tenant-SMTP
- `src/hooks/formSubmissionHooks.ts` - Automatisierungen (Gelesen-Status, Notiz-Autor)
- `src/blocks/ContactFormBlock.ts` - Block mit Formular-Referenz + Anzeigeoptionen

## Community Management System

Plattformübergreifendes Community Management für YouTube, Facebook und Instagram.

### Architektur

```
Admin UI (Inbox + Analytics)
  → /api/community/* (sync, stats, reply, generate-reply, export, stream)
    → UnifiedSyncService
      → YouTube Sync + Facebook Sync + Instagram Sync
        → CommunityInteractions (einheitliche Collection)
```

### Admin UI

**Community Inbox** (`/admin/community/inbox`):
- Einheitliche Ansicht aller Kommentare (YouTube + Facebook + Instagram)
- Filter nach Plattform, Status, Priorität, Sentiment, Flags
- AI-generierte Antwort-Vorschläge (Claude)
- Echtzeit-Updates via SSE (`/api/community/stream`)
- Export als CSV/JSON

**Community Analytics** (`/admin/community/analytics`):
- KPI-Cards, Sentiment-Trend, Topic-Cloud, Response-Metriken, Kanal-Vergleich

### Meta (Facebook + Instagram) Integration

**OAuth-Flow:**
1. Admin öffnet `/api/auth/meta?socialAccountId=X&accountType=both`
2. Redirect zu Facebook OAuth (12 Scopes)
3. Callback: Short-Lived → Long-Lived Token (60 Tage)
4. Facebook Pages + Instagram Business Accounts werden geladen
5. Token + Account-Daten in SocialAccounts gespeichert

**Scopes:**
- Facebook: `pages_show_list`, `pages_read_engagement`, `pages_manage_posts`, `pages_read_user_content`, `pages_manage_engagement`, `pages_messaging`
- Instagram: `instagram_basic`, `instagram_manage_comments`, `instagram_manage_messages`, `instagram_content_publish`

### API-Endpoints

| Endpoint | Methode | Beschreibung |
|----------|---------|--------------|
| `/api/community/sync` | POST | Manueller Sync (alle Plattformen) |
| `/api/community/sync` | GET | Sync-Status |
| `/api/community/sync-status` | GET | Detaillierter Status mit Account-Stats |
| `/api/community/stats` | GET | Interaktions-Statistiken |
| `/api/community/reply` | POST | Antwort senden |
| `/api/community/generate-reply` | POST | AI-Antwort generieren (Claude) |
| `/api/community/export` | GET | Daten-Export (CSV/JSON) |
| `/api/community/stream` | GET | SSE-Stream für Echtzeit-Updates |
| `/api/community/analytics/*` | GET | Analytics-Daten (6 Endpoints) |
| `/api/auth/meta` | GET | Meta OAuth starten |
| `/api/auth/meta/callback` | GET | Meta OAuth Callback |

### Sync-Trigger

| Trigger | Endpoint | Plattformen |
|---------|----------|-------------|
| Cron (alle 15 Min) | `/api/cron/community-sync` | Alle aktiven Accounts |
| Manuell (Inbox-Button) | `/api/community/sync` (POST) | Alle (optional filterbar) |
| YouTube-spezifisch | `/api/cron/youtube-sync` | Nur YouTube |

### CommunityRules (Automatisierung)

**Trigger-Typen:** `keyword`, `sentiment`, `question_detected`, `medical_detected`, `influencer` (>10k Follower), `all_new`, `contains_link`, `contains_email`

**Aktionen:** `set_priority`, `assign_to`, `set_flag`, `suggest_template`, `send_notification`, `flag_medical`, `escalate`, `mark_spam`, `set_deadline`

### Wichtige Dateien

```
src/lib/integrations/meta/
├── MetaBaseClient.ts          # HTTP-Basis mit Retry + Pagination
├── FacebookClient.ts          # Facebook Graph API Client
├── InstagramClient.ts         # Instagram Graph API Client
├── FacebookSyncService.ts     # Facebook Kommentar-Sync
├── InstagramSyncService.ts    # Instagram Kommentar-Sync
├── oauth.ts                   # Meta OAuth 2.0 Flow
└── index.ts                   # Re-Exports

src/lib/jobs/
├── UnifiedSyncService.ts      # Orchestriert alle Plattform-Syncs
├── syncAllComments.ts         # YouTube-spezifischer Sync (Legacy)
└── JobLogger.ts               # Strukturiertes Logging

src/app/(payload)/api/
├── auth/meta/                 # OAuth Start + Callback
├── community/                 # Community-Endpoints
└── cron/community-sync/       # Cron-Trigger
```

## Timeline Collection

Dedizierte Collection für chronologische Darstellungen.

**Typen:** `history`, `milestones`, `releases`, `career`, `events`, `process`

**Event-Features:** Flexible Datumsformate, Kategorien, Wichtigkeitsstufen, Bilder, Links, Rich-Text

**Display-Optionen:** Layouts (vertikal, alternierend, horizontal, kompakt), Sortierung, Gruppierung nach Jahr

**Prozess-spezifische Felder (type=process):**
- `stepNumber`, `duration`, `responsible`, `actionRequired`, `deliverables`

**API:** `GET /api/timelines?tenant=1[&type=history][&slug=...][&locale=en]`

## Workflows Collection

Komplexe Prozess-Darstellungen mit Phasen, Abhängigkeiten und Status-Tracking.

**Typen:** `project`, `business`, `approval`, `onboarding`, `support`, `development`, `marketing`, `other`

**Phasen-Struktur:**
```
Workflow → Phasen (Array)
  → name, description, icon, color, estimatedDuration, responsible, deliverables
  → Schritte (Array)
    → name, description, stepType, priority, dependencies, conditions, checklist, resources, outputs
```

**Display:** Layouts (vertical, horizontal, flowchart, kanban, gantt), Farbschema (phase, status, priority, brand)

**API:** `GET /api/workflows?tenant=1[&type=project][&complexity=medium][&slug=...][&locale=en]`

## HeroSliderBlock Features

**Slides (1-10):** Hintergrundbild (Desktop + Mobil), Headline + Subline (lokalisiert), Text-Ausrichtung, Overlay (Farbe, Deckkraft, Gradient), 2 CTA-Buttons

**Animationen:** fade, slide, zoom, flip, none (300-1200ms)

**Autoplay:** Intervall 3-10s, Pause bei Hover/Interaktion

**Navigation:** Pfeile (verschiedene Stile), Dots (Punkte, Striche, Nummern, Thumbnails, Fortschritt), Touch-Swipe, Tastatur

**Layout:** Höhe (Vollbild bis kompakt), Mobile-Höhe, Content-Breite

## CI/CD Pipeline Details

### ci.yml (Main CI Pipeline)
Läuft bei Push/PR auf `main` und `develop`:
- **lint:** ESLint (flat config, 0 errors)
- **typecheck:** TypeScript Compiler (`tsc --noEmit`, 4GB heap)
- **test:** Unit & Integration Tests (Vitest)
- **build:** Next.js Production Build
- **e2e:** Playwright E2E Tests

### security.yml
Gitleaks (Secret Scanning), pnpm audit, CodeQL (SAST), Security Tests

### deploy-staging.yml
- Trigger: Push auf `develop` oder manual dispatch
- Ablauf: Pre-checks → SSH → Git Pull → Dependencies → Migrations → Build → PM2 Restart → Health Check
- Secret: `STAGING_SSH_KEY`

```bash
# Manuelles Staging-Deployment
./scripts/deploy-staging.sh
./scripts/deploy-staging.sh --skip-build
./scripts/deploy-staging.sh --skip-migrations
```

### deploy-production.yml
- Trigger: Manual dispatch
- Features: Pre-flight Checks, Database Backup, Health Check, Auto-Rollback
- Secrets: `STAGING_SSH_KEY`, `PRODUCTION_SSH_KEY`

```bash
gh workflow run deploy-production.yml              # Via GitHub Actions
./scripts/deploy-production.sh                     # Auf Server
./scripts/deploy-production.sh --rollback          # Rollback
./scripts/deploy-production.sh --dry-run           # Dry-Run
```

## PgBouncer Connection Pooling

Läuft auf dem App-Server (127.0.0.1:6432), pooled Verbindungen zu PostgreSQL (10.10.181.101:5432).

**Konfiguration:** `/etc/pgbouncer/pgbouncer.ini`

| Parameter | Wert |
|-----------|------|
| pool_mode | transaction |
| default_pool_size | 20 |
| min_pool_size | 5 |
| reserve_pool_size | 5 |
| max_db_connections | 50 |
| max_client_conn | 200 |

**TLS:** `server_tls_sslmode = require` (TLS 1.3)

```bash
# Statistiken
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 6432 -U payload -d pgbouncer -c "SHOW POOLS;"

# Direkte Verbindung für Migrationen (umgeht PgBouncer)
./scripts/db-direct.sh migrate
./scripts/db-direct.sh psql
```

## Cron Jobs (Details)

Alle erfordern `Authorization: Bearer $CRON_SECRET`.

```bash
# Community Sync manuell
curl -X POST "https://your-domain/api/cron/community-sync" \
  -H "Authorization: Bearer $CRON_SECRET" \
  -d '{"platforms": ["youtube", "facebook"]}'

# Token Refresh (Dry-Run)
curl "https://your-domain/api/cron/token-refresh?dryRun=true" \
  -H "Authorization: Bearer $CRON_SECRET"
```

**Monitoring:**
- HEAD-Requests: Status-Header (`X-Sync-Running`, `X-Last-Run`)
- Bei laufendem Job: HTTP 423 (Locked)
- Fehlerhafte Tokens → `yt-notifications`

## YouTube Operations Hub

Umfassendes YouTube-Management-System mit Content-Pipeline, Analytics, Kalender und Task-Management.

### Collections (9)

| Slug | Beschreibung |
|------|--------------|
| `youtube-channels` | Multi-Kanal-Verwaltung mit OAuth |
| `youtube-content` | Videos + Shorts mit Status-Pipeline |
| `yt-series` | Serien mit Branding (Logo, Farben, Playlist) |
| `yt-notifications` | Handlungsbedarf-System |
| `yt-tasks` | Aufgaben mit Zuweisung und Fälligkeitsdatum |
| `yt-batches` | Produktions-Batches mit Ziel-Tracking |
| `yt-checklist-templates` | Checklisten-Vorlagen (Upload, Produktion, Review) |
| `yt-monthly-goals` | Monatsziele (Content, Audience, Engagement, Business) |
| `yt-script-templates` | Skript-Vorlagen pro Kanal/Serie |

### Content-Pipeline Status

`idea` → `script` → `review` → `production` → `editing` → `ready` → `published`

Hooks automatisieren Übergänge und erstellen Tasks bei Statuswechsel.

### Custom API Endpoints

| Methode | Endpoint | Beschreibung | Auth |
|---------|----------|--------------|------|
| GET | `/api/youtube/auth` | OAuth-Flow starten (`?socialAccountId=`) | User |
| GET | `/api/youtube/callback` | OAuth-Callback, Token-Speicherung | Public (State) |
| POST | `/api/youtube/refresh-token` | Access Token erneuern | User + CSRF |
| GET | `/api/youtube/dashboard` | Pipeline-Status, Freigaben, Aufgaben | YouTube-Rolle |
| GET | `/api/youtube/analytics` | Multi-Tab Analytics (`?tab=&channel=&period=`) | YouTube-Rolle |
| GET | `/api/youtube/calendar` | Content-Kalender mit Konflikterkennung | User |
| PATCH | `/api/youtube/calendar` | Drag-&-Drop-Umplanung | User |
| GET | `/api/youtube/capacity` | Team-Kapazität und Auslastung | User |
| GET | `/api/youtube/my-tasks` | Eigene offene Tasks | YouTube-Rolle |
| POST | `/api/youtube/complete-task/[id]` | Task als erledigt markieren | Assignee/Manager |
| POST/GET | `/api/youtube/upload` | Video-Upload via BullMQ-Queue | User |
| POST | `/api/youtube/thumbnails/bulk` | Bulk-Thumbnail-Download (`?dryRun=true`) | SuperAdmin |

### Cron Jobs

| Endpoint | Schedule | Beschreibung |
|----------|----------|--------------|
| `/api/cron/youtube-sync` | alle 15 Min | YouTube-Kommentar-Sync |
| `/api/cron/youtube-channel-sync` | täglich 04:00 UTC | Kanal-Statistiken (Abos, Views, Videos) |
| `/api/cron/youtube-metrics-sync` | alle 6 Stunden | Video-Performance-Metriken |

### Admin Views

- **YouTube Analytics Dashboard** (`/admin/views/youtube-analytics`) — 7 Tabs: Performance, Pipeline, Goals, Community, Comparison, Trends, ROI
- **Content Calendar** (`/admin/views/content-calendar`) — FullCalendar mit Drag-&-Drop und Konflikterkennung

### Services & Utilities

| Datei | Beschreibung |
|-------|--------------|
| `src/lib/youtube/ConflictDetectionService.ts` | Erkennt Terminüberschneidungen bei Content-Planung |
| `src/lib/youtube/analytics-helpers.ts` | Trend-Berechnung, Vergleiche, ROI |
| `src/lib/youtube/capacity-calculator.ts` | Team-Auslastungsberechnung |
| `src/lib/youtube/youtubeAccess.ts` | Rollen-basierte Zugriffskontrolle |

### Hooks (Automatisierung)

- **youtubeChannels/downloadChannelImage** — Profilbild automatisch herunterladen
- **youtubeContent/downloadThumbnail** — Video-Thumbnails automatisch herunterladen
- **youtubeContent/createTasksOnStatusChange** — Tasks bei Statuswechsel erstellen
- **youtubeContent/autoStatusTransitions** — Automatische Pipeline-Übergänge
- **ytTasks/notifyOnAssignment** — Benachrichtigung bei Task-Zuweisung

### Zugriffskontrolle

| Funktion | Beschreibung |
|----------|--------------|
| `hasYouTubeAccess()` | Lese-Zugriff (YouTube-Rolle) |
| `isYouTubeManager()` | Manager-Level (Erstellen, Löschen, Freigaben) |
| `isYouTubeCreatorOrAbove()` | Creator+ Zugriff |
| `canAccessAssignedContent()` | Nur zugewiesene Inhalte |
| `canAccessOwnNotifications()` | Nur eigene Benachrichtigungen |
