Files
mission-control-v2/frontend/src/lib/api.ts
T
Hitonabi 0c69c491e5 Single Source of Truth: Frontend/Backend-Ungereimtheiten ausgeraeumt (Review 15.07.)
Backend (Wahrheit reparieren):
- gateway_reachable() prueft jetzt den ECHTEN mc2-gateway (:9010 via V1_UPSTREAM)
  statt nur die Engine - toter Denkpfad war in Health/Cockpit unsichtbar
- /api/system/services vollstaendig: + LLM-Gateway, + Steuerpult, + Waechter
  (mc2-steward via is-active); scope-Feld (user/system); mc2-gateway/mc2-steward
  in die Restart-Allowlist
- Connect-Health Leitung 1 prueft den Client-Pfad (V1_UPSTREAM statt llama-swap)
- agent_status liefert pc_executor_url (UI zeigte hartcodierte IP/Ports)
- SSE-Endlosschleife gefixt: /api/auftragsbuch schrieb announce-branches bei JEDEM
  Aufruf neu -> mtime-Bump -> invalidate -> Refetch im 3-s-Takt je Client;
  jetzt nur noch bei echter Aenderung

Frontend (ein Datenweg):
- SystemDrawer komplett auf React-Query-Hooks (vorher eigenes 3-s-setInterval auf
  dieselben Endpunkte inkl. teurem Updates-Check); Dienste-Liste = Backend-Antwort
  (SERVICES-UI-Kopie geloescht); useDialog statt Eigenbau; fmtBytes statt Duplikat
- useLucyHealth: Dienst-Matching per systemd-unit statt Namensfragment; Gateway-
  Reparatur zielt auf mc2-gateway; neuer Waechter-Check; Backend-tot => ehrliches
  rotes Verdikt statt eingefrorener letzter Stand (auch Sidebar)
- Toter Code raus: views/models/Cockpit.tsx (908 Z.) + RoleAssignModal + Placeholder;
  LaneEditor (Routing-Policy) + WarmSetManager damit ZURUECK im UI als Tab
  "Routing & Warm-Set" im Modell-Manager
- Cockpit: blockierte Ideen-Karten erscheinen in "Braucht dich" + Kachel-Hint;
  Verbinden-Kachel zeigt 3 Leitungen live statt hartem "Zed"
- Maschinenraum: ehrliches "frei" (reale Belegung) + eigenes KV-Cache-Segment
- AgentView: Ports/PC-Adresse aus der API; Guide lehrt Lanes chat/coding
- queries.ts: useModels nutzt SSE-relax; Chronik-Limit im Query-Key;
  useJobs/useZeitmaschine mit enabled-Schalter

Verifiziert: tsc+vite gruen, Backend-Smoke beide Gateway-Modi, UI live gegen die
Box (Cockpit/Werkbank/Routing-Tab/Drawer rendern mit Echtdaten).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 22:27:32 +02:00

645 lines
18 KiB
TypeScript

// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const headers = {
"Content-Type": "application/json",
...init?.headers,
} as Record<string, string>
const method = init?.method?.toUpperCase() || "GET"
// Geheimnisse (Box-Passwort, HF-Token) reisen NUR bei mutierenden Requests mit —
// die Dauer-Polls (GET) brauchen sie nie und sollen sie auch nie im Netz tragen.
const sudoPassword = method === "GET" ? null : localStorage.getItem("mc_sudo_password")
const hfToken = method === "GET" ? null : localStorage.getItem("mc_hf_token")
if (sudoPassword) {
headers["X-Sudo-Password"] = sudoPassword
}
let body = init?.body
if (method === "POST") {
if (typeof body === "string") {
try {
const data = JSON.parse(body)
let changed = false
if (sudoPassword && !("sudo_password" in data)) {
data["sudo_password"] = sudoPassword
changed = true
}
if (hfToken && !("hf_token" in data)) {
data["hf_token"] = hfToken
changed = true
}
if (changed) {
body = JSON.stringify(data)
}
} catch (e) {
// ignore JSON parse errors
}
} else if (!body) {
const data: Record<string, any> = {}
if (sudoPassword) data["sudo_password"] = sudoPassword
if (hfToken) data["hf_token"] = hfToken
if (Object.keys(data).length > 0) {
body = JSON.stringify(data)
}
}
}
const res = await fetch(path, {
...init,
headers,
body,
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return res.json() as Promise<T>
}
// 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<string, GroupSpec>
}
export const getGroups = () => api<GroupsResp>("/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<string, string[]>[]
context_window_fallbacks: Record<string, string[]>[]
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<RoutingPolicyMeta>("/api/routing/policy")
export const updateRoutingPolicy = (patch: Partial<RoutingPolicy>) =>
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
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<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
}
export interface ConnectLine {
ok: boolean
detail: string
}
export interface ConnectHealth {
gateway: ConnectLine
memory: ConnectLine
desktop_gateway: ConnectLine
}
export interface Memory {
id: string
content: string
category: string
source: string
created_at: string
updated_at: string
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
}
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 DedupeResult {
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
duplicate_count: number
removed: number
applied: boolean
}
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 }
}
export interface TokenStats {
prompt_tokens: number
completion_tokens: number
total_tokens: number
saved_usd: number
saved_eur: number
pricing?: Record<string, { in: number; out: number }>
}
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
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 + Mem0 + LLM-TTFT)
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
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
// 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
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 | ""
}
export interface IdeenLogResp {
available: boolean
lines: string[]
}
export interface IdeenResp {
available: boolean
items: IdeenItem[]
offen: number
fehler?: string
}
// Ergebnis einer fertigen Queue-Aufgabe (Abschluss-Zusammenfassung des Workers)
export interface IdeenErgebnis {
available: boolean
summary: string
result: 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[]
}
// ── Eigenleben („Von allein": Skills + Vorschlags-Bilanz der Box) ────────────
export interface EigenlebenSkill {
id: string
name: string
kategorie: string | null
aktiv: boolean | null // null = nicht ermittelbar (Dev-Modus)
beschreibung: string
version: string
autor: string
herkunft: "selbst" | "repo" | "bundled"
herkunft_label: string
genutzt: number
zuletzt_genutzt: string | null
erstellt: string | null
status: string
geaendert: number
}
export interface EigenlebenEintrag {
art: string // "angenommen" | "abgelehnt" | sonstiger Chronik-State
titel: string
detail: string
ts: number
}
export interface EigenlebenResp {
available: boolean
skills: EigenlebenSkill[]
selbst_erstellt: number
bilanz: { eintraege: EigenlebenEintrag[]; angenommen: number; abgelehnt: number }
}
// ── 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<IdeenLogResp>(`/api/ideen/${id}/log`)