# CLAUDE.md — Mailforge Plugin

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

@AGENTS.md

## Was ist das?

**Mailforge Plugin** ist eine modulare Email-Marketing-Anwendung, die in fremde CRMs (z.B. Laravel-basierte) als Sub-App embedded werden kann. Sie ist eine sauber refaktorierte Extraktion des Mailforge-Live-Tools.

**Zielgruppe:** CRM-Entwickler, die einen Email-Marketing-Tab in ihr CRM einbauen wollen, ohne Cumas/M365/ITS-Brand-Hardcodings.

**Nicht-Ziel:** Eigenständige SaaS — das Plugin lebt eingebettet im Host-CRM.

## Stack & Versionen

- **Next.js 16.2** (App Router, `src/app/`)
- **React 19.2**, **TypeScript 5 strict**, **Tailwind v4** via `@tailwindcss/postcss`
- **Drizzle ORM** mit **dual driver**: `better-sqlite3` (Default, Dev) ODER `mysql2` (Production via Laravel-DB)
- **OpenRouter** als LLM-Gateway (Default-Pin: `anthropic/claude-sonnet-4.6`)
- **MJML** für Email-Rendering, **TipTap 2** als Inline-Editor
- **fal.ai** für Hero-Bild-Generierung (optional)
- **JWT** (jsonwebtoken) für Auth-Validierung gegen Host-CRM
- **nodemailer** + **Microsoft Graph** als beiliegende Send-Provider-Implementierungen

## Häufige Commands

```bash
npm run dev                        # Next.js Dev-Server (http://localhost:3000)
npm run build                      # Production-Build
npm run lint                       # ESLint
npm run typecheck                  # tsc --noEmit

npm run db:generate                # Driver-aware (liest DB_DRIVER aus .env)
npm run db:generate:sqlite         # Explizit SQLite-Migration
npm run db:generate:mysql          # Explizit MySQL-Migration
npm run db:migrate                 # Migrationen anwenden (gegen DB_DRIVER)
npm run db:sync-schemas            # SQLite-Schema → MySQL-Schema synchronisieren
npm run db:export-ddl              # docs/ddl/{mysql,sqlite}.sql neu generieren
npm run db:seed-dev                # Dev-Demo-Workspace anlegen (idempotent)
npm run db:studio                  # Drizzle Studio öffnen

npm run worker                     # Send-Worker (NUR wenn sending-Modul aktiv)
npm run worker:prod                # Worker ohne Watch

npm run modules:list               # Aktive Module aus modules.config.ts loggen
```

## Architektur-Prinzipien

### 1. Pluggable Adapters (kein Hardcoding von CRM/Mail-Provider)

**Niemals** direkt `cumas-client` oder `m365-sender` importieren. **Immer** über die Adapter-Resolver:

```typescript
import { adapters } from "@/adapters.config";

const contacts = await adapters.crm.listContacts(filter, ctx);
await adapters.send.send({ to, subject, html, text }, ctx);
```

- **CRM-Adapter** (`src/adapters/crm/`): `interface.ts`, `generic-rest.ts` (Default), `cumas.ts` (Beispiel)
- **Send-Adapter** (`src/adapters/send/`): `interface.ts`, `smtp.ts` (Default), `m365.ts` (Beispiel)

Eigene Adapter implementieren das Interface und werden in `adapters.config.ts` registriert.

### 2. Modul-Toggles (`modules.config.ts`)

Zentrale Config — definiert welche Tabs/Funktionen aktiv sind:

```typescript
export const modules = {
  briefing: true,           // AI-Generation
  editor: true,             // TipTap + MJML
  recipients: true,         // Zielgruppe
  sending: false,           // Versand + Worker (off = Tool ist read-only Strategie)
  scheduler: true,
  stats: true,
  heroImage: false,         // fal.ai (off = User uploadet eigene Bilder)
  spamCheck: true,
  sectionTemplates: true,
  triggerTemplates: false,
};
```

**Bei `false`:**
- UI-Tab wird NICHT gerendert
- API-Routes geben 404 zurück
- DB-Tabellen werden NICHT migriert
- Worker startet nicht (wenn `sending=false`)

**Niemals** Modul-Code direkt importieren — immer über `loadActiveModule(id)` aus `src/core/modules/loader.ts`.

### 3. Dual-Driver DB (SQLite oder MySQL)

`DB_DRIVER` env-var entscheidet:
- `sqlite` → `better-sqlite3` auf Single-File `./data/mailforge.db`
- `mysql` → `mysql2` Pool auf `DATABASE_URL=mysql://...`

Schemata sind unter `src/core/db/schema/{sqlite,mysql}/` gespiegelt. `scripts/sync-schemas.ts` synct Strukturänderungen von SQLite → MySQL (single source of truth: SQLite-Variante).

Optional: `DB_TABLE_PREFIX=mf_` für Shared-DB-Modus (Laravel- und Plugin-Tabellen in derselben MySQL-DB).

### 4. JWT-Auth gegen Host-CRM

- Host-CRM signiert JWT mit `MAILFORGE_JWT_SECRET` (shared)
- Plugin validiert in `src/proxy.ts` (Next.js 16 Proxy, ehemals Middleware)
- `UserContext` enthält `userId`, `workspaceIds[]`, `permissions[]`
- API-Routes: `withAuth(async (req, ctx) => …)` HOF
- Repo-Queries scopen IMMER auf `WHERE workspace_id IN (ctx.workspaceIds)`

