Vierte Etappe. Der Client-Zustand hatte bisher keinen Ort: zwei handgebaute
useSyncExternalStore-Speicher, drei CustomEvent-Kanaele und ein localStorage-Griff
mitten in einem useState — verteilt ueber fuenf Dateien.
## Ein Store (Befund B-08/B-09)
app/store.ts (Zustand, 1,2 kB gzip) haelt genau vier Dinge: Bedienvorlieben
(Schiene, Expertenmodus, Palette), die Lage des Ereignisstroms und die
Meldungs-Warteschlange. Der Kopfkommentar nennt die Regel, nach der etwas dort
landen darf — und ein Test prueft sie: gespeichert werden AUSSCHLIESSLICH
Schienen- und Expertenmodus-Zustand, kein Strom, keine Meldungen, kein Geheimnis.
Der Metrik-Verlauf bleibt bewusst DRAUSSEN (lib/metricsStore.ts): Er nimmt ab P4
jede Sekunde einen Messpunkt entgegen, ein Store-Update pro Sekunde wuerde jede
abonnierende Komponente neu rendern.
Die drei CustomEvent-Kanaele sind ERSATZLOS weg — `grep -r dispatchEvent src`
liefert nichts mehr. Wer die Schublade oeffnen will, setzt `?system=`; wer springen
will, navigiert. Auch der letzte Rest von P2 (`mc-palette-open`) ist aufgeloest.
## Statusleiste (Spezifikation §4.1)
Der Zustand der Box stand bisher verstreut: die Ampel unten in der Sidebar, die
Auslastung nur im Cockpit, das aktive Modell nur im Modell-Manager. Wer in der
Konsole arbeitete, sah gar nichts.
Jetzt eine feste Leiste ueber allen Ansichten: Motor/Hirn-Ampel (klickbar zum
Agenten) · aktive Rolle + Durchsatz mit Sparkline · geteilter Speicher ALS BALKEN
(nicht als Prozent — 128 GB Unified Memory ist die Kernzahl dieser Box) · CPU/GPU
· Temperatur (ab 85 °C bernstein) · Betriebszeit · Token-Summe · Live-Anzeige.
Die Sparklines sind eigenes SVG, ~20 Zeilen. Fuer eine 56-Pixel-Linie waere eine
Diagramm-Bibliothek Verschwendung.
Dafuer neu im Backend: `uptime_s` in /api/system/status (psutil.boot_time).
Gehoert dorthin, weil die Box sich woechentlich selbst neu startet — dann ist
"laeuft seit 20 Minuten" die Antwort auf eine ganze Klasse von Fragen.
Liefert psutil nichts, steht dort None und die Leiste blendet das Feld aus,
statt eine Zahl zu erfinden.
## aria-live (Befund B-11) — und diesmal mit Absender
Im ganzen Frontend gab es KEIN einziges aria-live. app/shell/Meldungen.tsx ist
jetzt die eine Stelle fuer beilaeufige Rueckmeldungen; Fehler bekommen
`assertive` und bleiben stehen, alles andere `polite` und raeumt sich weg.
Wichtig: Die Region ist keine Attrappe. Gespeist wird sie von den Uebergaengen des
Ereignisstroms und vom Konsolen-Neustart (mit dem echten Grund aus ApiError.detail).
Der erste Verbindungsaufbau wird bewusst NICHT gemeldet — sonst begruesst jede
Seite den Nutzer mit "Verbindung wieder da".
## Ehrliche Anzeige statt stillem Altern (Befund B-15/B-18)
lib/events.ts fuehrt die Stromlage jetzt im Store: live · nachlauf · getrennt.
Beim Wechsel wird EINMAL invalidiert — vorher las relax() den Zustand erst beim
naechsten Refetch, nach einem Riss blieb die UI bis zu 150 s im langsamen Modus.
## Bahnbreiten-Deckel (Befund B-10)
Die Arbeitsflaeche endet bei 1600 px. Ohne Deckel zog sich das Cockpit auf einem
3440-px-Ultrawide auf ueber 3 000 px, waehrend die Wurzel-Schriftgroesse mitwuchs
und die Zeilen GROESSER statt lesbarer machte.
## Nebenbei
Der Buendel-Budget-Check aus P0 war fragil: `ls dist/assets/index-*.js | head -1`
kann den falschen treffen, weil Rollup auch kleine geteilte Module "index-*.js"
nennt (gemessen: ein 67-Byte-Chunk neben dem 374-kB-Einstieg). Er waere dann still
immer gruen gewesen. Beide Ampel-Dateien lesen den Einstieg jetzt aus index.html.
## Verifiziert im Browser
Statusleiste: alle Felder mit Live-Daten, "Laeuft seit 1 Std 10 min"
Backend abgewuergt -> Leiste springt auf "Nachlauf" UND die aria-live-Region
meldet "Verbindung zur Zentrale verloren" (polite)
Backend zurueck -> "Live", ohne Begruessungs-Meldung
Cockpit-Kachel -> /cockpit?system=logs, Schublade offen, Reiter "System-Logs"
Arbeitsflaeche -> max-width 1600 px
33/33 Tests gruen (7 neue fuer den Store) · ESLint 0 Fehler · tsc sauber ·
Einstieg 118 153 B gzip / Budget 125 000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
651 lines
19 KiB
TypeScript
651 lines
19 KiB
TypeScript
// 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<string | null> {
|
|
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<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
|
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<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
|
|
/** 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<string, ConnectTool> // 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<string, { in: number; out: number }>
|
|
}
|
|
|
|
// 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<IdeenLogResp>(`/api/ideen/${id}/log`)
|