# Architektur ## Bild ``` Twitch OBS ┌──────────────────┐ ┌───────────┐ │ EventSub (WS) │ │ obs- │ │ IRC / Chat │ │ websocket │ │ Helix REST │ └─────┬─────┘ └────────┬─────────┘ │ │ │ ┌────────▼────────────────────────▼────────┐ │ apps/ingest (Daemon) │ │ normalisiert alles zu Events │ └────────┬─────────────────────┬───────────┘ │ │ schreibt │ │ publiziert ▼ ▼ ┌─────────────────┐ ┌──────────────────────┐ │ SQLite │ │ Event-Bus │ │ data/db/ │ │ (in-proc + WS 4790) │ └────────┬────────┘ └──────────┬───────────┘ │ │ ┌───────────┬───────┼───────────┬───────────┼───────────┬──────────┐ ▼ ▼ ▼ ▼ ▼ ▼ ▼ dashboard recap raid-scout vod-chapters community overlay- clipper server │ │ │ overlay-web ffmpeg (Browser- whisper Source) │ data/clips │ broadcaster ``` ## Prozesse Der Verbund besteht aus wenigen langlaufenden Prozessen und mehreren Kurzläufern, gestartet über `docker compose up -d`. > **Im MVP gilt das noch nicht.** Dort laufen Ingest und Clipper als *ein* > Prozess in *einem* Container; die Aufteilung unten ist der Zielzustand ab > Meilenstein M3, wenn es mehr als einen Konsumenten gibt. Siehe > [`07-mvp-plan.md`](07-mvp-plan.md). **Langläufer (laufen immer, auch offline):** | Prozess | Port | Aufgabe | |---------|------|---------| | `ingest` | 4790 (WS) | EventSub, IRC, OBS-WS, Viewer-Polling → DB + Bus | | `overlay-server` | 4791 (HTTP+WS) | liefert Overlay-Seiten, nimmt Chat-Commands entgegen | | `dashboard` | 4792 (HTTP) | Analytics-UI | **Kurzläufer (event- oder zeitgetriggert):** | Prozess | Trigger | |---------|---------| | `clipper` | Clip-Event vom Bus, oder manuell über CLI | | `broadcaster` | `stream.online` | | `recap` | `stream.offline` (+ 5 min Karenz) | | `vod-chapters` | `stream.offline` | | `raid-scout` | manuell / Chat-Command `!raidziel` | `community` und `loyalty` sind keine eigenen Prozesse, sondern Module, die im `overlay-server` mitlaufen — sie brauchen dieselbe Chat-Verbindung und denselben WS-Kanal zum Overlay. ## Event-Bus Der Ingest normalisiert alles auf ein einheitliches Event-Format: ```ts type StreamEvent = { id: string; // ULID ts: string; // ISO-8601, UTC type: EventType; // 'chat.message' | 'stream.online' | ... streamId: string | null; userId: string | null; payload: unknown; // typisiert pro EventType }; ``` Transport: In-Prozess ein `EventEmitter`, prozessübergreifend ein WebSocket-Broadcast auf `ws://127.0.0.1:4790`. Kein Kafka, kein Redis. Wenn ein Konsument abstürzt, holt er sich beim Neustart die verpassten Events aus der `events`-Tabelle über `ts > lastSeen`. Vollständige Event-Liste: siehe [`03-data-model.md`](03-data-model.md). ## Pakete | Paket | Inhalt | |-------|--------| | `packages/shared` | Ereignistypen, ULID | | `packages/core` | Config, Layout, Logger mit Maskierung, Retry, YAML-Leser | | `packages/twitch` | Secret-Speicher, OAuth, Helix-Client, IRC-Client | | `packages/db` | SQLite-Verbindung, Migrationen, Repositories | Regel: Apps dürfen von Paketen abhängen, Pakete nie von Apps. Apps hängen nicht voneinander ab — Kommunikation läuft über Bus und DB. Importiert wird über die `imports`-Map der Wurzel-`package.json` (`#core/config.ts`, `#db/repos.ts`). Das ist eine Node-Funktion, kein Build-Werkzeug — es gibt keinen Bundler und keine Pfad-Aliasse im Compiler. ## Technologie - **TypeScript auf Node 24**, ohne Build-Schritt und ohne Laufzeitabhängigkeiten — Node führt die `.ts`-Dateien direkt aus. Siehe [ADR-0001](decisions/ADR-0001-monorepo-typescript.md) - **SQLite** über das eingebaute `node:sqlite` — siehe [ADR-0002](decisions/ADR-0002-sqlite.md) - **EventSub über WebSocket**, nicht Webhooks — siehe [ADR-0003](decisions/ADR-0003-eventsub-websocket.md) - **ffmpeg** (CLI) für Videoverarbeitung - **whisper.cpp** (CLI) für Untertitel — kein Python, keine GPU - **Serverseitiges HTML** ohne Framework für Einrichtung und Sichtungsseite; für Dashboard und Overlays ab M3 ggf. Vite + Preact - **Docker Compose** für den Betrieb — siehe [ADR-0004](decisions/ADR-0004-docker.md) ## Fehlerverhalten - Jeder Netzwerkaufruf hat Retry mit exponentiellem Backoff (`packages/core/retry`). - Der Ingest darf niemals wegen eines Konsumentenfehlers sterben. Konsumenten laufen in eigenen Prozessen; ein Fehler dort schreibt in `logs/` und wird im Dashboard als Warnung angezeigt. - Wenn EventSub die Verbindung verliert: Reconnect mit Session-Resume, danach Lücke über Helix-Polling schließen (`stream.online`-Status, Follower-Liste).