**Niemals** `userId` aus Request-Body oder URL-Params lesen — nur aus `ctx`.

### 5. Workspaces statt Brands

Im Plugin heißt das Mandanten-Konzept **`workspaces`** (nicht mehr `brands`). Keine harten Defaults wie ITS/KSM/Spartarif — `db:seed-dev` legt einen generischen Demo-Workspace an.

### 6. Settings statt env (für UI-konfigurierbare Keys)

OpenRouter-Key, fal.ai-Key, SMTP/M365-Config werden im Settings-UI eingestellt und verschlüsselt in `appSettings` gespeichert (AES-256 mit `APP_ENCRYPTION_KEY` aus env). ENV-Vars sind nur Fallback für CI/Dev.

Geheime Settings-Keys (verschlüsselt): `openrouter_api_key`, `fal_api_key`, `send_provider_config`, `crm_adapter_config`.

## Verzeichnis-Struktur

```
src/
├── core/                    # Framework-agnostische Foundation
│   ├── db/                  # Schema (dual), Client, Migrations
│   ├── auth/                # JWT + Middleware
│   ├── settings/            # appSettings-Repo + Crypto
│   ├── modules/             # ModuleManifest-Typ, Loader
│   └── types.ts             # UserContext, WorkspaceContext
├── modules/                 # 10 Module, jedes self-contained
│   ├── briefing/
│   ├── editor/
│   ├── recipients/
│   ├── sending/
│   ├── scheduler/
│   ├── stats/
│   ├── hero-image/
│   ├── spam-check/
│   ├── section-templates/
│   └── trigger-templates/
├── adapters/
│   ├── crm/                 # CrmAdapter + Defaults
│   └── send/                # SendProvider + Defaults
├── app/                     # Next.js App-Router (Shell + dynamic dispatcher)
├── ui/                      # Shared UI (Settings-Drawer, Sidebar)
└── worker/                  # Nur wenn sending=true

docs/
├── README.md                # 1-Seite Quickstart
├── INTEGRATION.md           # Laravel-Integration Schritt-für-Schritt
├── SCHEMA.md                # ERD + Tabellen
├── MODULES.md               # Modul-Referenz
├── ADAPTERS.md              # Adapter selbst schreiben
└── ddl/                     # Pure DDL für SQLite + MySQL

examples/
├── laravel-jwt-middleware.php
└── docker-compose.yml
```

## Wichtige Regeln für Code-Änderungen

1. **Niemals** direkte Imports von `@/lib/crm/cumas-client` — Cumas existiert nur in `src/adapters/crm/cumas.ts` als optionaler Adapter.
2. **Niemals** direkte Imports von `@/lib/email/m365-sender` — M365 existiert nur in `src/adapters/send/m365.ts` als optionaler Adapter.
3. **Niemals** `DEFAULT_BRANDS` re-introduzieren — Workspaces werden vom Host-CRM/User angelegt.
4. **Niemals** Tabellen außerhalb des aktiven Modul-Sets in einer Migration kreieren — Build-Skript filtert sonst inkonsistent.
5. **Bei jedem neuen API-Endpoint:** `withAuth()`-Wrapper + Workspace-Scope-Check.
6. **Bei jedem neuen DB-Read:** prüfen ob beide Driver (SQLite + MySQL) funktionieren.
7. **Bei jedem neuen Settings-Key:** entscheiden ob verschlüsselt — defaults: API-Keys = ja, Prompts = nein.

## Stolpersteine (übernommen aus Mailforge-Live)

- **TipTap-Doc → MJML**: nicht direkt — `sections-to-tiptap.ts:docToHtml()` baut sicheres HTML, das von `inject-sections.ts` in MJML-`<mj-raw>` gewrapped wird.
- **Section-Templates `bodyMode='mjml_raw'/'html_raw'`** haben keinen `{{TIPTAP_HTML}}`-Slot und kein KI-Suggest.
- **Worker-Pick-Job-Transaktion**: `pickPendingJobs()` macht `UPDATE … SET status='sending'` in einer Transaktion mit `LIMIT batch` — never bypass.
- **`finalizeCampaignIfDone()`** muss am Ende JEDES Job-Tries laufen, auch im Failure-Path.
- **MJML-Platzhalter NIE im LLM-Call ersetzen** — Substitution passiert deterministisch im Worker.
- **OpenRouter-Pin** auf `anthropic/claude-sonnet-4.6` (kein `latest`/`~`-Pointer) — deterministisches Tool-Use-Verhalten.

## Refactoring-Status (siehe Plan)

Plan-Datei: `/Users/tijan/.claude/plans/wir-wollen-eine-kopie-lexical-hellman.md`

- ✅ Phase A — Codebase initialisieren (Vollkopie, package.json, CLAUDE.md, Git)
- ⏳ Phase B — Hardcoded Coupling entfernen (Brands→Workspaces, CRM-Adapter, Send-Adapter, AI-Keys-Migration)
- ⏳ Phase C — Modul-System einführen
- ⏳ Phase D — Dual-Driver DB
- ⏳ Phase E — JWT-Auth
- ⏳ Phase F — Dokumentation
- ⏳ Phase G — Cleanup & Verification

**Aktueller Zustand:** Code stammt zu 100% aus Mailforge-Live. Hardcoded Coupling, monolithic `page.tsx`, fehlende Adapter — alles noch da. Refactoring läuft inkrementell pro Phase.
