// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*). // // v3-Umbau P1 (28.08.2026): Hier wurden frueher zwei Geheimnisse aus dem localStorage // gelesen und an jeden mutierenden Request gehaengt — das Box-Sudo-Passwort (als Header // UND im JSON-Rumpf) und der HuggingFace-Token. Da der Dienst-Nutzer auf der Box mit // `NOPASSWD: ALL` laeuft, war ein XSS in dieser SPA gleichbedeutend mit Root. // Das Sudo-Passwort ist ersatzlos weg (auf der Box gemessen: `sudo -n true` laeuft durch, // es wurde nie gebraucht), der HF-Token liegt jetzt auf der Box (services/geheimnisse.py). // Diese Datei kennt kein Geheimnis mehr — und soll auch nie wieder eines kennen. /** Fehler mit dem, was das Backend WIRKLICH gesagt hat. * * Vorher warf der Helfer `new Error("500 Internal Server Error")` und verwarf den Rumpf. * FastAPI legt den eigentlichen Grund aber in `detail` — der erreichte die Oberflaeche nie, * und der Nutzer las "500" statt "Dienst 'llama-swap' nicht erlaubt". */ export class ApiError extends Error { readonly status: number readonly detail: string | null constructor(status: number, statusText: string, detail: string | null) { super(detail || `${status} ${statusText}`) this.name = "ApiError" this.status = status this.detail = detail } } /** Liest den Fehler-Rumpf, ohne selbst zu scheitern: Ein kaputter oder leerer Rumpf darf * die Fehlermeldung nicht in einen zweiten Fehler verwandeln. */ async function fehlerText(res: Response): Promise { try { const roh = await res.text() if (!roh) return null try { const d = JSON.parse(roh) const detail = d?.detail ?? d?.err ?? d?.message if (typeof detail === "string" && detail.trim()) return detail.trim() if (Array.isArray(detail) && detail.length) { // FastAPI-Validierungsfehler: Liste aus {loc, msg, type} return detail.map((e: any) => e?.msg).filter(Boolean).join("; ") || null } return null } catch { // Kein JSON (HTML-Fehlerseite eines Proxys o. AE.) — erste Zeile reicht als Hinweis. return roh.slice(0, 200).split("\n")[0].trim() || null } } catch { return null } } export async function api(path: string, init?: RequestInit): Promise { const res = await fetch(path, { ...init, headers: { "Content-Type": "application/json", ...init?.headers }, }) if (!res.ok) throw new ApiError(res.status, res.statusText, await fehlerText(res)) return res.json() as Promise } // llama-swap-`groups`: Mitglieder mit swap=false dürfen GLEICHZEITIG resident sein (Ko-Residenz). // Die `brains`-Gruppe hält Hirn (fast) + Augen (vision) gemeinsam warm, statt sich zu verdrängen. export interface GroupSpec { swap: boolean persist: boolean members: string[] } export interface GroupsResp { groups: Record } export const getGroups = () => api("/api/groups") export const setGroup = (group: string, members: string[], swap = false, persist = true) => api("/api/groups", { method: "PUT", body: JSON.stringify({ group, members, swap, persist }) }) export interface Capabilities { moe: boolean active_b: number | null tools: "yes" | "likely" | "no" vision: boolean coder: boolean reasoning: boolean embedding: boolean ctx: number | null params_b: number | null arch: string | null } export interface Fit { level: "perfect" | "marginal" | "too_tight" text: string req_gb: number tps: number } export interface RoleRecModel { name: string current_role: string | null params_b: number quant: string fit: Fit suitable: boolean incomplete: boolean score: number reason: string recommended: boolean } export interface RoleRecResp { role: string recommended: string | null models: RoleRecModel[] } export interface FitResp { params_b: number fit: Fit optimal_ctx: number assigned_ctx: number budget: { gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string } sys_ram_gb: number } export interface ModelInfo { name: string role: string | null aliases: string[] api_ids: string[] ctx: number | null ttl: number | null cmd: string gguf_path: string filename: string quant: string size_bytes: number | null incomplete: boolean prompt_cache: boolean spec_draft_model: string | null spec_type: string | null spec_active: boolean parallel_slots: number capabilities: Capabilities } export interface VocabFingerprint { model: string | null pre: string | null n_vocab: number | null arch: string | null } export interface DraftInfo { path: string filename: string size_bytes: number | null vocab: VocabFingerprint | null compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt) mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft } export interface DraftsResp { target_path: string target_exists: boolean target_vocab: VocabFingerprint | null drafts: DraftInfo[] } export interface DiscoverModel { name: string author: string repo: string role: string params_b: number quant: string tags: string[] downloads: number fit: Fit optimal_ctx: number caps: Capabilities } export interface DiscoverCategory { role: string title: string icon: string models: DiscoverModel[] recommended: string | null } export interface DiscoverResp { updated: number categories: DiscoverCategory[] sys_ram_gb: number } export interface RoutingResp { mode?: string endpoint?: string heavy_threshold_chars?: number routes: { name: string; target: string }[] lanes?: { name: string; target: string; threshold_chars?: number; escalate_chars?: number; aka?: string }[] fallbacks: Record[] context_window_fallbacks: Record[] gateway_reachable: boolean } // UI-editierbare Routing-Policy (GET/PUT /api/routing/policy). Aliase + Zeichen-Schwellen // hinter den Lanes; hot-reload im Backend (kein Restart). export interface RoutingPolicy { fast: string heavy: string coder: string coder_lite: string heavy_chars: number coding_escalate_chars: number fast_no_think: boolean } export interface RoutingPolicyField { key: keyof RoutingPolicy label: string type: "str" | "int" | "bool" min?: number max?: number } export interface RoutingPolicyMeta { policy: RoutingPolicy defaults: RoutingPolicy fields: RoutingPolicyField[] } export const getRoutingPolicy = () => api("/api/routing/policy") export const updateRoutingPolicy = (patch: Partial) => api<{ policy: RoutingPolicy }>("/api/routing/policy", { method: "PUT", body: JSON.stringify(patch) }) export interface GitInfo { hash: string date: string subject: string branch: string dirty: boolean path: string } // Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`. export interface ComponentVersion { type: "git" | "binary" | "unknown" hash?: string date?: string subject?: string branch?: string dirty?: boolean path?: string version_text?: string } export interface Versions { mc2: GitInfo | null engine: ComponentVersion hermes_agent: GitInfo | null } export interface SystemStatus { cpu: { percent: number; cores: number | null } ram: { total: number; used: number; percent: number } gpu: { busy_percent: number | null vram_used: number | null vram_total: number | null gtt_used?: number | null gtt_total?: number | null } | null temp: { cpu?: number; gpu?: number } | null disk: { total: number; used: number; percent: number } | null /** Sekunden seit dem Systemstart (v3-Umbau P3). null, wenn die Box es nicht liefert — * die Statusleiste blendet das Feld dann aus, statt eine Zahl zu erfinden. */ uptime_s?: number | null versions?: Versions } export interface ServiceInfo { name: string unit: string // echter systemd-Name — Basis für Restart/Logs/Matching (kein Namens-Raten) url: string ok: boolean scope: "user" | "system" } export interface ServicesResp { services: ServiceInfo[] links: { engine_ui: string; gateway: string; hermes_ui: string } } export interface ComponentUpdate { key: string // z.B. "hermes_agent" name: string current: string | null latest: string | null update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar) reachable: boolean | null } export interface UpdatesResp { os: number engine: number swap: number models: number model_list: { role: string; title: string; repo: string }[] last_check?: number | null components?: ComponentUpdate[] } export interface UpdateDetails { kind: "os" | "engine" | "swap" | "hermes" error?: string // LLM-Zusammenfassung in Lucys Stimme (hermes/engine/swap; Breaking Changes zuerst) summary?: string // Aktions-Verdikt: muss der Besitzer selbst etwas tun? (null = kein Verdikt/keine Summary) action_needed?: boolean | null action_text?: string // os count?: number packages?: { name: string; current: string; candidate: string }[] // os: vom System zurückgestellte Pakete (Phasen-Rollout / kept back) — ehrlich statt "hängt" held_back?: { name: string; reason: "phasing" | "kept_back" }[] // engine installed_build?: number | null latest_build?: number | null latest_tag?: string | null name?: string | null url?: string | null body?: string | null // hermes branch?: string | null behind?: number commits?: { hash: string; subject: string; when: string }[] } export interface ConnectTool { label: string lang: string snippet: string note: string } export interface ConnectResp { host: string gateway_url: string mc_url: string tools: Record // Leitung 1 — Modell (IDEs/Agenten → Gateway) } export interface ConnectLine { ok: boolean detail: string } export interface ConnectHealth { gateway: ConnectLine memory: ConnectLine // Leitung 2 — Hermes-natives Gedächtnis (nur Status, nichts zu konfigurieren) desktop_gateway: ConnectLine } // Graph-Ansicht des Wissens-Vaults (GET /api/wissen/graph) — trägt aus historischen Gründen // noch „Memory" im Namen; die Daten kommen aus dem Vault, nicht aus einem Gedächtnis-Dienst. export interface MemoryGraphNode { id: string content: string category: string source: string } export interface MemoryGraph { nodes: MemoryGraphNode[] edges: { source: string; target: string; weight: number }[] } export interface AgentStatus { gateway_url: string box_console_url?: string hermes_ui_url?: string gateway_reachable: boolean box_console_reachable?: boolean hermes_ui_reachable?: boolean home_exists: boolean brain_model?: string has_config: boolean has_skills: boolean has_memories: boolean telegram_enabled?: boolean mcp_server_count?: number pc_executor_reachable?: boolean pc_executor_url?: string } export interface Job { id: string label: string group?: string | null // "maintenance" = system-veränderndes Update (Wartungs-Riegel) state: "queued" | "running" | "done" | "failed" | "canceled" progress?: number total_bytes?: number done_bytes?: number rate_bps?: number eta_s?: number } export interface Health { status: string version: string engine_reachable: boolean gateway_reachable: boolean brain?: { role: string; model: string | null; ready: boolean } } // 24-h-Metrik-Verlauf (GET /api/system/history?minutes=N) — Basis der Cockpit-Zeitachse. // tp/tc sind Token-GESAMTZÄHLER; Raten rechnet das Frontend aus den Deltas. export interface HistoryPoint { t: number // Epoch-Sekunden cpu: number | null ram: number | null gpu: number | null disk: number | null tp: number | null tc: number | null } export interface HistoryResp { sample_s: number points: HistoryPoint[] } export interface TokenStats { prompt_tokens: number completion_tokens: number total_tokens: number saved_usd: number saved_eur: number pricing?: Record } // Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt): // stt · vision · hirn · gen. Der Gedächtnis-Abruf läuft seit der Mem0-Ablösung Hermes-intern // (kein Rückruf an MC2) und steckt daher unsichtbar in hirn_ms. export interface VoiceTurn { id: string ts: number // Epoch-Sekunden session: string kind: string had_images: boolean stt_ms: number | null vision_ms: number | null hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Gedächtnis + LLM-TTFT) gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende total_ms: number error: string | null } export interface VoiceTraceResp { turns: VoiceTurn[] } export interface ModelsResp { models: ModelInfo[] running?: string[] } // ── Auftragsbuch (Vorschlags-Inbox) ────────────────────────────────────────── export interface AuftragStatus { state: "laeuft" | "eingespielt" | "fehlgeschlagen" | "rollback" | "zurueckgerollt" | "kritisch" | "abgelehnt" detail: string ts: number } export interface AuftragItem { repo?: "mc2" | "lucy" // Quelle: Box-Stack (mc2) oder Lucy-Desktop (Annahme baut am PC) branch: string kind: string // wartung | orchestrator | doku | feature subject: string body: string author: string ts: number | null ahead: number behind: number shortstat: string files: { status: string; path: string }[] files_truncated: boolean frontend_ohne_build: boolean // Frontend-Quelltext ohne dist → Box kann nicht bauen verwaist?: boolean // kein gemeinsamer Ursprung mit main (kaputt aufgesetzt) → Merge unmöglich leer?: boolean // Diff gegen main ist leer → bringt nichts (schon in main / am Repo vorbei) status: AuftragStatus | null ci_status?: "success" | "failure" | "pending" | "skipped" | null // Empfehlungs-Stempel des nächtlichen Karten-Gutachters (Meinung, kein Gate). empfehlung: { empfehlung: "ANNEHMEN" | "ABLEHNEN" | "UNKLAR"; satz: string; richter: string } | null } export interface SkillKandidat { file: string title: string preview: string mtime: number } export interface AuftragsbuchResp { available: boolean items: AuftragItem[] skill_kandidaten: SkillKandidat[] open_count: number } // ── Ideen-Queue (natives Hermes-Kanban) ────────────────────────────────────── export interface IdeenItem { id: string titel: string body: string status: string // triage | todo | ready | running | blocked | review | done assignee: string | null welt?: string // "box" = Werkstatt baut · "ide" = für Zed vorbereitet art?: string // "box" | "idee" (nur durchdacht) | "projekt" (Repo vorbereitet) erstellt: number | null fertig: number | null von: string | null frage?: string | null // bei status=blocked: die Frage/der Grund des Workers frage_kind?: string | null // needs_input | capability | transient | "" eltern?: string[] // offene Vorgänger-Karten (task_links) — leer = startbereit wartet_auf?: string[] // Titel der offenen Vorgänger (max. 3, für die Anzeige) notiz?: string | null // bei running: jüngste Heartbeat-Notiz des Workers projekt?: string | null // Familien-Schlüssel (verkettete Karten) — null = Einzelkarte prio?: number // Dispatcher-Reihenfolge: priority DESC, created ASC puls_alter?: number | null // bei running: Sekunden seit letztem Lebenszeichen (Hänger-Warnung) gestartet?: number | null // bei running: started_at (Epoch) } // Karten-Familie (verkettete Karten = ein Projekt) für die gruppierte Queue-Sicht export interface IdeenProjekt { key: string titel: string gesamt: number fertig: number laeuft: number haengt: number } export interface IdeenLogResp { available: boolean lines: string[] } export interface IdeenResp { available: boolean items: IdeenItem[] offen: number fehler?: string projekte?: IdeenProjekt[] } // Ergebnis einer fertigen Queue-Aufgabe (Abschluss-Zusammenfassung des Workers) export interface IdeenErgebnis { available: boolean summary: string result: string } // Konzept einer IDE-Karte — aus dem Gitea-Repo oder aus ~/konzepte auf der Box export interface IdeenKonzept { available: boolean repo?: string | null clone_url?: string quelle?: "repo" | "datei" pfad?: string // nur quelle=datei: Pfad auf der Box datei?: string konzept?: string error?: string } // ── Chronik (Timeline aus dem Melde-Briefkasten) ───────────────────────────── export interface ChronikItem { id: number ts: number subject: string text: string source: string priority: "normal" | "silent" } export interface ChronikResp { items: ChronikItem[] } // ── Wissens-Vault (Traum-Notizen, read-only) ───────────────────────────────── export interface WissenFile { path: string name: string dir: string title: string mtime: number neu: boolean } export interface WissenResp { available: boolean files: WissenFile[] } export interface WissenDatei { path: string content: string mtime: number } // ── Zeitmaschine (Snapshots + Restore) ─────────────────────────────────────── export interface Snapshot { snapshot: string file: string size_mb: number } export interface ZeitmaschineResp { available: boolean backups: Snapshot[] } // ── Erinnerungen (Wecker/Routinen) ─────────────────────────────────────────── export interface Reminder { id: number text: string next_ts: number repeat: string // '' einmalig | daily | weekdays | weekly created_ts: number } export interface HermesBrainModel { name: string filename?: string params_b: number | null quant?: string size_bytes?: number | null version?: number | null gguf_path?: string incomplete?: boolean } export interface HermesBrainCandidate { repo: string name: string version: number params_b: number downloads: number fit: Fit } export interface HermesBrainBudget { gtt_gb: number brain_gb: number // Footprint des (empfohlenen) Brains, das immer resident bleibt warm_projected_gb: number // alle persist-Modelle warm (Info) free_after_gb: number // frei nach Brain + größtem on-demand largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy/coder) fits: boolean // passt Brain + größtes on-demand zusammen ins Budget? } export interface HermesBrainResp { current: HermesBrainModel | null recommended: HermesBrainCandidate | null update_available: boolean budget?: HermesBrainBudget | null } // ── Ideen-Log (Live Worker Log) ────────────────────────────────────────────── export const getIdeenLog = (id: string) => api(`/api/ideen/${id}/log`)