diff --git a/CLAUDE.md b/CLAUDE.md index 703c6f1..3341841 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,127 +1,123 @@ # Mission Control Web-Dashboard zur Verwaltung eines **lokalen LLM-Stacks (`llama-swap`)** auf dem Bosgame M5. -FastAPI-Backend + Vanilla-JS-Dashboard. **Leitprinzip: KISS — kein Build-Schritt, kein Frontend-Framework, keine Datenbank.** Concerns sind getrennt (SoC) — aber *ohne* Build: native ES-Module im Frontend, FastAPI-`APIRouter` im Backend. +FastAPI-Backend + Svelte 5-Frontend (Vite-Build). **Leitprinzip: KISS — SoC ohne Overhead.** ## Architektur **Backend** (Top-Level-Helfer + ein Router je Bereich): -- **`app.py`** — dünner Einstieg: `FastAPI`, hängt die Router ein, liefert das statische UI aus, Exception-Handler + eine `Cache-Control: no-cache`-Middleware für `/` und `/static` (UI wirkt sofort nach rsync, kein Stale-JS im Browser). +- **`app.py`** — dünner Einstieg: `FastAPI`, hängt die Router ein, liefert das statische UI aus, Exception-Handler + `Cache-Control: no-cache`-Middleware. - **`config.py`** — alle Env-Vars + die gemeinsame `ruamel.yaml`-Instanz. Auch `SOURCE_DIR`/`PROD_DIR` fürs Self-Update. -- **`auth.py`** — optionale Token-Auth: Header `X-MC-Token` **oder** Query `?token=` (Query ist Pflicht für WebSockets, da Browser dort keine Header senden). -- **`jobengine.py`** — In-Memory-Job-System (Threads + Subprocess) mit Live-Log. `start_job(..., stdin_data=, log_cmd=)`: Secrets (sudo-PW) gehen über **stdin**, die Log-Zeile wird sanitisiert (kein Passwort im Log/`ps`). -- **`llamaswap.py`** — spricht `llama-swap` an (`/running`, `/v1/models`, unload) und liest/schreibt dessen `config.yaml` per `ruamel.yaml` (Kommentare bleiben erhalten). -- **`hw_math.py`** — Odysseus-Fit-Mathe: VRAM/RAM-Bedarf, tps-Schätzung, **`max_ctx_for`** (optimaler Kontext aus Hardware), `extract_params_b`. Genutzt von cookbook + models. -- **`recipes.py`** — kuratierte Use-Case-Stacks (Daten, kein Code) fürs Cookbook 2.0. -- **`routers/*.py`** — ein Router je Bereich: `models.py` (`status` mit Meta/Caps/**optimal_ctx**, `download`, `register`, `update_model`, `unload`, `chat`), `jobs.py` (`jobs`), `maintenance.py` (`update` = **llama.cpp**, `self-update` = **MC selbst**, `os-update`, `service/{name}/restart`, `reboot`, `logs`-WebSocket), `system.py` (`status` + `stream`-WebSocket, Live-Metriken via psutil/sysfs), `cookbook.py` (`analyze`/`evaluate`/`recipes`/`install-recipe`), `integration.py` (`test` — Engine-Verbindung), `news.py` (`news` — RSS-Aggregation via stdlib). Alle unter `/api/*`. +- **`auth.py`** — optionale Token-Auth: Header `X-MC-Token` **oder** Query `?token=` (Query ist Pflicht für WebSockets). +- **`jobengine.py`** — In-Memory-Job-System (Threads + Subprocess) mit Live-Log. Secrets gehen über **stdin** (kein Leak in Log/`ps`). +- **`llamaswap.py`** — spricht `llama-swap` an und liest/schreibt dessen `config.yaml` per `ruamel.yaml`. +- **`hw_math.py`** — Odysseus-Fit-Mathe: VRAM/RAM-Bedarf, tps-Schätzung, `max_ctx_for`, `extract_params_b`. +- **`recipes.py`** — kuratierte Use-Case-Stacks fürs Cookbook 2.0. +- **`hermes_agent.py`** — Kern des Hermes-Agenten: Tool-Definitionen, Tool-Dispatcher, LLM-Loop (ReAct), Modell-Routing. +- **`mcp_memory.py`** — stdio-MCP-Server: Gedächtnis-Tools für Cline/OpenCode/Claude Code. +- **`routers/*.py`** — ein Router je Bereich: + - `models.py` — status/download/register/update_model/unload/chat + - `jobs.py` — Jobs-Liste + - `maintenance.py` — update/self-update/os-update/service-restart/reboot/logs-WebSocket + - `system.py` — status + stream-WebSocket (Live-Metriken psutil/sysfs) + - `cookbook.py` — analyze/evaluate/recipes/install-recipe/upgrades + - `integration.py` — test (Engine-Verbindungstest) + - `news.py` — RSS-Aggregation via stdlib + - `memory.py` — Gedächtnis-CRUD (SQLite, WAL-Mode), 5 Kategorien + - `hermes.py` — Chat-WebSocket/transcribe(Whisper)/tts(Piper)/status/pubkey -**Frontend** (`static/`, dünne Hülle + ES-Module, kein Build): -- **`index.html`** — nur Gerüst: Sidebar-Nav, Topbar, Alert-Banner, ein `.view`-Container je Bereich (Hash-Routing). Lädt `css/*` und `js/main.js` als Modul. -- **Design-System (v3):** **EINE** Akzentfarbe (Teal `#2dd4bf`) für alles Klickbare; Grün/Gelb/Rot **nur** für Status. Dichtes Control-Plane-Layout, Monospace-Zahlen, Bento. **Keine Inline-Styles in Panels** — alles über Klassen aus `components.css` (`.card`, `.tile`, `.qa`, `.fit-badge`, `.modal-*`, `.badge`, `.bar`/`.meter`, `.chip` …). Tokens in `css/base.css` (`:root`). -- **`js/core/*`** — `api.js` (Fetch + Token), `ui.js` (DOM-Helfer, Toast, Inline-Icon-Set, **`confirmModal`/`promptModal`** für Beginner-UX, `fmtBytes`/`fmtPct`), `nav.js` (beschrifteter View-Switch). -- **`js/panels/*`** — ein Panel je Bereich (`overview`, `models`, `server`, `cookbook`, `connect`=Verbinden, `news`, `jobs`=Aktivität, `guides`). Panel-Vertrag: `{ id, mount?(), onStatus?(s), onJobs?(jobs), onSystem?(sys) }`. -- **`js/main.js`** — bootet Panels, pflegt Topbar/Alert + **ehrlichen Security-Chip** (`status.secured`), WebSocket für Live-Metriken (`/api/system/stream`, 2 Hz), Polling (`/api/status` 3 s, `/api/jobs` 1.5 s). -- **Leitprinzip UX:** verständlich/idiotensicher — Klartext-Microcopy, Fachbegriffe übersetzt (CPU→Prozessor, VRAM→Grafikspeicher), geführte Aktionen, heikle Aktionen mit `confirmModal` (Klartext-Konsequenz). +**Frontend** (`frontend/src/`, Svelte 5 + Vite, Build → `static/dist/`): +- **`index.html`** — Gerüst: Sidebar-Nav (10 Tabs), Topbar, Alert-Banner, View-Container je Bereich. +- **`frontend/src/main.ts`** — bootet alle Svelte-Panels, Topbar/Alert, WebSocket Live-Metriken, Polling. +- **`frontend/src/panels/`** — je ein Panel: Overview, Models, Jobs(Aktivität), Server, Cookbook, Connect(Verbinden), News, Guides, Memory(Gedächtnis), Hermes. +- **`frontend/src/stores/`** — Svelte 5 Runes: `status.svelte.ts`, `jobs.svelte.ts`, `system.svelte.ts`. +- **Design-System (v3):** EINE Akzentfarbe Teal `#2dd4bf`. Grün/Gelb/Rot nur für Status. Tokens in `css/base.css`. -- **`mission-control.service`** — systemd-Unit (uvicorn auf Port 9000). -- **Konfiguration** rein über Env-Vars: `MC_LLAMA_SWAP_URL`, `MC_CONFIG_PATH`, `MC_MODELS_DIR`, `MC_CMD_TEMPLATE`, `MC_UPDATE_CMD`, `MC_DEFAULT_TTL`, `MC_TOKEN`, `MC_SOURCE_DIR` (Self-Update-Quelle, Default `~/mission-control`). +## Der Stack drumherum -## Der Stack drumherum (Kontext) - -- **Bosgame M5**: AMD Strix Halo (gfx1151), Ubuntu 26.04, Kernel 7.0, ~124 GB GTT-Speicher. LAN-IP `192.168.178.151`. -- **Inferenz**: vorgebaute llama.cpp-ROCm-Binaries → `llama-swap` auf **`*:8080` (LAN-offen**, mit `-watch-config`) → die Guides-Integrationen funktionieren von anderen Geräten. Engine-Update via `MC_UPDATE_CMD=/usr/local/bin/update-llamacpp` (lädt llama.cpp-ROCm für gfx1151). -- **3 Modelle / 5 Rollen**: `coder`, `scout`, `vision` (Qwen3-Familie). Modelle werden geswappt, nicht parallel geladen. -- **Mission Control**: Produktiv unter `/opt/mission-control` (als Dienst), Source-Repo unter `~/mission-control`. +- **Bosgame M5**: AMD Strix Halo (gfx1151), Ubuntu 26.04, Kernel 7.0, ~124 GB GTT. LAN-IP `192.168.178.151`. +- **Inferenz**: llama.cpp-ROCm-Binaries → `llama-swap` auf `*:8080` (LAN-offen, `-watch-config`). +- **3 Modelle / 5 Rollen**: `coder` (Qwen3-30B-A3B), `scout` (Qwen3-8B), `vision` (Qwen3-VL). +- **Mission Control**: Produktiv unter `/opt/mission-control`, Source unter `~/mission-control`. +- **Gedächtnis**: SQLite unter `/srv/models/mission-control-memory.db` (5 Kategorien: user/instruction/stable/versioned/ephemeral). +- **Hermes Agent**: läuft als Teil von Mission Control (kein separater Dienst), Voice via Whisper (CPU) + Piper TTS. ## Entwickeln & Deployen -**Wo entwickelt wird:** primär auf einem **Windows-PC** (Repo z. B. `F:\Coding Stuff\mission-control`), -Zielsystem ist der **Linux-Bosgame**. Lokal nur Smoke-Test (rendert/bootet sauber?), echter -Funktionstest nur auf dem Bosgame (dort läuft llama-swap). +**Wo entwickelt wird:** Windows-PC (`F:\Coding Stuff\mission-control`), Ziel: Linux-Bosgame. +Lokal nur Smoke-Test (rendert/bootet?), echter Funktionstest auf dem Bosgame. **Lokaler Smoke-Test (Windows):** ```bash python -m venv .venv && .venv/Scripts/python -m pip install -r requirements.txt .venv/Scripts/python -m uvicorn app:app --port 9001 ``` -Ohne llama-swap ist alles im Offline-Zustand (rote Pill, Warn-Banner) — das ist erwartet. -**Bosgame-Zugang (SSH, key-basiert, passwortlos):** +**Bosgame-Zugang:** ```bash ssh -i ~/.ssh/id_ed25519_hermes -o IdentitiesOnly=yes hitonabi@192.168.178.151 ``` -User ist **`hitonabi`** (klein!). `sudo` braucht generell ein Passwort — **außer** einer engen NOPASSWD-Whitelist für genau `systemctl restart mission-control|llama-swap` und `journalctl` (dadurch laufen Self-Update, Dienst-Restarts und Log-Stream passwortlos). OS-Update/Reboot fragen das Passwort weiterhin ab. +User: `hitonabi`. sudo NOPASSWD-Whitelist für `systemctl restart mission-control|llama-swap` und `journalctl`. -**Self-Update statt manuellem Deploy:** Der Button „Mission Control aktualisieren" (`POST /api/self-update`) macht `git pull` (Source) → rsync → `systemctl restart` — der dokumentierte manuelle Weg unten ist nur noch Fallback/Erstinstallation. - -**Frontend-Build (Vite + Svelte 5, ab Phase C):** +**Frontend-Build:** ```bash -cd frontend && npm install # einmalig -npm run build # vor jedem git push — Output: static/dist/main.js +cd frontend && npm run build # vor jedem git push ``` -Das Build-Ergebnis (`static/dist/`) wird committet → kein Build-Schritt auf dem Server. -Beim Entwickeln: `npm run dev` in `frontend/` startet den Vite Dev-Server (Port 5173) für HMR. -Neue Svelte-Panels kommen in `frontend/src/panels/`. Sobald ein Panel migriert ist, -wird es in `frontend/src/main.ts` importiert und das alte `static/js/panels/.js` nicht mehr gebraucht. +Output: `static/dist/main.js` + `static/dist/main.css` — wird committet (kein Build-Schritt auf dem Server). -**Deploy-Kette (Windows → Gitea → Bosgame):** -1. Frontend bauen: `cd frontend && npm run build` → `static/dist/main.js` aktualisiert. -2. Lokal Smoke-Test → `git push` (origin = Gitea `http://192.168.178.153:3000/Hitonabi/mission-control.git`). -3. Bosgame: `cd ~/mission-control && git pull --ff-only` (Source-Repo). -4. Nach `/opt` ausrollen — **ohne sudo**, `node_modules` ausnehmen: - ```bash - rsync -a --exclude='.git' --exclude='.venv' --exclude='__pycache__' --exclude='*.pyc' --exclude='frontend/node_modules' ~/mission-control/ /opt/mission-control/ - ``` -5. `sudo systemctl restart mission-control` (Passwort nötig). Prod = `:9000`, Dev-Spielwiese = `:9001`. -6. **Niemals direkt in `/opt` arbeiten.** Logs: `journalctl -u mission-control -f`. +**Deploy-Kette:** +``` +npm run build → git push (Gitea :3000) → Bosgame: git pull → rsync → systemctl restart +``` +Oder über den Self-Update-Button in Mission Control (empfohlen). -Statische Dateien werden je Request frisch von Platte gelesen → UI-Änderungen wirken schon nach -rsync; **Python-Code-Änderungen brauchen den Restart**. +**Self-Update-Button** (`POST /api/self-update`): git pull → rsync → restart — alles in einem Klick. + +## Konfiguration (Env-Vars) + +| Variable | Default | Zweck | +|---|---|---| +| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | llama-swap URL | +| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap Config | +| `MC_MODELS_DIR` | `/srv/models` | GGUF-Ablageort | +| `MC_TOKEN` | leer | Auth-Token (optional, LAN-only) | +| `MC_UPDATE_CMD` | leer | Engine-Update-Befehl | +| `MC_DEFAULT_TTL` | `300` | Sekunden bis Auto-Unload | +| `MC_SOURCE_DIR` | `~/mission-control` | Self-Update Quelle | +| `MC_MEMORY_DB` | `{MODELS_DIR}/mission-control-memory.db` | SQLite Gedächtnis | +| `HERMES_SIMPLE_MODEL` | `scout` | Modell für einfache Tasks | +| `HERMES_COMPLEX_MODEL` | `coder` | Modell für komplexe Tasks | +| `HERMES_WINDOWS_HOST` | leer | Windows-PC IP für SSH | +| `HERMES_WINDOWS_USER` | `TobisPC` | Windows SSH-Username | +| `HERMES_SSH_KEY` | `~/.ssh/id_ed25519_hermes_agent` | SSH-Key für Hermes | +| `PIPER_BIN` | `/opt/mission-control/piper/piper` | Piper TTS Binary | +| `PIPER_VOICE` | `.../de_DE-kerstin-low.onnx` | Piper Stimm-Modell | +| `WHISPER_MODEL` | `medium` | Whisper Modell-Größe | ## Konventionen -- **Frontend: Vite + Svelte 5** (Phase C). Neuer Bereich = `frontend/src/panels/Panel.svelte` + `routers/.py` + `initNav`-Eintrag in `index.html`. Danach `npm run build` in `frontend/` → `static/dist/main.js` wird committet. -- Stores: `frontend/src/stores/status.svelte.ts`, `jobs.svelte.ts`, `system.svelte.ts` — Svelte 5 Runes (`$state`). -- Statische Hilfsfunktionen in `static/js/core/` (api.js, ui.js, nav.js) bleiben als gemeinsame Basis — werden von Vite gebündelt, nicht direkt geladen. -- **Backend SoC**: ein `routers/.py` je Bereich, gemeinsame Logik in `config/auth/jobengine/llamaswap/hw_math`. -- Endpoint-URLs bleiben unter `/api/*`; neue Bereiche degradieren sauber, wenn ihre Quelle (sysfs, `amd-smi`, `systemctl`) fehlt (z. B. beim Entwickeln auf Windows). -- Funktion darf **nicht** von `localStorage` abhängen (nur das Token-Feld nutzt es, das ist ok). -- **Sicherheit**: Das Backend führt Shell-Befehle aus → ausschließlich im vertrauenswürdigen LAN betreiben, niemals offen ins Internet. +- **Neuer Bereich**: `routers/.py` + `frontend/src/panels/Panel.svelte` + Nav/View in `index.html` + Import in `main.ts`. Danach `npm run build`. +- **Backend SoC**: gemeinsame Logik in `config/auth/jobengine/llamaswap/hw_math`. Alles unter `/api/*`. +- **Sicherheit**: Shell-Befehle → nur LAN. Secrets via stdin. Token optional. +- **SQLite**: WAL-Mode aktiv, `check_same_thread=False` — ausreichend für Single-User. +- **`asyncio.get_running_loop()`** statt `get_event_loop()` (Python 3.10+ kompatibel). +- **Memory-Kategorien**: `user` | `instruction` | `stable` | `versioned` | `ephemeral` — Enum in `routers/memory.py`. -## Gotchas (wichtig!) +## Gotchas -- **`${PORT}`** in der generierten `llama-swap`-Config muss **literal** stehen bleiben → beim Bauen des cmd-Strings `str.replace` benutzen, NICHT `.format` (sonst KeyError auf `PORT`). -- `llama-swap` muss mit **`-watch-config`** laufen, sonst greift das Auto-Einpflegen neuer Modelle nicht. -- HuggingFace-Downloads mit **`HF_HUB_DISABLE_XET=1`** (sonst reproduzierbarer Hänger bei ~6 MB). -- Vision-Modelle in llama.cpp brauchen zusätzlich **`--mmproj ` und `--jinja`**. +- **`${PORT}`** in llama-swap-Config → `str.replace` statt `.format` (KeyError sonst). +- **`-watch-config`** bei llama-swap nötig für Auto-Einpflegen. +- **`HF_HUB_DISABLE_XET=1`** bei HuggingFace-Downloads (Hänger bei ~6 MB). +- **Vision-Modelle**: brauchen `--mmproj ` und `--jinja`. +- **Gitea-Push** kann transient mit „Failed to authenticate" fehlschlagen → einfach nochmal. +- **Piper TTS**: Binary + Stimm-Modell müssen separat installiert werden (nicht via pip). +- **Whisper**: lädt beim ersten Aufruf ~1.4 GB Modell herunter (einmalig, dann gecacht). +- **Hermes SSH-Key**: muss auf dem Bosgame generiert werden (`~/.ssh/id_ed25519_hermes_agent`). -## Gotchas v3 (zusätzlich) +## Projektstatus -- **`.qa` ist ein `
',1);function Nv(e,n){On(n,!0);let a=O(jt([])),s=O(""),l=O(!1),o=O(!1),c=O(!1),d=O(null),v=O(!1),p=O(""),_=O(null),T=O("");function y(){const F=location.protocol==="https:"?"wss:":"ws:",X=localStorage.getItem("mc_token")||"",E=`${F}//${location.host}/api/hermes/chat${X?"?token="+X:""}`,I=new WebSocket(E);I.onmessage=se=>{const le=JSON.parse(se.data);if(le.type==="thinking")u(l,!0);else if(le.type!=="info")if(le.type==="token"){u(l,!1),u(T,t(T)+le.content);const m=t(a)[t(a).length-1];(m==null?void 0:m.role)==="hermes"?u(a,[...t(a).slice(0,-1),{role:"hermes",content:t(T)}],!0):u(a,[...t(a),{role:"hermes",content:t(T)}],!0)}else if(le.type==="tool_call"){u(l,!1);const m=le.args||{},L=Object.values(m).join(" ").slice(0,60);u(a,[...t(a),{role:"tool",content:L,tool:le.name}],!0)}else if(le.type==="tool_result"){const m=t(a).findLast(L=>L.role==="tool"&&L.tool===le.name);m&&(m.content=le.content),u(a,[...t(a)],!0)}else le.type==="error"?(u(l,!1),u(a,[...t(a),{role:"error",content:le.content}],!0)):le.type==="done"&&(u(l,!1),u(T,""),t(o)&&ke())},I.onclose=()=>{u(_,null),setTimeout(y,3e3)},I.onerror=()=>I.close(),u(_,I,!0)}async function x(){const F=t(s).trim();!F||!t(_)||t(_).readyState!==1||(u(a,[...t(a),{role:"user",content:F}],!0),u(s,""),u(T,""),t(_).send(JSON.stringify({message:F})))}let A=null,D=[];async function M(){try{const F=await navigator.mediaDevices.getUserMedia({audio:!0});D=[],A=new MediaRecorder(F),A.ondataavailable=X=>D.push(X.data),A.onstop=()=>{F.getTracks().forEach(X=>X.stop()),qe()},A.start(),u(c,!0)}catch(F){ie("Mikrofon-Zugriff verweigert: "+F.message,!0)}}function B(){A==null||A.stop(),u(c,!1)}async function qe(){const F=new Blob(D,{type:"audio/webm"}),X=new FormData;X.append("audio",F,"voice.webm");try{const I=await(await fetch("/api/hermes/transcribe",{method:"POST",headers:localStorage.getItem("mc_token")?{"X-MC-Token":localStorage.getItem("mc_token")}:{},body:X})).json();I.text?(u(s,I.text,!0),x()):ie("Konnte Audio nicht verstehen.",!0)}catch(E){ie("Transkription fehlgeschlagen: "+E.message,!0)}}async function ke(){const F=[...t(a)].reverse().find(X=>X.role==="hermes");if(F)try{const X=await fetch("/api/hermes/tts",{method:"POST",headers:{"Content-Type":"application/json",...localStorage.getItem("mc_token")?{"X-MC-Token":localStorage.getItem("mc_token")}:{}},body:JSON.stringify({text:F.content})});if(!X.ok)return;const E=await X.blob(),I=URL.createObjectURL(E),se=new Audio(I);se.onended=()=>URL.revokeObjectURL(I),se.play()}catch{}}async function xe(){try{u(d,await $e("/api/hermes/status"),!0)}catch{u(d,null)}}async function Ae(){try{const F=await $e("/api/hermes/pubkey");u(p,F.pubkey,!0)}catch{u(p,"")}}function W(F){return{read_file:"📄 Lese Datei",list_directory:"📁 Liste Verzeichnis",run_command:"⚡ Befehl",get_system_status:"📊 System-Status",get_memories:"🧠 Gedächtnis",add_memory:"💾 Speichern",web_search:"🌐 Websuche"}[F]||F}function ae(){var F;(F=navigator.clipboard)==null||F.writeText(t(p)),ie("SSH-Key kopiert")}mi(()=>{y(),xe()});var we=Kv(),Ce=et(we),Be=i(r(Ce),2),je=r(Be),bt=r(je),z=i(je,2),re=r(z),ge=i(z,2),tt=r(ge),oe=i(ge,2),Fe=i(Ce,2);{var ue=F=>{var X=zv(),E=i(r(X),2),I=r(E),se=i(r(I),2),le=r(se),m=i(I,2);{var L=ze=>{var me=Mv();f(ze,me)};K(m,ze=>{var me;((me=t(d))==null?void 0:me.whisper)==="not_installed"&&ze(L)})}var ne=i(E,2),ee=r(ne),U=i(r(ee),2),be=r(U),_e=i(ee,2);{var Le=ze=>{var me=Ev(),Tt=r(me),$t=r(Tt);P(zt=>g($t,zt),[()=>["# 1. Piper Binary (Linux x86_64)","mkdir -p /opt/mission-control/piper/voices","cd /tmp && wget -q https://github.com/rhasspy/piper/releases/download/2023.11.14-2/piper_linux_x86_64.tar.gz","tar -xzf piper_linux_x86_64.tar.gz -C /opt/mission-control/piper/ --strip-components=1","","# 2. Deutsche Stimme — Kerstin (weiblich, ~25 MB)","wget -q -O /opt/mission-control/piper/voices/de_DE-kerstin-low.onnx \\",' "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/de/de_DE/kerstin/low/de_DE-kerstin-low.onnx"',"wget -q -O /opt/mission-control/piper/voices/de_DE-kerstin-low.onnx.json \\",' "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/de/de_DE/kerstin/low/de_DE-kerstin-low.onnx.json"'].join(` + justify-content:center;font-size:14px" class="svelte-h40163">H denkt nach…`),Kv=b('

