CRM API — Endpunkt-Referenz

Arc OS — The Orchestration System für KI-Teams

Allgemeine Informationen

Parameter Wert
Base URL https://arc-os.co/api/crm
Autorisierung Authorization: Bearer <JWT> oder ?token=<JWT> (für SSE/WebSocket)
Content-Type application/json
JWT-Algorithmus HMAC-SHA256
JWT TTL 24 Stunden

Authentifizierung

Alle Endpunkte (außer /docs/*) erfordern einen JWT-Token im Header Authorization: Bearer <token>.

Für SSE- und WebSocket-Verbindungen wird der Token über den Query-Parameter ?token=<JWT> übermittelt.

Autorisierungsfehler

Code Beschreibung
401 Fehlender oder ungültiger Token
403 Kein Zugriff auf das Projekt (Multi-Tenancy)

Endpunkte nach Kategorien

Konto und Einstellungen

Methode Pfad Beschreibung
GET /account/settings Kontoeinstellungen abrufen
PUT /account/settings Kontoeinstellungen aktualisieren
GET /account/usage Token-Usage-Verlauf für den autorisierten Benutzer (Phase 63, #148). Response: { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × bis zu 200 ], totals: { total, input, output } }. Liest token_usage_log nach owner_id. Angezeigt in UserDropdown (UsageCard) und BillingPage (Token-Usage-Sektion).
GET /account/billing-summary Konsolidierte Billing-Zusammenfassung (#309). Response: { arc: { plan, status, tokens_this_month, tokens_input, tokens_output, renewal_date }, anthropic: { connected, key_prefix, credit_balance_usd, spend_month_usd, tokens_this_week } }. Die Anthropic-Sektion wird server-seitig über die Anthropic API mit dem account_settings.anthropic_key des Users gefüllt (Fallback → PLATFORM_ANTHROPIC_KEY). Angezeigt in der UserDropdown UsageCard.

Onboarding + Trial Credits (Phase 50.1)

Methode Pfad Beschreibung
POST /onboarding/setup Erstes Projekt erstellen. Body multipart: config (JSON) + files. Feld anthropicKey ist jetzt optional — wenn leer + User hat email_verified + hat noch keine Testphase erhalten, wird das Projekt im trial_mode=1 mit 100K Free Tokens erstellt. Response: { ok, project, trial_activated }. Phase 51: Gibt 402 mit {error:"plan_limit_reached", reason, current, limit, plan} zurück, wenn der User das Projekt-Limit seines Plans überschritten hat.
GET /account/trial-status Testphasen-Status für das UI-Banner. Response: { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] }

Onboarding Checklist (Phase 54.1, Issue #56)

Post-Wizard-5-Schritt-Engagement-Checkliste. Jeder Schritt (workers, cli, skill, bot, issue) akzeptiert den Status completed oder skipped. Mutationen sind idempotent: Ein erneuter identischer POST gibt denselben State zurück, ohne einen Duplikateintrag in activity_log zu schreiben. Replay setzt den State nicht zurück, entfernt nur dismissed_at — das UI zeigt das Panel erneut mit demselben Fortschritt.

Methode Pfad Beschreibung
GET /onboarding/progress Aktueller State für den autorisierten User. Response: { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Unberührter User → Nullwerte/null ohne Zeilenerstellung.
POST /onboarding/event Schritt-Transition aufzeichnen. Body: { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Whitelist-Validierung → 400 bei unbekanntem step/status. Response: dieselbe Form wie GET. Emittiert onboarding_step_completed/onboarding_step_skipped im activity_log nur bei changed; bei Transition auf 5/5 wird zusätzlich onboarding_completed mit duration_ms emittiert.
POST /onboarding/dismiss Panel schließen (dismissed_at = now). Idempotent. Emittiert onboarding_dismissed beim ersten Aufruf mit Payload {completed_count}.
POST /onboarding/replay Geschlossenes Panel wieder öffnen (dismissed_at = NULL). Schritt-State bleibt unberührt. Emittiert onboarding_replayed beim Clear-Event.
POST /projects/:name/active-issue Issue #115. Aktuelle Web-Sitzung an ein Issue binden. Body: { issue_id: number, title?: string }. Schreibt activity_log-Event session_active_issue (source=web).
GET /projects/:name/active-issue Issue #115. Zuletzt gebundenes Issue dieses Owners innerhalb von 7 Tagen. Response: { active_issue_id, title, ts }.
GET /onboarding/cli-status Phase 54.3 (Issue #58). Hat sich der User in den letzten 30 Tagen über arc login eingeloggt? Response: { installed: boolean, last_cli_at: string|null }. SSOT — Zeilen in activity_log mit event_type='cli_invocation' und actor=chatId. Das Frontend-Onboarding-Checklisten-UI pollt diesen Endpunkt alle 10s, solange der CLI-Schritt aussteht; wenn installed=true — wird der cli-Schritt automatisch als abgeschlossen markiert.
GET /analytics/onboarding-funnel Phase 54.6 (Issue #61). Aggregierte Funnel-Statistiken über ein Rolling Window. Query: hours=168 (1-720, Standard 7d). Response: { hours, total_steps:5, started_users, completed_users, completion_rate, per_step: [{step, completed, skipped}…], duration_p50_ms, duration_p90_ms, ttfc_p50_ms, ttfc_sample_size }. SSOT — activity_log-Events onboarding_step_* + onboarding_completed + cli_invocation. TTFC = Time-to-First-arc (julianday-Delta vom ersten Onboarding-Schritt bis zur ersten cli_invocation pro Actor).

SSOT für Funnel-Metriken (Phase 54.6 / Issue #61) — Events in activity_log (event_type LIKE 'onboarding_%'). Tabelle onboarding_progress — Derived Cache: UI rendert mit einer Abfrage statt durch Event-Aggregation.

Beta Feedback (Phase 53.3)

Methode Pfad Beschreibung
POST /feedback Beta-Feedback senden. Body: {type: "bug"|"feature"|"other", title, description, project?, browser?}. Schreibt in activity_log (event_type=feedback_report) und pingt den CEO auf Telegram.
GET /admin/feedback Liste der letzten Einsendungen (nur Admin). Query: limit=50 (max 500). Response: {items: [...], count}.
POST /feedback/translation Übersetzungs-Issue senden (Phase 59.4). Body: {locale, msgid, suggestion, severity: "minor"|"major"|"wrong", current_translation?, page_url?}. Speichert in translation_feedback.
GET /admin/translations Liste des Translation-Feedbacks (Admin). Query: locale, status=open|accepted|rejected|all, limit. Response: {items, count}.
GET /admin/translations/stats Health-Stats pro Locale (Admin). Response: {stats: [{locale, total, open_count, accepted, rejected, critical_open}]}.
POST /admin/translations/:id/accept Vorschlag annehmen — patcht die .po-Datei auf der Festplatte. Body: {note?}. Response: {ok, po_patched, glossary_suggestion}.
POST /admin/translations/:id/reject Vorschlag ablehnen. Body: {note?}. Response: {ok}.

POST /feedback/translation — validiert: locale ∈ {uk,de,es,fr,pl,pt-BR,ru}, msgid ≤1000, suggestion ≤2000, severity ∈ {minor,major,wrong}. Nach 3+ angenommenen Vorschlägen für dieselbe msgid → glossary_suggestion: true in der Accept-Response.

Das Floating-Widget in FeedbackWidget.jsx hat jetzt einen 4. Typ „Translation" — füllt locale automatisch aus i18n.locale, erfasst msgid + suggestion + severity.

Arc Help AI Chat (Phase 61, #147)

Methode Pfad Beschreibung
POST /help/chat In-App-AI-Q&A. Body: {message, history: [{role,text}]}. Response: {reply, sources: string[], remaining, limit}. Rate-Limit: 30/Tag/User.
GET /help/usage Aktuelles Limit. Response: {remaining, limit, used}.

POST /help/chat — Pipeline: (1) Rate-Limit-Check (429 bei Überschreitung), (2) RAG über shared/rag.ts (Cohere + sqlite-vec, Phase 71), das Projekt- + _global_-Skill-Treffer merged → Fallback-Keyword-Suche über docs/public/, (3) Claude Haiku mit System-Prompt + Doc-Kontext + History. message ≤2000 Zeichen. Antwortet in der Sprache der Anfrage.

Beta Invites (Phase 52.1, nur Admin)

Methode Pfad Beschreibung
GET /admin/dashboard System Dashboard (Phase 60.9, #145). Nur Admin. Liefert: CPU/RAM/Disk aus /proc, Users nach Plan, Container-Flotte, letzte 50 Activity-Events, Waitlist- + Projekt- + Issue-Stats.
GET /admin/wipe-metrics WIP-E-Telemetrie-Dashboard (#308). Nur Admin. Liefert: {render: {count, avg_ms, p50_ms, p95_ms, max_ms}, interaction: {count, avg_per_session, p95_per_session, max_per_session, sessions_zero}, by_worker: [{worker_id, render_count, avg_render_ms, session_count, avg_interactions}], daily: [{date, renders, interactions, avg_render_ms}], recent: [...]}.
GET /admin/waitlist Liste aller Waitlist-Anfragen. Nur Admin. Response: {entries: [{id, email, message, status, created_at}]}.
POST /admin/waitlist/:id/approve Anfrage genehmigen — generiert einen Invite-Code (arc-XXXX-XXXX), sendet E-Mail mit dem Code, setzt status→approved. Response: {ok, invite_code, email_sent}.
POST /admin/waitlist/:id/reject Anfrage ablehnen. Response: {ok}.
GET /admin/invites Liste aller Einladungscodes + Zählungen (total_active, total_used). Nur Admin.
POST /admin/invites N Codes generieren. Body: {count: N, note?: string}. Nur Admin. Response: {ok, codes, count}.
DELETE /admin/invites/:code Ungenutzten Einladungscode widerrufen.
/admin/notebooklm/* Entfernt in Phase 71.8 zusammen mit der NotebookLM Bridge. Die semantische Suche läuft jetzt über self-hosted RAG (rag-architecture.md).

Auth-Flow-Update: POST /api/auth/register erfordert nun das Feld invite_code (Phase 52.1 Closed Beta). Ohne Code → 403 {error: "invite_required"}. Ungültiger/bereits verwendeter Code → 403 {error: "invalid_invite"}.

Standard Cloud — WebSocket Terminal + SSE Logs (Phase 60 #139)

Protokoll Pfad Beschreibung
WS /ws/cloud/:userId/terminal?token=<JWT> Proxy zu docker exec -i <containerId> /bin/bash. IDOR: userId muss mit der chatId aus dem JWT übereinstimmen. Pausierter Container wird automatisch fortgesetzt. Eingehende WS-Frames → Container-stdin; stdout+stderr → WS-Frames.
SSE /api/sse/cloud/:userId/logs docker logs -f --tail 50 für den Container des Users. Auth: Bearer JWT. IDOR: userId === chatId. Events: data: {"line": "..."} pro Zeile, data: {"closed": true} beim Beenden.

Standard Cloud (Phase 60)

Methode Pfad Beschreibung
POST /cloud/claude-verify Prüft claude --version im Container (transport-safe shell-quoted über SSH im Remote-Host-Modus, #329). Setzt claude_authed=true. Response: { ok, output }
POST /cloud/ssh-keygen Generiert einen ed25519-Key im Container (idempotent). Response: { public_key }
POST /cloud/ssh-verify ssh -T [email protected] im Container. Setzt github_authed=true bei Erfolg. Response: { ok, output }
POST /cloud/provision Provisionierung eines Docker-Containers für den User. Erfordert Plan cloud, sonst 402. Idempotent: Existiert der Container bereits — gibt den aktuellen Zustand zurück. Response: { container_id, status, server_ip, port, claude_authed, github_authed }
GET /cloud/status Container-Zustand + Live-docker-inspect-Reconciliation. Response: { container_id, status, server_ip, internal_port, claude_authed, github_authed, docker_running, last_active, created_at } oder { status: "none" }
POST /cloud/deprovision Container stoppen + löschen (docker stop + docker rm -f + docker network rm arc-net-{id}). Setzt status=deleted in der DB. Response: { ok: true, container_id }

Container-Statuses: provisioningreadypausedsuspended / deleted.

Security (SEC-60 #152, #154, #155, #156): Jeder Container ist in seinem eigenen Netzwerk arc-net-{id} isoliert (Lateral-Movement-Prävention). SSH-Verbindung Contabo→Hetzner über dedizierten arcapi-User (docker group, kein root) mit docker-only-Wrapper — Nicht-Docker-Befehle sind auf authorized_keys-Ebene blockiert. ARC_TOKEN wird nach dem Start über docker exec injiziert (nicht in docker inspect sichtbar). git clone ist auf timeout 60 begrenzt. WebSocket-Idle-Timeout: 120s. SSE docker logs begrenzt auf --since 1h. IDOR-Prävention: Alle Endpunkte gleichen container.user_id === req.userId ab. Security-Flags bei docker run: --cap-drop=ALL --security-opt=no-new-privileges --cpus=1.5 --memory=2g --pids-limit=200. Volumes: arc-{id}-workspace:/workspace, arc-{id}-claude:/home/arcuser/.claude, arc-{id}-ssh:/home/arcuser/.ssh. Lifecycle (#141): GET /cloud/status aktualisiert immer last_active. 30 Min Idle → docker pause (Cron alle 5 Min, scripts/cloud-lifecycle-cron.ts). Wake: CRM-Nachricht, TG-Nachricht, WS-Upgrade → docker unpause automatisch.

Waitlist (#134):

Methode Pfad Beschreibung
POST /cloud/waitlist Der Warteschlange beitreten. Idempotent. Response: { position, status, joined_at, message }. 409 wenn bereits auf dem Cloud-Plan oder Container existiert.
GET /cloud/waitlist/status Eigener Status in der Warteschlange. Response: { position, status, joined_at, invited_at } oder { status: "not_joined" }.
GET /cloud/waitlist Nur Admin. Vollständige Liste + Stats. Response: { stats: { total, waiting, invited, activated }, list: [...] }.
POST /cloud/waitlist/invite Nur Admin. User einladen. Body: { user_id }. Setzt status=invited + upgraded den Plan automatisch auf cloud. Response: { ok, user_id, position }.

Billing (Phase 51 → #202 Plata by mono)

Phase #202: Stripe wurde durch Plata by mono (monobank Internet-Acquiring) ersetzt. Recurring-Abos über Tokenization (Karte wird bei der ersten Zahlung gespeichert).

Methode Pfad Beschreibung
GET /billing/status Aktueller Plan, Limits, Usage, Features. Response: { plan, status, current_period_end, next_billing_date, plata_masked_pan, limits, usage, features, pricing, can_upgrade, plata_ready }
POST /billing/checkout-session Erstellt eine Plata-Invoice mit Tokenization. Body: { plan: "min"|"cloud", success_url?, cancel_url? }. Response: { url, invoice_id, plan, amount_uah }. 503 wenn PLATA_MERCHANT_TOKEN nicht im Vault.
POST /billing/webhook Plata-Callback (KEIN CRM-Auth — verifiziert über X-Token-Header). Statuses: success (aktiviert den Plan + speichert cardToken), failure/expired (inkrementiert billing_failures, 3+ → Downgrade auf free). Idempotent über die Tabelle plata_events.
POST /billing/cancel Abo kündigen (Downgrade auf free). Pausiert den Docker-Container beim Cloud-Plan. Response: { ok, plan: "free" }.

#205 (2026-05-26): Die Legacy-Route /billing/portal-session wurde zusammen mit totem Stripe-Code entfernt. Verwende /billing/cancel, um ein Abo zu kündigen.

Plan-Limits (OR-Semantik):

402-Response bei POST /onboarding/setup oder POST /projects/:name/workers, wenn das Limit überschritten wird: { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }

Admin-User (role=admin) umgehen die Plan-Limit-Prüfung vollständig — sie sind Operatoren, keine zahlenden Mandanten.

Beta-Tester (subscriptions.plan='beta', Phase 52 F&F) umgehen das ebenfalls — unbegrenzte Anzahl an Projekten/Worker plus alle Max-Features. Wird manuell zugewiesen: UPDATE subscriptions SET plan='beta' WHERE user_id=?.

Bugfix (Issue #25): POST /projects/create (Quick Start, Phase 50.2) schlug zuvor mit ownerChatId is not defined wegen eines Tippfehlers fehl — behoben, der Audit-Actor wird jetzt korrekt eingetragen.

Bugfix (Issue #26): allocatePort() für neue Projekte prüft jetzt echte TCP-Bindings (ss -tln) statt nur die Registry. Zuvor konnte ein Port zurückgegeben werden, der von einem Nicht-Registry-Dienst belegt war (NotebookLM Bridge :19213, interne Bridges) → Workspace-Bot schlug mit EADDRINUSE fehl.

Auth-Flow (Phase 50.1): /api/auth/register und /api/auth/login geben nun JWT auch für unverified E-Mails zurück + Flag needs_verification: true. Sensitive Aktionen (Trial Grant, Billing, Invites) prüfen email_verified separat. Rate-Limit bei Registrierung: 3 / IP / 24h.


Projekte (9 Endpunkte)

Methode Pfad Beschreibung
GET /projects Liste der Projekte des Users
POST /projects/create Projekt erstellen — Body: {displayName, projectName, niche?, teamPreset?}; setzt für Trial-User automatisch trial_mode=1 und injiziert PLATFORM_ANTHROPIC_KEY
POST /projects/create-with-team Atomare Erstellung von Projekt + Workern + (opt.) TG-Bot in einer Anfrage — Body: {project, workers[], telegram?}; Rollback bei Fehler
GET /projects/suggest-preset Preset-Vorschlag nach Nische — Query: niche=<text>; gibt {preset_id} auf Basis einer Keyword-Map zurück
GET /projects/:name Projektdetails
GET /projects/:name/config Projektkonfiguration
PUT /projects/:name/config Konfiguration aktualisieren
POST /projects/:name/upload-icon PNG/GIF-Icon für das Projekt hochladen
POST /projects/:name/workers/:id/upload-icon PNG/GIF-Icon für den Worker hochladen
GET /projects/:name/protocol Projektprotokoll
PUT /projects/:name/protocol Protokoll aktualisieren
GET /projects/:name/logs Projekt-Logs
GET /projects/:name/metrics Projektmetriken

POST /projects/create — Body:

{
  "technical_name": "string",
  "displayName": "string",
  "description": "string",
  "icon": "string",
  "color": "string"
}

GET /projects/:name/logs — Query: category, lines

GET /projects/:name/metrics — Query: since, until


Worker (11 Endpunkte)

Methode Pfad Beschreibung
GET /workers #304 Phase A — alle Worker aller Projekte des aktuellen Users. Response: { workers: [{ id, label, icon, type, model, tools, context_assets, project_name }] }. Filterung nach owner_id (Multi-Tenancy). Der CEO sieht alle Projekte.
GET /workers/presets #228 — globale Preset-Bibliothek (projekt-agnostisch). Gibt 13 Worker aus dem kanonischen config/workers_registry.json zurück: { presets: [{ id, label, icon, type, model, max_turns, tools, system_prompt, context_assets, focus_dirs, prompt_style }] }. Vom WorkerCreationWizard für Step 1 verwendet.
GET /workers/templates #304 Phase I — Templates des aktuellen Users. Response: { templates: [{ id, name, description, config, is_public, created_at }] }.
POST /workers/templates #304 Phase I — Template speichern/aktualisieren. Body: { name, description?, config }. Response: { ok, id }.
DELETE /workers/templates/:id #304 Phase I — Template löschen (nur Eigentümer). Response: { ok }.
GET /projects/:name/workers Liste der Worker
POST /projects/:name/workers Worker erstellen
POST /projects/:name/workers/reorder Phase 53.8 — Worker-Reihenfolge ändern. Body: {order: [id1, id2, ...]}. Überschreibt workers_registry.json atomar. Worker, die nicht in order enthalten sind, werden ans Ende angehängt (Schutz vor Datenverlust). Response: {ok, count, order}.
PUT /projects/:name/workers/:id Worker aktualisieren
DELETE /projects/:name/workers/:id Worker löschen
POST /projects/:name/workers/generate-prompt System-Prompt generieren
GET /projects/:name/workers/:id/telegram-token Telegram-Token abrufen
POST /projects/:name/workers/:id/telegram-token Phase 53.4 — Validiert den Token über Telegram getMe, speichert bot_username im Vault, verweigert die Anfrage, wenn derselbe Bot bereits an einen anderen Worker gebunden ist (409). Response: {ok, started, bot_username}.
DELETE /projects/:name/workers/:id/telegram-token Telegram-Token löschen
POST /projects/:name/workers/:id/avatar #304 Phase D — Avatar hochladen (multipart file, JPEG/PNG/WebP, max 2 MB). Magic-Byte-Prüfung. Speichert in data/worker-avatars/, schreibt in worker_avatars (Migration 043). Response: { ok, url }.
GET /projects/:name/workers/:id/avatar #304 Phase D — Avatar binär abrufen (Content-Type entsprechend MIME). 404 wenn kein Avatar hochgeladen.
DELETE /projects/:name/workers/:id/avatar #304 Phase D — Avatar löschen, avatar_pack='role' im Worker-JSON zurücksetzen.
GET /projects/:name/workers/:id/activity #306 — Activity-Feed des Workers (letzte 50 Events). Merged: activity_log (actor=workerId) + project_issues.activity (author=workerId) + token_usage_log (Daily Snapshots). Response: { events: [{ type, title, detail, when }] }. Types: git_commit, skill_loaded, skill_unloaded, issue_pick, issue_close, issue_log, token_budget, session_start.
GET /projects/:name/workers/:id/runtime #306 — Runtime-State des Workers. Response: { status: 'working'|'idle', status_started_at, tokens_today, tokens_pct, tokens_cap, current_skill }. Liest zuerst aus workers_runtime_state (Migration 045); Staleness-Fallback: status='working' + tmux tot + updated_at > 10 Min → idle (Crash-Erkennung). Plan-basiertes Tageslimit über subscriptions.plan-Lookup: free=100K, starter=400K, starter_cloud=2M, beta=unmetered (gibt tokens_cap: null, tokens_pct: 0 zurück). Poll-Intervall 15s.
POST /projects/:name/workers/:id/notify Phase 53.2 — TG-Event-Ping senden ({event?, text, buttons?}). Stiller No-Op, wenn kein Token gebunden oder CRM_DISABLE_TG_NOTIFY=1.
POST /projects/:name/workers/:id/suggest-bot-username 53.11.1 (Issue #48) — Gibt 5 Kandidaten für den TG-Username im Bot-Creation-Wizard zurück, im Format <project>_<worker>_bot + nummerierte Fallbacks. Slugify entfernt Bindestriche, Truncate auf 32 Zeichen (Worker-Teil wird zuerst gekürzt). Response: {candidates: string[]}.
POST /metrics/wizard 53.11.1 (Issue #48) — Telemetrie-Sink für den Bot-Creation-Wizard. Body: {action, duration_ms?, attempts?, success?, project?, worker_id?, locale?} (#124: locale_active/locale_switch-Events). Schreibt in activity_log (event_type=wizard_metric), Best-Effort.
GET /analytics/wizard-metrics?hours=168 53.11.1 (Issue #48) — Funnel-Zusammenfassung: {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Standard 7 Tage, Clamp 1-720h.
POST /projects/:name/restart Worker neu starten
GET /projects/:name/active-role Aktuell aktive Rolle
POST /projects/:name/active-role Aktive Rolle ändern

POST /projects/:name/workers — Body:

{
  "label": "string",
  "icon": "string",
  "type": "terminal | telegram",
  "model": "string",
  "max_turns": 20,
  "tools": ["Read", "Write", "Bash"],
  "system_prompt": "string",
  "focus_dirs": ["src/", "docs/"]
}

max_turns standardmäßig 20 (zuvor 5, was den Fehler "Reached max turns" bei mehrstufigen Dialogen mit Tool Calls verursachte).

POST /projects/:name/restart — Query: worker_id


Dateien und Speicher (8 Endpunkte)

Methode Pfad Beschreibung
GET /projects/:name/files Dateibaum
POST /projects/:name/files/upload Datei hochladen (multipart, max 100MB)
POST /projects/:name/files/mkdir Verzeichnis erstellen
POST /projects/:name/files/create Datei erstellen
GET /projects/:name/files/read Datei lesen
PUT /projects/:name/files/save Datei speichern
DELETE /projects/:name/files/delete Datei löschen
POST /projects/:name/files/clone Git-Repository klonen

GET /projects/:name/files — Query: path

GET /projects/:name/files/read — Query: path, raw


Skills (18 Endpunkte)

Projekt-Skills

Methode Pfad Beschreibung
GET /projects/:name/skills Liste der Projekt-Skills. Gibt globale (owner_project=NULL) + Skills dieses Projekts (owner_project=name) zurück. Fremde Projekt-Skills werden nicht eingeschlossen (#157).
POST /projects/:name/skills Skill erstellen. Wird mit owner_project=name gespeichert, nur für dieses Projekt sichtbar.
PUT /projects/:name/skills/:id Skill aktualisieren
DELETE /projects/:name/skills/:id Skill löschen

#210 (2026-05-26): Die DB (skills_global) ist jetzt der SSOT-Writer. UI-Saves gehen zuerst in die DB; .claude/skills/<name>/SKILL.md wird als Artefakt durchgeschrieben, damit die Claude Code CLI Skills automatisch entdeckt. Legacy-Writes nach skills/<name>.md wurden entfernt — bestehende Dateien werden weder gelesen noch gepflegt. Migrations-Helfer: scripts/migrate-skills-to-db.ts.

Globaler Marketplace

Methode Pfad Beschreibung
GET /skills Liste der globalen Skills
POST /skills Skill veröffentlichen
GET /skills/:id Skill-Details
PUT /skills/:id Skill aktualisieren
DELETE /skills/:id Skill löschen

Evolution und Updates

Methode Pfad Beschreibung
GET /skills/:id/evolution Evolutionshistorie eines Skills
GET /skill-updates Liste verfügbarer Updates
POST /skill-updates/:id/approve Update annehmen
POST /skill-updates/:id/reject Update ablehnen

Skill-Forks

Methode Pfad Beschreibung
GET /projects/:name/skill-forks Liste der Forks
POST /projects/:name/skill-forks Fork erstellen
PUT /projects/:name/skill-forks/:id Fork aktualisieren
DELETE /projects/:name/skill-forks/:id Fork löschen

Chat und Nachrichten

Methode Pfad Beschreibung
POST /projects/:name/chat Nachricht in den Chat senden
GET /projects/:name/chat/history Chatverlauf
POST /projects/:name/message Nachricht an einen Worker senden (Phase 48.6: weckt automatisch einen im Idle getöteten Worker auf, ~2-4s Cold Start; Phase 48.6.1: Wake-Up funktioniert jetzt auch in Single-Mode-Projekten, nicht nur im Parallel-Modus)
GET /projects/:name/pins Liste der Notizen (Pins)
POST /projects/:name/pins Notiz erstellen
DELETE /projects/:name/pins/:id Notiz löschen

Wiki (4 Endpunkte)

Methode Pfad Beschreibung
GET /projects/:name/wiki/tree Wiki-Seitenbaum
GET /projects/:name/wiki/file Wiki-Seite lesen
PUT /projects/:name/wiki/save Wiki-Seite speichern. Phase 71.5: feuert syncWiki → Re-Embed über Cohere (fire-and-forget; Fehler werden geloggt, der Write schlägt nicht fehl).
GET /projects/:name/wiki/download Wiki als ZIP-Archiv herunterladen

Analytik (4 Endpunkte)

Methode Pfad Beschreibung
GET /analytics/activity Aktivitäts-Feed
GET /analytics/sidebar Daten für die Seitenleiste
GET /analytics/phases Liste der Projektphasen
POST /analytics/phases Projektphasen aktualisieren

Marketplace und Sage (8 Endpunkte)

Methode Pfad Beschreibung
GET /sage/scout/categories Marketplace-Kategorien
POST /sage/scout Skills suchen
POST /sage/scout/quick-scan Schnell-Scan
POST /sage/scout/analyze Tiefenanalyse eines Skills
POST /sage/scout/install Skill installieren
POST /sage/analyze Sage-Analyse
GET /sage/status Sage-Dienststatus
POST /sage/benchmark Benchmark starten

Speicher und Knowledge

Methode Pfad Beschreibung
GET /projects/:name/rag/search?q=...&k=6&include_global=true&doc_types=wiki,issue,skill,transcript Phase 71.7 (#364): semantische Suche über embeddings + embeddings_vec (Cohere + sqlite-vec). Parameter: q (Anfragetext), k (1-25, Standard 6), include_global (Standard true — Merge mit dem _global_-Skill-Namespace), doc_types (Teilmenge, kommasepariert; Phase 73.6 zusätzlicher Typ: transcript). Response: { query, project, hits: [{rank, doc_type, doc_id, chunk_ix, distance, scope: 'project'|'global', text}] }. Treibt arc kb search + das Chat-Tool ask_notebooklm an. Architektur: rag-architecture.md.
POST /projects/:name/memory/refresh Phase 71.8 (#365): Re-Embed von MANIFEST + ROADMAP + Schlüsseldateien in den RAG-Store (früher — Sync zu NotebookLM). Derselbe Endpoint, neue Semantik.
POST /projects/:name/memory/fetch-artifact Entfernt in Phase 71.8 (Audio Overview hat kein RAG-Äquivalent) — gibt 410 Gone zurück.
GET /projects/:name/learnings Liste der Learnings
POST /projects/:name/learnings Learning hinzufügen
GET /projects/:name/knowledge-graph Wissensgraph des Projekts

Dokumentation (global, ohne Auth)

Methode Pfad Beschreibung
GET /docs/tree?lang=<lang> Dokumentationsbaum; lang optional (en/uk), Standard en
GET /docs/file?path=<p>&lang=<lang> Dokumentationsdatei mit Language-Fallback lesen

GET /docs/tree — Query: lang (optional)

GET /docs/file — Query: path (erforderlich), lang (optional)


System

Methode Pfad Beschreibung
GET /system/configs Systemkonfigurationen abrufen
PUT /system/configs Systemkonfigurationen aktualisieren

Fehlercodes

Code Bedeutung
200 Erfolg
201 Erstellt
400 Ungültige Anfrage
401 Nicht autorisiert
403 Verboten (Multi-Tenancy)
404 Nicht gefunden
409 Konflikt (Duplikat)
429 Zu viele Anfragen
500 Serverfehler

GitHub Integration (Phase 49.3)

Endpoint Method Beschreibung
/api/crm/projects/:name/github GET Liste der mit dem Projekt verknüpften GitHub-Repos
/api/crm/projects/:name/github POST Repo verknüpfen (Body: {owner, repo}) — gibt Webhook-URL + Secret + Setup-Anweisungen zurück
/api/crm/projects/:name/github/:id DELETE Repo-Verknüpfung aufheben
/api/crm/projects/:name/github/events GET Liste der letzten GitHub-Events (Phase 49.3.1, Query: ?limit=50)
/api/webhooks/github POST Öffentlicher Webhook-Receiver (HMAC-SHA256-validiert, Rate-Limit 100/min)

Unterstützte Events: push, pull_request, workflow_run, issues. Benachrichtigungen werden an den Telegram-Account des Projekteigentümers weitergeleitet.

Account Security (Phase 45.4)

Endpoint Method Beschreibung
/api/crm/account/recovery GET Liste der aktiven Recovery Keys
/api/crm/account/recovery POST Recovery Key erstellen (Body: encryptedKey, keyHint)
/api/crm/account/recovery DELETE Recovery Key(s) widerrufen (Body: { id } oder {} für alle)
/api/crm/account/recovery/restore GET Verschlüsselten Master Key zur Wiederherstellung abrufen

Sicherheit


Phase 53.13 — Type-Safety-Baseline (2026-05-10)

Keine Verhaltensänderung der Endpunkte — nur interne Typen. tsc --noEmit blockiert jetzt Push/CI:


Sentinel Pentest Remediation (2026-06-10, #433–#444)

White-Box-Pentest-Sprint — Verhaltensänderungen der Endpunkte nach dem Fix von 3×P1 + 4×P2 + 3×P3:


Phase 53.15 — Sentinel Sprint 1 (2026-05-10)

Verhaltensänderungen bei Auth- und Admin-Endpunkten (Sentinel Audit P0-Fixes):


Phase 53.21 — Sentinel P2 Batch 2 (2026-05-12)

Phase 53.18 — tmux Secret-Leak-Fix (2026-05-11)

Keine Verhaltensänderung der Endpunkte — nur Refactoring interner Spawn-Pfade.

Phase 53.16 — Sentinel Sprint 2 (2026-05-10)

Verhaltensänderungen der Endpunkte nach Hardening von 13 × P1:

Phase 55 — Cosmic Editorial Login (2026-05-13)

Neue Endpunkte für Magic-Link-Sign-in:

EphemeralTokenType-Union erweitert: enthält nun "magic_link" neben den bestehenden Typen oauth_state / password_reset / email_verification / tfa_challenge.

Das Frontend (CosmicCard.jsx) verarbeitet den magic-State (60-s-Resend-Countdown) und den ?magic_token=-URL-Parameter (Auto-Consume → Login → Erfolgsanimation).

Phase 56 — AI Interop / Project Context Export (2026-05-13)

Nur-für-Eigentümer-Export eines sanitizierten Projekt-Snapshots als .md zur Übergabe an externe KI-Systeme (Gemini / ChatGPT / Perplexity / Claude.ai).

Alert: Wenn der Eigentümer innerhalb von 24h mehr als 3 Exporte durchführt UND prefs.notify_on_export = true (Standard OFF) — geht logActivity("export_alert", ...) über die bestehende Phase-53.10-TG-Notify-Pipeline (alertFired: true im Response-Body).

Multi-Tier-Scanner (shared/secret-scanner.ts) — Tier 1 Regex (PATTERN_REGISTRY aus PII-Sanitizer), Tier 2 Shannon-Entropie ≥4,5 Bits/Zeichen auf ≥20-Zeichen-Runs, Tier 3 Kontext-Heuristiken (key=/token:/secret=/password=). Whitelist: UUID / Git SHA / SHA-256 / wiederholte Zeichen / kurzes Hex / Low-Entropy-Base58. Severity-Tiers (critical/high/medium/low). Performance: <500 ms / 1 MB.

DB-Migration 024 — Tabellen export_audit_log + export_preferences.


Phase 57 — Platform Einstellungen (Sentinel #103 Follow-up, 2026-05-15)

Super-Admin-Secret-Management über das CRM-UI statt SSH/edit-.env/paste-in-chat. Backend-MVP (Stage 1 von 4 Stages). Alle Endpunkte sind durch requireAdmin gesichert (Phase 53.15) — gibt 403 Forbidden — admin only für Nicht-Admins zurück, 401 Unauthorized ohne JWT.

Strikte Ausschlussliste NEVER_EXPOSE: CRM_SECRET (JWT-Signing) + SECRET_ENCRYPTION_KEY (Vault-Meta-Key) — selbst eine Admin-Anfrage mit gültigem Token gibt 400 "not managed" zurück. Audit-Log append-only (kein UPDATE/DELETE-Handler), jede Aktion (inkl. fehlgeschlagene) schreibt eine Zeile mit IP + UA + E-Mail.

DB-Migration 026 — Tabelle platform_audit_log. Stage 2 (Frontend PlatformSettings.jsx) — shipped 2026-05-15 (cbc8bac): Admin-only Card-Grid + Rotate-Modal (<input type="password"> + Retype-Confirm) + Audit-Drawer; Sidebar-Eintrag gefiltert nach userRole === "admin", abgerufen von /api/auth/me.

Polish (2026-05-15, Commit 56191b0) — Platform-Einstellungen-UI-Restrukturierung. GET /api/crm/platform/settings-Response-Items erhalten 5 neue Felder: category (anthropic|oauth|telegram|email), usedIn (string[] — Dateien/Flows, die den Key verwenden), getFromUrl (wo ein frischer Wert abgerufen werden kann), effectAfterRotate, riskIfLeaked. Vom Frontend verwendet, um 4 sektionierte Card-Gruppen + kollapsierbare Hilfe-Panels pro Card mit strukturiertem Kontext zu rendern (Used in / Get from / Effect / Risk). Keine Verhaltensänderung bei den Mutator-Endpunkten (PUT/POST/restart/test).

Refactor (2026-05-16)shared/routes/platform.ts internes Cleanup. 39 Zeilen entfernt (16 hinzugefügt), keine Änderung der öffentlichen API-Oberfläche. PUT/POST/restart/test/audit-Endpunkt-Signaturen und Responses unverändert. Hier dokumentiert, da der Doc-Coverage-Pre-Push-Gate bei jedem shared/routes/*.ts-Diff auslöst.

Rückdatierte Aktivität (#117, 2026-05-16)POST /api/mcp/issues/:project/:id/log akzeptiert nun das optionale Feld ts (ISO-8601-String). Wird von arc retro-Rekonstruktion verwendet, damit historische Einträge mit ihren ursprünglichen Zeitstempeln landen. Zukünftig datierte Werte werden innerhalb von addActivity() stillschweigend auf jetzt gedeckelt (Schutz vor Versehen). Ungültiges ISO → 400.

Stage 3 (2026-05-15) — Hot-Reload von OAuth- und Resend-Secrets ohne Neustart. shared/auth.ts loadOAuthConfig() liest jetzt getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") pro Aufruf statt process.env. Callsites in master-bot/routes/auth.ts riefen bereits getOAuthConfig() pro Request auf → 0 Callsite-Änderungen. RESEND_API_KEY ist bereits Hot-Reload über shared/email.ts:47. Verhaltensänderung: PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} tritt jetzt mit dem nächsten Request in Kraft, erfordert keinen Neustart. restartTargets für diese 5 Keys ist leer → Restart-Schaltfläche im UI ausgeblendet. Edge Case: Ein OAuth-Flow mit einem vor der Rotation ausgestellten State-Token kann beim Code-Exchange beim Callback eine 400 erhalten — User-Retry löst das. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN erfordern weiterhin einen Neustart (werden beim Child-Bot-Spawn / TG-Long-Poll-Init gelesen).

Phase 57.3.5 Cleanup (2026-05-16)MANAGED_KEYS-Allowlist von 9 auf 6 reduziert. Entfernt: ANTHROPIC_API_KEY (Operatoren verwenden jetzt den einheitlichen PLATFORM_ANTHROPIC_KEY für Trial-Credits und Platform-Inferenz; .env-Fallback funktioniert weiterhin für Legacy-Code-Pfade, bis Sage/Karpathy migrieren), CITADEL_BOT_TOKEN (Per-Projekt-Bot gehört zu child:<name>:token-Vault-Einträgen, verwaltet durch den Worker-Onboarding-Flow — nicht durch Platform Einstellungen). MASTER_BOT_TOKEN umbenannt: Label → "Telegram — System Monitor Bot", Beschreibung → "Server-Health-Alerts + On-Demand-Status-Probes (nur Admin, kein Chat-Bot)". Phase 58 wird die Monitoring-Schleife hinzufügen (Push-Alerts für Worker-Crash / Disk / RAM / SSH-Brute-Force / CF-Bypass + /status, /health, /errors, /restart-Befehle). Finales Set: PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).

Phase 63 — UI/UX Konsolidierung + Token-Usage-Tracking (2026-05-21, #148)

Neuer Endpunkt:

Änderungen in claude-runner.ts:

UI-Änderungen (kein API):

Arc Help (Phase 61 / #147)

History (Phase 61 / #153):

GDPR / Compliance (Sprint 1+2, #161–#174, 2026-05-22)

Right to Erasure — DELETE /api/auth/account (#162)

Löscht den authentifizierten User und alle seine Daten endgültig (DSGVO Art. 17).

Password Version / Token Invalidation (#174)

Migration 035 fügt password_version INTEGER NOT NULL DEFAULT 0 zu users hinzu. Bei Passwortänderung wird password_version inkrementiert. Das JWT-Payload enthält das Feld pv. crmAuthMiddleware validiert pv bei jeder Anfrage gegen die DB und weist Tokens zurück, die vor der letzten Passwortänderung ausgestellt wurden (401 "Token invalidated — please log in again"). Fails open, wenn die DB nicht erreichbar ist.

Data Retention Cron (#168)

Der Master-Bot führt einen täglichen Purge beim Start + alle 24h aus. Aufbewahrungsfristen: chat_messages 180 Tage (nach timestamp), activity_log 365 Tage (nach created_at), auth_events 90 Tage (nach ts), token_usage_log 730 Tage (nach created_at unixepoch), export_audit_log 365 Tage (nach exported_at). Nicht-fatal — die Löschung blockiert den Start nicht.

Email Compliance (#167)

Alle ausgehenden Transaktions-E-Mails (Passwort-Reset, Verifizierung, Magic-Link) enthalten jetzt:

Security — HIBP Breached Password Check (#171)

Bei POST /api/auth/register und POST /api/auth/reset-password wird das übermittelte Passwort vor dem Speichern gegen die HaveIBeenPwned-k-Anonymity-API geprüft. Nur die ersten 5 Hex-Zeichen des SHA-1-Hashes werden an HIBP gesendet — das vollständige Passwort verlässt den Server nie. Erscheint das Passwort in einer Breach-Datenbank mit count > 0, wird die Anfrage mit HTTP 400 abgewiesen: "This password was found in a known data breach. Please choose a different password." Fails open bei HIBP-Timeout/-Fehler (4s Timeout) — ein ausgefallenes HIBP blockiert die Registrierung nicht.

Data Portability — GET /api/auth/export (#163)

DSGVO Art. 20 — Recht auf Datenübertragbarkeit. Gibt eine strukturierte JSON-Datei mit allen personenbezogenen Daten zurück, die Arc OS über den authentifizierten User hält.

Arc Help — Hardened System Prompt + Anti-Injection (#151)

Verhaltensänderungen bei POST /api/crm/help/chat (keine Änderung der API-Oberfläche):

Worker Discipline Hardening (#187, #188, #189, 2026-05-23)

Issue Status Expansion (#187)

PUT /api/mcp/issues/:project/:id akzeptiert jetzt erweiterte Statuswerte:

Status Bedeutung
open Noch nicht begonnen
in_progress Wird aktiv bearbeitet (gesetzt durch arc issue take)
blocked Wartet auf externe Abhängigkeit
deferred Aufgeschoben (war zuvor nur als Text gespeichert)
closed Erledigt

Neues assignee-Feld: Issues haben jetzt assignee: string | null. Gesetzt über arc issue take <id> oder --assignee <worker_id> in arc issue update.

Migration 036: ALTER TABLE project_issues ADD COLUMN assignee TEXT (nullable, automatisch beim Serverstart angewendet).

arc issue take <id> CLI Command (#187)

Shortcut zum Übernehmen eines Issues: setzt assignee = current_worker_id, status = in_progress, loggt die Aktivität, schreibt den Session-State. Entspricht:

arc issue update <id> --status in_progress --assignee developer
arc issue log <id> "Taken by developer — status set to in_progress"

commit-msg Hook Validation (#187)

.githooks/commit-msg validiert referenzierte #N-Issues jetzt gegen das lokale issues/issues.json:

PROJECT_MANIFEST.md Bridge Injection (#188)

handleCliInit (shared/cli-routes.ts) liest jetzt PROJECT_MANIFEST.md aus dem Projekt-Root und injiziert es in den CITADEL-Block unter ## Project Context. Limit: 8000 Zeichen. Damit erhalten Bridge-Worker (die über arc auf Client-Maschinen laufen) Zugriff auf kompakte Architektur, Security-Patterns, Dateistruktur und Schlüssel-Learnings aus dem vollständigen CLAUDE.md.

Platzierung: nach PROJECT_RULES.md, vor der Skills-Liste.

context_assets Worker Config Field (#189)

Die Worker-Config in workers_registry.json unterstützt das optionale Feld context_assets: string[] — eine Liste von Skill-Namen, die automatisch in jede Bridge-Session dieses Workers injiziert werden (ohne arc skill <name>):

{
  "id": "developer",
  "context_assets": ["crm-api-reference", "archivist_system"]
}

Jeder Skill-Inhalt wird unter ### Auto-Loaded Skills → #### Skill: <name> injiziert, gekürzt auf je 3000 Zeichen.

Phase 62 — Voice Input (#373, 2026-06-05)

Echtzeit-Sprachtranskription, geproxied über den self-hosted whisper.cpp-Server (arc-whisper.service, Port 19214, ggml-base-Modell vorab geladen).

POST /api/crm/voice/transcribe (#373, Phase 62.4)

Transkribiert kurze Sprachclips (Chat-Diktat). Proxied das Audio an den lokalen whisper-server und gibt Text zurück.

Auth: Bearer-Token (oder ?token= Query).

Body: multipart/form-data

Feld Typ Hinweise
audio Blob webm / ogg / wav. Max 25 MB.
locale string BCP-47, z. B. uk-UA, en-US. Wird als language-Param an whisper übergeben.

Response 200:

{ "transcript": "Що ти зробив вчора?" }

Fehlercodes:

Code Bedeutung
400 Feld audio oder locale fehlt
413 Audio über 25 MB
429 Tagesquote erreicht (60 Min/User/Tag) ODER Server ausgelastet (max 2 parallele Transkriptionen)
502 whisper-server gab non-200 zurück
500 Unerwarteter Fehler

Rate-Limit: voice_usage_log (Migration 051) trackt ungefähre Sekunden pro (User, Tag) anhand der Upload-Bytegröße als Proxy (angenommen ~32 kbps Voice-Codec, ±30% Genauigkeit). Hartes Limit: 3600 s / Tag. Anfragen, die das Limit überschreiten würden, geben 429 zurück, bevor sie an whisper weitergeleitet werden.

Architektur-Hinweis: whisper läuft nur auf Contabo (nicht in den per-User-Hetzner-Containern). Audio-Bytes verlassen Contabo nie; der resultierende Text ist das, was das Phase-70-Cloud-Chat-Routing sieht. arc-whisper.service hält das ggml-base-Modell vorab geladen, sodass die Kosten pro Aufruf reine Inferenz sind (~3,4 s warm für 11 s Audio, 3,1× Realtime auf der aktuellen 6-vCPU-EPYC-Box).


Phase 73 — Meeting Transcription + Analysis (#377-#384, 2026-06-05)

Meeting-Audio/-Video in ein Projekt hochladen, whisper-Transkription + Claude-Summary erhalten, optional ins RAG eingebettet. Alle Routen sind durch canAccessProject gesichert (Owner oder Admin).

POST /api/crm/projects/:name/transcripts/upload (#377, Phase 73.1)

Multipart-Upload, gibt 202 mit transcript_id + job_id + status:'queued' zurück. Der Job wird von der In-Process-Queue abgeholt (max 1 parallel).

Body-Felder:

Limits: max 1 GB Upload, MIME-Allowlist (mp3/wav/m4a/aac/ogg/opus/flac + mp4/mov/webm/mkv).

Fehler: 400 (fehlendes Feld / falsches MIME), 401, 413 (über dem Limit), 500 (Disk-Write).

GET /api/crm/projects/:name/transcripts (#379, Phase 73.3)

Listet die Transkripte des Projekts, cursor-paginiert. Query: ?limit=20&cursor=<id>. Gibt {items: TranscriptSummary[], next_cursor: number|null} zurück.

GET /api/crm/projects/:name/transcripts/:id (#379)

Vollständige Zeile inklusive transcript_text, summary_json (als Objekt geparst) und frames_json (geparst seit Phase 73.4).

GET /api/crm/projects/:name/transcripts/job/:jobId/progress (#379)

SSE-Stream des Job-Fortschritts. Pusht event: progress mit {status, progress_pct, step_label, error} bei jeder Feldänderung, plus : keep-alive-Kommentar-Heartbeats jede Sekunde, damit Buns 10s-idleTimeout lange whisper-Läufe nicht abbricht. Schließt mit event: end, sobald der Status terminal ist.

Auth: Browser-EventSource hängt ?token=<bearer> an (kann keinen Authorization-Header setzen).

Terminale Statuses: done (nach Phase-73.6-RAG-Embed + Datei-Cleanup), failed. Hinweis: summarized ist ein transienter Schritt — der SSE-Stream bleibt durch embeddingdone offen. Der Send-Button im Frontend wird bei summarized freigeschaltet (wartet nicht auf RAG).

State machine (Phases 73.1-73.6)

queued
  → extracting_audio    (ffmpeg → 16 kHz mono WAV)
  → transcribing        (whisper-cli -t 4)
  → (video) extracting_frames → frames_extracted   (ffmpeg scene-change)
            → vision_analyzing → vision_analyzed   (Phase 73.4 Claude vision per frame)
  → (audio) transcribed
  → summarizing         (Claude Sonnet → summary_json, receives vision frames as context)
  → summarized
  → embedding           (Phase 73.6 Cohere upsert via shared/rag.ts, skipped if embed_to_rag=0)
  → done                (source file + frames dir deleted — CEO decision D4)

Vision frames JSON shape (Phase 73.4, #380)

Gespeichert als JSON-String in transcripts.frames_json (von GET /transcripts/:id wieder zum Objekt geparst).

[
  { "ts_ms": 3000, "description": "Slide titled 'Q3 Revenue' with bar chart showing 30% growth." },
  { "ts_ms": 6000, "description": "Architecture diagram with three boxes labeled API/Worker/DB." }
]

Hartes Limit MAX_FRAMES=50 pro Transkript (~$0.15 Worst Case bei typischem Sonnet-Vision-Pricing). Frames über dem Limit werden stillschweigend verworfen, die letzte behaltene Beschreibung erhält den Suffix [+N more frames dropped]. Fehler pro Frame werden zu [vision failed: <msg>]-Strings — sie brechen den Durchlauf nicht ab. Frames mit der Beschreibung "No informational content" sind reine Webcam- oder Deko-Frames.

Summary JSON shape (Phase 73.5, #381)

Gespeichert als JSON-String in transcripts.summary_json. Von GET /transcripts/:id wieder zum Objekt geparst.

{
  "tldr": "1-2 sentence executive summary",
  "key_points": ["..."],
  "action_items": [{"task": "...", "owner": "name or null"}],
  "decisions": ["..."],
  "topics": ["..."],
  "model": "claude-sonnet-4-5",
  "generated_at": "2026-06-05T20:04:15.573Z"
}

Die Anthropic-Key-Auflösung spiegelt shared/worker-spawn.ts: BYOK account_settings.anthropic_key (entschlüsselt, falls verschlüsselt), Fallback PLATFORM_ANTHROPIC_KEY für Trial-Mode-Owner. Summary-Fehler sind nicht-fatal — transcript_text bleibt intakt, der Status rollt auf transcribed/frames_extracted zurück, sodass der User es nach dem Beheben seines Keys erneut versuchen kann.

Phase 78 — Notes: Knowledge Collections (#394–#404, 2026-06-08)

Notes pro Projekt im NotebookLM-Stil. Jede Note ist eine Sammlung von Quellen (Video, Audio, YouTube, Web, PDF, DOCX, TXT, Bild) mit gemeinsamem RAG-Index und Chat.

GET /api/crm/projects/:name/notes

Gibt alle Notes des Projekts zurück. Auth erforderlich + canAccessProject.

Response 200:

[{ "id": 1, "title": "Sprint planning", "description": null, "created_at": "...", "source_count": 3 }]

POST /api/crm/projects/:name/notes

Neue Note erstellen.

Body: { "title": "string", "description": "string?" }
Response 201: { "id": 1, "title": "Sprint planning" }

GET /api/crm/projects/:name/notes/:id

Note-Detail mit Quellen, Issue-Verknüpfungen und Chat-Historie abrufen.

Response 200:

{
  "id": 1, "title": "Sprint planning",
  "sources": [{ "id": 1, "source_type": "youtube", "title": "My video", "url": "...", "status": "done", "duration_seconds": 3600 }],
  "issue_links": [{ "issue_id": 42, "title": "Issue title" }],
  "chats": [{ "role": "user", "content": "Summarize", "created_at": "..." }]
}

DELETE /api/crm/projects/:name/notes/:id

Note und alle Quellen/Chats löschen. Kaskadiert auf note_sources, note_chats, note_issue_links.

POST /api/crm/projects/:name/notes/:id/sources

Eine Quelle hinzufügen (Datei-Upload oder URL).

Content-Type: multipart/form-data ODER application/json

Response 201: { "source_id": 5, "status": "queued" }

Die Verarbeitung ist asynchron. Polle GET /notes/:id, bis source.status === "done".

PATCH /api/crm/projects/:name/notes/:id/sources/:sourceId

Eine Quelle umbenennen (Inline-Titel-Bearbeitung).

Body: { "title": "New name" }
Response 200: {}
Leerer String oder null setzt auf den Dateinamen/URL-Standard zurück.

DELETE /api/crm/projects/:name/notes/:id/sources/:sourceId

Eine Quelle und ihren Inhalt entfernen.

GET /api/crm/projects/:name/notes/:id/sources/:sourceId/progress

SSE-Stream des Verarbeitungsfortschritts der Quelle.

Events: progress { "status": "processing"|"done"|"error", "message": "..." }

POST /api/crm/projects/:name/notes/:id/chat

Nachricht an den Chat der Note senden. SSE-Stream-Response.

Body:

{
  "message": "Summarize all sources",
  "selectedSourceIds": [1, 3]
}

selectedSourceIds ist optional — weglassen, um alle Quellen einzuschließen.

SSE-Events:

RAG-Strategie: sqlite-vec-Suche auf note_source-Embeddings → Fallback auf direkte content_text-Injektion (max 80 K Zeichen), wenn die Vektorsuche nicht verfügbar ist oder keine Treffer liefert. Ein Anti-Halluzinations-System-Prompt-Guard wird injiziert, wenn unverarbeitete Quellen enthalten sind.

Tool Use — create_issue: Claude kann aus dem Chat heraus Projekt-Issues erstellen. Multi-Turn: Turn 1 streamt bis zum Tool-Call, das Backend führt aus (issueQueries.nextId + issueQueries.insert), Turn 2 setzt das Streaming mit injiziertem Tool-Result fort.

Source status state machine

queued → processing → done
                    ↘ error

Werte des status-Felds der Quelle:

YouTube transcript strategy (Phase 78.3)

  1. youtube-transcript npm: Sprach-Kaskade ["en", "en-US", "en-GB"] → Fallback beliebig
  2. Supadata.ai API: GET https://api.supadata.ai/v1/youtube/transcript?url=...&text=true&lang=en → Fallback ohne lang-Param
  3. yt-dlp + Whisper: letzter Fallback für Videos ohne Untertitel

Priorität: englische Untertitel bevorzugen, um auto-übersetzte arabische/anderssprachige Transkripte zu vermeiden.