# Konzept: Akquise-Email Approval-Flow + Seq-Logging (S6b v2)

**Status:** Umgesetzt (Phase 1 + 2) · **Autor:** Claude Opus 4.7 + mn · **Datum:** 2026-04-19

## Problem
Die heutige S6b-Pipeline schickt Vorschau-Mails als HTML-Attachment an Michael. Freigabe passiert manuell durch Setzen des Jira-Labels `akquise-email-approved` — das ist umständlich, bietet keine Ablehnen-/Edit-/Feedback-Optionen, und niemand sieht was wirklich passiert (kein durchgängiges Logging).

Ziel: **denselben komfortablen Approval-Flow wie in der CR-Pipeline (Token-Button-Links in der Vorschau-Mail)** plus strukturiertes Logging nach `logging.minicon.eu` (Seq).

## Was wir aus dem CR-Flow übernehmen
Der bestehende CR-Flow (`cr.routes.ts`) liefert ~80 % der Mechanik fertig:

| Baustein | CR-Pipeline (bestehend) | Wiederverwenden für S6b |
|---|---|---|
| Token-basierte Approval-Links in Mail | `cr.routes.ts:32-38 approvalLinks()` + `:91-214 sendAdminApprovalEmail()` | ✅ 1:1 — nur neue Route `/api/s6b/admin-approve` mit kleinerem Action-Set |
| GET-zeigt-Form / POST-führt-aus Pattern | `cr.routes.ts:559-633` | ✅ 1:1 |
| Feedback-Flow (Textarea → wieder in Queue) | `cr.routes.ts:1062-1168 /customer-feedback` | ✅ Analog: Feedback appended an Jira-Akquise-Ticket, S6b-Cron picked beim nächsten Run |
| Reject mit Begründung | `executeAdminDecision()` | ✅ 1:1 |
| Token-Security (signierter JWT o.ä.) | bestehend | ✅ Gleiches Secret verwenden |

## Neue Approval-Aktionen für S6b
Pro Vorschau-Mail bekommt Michael **4 Buttons** im HTML-Header:

| Button | Route | Aktion |
|---|---|---|
| ✅ **Freigeben & versenden** | `POST /api/s6b/admin-approve?token=X&action=approve` | Label `akquise-email-{N}-approved` setzen → nächster Cron-Run versendet an Kunden |
| ❌ **Abbrechen** | `POST /api/s6b/admin-approve?token=X&action=reject` (Form mit Grund) | Label `akquise-pause` setzen, Kommentar am Ticket mit Grund, keine weitere Mail |
| ✏️ **Inline editieren** | `GET /api/s6b/admin-approve?token=X&action=edit` | Zeigt HTML-Editor (CodeMirror/textarea) mit Vorschau-Mail, Speichern überschreibt HTML-Attachment, Approve-Button aktiv |
| 🔁 **Neu generieren mit Feedback** | `GET /api/s6b/admin-approve?token=X&action=regenerate` | Textarea für Feedback; POST appendet Feedback als Jira-Kommentar, entfernt `akquise-email-{N}-preview-sent` und `akquise-email-{N}-approved` → S6b rendert Mail beim nächsten Run neu unter Berücksichtigung des Feedback-Kommentars |

**Wichtig:** Kein Charge/Retry (CR-spezifisch). S6b-Flow ist leaner.

## Seq-Logging (CLEF)
Die Logger-Infrastruktur existiert schon ungenutzt in `minicon-website-service/src/utils/logger.ts` (POST an `${SEQ_URL}/api/events/raw?clef`). Wir ziehen sie durchgängig rein.

### Event-Katalog
| Event | Trigger | Kern-Properties |
|---|---|---|
| `s6b.preview.rendered` | Mail-Template für {siteid}/{stage} gerendert | siteId, stage (1/2/3), custEmail, botName, previewUrl, promptVersion |
| `s6b.preview.sent` | Preview-Mail an Michael raus | siteId, stage, approvalToken (hash, nicht Klartext!), ticketKey |
| `s6b.admin.decision` | Michael klickt Button | siteId, stage, decision (approve/reject/edit/regenerate), feedbackLength?, reason? |
| `s6b.customer.sent` | Mail beim Kunden raus (Resend-200) | siteId, stage, custEmail, resendMessageId, ticketKey |
| `s6b.customer.failed` | Resend ≠ 200 | siteId, stage, custEmail, errorCode, errorMessage |
| `s6b.cascade.ended` | Email-3 verschickt oder `akquise-kontakt` gesetzt | siteId, endReason (kontakt/email-3/pause/verkauft) |

