# 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. ## 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). - **`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/*`. **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). - **`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 (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`. ## 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). **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):** ```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. **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):** ```bash cd frontend && npm install # einmalig npm run build # vor jedem git push — Output: static/dist/main.js ``` 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. **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`. Statische Dateien werden je Request frisch von Platte gelesen → UI-Änderungen wirken schon nach rsync; **Python-Code-Änderungen brauchen den Restart**. ## Konventionen - KISS über alles: kein schweres Framework, kein Build-Schritt, solange es ohne geht. - **SoC ohne Build**: neuer Bereich = ein `routers/.py` (FastAPI-Router) + ein `static/js/panels/.js` (ES-Modul nach Panel-Vertrag) + ein Nav-Eintrag in `index.html`. Gemeinsame Backend-Logik in `config/auth/jobengine/llamaswap`, gemeinsame Frontend-Logik in `js/core/*`. Keine schweren Libs, kein CDN — nur relative `import`s. - 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. ## Gotchas (wichtig!) - **`${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`**. ## Gotchas v3 (zusätzlich) - **`.qa` ist ein `