# 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 **Ein Prozess.** Oberflaeche, Ingest und die Werkzeugmodule laufen gemeinsam in einem Container; schwere Arbeit (ffmpeg, whisper.cpp) laeuft als Kindprozess und darf abstuerzen, ohne die Chat-Aufnahme mitzureissen. Begruendung und Grenzen: [ADR-0006](decisions/ADR-0006-ein-dach.md). | Port | Wofuer | |------|--------| | 4792 | Weboberflaeche: Einrichtung, OAuth-Rueckruf, Sichtung, Dashboard, `/health` | | 4791 | Overlay-Server fuer die OBS-Browser-Quellen (ab M3) | Beide nur auf `127.0.0.1` des Hosts veroeffentlicht. Die Werkzeuge sind Module, keine eigenen Dienste: | Modul | laeuft | Ausloeser | |-------|--------|-----------| | Ingest | dauernd | IRC, EventSub, Helix-Polling | | Community, Loyalty | dauernd | Chat-Ereignisse | | Clipper | bei Bedarf | `!clip`, OBS-Hotkey, spaeter Chat-Spitze | | Broadcaster | bei Bedarf | `stream.online` | | Recap, VOD-Kapitel | bei Bedarf | `stream.offline` | | Raid-Scout | bei Bedarf | `!raidziel`, Oberflaeche | ## 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: ein `EventEmitter` im Prozess. Solange alles unter einem Dach laeuft (ADR-0006), braucht es keinen Netzwerk-Bus. Die `events`-Tabelle ist trotzdem die Wahrheit: sie wird immer geschrieben, und ein spaeter herausgeloester Prozess kann seinen Rueckstand ueber `ts > lastSeen` aufholen. 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).