Hermes

Dein lokaler KI-Assistent — kennt den Bosgame, lernt mit.
',1);function Nv(e,n){On(n,!0);let a=O(jt([])),s=O(""),l=O(!1),o=O(!1),c=O(!1),d=O(null),v=O(!1),p=O(""),_=O(null),T=O("");function y(){const F=location.protocol==="https:"?"wss:":"ws:",X=localStorage.getItem("mc_token")||"",E=`${F}//${location.host}/api/hermes/chat${X?"?token="+X:""}`,I=new WebSocket(E);I.onmessage=se=>{const le=JSON.parse(se.data);if(le.type==="thinking")u(l,!0);else if(le.type!=="info")if(le.type==="token"){u(l,!1),u(T,t(T)+le.content);const m=t(a)[t(a).length-1];(m==null?void 0:m.role)==="hermes"?u(a,[...t(a).slice(0,-1),{role:"hermes",content:t(T)}],!0):u(a,[...t(a),{role:"hermes",content:t(T)}],!0)}else if(le.type==="tool_call"){u(l,!1);const m=le.args||{},L=Object.values(m).join(" ").slice(0,60);u(a,[...t(a),{role:"tool",content:L,tool:le.name}],!0)}else if(le.type==="tool_result"){const m=[...t(a)].reverse().find(L=>L.role==="tool"&&L.tool===le.name);m&&(m.content=le.content),u(a,[...t(a)],!0)}else le.type==="error"?(u(l,!1),u(a,[...t(a),{role:"error",content:le.content}],!0)):le.type==="done"&&(u(l,!1),u(T,""),t(o)&&ke())},I.onclose=()=>{u(_,null),setTimeout(y,3e3)},I.onerror=()=>I.close(),u(_,I,!0)}async function x(){const F=t(s).trim();!F||!t(_)||t(_).readyState!==1||(u(a,[...t(a),{role:"user",content:F}],!0),u(s,""),u(T,""),t(_).send(JSON.stringify({message:F})))}let A=null,D=[];async function M(){try{const F=await navigator.mediaDevices.getUserMedia({audio:!0});D=[],A=new MediaRecorder(F),A.ondataavailable=X=>D.push(X.data),A.onstop=()=>{F.getTracks().forEach(X=>X.stop()),qe()},A.start(),u(c,!0)}catch(F){ie("Mikrofon-Zugriff verweigert: "+F.message,!0)}}function B(){A==null||A.stop(),u(c,!1)}async function qe(){const F=new Blob(D,{type:"audio/webm"}),X=new FormData;X.append("audio",F,"voice.webm");try{const I=await(await fetch("/api/hermes/transcribe",{method:"POST",headers:localStorage.getItem("mc_token")?{"X-MC-Token":localStorage.getItem("mc_token")}:{},body:X})).json();I.text?(u(s,I.text,!0),x()):ie("Konnte Audio nicht verstehen.",!0)}catch(E){ie("Transkription fehlgeschlagen: "+E.message,!0)}}async function ke(){const F=[...t(a)].reverse().find(X=>X.role==="hermes");if(F)try{const X=await fetch("/api/hermes/tts",{method:"POST",headers:{"Content-Type":"application/json",...localStorage.getItem("mc_token")?{"X-MC-Token":localStorage.getItem("mc_token")}:{}},body:JSON.stringify({text:F.content})});if(!X.ok)return;const E=await X.blob(),I=URL.createObjectURL(E),se=new Audio(I);se.onended=()=>URL.revokeObjectURL(I),se.play()}catch{}}async function xe(){try{u(d,await $e("/api/hermes/status"),!0)}catch{u(d,null)}}async function Ae(){try{const F=await $e("/api/hermes/pubkey");u(p,F.pubkey,!0)}catch{u(p,"")}}function W(F){return{read_file:"📄 Lese Datei",list_directory:"📁 Liste Verzeichnis",run_command:"⚡ Befehl",get_system_status:"📊 System-Status",get_memories:"🧠 Gedächtnis",add_memory:"💾 Speichern",web_search:"🌐 Websuche"}[F]||F}function ae(){var F;(F=navigator.clipboard)==null||F.writeText(t(p)),ie("SSH-Key kopiert")}mi(()=>{y(),xe()});var we=Kv(),Ce=et(we),Be=i(r(Ce),2),je=r(Be),bt=r(je),z=i(je,2),re=r(z),ge=i(z,2),tt=r(ge),oe=i(ge,2),Fe=i(Ce,2);{var ue=F=>{var X=zv(),E=i(r(X),2),I=r(E),se=i(r(I),2),le=r(se),m=i(I,2);{var L=ze=>{var me=Mv();f(ze,me)};K(m,ze=>{var me;((me=t(d))==null?void 0:me.whisper)==="not_installed"&&ze(L)})}var ne=i(E,2),ee=r(ne),U=i(r(ee),2),be=r(U),_e=i(ee,2);{var Le=ze=>{var me=Ev(),Tt=r(me),$t=r(Tt);P(zt=>g($t,zt),[()=>["# 1. Piper Binary (Linux x86_64)","mkdir -p /opt/mission-control/piper/voices","cd /tmp && wget -q https://github.com/rhasspy/piper/releases/download/2023.11.14-2/piper_linux_x86_64.tar.gz","tar -xzf piper_linux_x86_64.tar.gz -C /opt/mission-control/piper/ --strip-components=1","","# 2. Deutsche Stimme — Kerstin (weiblich, ~25 MB)","wget -q -O /opt/mission-control/piper/voices/de_DE-kerstin-low.onnx \\",' "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/de/de_DE/kerstin/low/de_DE-kerstin-low.onnx"',"wget -q -O /opt/mission-control/piper/voices/de_DE-kerstin-low.onnx.json \\",' "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/de/de_DE/kerstin/low/de_DE-kerstin-low.onnx.json"'].join(` `)]),f(ze,me)};K(_e,ze=>{var me;((me=t(d))==null?void 0:me.piper)!=="ready"&&ze(Le)})}var Qe=i(ne,2),Me=r(Qe),Ze=i(r(Me),2),$=r(Ze),de=i(Me,2);{var Te=ze=>{var me=Tv(),Tt=i(r(me),4),$t=i(r(Tt),2),zt=r($t);zt.textContent=`Start-Service sshd Set-Service -Name sshd -StartupType Automatic`;var C=i(Tt,2),Z=i(r(C),2),Ge=r(Z);Ge.textContent='New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\\.ssh"';var he=i(C,2);{var G=R=>{var H=Av(),te=i(r(H),2),ve=r(te),Ie=i(ve,2),Ee=r(Ie),ut=r(Ee);P(()=>g(ut,'echo "'+t(p)+'" >> "$env:USERPROFILE\\.ssh\\authorized_keys"')),S("click",ve,ae),f(R,H)},Q=R=>{var H=Cv();f(R,H)};K(he,R=>{t(p)?R(G):R(Q,-1)})}var V=i(he,4);S("click",V,()=>{xe(),ie("Status aktualisiert")}),f(ze,me)};K(de,ze=>{var me;(me=t(d))!=null&&me.ssh_ok||ze(Te)})}P(()=>{var ze,me,Tt,$t,zt,C;Ot(se,`font-size:11px;${((ze=t(d))==null?void 0:ze.whisper)!=="not_installed"?"color:#7ee29a":"color:#ff9b95"}`),g(le,((me=t(d))==null?void 0:me.whisper)!=="not_installed"?"✓ OK":"✗ fehlt"),Ot(U,`font-size:11px;${((Tt=t(d))==null?void 0:Tt.piper)==="ready"?"color:#7ee29a":"color:#ff9b95"}`),g(be,(($t=t(d))==null?void 0:$t.piper)==="ready"?"✓ OK":"✗ fehlt"),Ot(Ze,`font-size:11px;${(zt=t(d))!=null&&zt.ssh_ok?"color:#7ee29a":"color:var(--mut)"}`),g($,(C=t(d))!=null&&C.ssh_ok?"✓ verbunden":"optional")}),f(F,X)};K(Fe,F=>{t(v)&&F(ue)})}var q=i(Fe,2),Y=r(q),pe=r(Y);{var fe=F=>{var X=Dv(),E=i(r(X),4);rt(E,20,()=>["Was läuft gerade auf dem System?","Zeig mir den Inhalt von /etc/llama-swap/config.yaml","Suche nach den neuesten Qwen3-Modellen"],dt,(I,se)=>{var le=Pv(),m=r(le);P(()=>g(m,se)),S("click",le,()=>{u(s,se,!0),x()}),f(I,le)}),f(F,X)};K(pe,F=>{t(a).length===0&&F(fe)})}var Ke=i(pe,2);rt(Ke,16,()=>t(a),F=>F,(F,X)=>{var E=Va(),I=et(E);{var se=ee=>{var U=$v(),be=r(U),_e=r(be);P(()=>g(_e,X.content)),f(ee,U)},le=ee=>{var U=Lv(),be=i(r(U),2),_e=r(be);P(()=>g(_e,X.content)),f(ee,U)},m=ee=>{var U=Ov(),be=r(U),_e=r(be),Le=i(be,2);{var Qe=Me=>{var Ze=Iv(),$=r(Ze);P(()=>g($,X.content)),f(Me,Ze)};K(Le,Me=>{X.content&&Me(Qe)})}P(Me=>g(_e,Me),[()=>W(X.tool||"")]),f(ee,U)},L=ee=>{var U=Bv(),be=r(U);P(()=>g(be,X.content)),f(ee,U)},ne=ee=>{var U=Rv(),be=r(U);P(()=>g(be,`⚠ ${X.content??""}`)),f(ee,U)};K(I,ee=>{X.role==="user"?ee(se):X.role==="hermes"?ee(le,1):X.role==="tool"?ee(m,2):X.role==="info"?ee(L,3):X.role==="error"&&ee(ne,4)})}f(F,E)});var Ye=i(Ke,2);{var ct=F=>{var X=Fv();f(F,X)};K(Ye,F=>{t(l)&&F(ct)})}var st=i(Y,2),lt=r(st),Re=i(lt,2),Je=r(Re),vt=i(Re,2);P(F=>{var X,E,I,se,le,m;Ot(je,`font-size:11px;${((X=t(d))==null?void 0:X.whisper)==="ready"||((E=t(d))==null?void 0:E.whisper)==="installed"?"color:var(--accent);border-color:var(--accent)":"color:var(--mut)"}`),g(bt,`🎙 Whisper: ${((I=t(d))==null?void 0:I.whisper)??"…"??""}`),Ot(z,`font-size:11px;${(se=t(d))!=null&&se.ssh_ok?"color:var(--accent);border-color:var(--accent)":"color:var(--mut)"}`),g(re,`🔑 SSH: ${(le=t(d))!=null&&le.ssh_ok?"verbunden":(m=t(d))!=null&&m.ssh_configured?"Fehler":"nicht eingerichtet"}`),Ot(ge,`padding:4px 12px;font-size:12px;${t(o)?"border-color:var(--accent);color:var(--accent)":""}`),g(tt,`🔊 Voice ${t(o)?"an":"aus"}`),Ot(Re,`padding:10px 12px;flex-shrink:0;font-size:18px;line-height:1; ${t(c)?"border-color:var(--err);color:var(--err)":""}`),_i(Re,"title",t(c)?"Aufnahme stoppen":"Spracheingabe"),g(Je,t(c)?"⏹":"🎤"),vt.disabled=F},[()=>!t(s).trim()||!t(_)||t(_).readyState!==1]),S("click",ge,()=>u(o,!t(o))),S("click",oe,()=>{u(v,!t(v)),t(v)&&(xe(),Ae())}),S("keydown",lt,F=>{F.key==="Enter"&&!F.shiftKey&&(F.preventDefault(),x())}),vn(lt,()=>t(s),F=>u(s,F)),S("mousedown",Re,M),S("mouseup",Re,B),S("touchstart",Re,M),S("touchend",Re,B),S("click",vt,x),f(e,we),Bn()}jn(["click","keydown","mousedown","mouseup","touchstart","touchend"]);let es={};function qv(){const e=document.getElementById("top-active-text");e&&(e.classList.add("swap-flash"),setTimeout(()=>e.classList.remove("swap-flash"),900))}function ts(e){var l;const n=document.getElementById("swdot"),a=document.getElementById("swlabel"),s=document.getElementById("top-active-text");if(!e)n&&(n.className="dot off"),a&&(a.textContent="Backend nicht erreichbar"),s&&(s.textContent="Backend offline"),ns("Backend nicht erreichbar – läuft uvicorn?",!1);else{const o=(e.swap_url||"").replace(/^https?:\/\//,"");n&&(n.className="dot "+(e.swap_ok?"on":"off")),a&&(a.textContent=(e.swap_ok?"LLM-Engine: Online – ":"LLM-Engine: Offline – ")+o);const c=["running","ready","loading","starting"],d=(e.models||[]).find(p=>c.includes(p.state));if(s)if(d){const p=(l=d.meta)!=null&&l.ctx?` · ${Math.round(d.meta.ctx/1024)}k ctx`:"",_=d.state==="loading"||d.state==="starting"?' lädt…':"";s.innerHTML=`${d.name}${p}${_}`}else s.innerHTML='Kein Modell im VRAM';e.swap_ok?jv():ns(`LLM-Engine nicht erreichbar unter ${o} – läuft der llama-swap Dienst?`,!0);const v={};for(const p of e.models||[])v[p.name]=p.state;for(const[p,_]of Object.entries(v)){const T=es[p];if(T&&T!==_&&(_==="running"||_==="loading")){qv();break}}es=v}Wv(e),ki.set(e)}function Wv(e){const n=document.getElementById("sec-chip"),a=document.getElementById("sec-chip-tx");!n||!a||(n.classList.toggle("ok",!!(e!=null&&e.secured)),a.textContent=e!=null&&e.secured?"Geschützt · Heimnetz":"Nur im Heimnetz (offen)")}function Gv(e){ua.set(e||[])}function Vv(e){Pr.set(e)}function ns(e,n){const a=document.getElementById("alert");a.className="alert"+(n?" warn":""),a.innerHTML=`${e}`,a.style.display="flex"}function jv(){const e=document.getElementById("alert");e&&(e.style.display="none")}const Uv=e=>{var n;return(n=document.querySelector(`.nav-item[data-view="${e}"]`))==null?void 0:n.click()};function tr(e,n,a,s){const l=document.createElement("span");return l.className="upd-badge"+(n?" "+n:""),l.textContent=e,s&&(l.title=s),l.onclick=()=>Uv(a),l}async function nl(){try{const e=await $e("/api/updates"),n=document.getElementById("update-badges");if(!n)return;const a=e.apt_cache_age_h,s=a!=null&&a>24,l=a!=null?` · apt-Cache: vor ${a}h`:"";n.innerHTML="",n.appendChild(tr(e.os>0?`OS: ${e.os} Updates`:s?"OS: ?":"OS: aktuell",e.os>0||s?"warn":"ok","server",e.os>0?`${e.os} Pakete${l}`:s?`apt-Cache veraltet (${a}h)`:`Keine Updates${l}`)),n.appendChild(tr(e.engine>0?"Engine: Update":"Engine: aktuell",e.engine>0?"warn":"ok","server")),n.appendChild(tr(e.models>0?`Modelle: ${e.models}`:"Modelle: aktuell",e.models>0?"warn":"ok","news"))}catch{}}async function Gi(){try{ts(await $e("/api/status"))}catch{ts(null)}}async function al(){try{Gv(await $e("/api/jobs"))}catch{}}function il(){const e=window.location.protocol==="https:"?"wss:":"ws:",n=new WebSocket(`${e}//${window.location.host}/api/system/stream`);n.onmessage=a=>{try{Vv(JSON.parse(a.data))}catch{}},n.onclose=()=>{setTimeout(il,3e3)},n.onerror=()=>n.close()}var os;((os=document.getElementById("logo"))==null?void 0:os.innerHTML)!==void 0&&(document.getElementById("logo").innerHTML=Wi.logo);document.querySelectorAll("[data-ic]").forEach(e=>{e.innerHTML=Wi[e.dataset.ic||""]||""});const Hv=[["overview",vd],["models",Ed],["activity",Bd],["server",Ud],["cookbook",Tc],["connect",Qc],["news",sv],["guides",pv],["memory",Sv],["hermes",Nv]];for(const[e,n]of Hv){const a=document.querySelector(`.view[data-view="${e}"]`);a&&So(n,{target:a})}jo("overview");const nr=document.getElementById("token");nr&&(nr.value=tl(),nr.addEventListener("change",e=>{No(e.target.value),Gi()}));const ar=document.getElementById("hf-token");ar&&(ar.value=oi(),ar.addEventListener("change",e=>Wo(e.target.value)));const as=document.getElementById("nav-settings"),ir=document.getElementById("settings-modal"),is=document.getElementById("sm-close");as&&ir&&is&&(as.addEventListener("click",()=>ir.style.display="flex"),is.addEventListener("click",()=>ir.style.display="none"));function Jv(){const e=document.getElementById("clock");e&&(e.textContent=new Date().toTimeString().slice(0,5))}document.addEventListener("mc:refresh",Gi);Gi();al();il();setInterval(Jv,1e3);setInterval(Gi,3e3);setInterval(al,1500);nl();setInterval(nl,6e4);