### Standard-Properties auf jedem Event
```json
{
  "@t": "2026-04-19T14:30:00Z",
  "@l": "Information",
  "@mt": "...",
  "service": "s6b-email-timer",
  "host": "llm.local",
  "env": "prod",
  "correlationId": "<UUID pro Cron-Run>",
  "siteId": "felsland-badeparadies"
}
```

### PowerShell-Helper für den Cron
In `scripts/seq-log.ps1` (neu) eine Wrapper-Function:
```powershell
function Write-SeqEvent { param($level, $mt, $props)
  $body = @{ "@t"=(Get-Date -Format o); "@l"=$level; "@mt"=$mt } + $props | ConvertTo-Json -Compress -Depth 5
  Invoke-RestMethod -Uri "$env:SEQ_URL/api/events/raw?clef" -Method POST `
    -Headers @{ "X-Seq-ApiKey"=$env:SEQ_API_KEY } -Body $body -ContentType "application/vnd.serilog.clef"
}
```
S6b-Prompt wird an den Stellen 1.7 / 2.5 / 3.5 / 4.5 / 5.5 / 6.5 ein `Write-SeqEvent` aufrufen.

### TypeScript-Integration (Server)
`minicon-website-service/src/routes/s6b.routes.ts` (neu) nutzt das bestehende `logger`-Modul:
```ts
logger.info("s6b.admin.decision", { siteId, stage, decision: "approve", correlationId })
```

## Implementierungs-Scope
Konkret zu bauen:

**minicon-website-service**
1. `src/routes/s6b.routes.ts` neu — Admin-Approval + Feedback-Endpoints (≈ 200 Zeilen, geschnitten aus `cr.routes.ts`)
2. `src/services/s6b-email.service.ts` neu — Email-Rendering & Resend-Versand extrahiert aus S6b-Prompt (damit Server UND Cron dieselbe Logik nutzen)
3. In `index.ts` Router mounten
4. Alle `console.log` im neuen Modul durch `logger.xxx` ersetzen

**website-agentic-website-pipeline**
5. `scripts/seq-log.ps1` neu — Write-SeqEvent Helper
6. `prompts/s6b-email-timer.md` erweitern:
   - Am Ende jedes der 6 Schritte `Write-SeqEvent` aufrufen
   - In Vorschau-Mail HTML die 4 Approval-Buttons einbetten (Link auf `api.minicon.eu/api/s6b/admin-approve?token=...`)
   - Token generieren (Cron nutzt kurzen signierten JWT mit Payload `{siteId, stage, ticketKey, exp: 7d}`)
7. `prompts/README.md` kurz um Seq-Convention ergänzen

**Hub (optional, Phase 2)**
8. In `mein.minicon.eu` (Admin-Bereich) Dashboard-View `/admin/akquise-reviews` — listet offene Vorschauen, bindet dieselben Endpoints

## Migration & Rollout
1. **Phase 1 (morgen, nur Seq-Logging)**: `seq-log.ps1` + `Write-SeqEvent`-Calls in S6b-Prompt. Keine Frontend-Änderung. Ziel: Sichtbarkeit im Seq-Dashboard vor Morgen-Run 9:00.
2. **Phase 2 (diese Woche)**: `s6b.routes.ts` deployen, Vorschau-Mail mit 4 Buttons statt nur Hinweis-Text. Alter Label-Approval-Mechanismus bleibt kompatibel.
3. **Phase 3 (später)**: Dashboard-UI in `mein.minicon.eu`.

## Entschieden
- **Token-Secret:** wiederverwendet vom CR-Flow (`APPROVAL_TOKEN_SECRET`). Zusätzlicher Claim `scope: "s6b"` im JWT — damit ein CR-Token nicht für S6b-Endpoints gültig ist und umgekehrt. Server prüft Scope beim Decode.
- **Edit-Editor:** WYSIWYG + HTML-Tab. Toggle zwischen visuellem Editor (z. B. [TipTap](https://tiptap.dev) oder TinyMCE — TinyMCE liegt eh schon unter `docs/tinymce/` im Repo) und Raw-HTML-Ansicht. Speichern überschreibt das Ticket-Attachment `email-{N}-{siteid}.html`.
- **Genau ein Approver:** Michael. Token-Einlösung prüft Claim `approverEmail === "michael.nikolaus@minicon.eu"` und bindet `X-Approver` im Event-Log. Keine Allowlist-Konfiguration nötig — ein Hardcoded-Wert im Server-Config.

---

## Implementierung — Was tatsächlich gebaut wurde

Stand nach Commit `bf534ab` (minicon-website-service) + `a542c69` / `2a0d5e8` (agentic-pipeline):

**minicon-website-service**
- `src/models/akquise-email-draft.model.ts` — MongoDB-Draft mit Lifecycle `pending → approved|rejected|regenerate → sent`, 7-Tage-TTL, Index auf `(ticketKey, stage, decision)`
- `src/routes/s6b.routes.ts` — Gateway-Endpoints:
  - `POST /api/s6b/draft` (Cron, `X-Internal-Secret`) — Draft anlegen, Token + 4 URLs zurück. Auto-Invalidierung vorheriger `pending` Drafts für dasselbe `(ticketKey, stage)`.
  - `GET /api/s6b/draft/:token` (Cron) — Entscheidung pollen, HTML + Feedback abholen.
  - `POST /api/s6b/draft/:token/mark-sent` (Cron) — nach Resend-Success `decision=sent` setzen.
  - `GET /api/s6b/admin-approve?token=…&action=(approve|reject|edit|regenerate)` — Admin-UI, Formulare für reject/regenerate, TinyMCE für edit.
  - `POST /api/s6b/admin-approve` — Formular-Submit, setzt Decision.
- Mount in `src/app.ts` unter `/api/s6b`.

**Abweichung vom Konzept:** Statt JWT mit `scope: "s6b"`-Claim nutzen wir URL-safe Random-Tokens (24 Bytes `base64url`) gekoppelt an den Draft in MongoDB. Der Route-Prefix `/api/s6b/*` stellt die Scope-Isolation her; ein CR-Token ist hier strukturell nicht einlösbar. Einfacher & eine Abhängigkeit weniger.

**Editor:** TinyMCE 6 aus dem CDN (`cdn.tiny.cloud/1/no-api-key/tinymce/6`), lizenzfreier No-Api-Key-Modus. 3 Tabs: WYSIWYG / Raw-HTML / Preview-iframe. Nicht aus `docs/tinymce/` gebundled (ist nur für das Dashboard dort).

**website-agentic-website-pipeline**
- `scripts/seq-log.ps1` — `Write-SeqEvent` + `New-CorrelationId`, CLEF an `SEQ_URL/api/events/raw?clef`, fire-and-forget.
- `prompts/s6b-email-timer.md` — zweiphasiger Flow:
  - **Phase A (Preview):** Finde Tickets nach Stufen-spezifischer JQL (updated < -7d bei 2/3), render HTML, `POST /api/s6b/draft` → Token + URLs, Vorschau-Mail mit 4 Buttons an Michael, Label `akquise-email-{N}-preview-sent`.
  - **Phase B (Dispatch):** Suche Preview-Labels, `GET /api/s6b/draft/:token`, verzweige nach `decision`: approved → Resend an Kunde + Label `akquise-email-{N}` + `mark-sent`, rejected → `akquise-pause`, regenerate → `[S6b-Feedback]`-Jira-Kommentar + Preview-Label entfernen (Ticket fällt zurück in Phase A).
  - Seq-Events an jeder Transition: `s6b.run.started/.completed`, `s6b.preview.rendered/.sent`, `s6b.customer.sent/.failed/.skipped`, `s6b.admin.regenerate`, `s6b.cascade.ended`, `s6b.dispatch.pending`.

**Abweichung vom Konzept:** Event-Namen leicht präzisiert. Server-seitig ruft `logger.info(...)` das CLEF-Modul an (`utils/logger.ts`), das `SEQ_URL` + optional `SEQ_API_KEY` nutzt.

**OpenClaw-Cron**
- Cron-ID-Mapping in `scripts/sync-cron.ps1` + `scripts/sync-crons-via-cli.js` korrigiert (S6a: `6ab77edf-…`, S6b: `f7a72879-…`).
- Prompt synchronisiert via `pwsh -File scripts\sync-cron.ps1` auf `llm.local`.

**Phase 3 (Dashboard in mein.minicon.eu):** noch offen.

