4 Commits

Author SHA1 Message Date
Hitonabi cb3a5adaf9 Merge origin/main: fix absolute imports and add reset-failed for hermes-builtin-ui 2026-07-06 20:10:33 +02:00
Hitonabi 87c479323c chore: remove invalid allowScripts field from package.json 2026-07-06 20:06:54 +02:00
Hitonabi b95530ee70 fix: absolute imports and type annotations in app.py
- replace implicit relative imports with absolute ones (from backend.*)
- add type hints for lifespan tasks list and middleware
- fix unused bool return value in task.cancel() call
2026-07-06 20:06:40 +02:00
Hitonabi 3a6e3c5432 Hermes-GUI: eingebaute hermes serve-Weboberflaeche same-origin in MC2 einbetten (ersetzt CLI-Terminal-Tab)
Statt des ttyd-CLI-Terminals zeigt der Tab jetzt die reiche eingebaute Hermes-Web-GUI
(hermes serve/dashboard: Threads, sichtbare Tool-Calls, Sessions, Config) - same-origin,
ohne offenen Port, ohne Login (Loopback :9119 = LAN-Trust wie Terminal/Konsole).

- backend/routers/hermes_ui.py: generischer Reverse-Proxy /hermes-ui/* (alle Methoden + beliebige
  WebSockets) -> 127.0.0.1:9119; injiziert window.__HERMES_BASE_PATH__=/hermes-ui in die index.html,
  damit die SPA ihre API/WS-Calls unter dem Praefix absetzt (sonst Kollision mit MC2s /api).
- deploy/hermes-builtin-ui.service: hermes serve --skip-build auf Loopback :9119.
- deploy/deploy.sh: baut das GUI-Bundle mit Vite-base=/hermes-ui/ (Temp->Swap, ueberlebt Hermes-
  Updates) + installiert/startet den Dienst.
- backend/app.py + config.py + services/agent.py: Router registriert, Upstream/Pfad + Status-Feld.
- frontend: Terminal-Tab -> "Hermes GUI" (TerminalView iframe /hermes-ui/, nav-Label, vite-Dev-Proxy).

E2E im Browser verifiziert: GUI rendert eingebettet, /hermes-ui/api/* liefern echte Daten
(289 Sessions, verbundene Plattformen), Assets + Plugins laden, Basis-Pfad korrekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:48:12 +02:00
6 changed files with 186 additions and 19 deletions
+27
View File
@@ -0,0 +1,27 @@
# .aiexclude — was der KI-Assistent (Antigravity/Gemini) NICHT lesen/indexieren soll.
# Gitignore-Syntax. Ziel: der Review fokussiert auf QUELLCODE, nicht auf Abhängigkeiten,
# Build-Output oder Binärdateien.
# Abhängigkeiten & Build-Output (kein Review-Ziel)
backend/.venv/
frontend/node_modules/
frontend/dist/
**/__pycache__/
*.pyc
.git/
# Große / binäre / sensible Dateien
*.vrm
*.gguf
*.onnx
*.safetensors
*.wav
*.env
.env
credentials*
*.key
*.pem
# Lokale Scratch-/Recon-Skripte
box_recon*
gemma_swap*
+51
View File
@@ -0,0 +1,51 @@
# Savepoint — Mission Control 2.0 (MC2)
_Stand: 2026-07-05. Zustands-Snapshot für einen externen Code-Review (Antigravity). Zuerst
`AGENTS.md` lesen (Projekt-Regeln), dann diese Datei (wo wir stehen), dann `README.md`/`docs/`._
## Was das ist (in einem Satz)
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
Agenten-Status, Updates & Wartung — und dient teils als Gedächtnis-Oberfläche für die Sprach-
Begleiterin „Lucy".
## Architektur (Kurzform — Details in AGENTS.md/docs/)
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
Router (`routers/`, dünn) → Logik (`services/`, Single Source of Truth).
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
(Box hat kein Node; Backend liefert die gebauten Assets aus).
- **Eingebautes Gateway** `:9001/v1` (OpenAI-kompatibel) → llama-swap `:8080` (Engine, Vulkan/RADV).
- **Sidecars:** `mem0_service/` (Mem0-Gedächtnis), `voice_service/` (STT/TTS für den „Sprechen"-Tab),
`mcp/` (MCP-Server), `hermes/` (Hermes-Agent-Plugin), `client/hermes-pc` (PC-Executor).
## Aktueller Zustand
- **Stabil, in Produktion, „MC2 Final".** Ein Neubau („MC3") wurde bewusst VERWORFEN — MC2 wird
sauber gehalten und eingefroren, nicht neu erfunden.
- **Autonomie-Ausbau abgeschlossen** (Observability-Latenztrace, Fremd-Modell-Kritiker/Härtung,
nächtliches „Dreaming" mit Wissens-Vault) — jeweils propose-only, Mensch = Gate.
- **Zuletzt (Commit `ff1b9a0`, 05.07.):** `coder-lite` (Qwen3-Coder-30B) entfernt; der Verbinden-Tab
(`services/connect.py`) empfiehlt IDEs jetzt die realen Direkt-Modelle `coder`/`heavy`/`vision`
statt der virtuellen „coding"-Lane. Davor u. a. Engine-Update-Fix, Hermes-Terminal same-origin.
- **Modelle live:** hermes=Qwen3.6-35B-A3B (Agent-Hirn, immer warm) · coder=Qwen3-Coder-Next ·
heavy=gpt-oss-120b · vision=Qwen3-VL-30B · scout=GLM-4.6V · embed. Warm-Set = hermes+vision+embed.
## Nordstern & Randbedingungen (WICHTIG für den Review)
- **Zieht ohne Wartung über JAHRE durch.** Robustheit + Einfachheit schlagen Features. Der Betreiber
ist **kein Entwickler** — alles muss reboot-/update-fest und selbsterklärend sein.
- **100 % lokal, kein Cloud-Zwang** (Privatsphäre ist der Sinn). Hardware-Decke: ~124 GB, ein Topf.
- **Lucy ist ein SEPARATES Repo** (`F:\Coding Stuff\lucy`) — **nicht Teil dieses Reviews.**
## Bekannte raue Kanten (Kandidaten — gern bestätigen/erweitern)
- Doku-Drift möglich: `AGENTS.md` nennt „Box läuft in UTC" — die Box wurde inzwischen auf
`Europe/Berlin` umgestellt.
- Alte, disabled v1-System-Unit `mission-control.service` liegt kosmetisch herum (sudo zum Entfernen).
- Ältere Review-Notiz nannte toten Code-Verdacht (`pricing.py`, `token_stats.py`) — bitte prüfen.
- Ein paar frühere Frontend-Monolithen wurden entflochten; Rest-Altlasten möglich.
## Was NICHT empfohlen werden soll (bereits entschieden)
Cloud-Migration · Voll-Rewrite · schwere neue Frameworks · Engine-/Modell-Wechsel, die nicht in
124 GB passen · `frontend/dist` aus git nehmen (Absicht) · Security-Config anfassen. Erwünscht sind
**Bugs, Fragilität, tote Pfade, Sicherheitslücken, Vereinfachung (KISS/DRY), Wartbarkeit.**
## Letzter Sicherungspunkt
- git `ff1b9a0` „coder-lite raus + Verbinden-Tab auf echte Direkt-Modelle" (auf `main`, deployt, live).
+50 -16
View File
@@ -10,6 +10,7 @@ import asyncio
import logging import logging
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator, Awaitable, Callable
import httpx import httpx
from fastapi import FastAPI from fastapi import FastAPI
@@ -18,9 +19,24 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.requests import Request from starlette.requests import Request
from config import FRONTEND_DIST, VERSION from backend.config import FRONTEND_DIST, VERSION
from routers import agent, connect, console, gateway_proxy, health, hermes_ui, maintenance, memory, models, reminders as reminders_router, routing, system, voice from backend.routers import (
from services import memory as memory_svc, reminders, sentry, warmer agent,
connect,
console,
gateway_proxy,
health,
hermes_ui,
maintenance,
memory,
models,
routing,
system,
voice,
)
from backend.routers import reminders as reminders_router
from backend.services import memory as memory_svc
from backend.services import reminders, sentry, warmer
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration # Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
# für alle Module (logging.getLogger(__name__)). # für alle Module (logging.getLogger(__name__)).
@@ -30,21 +46,28 @@ logging.basicConfig(
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn """Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram).""" + Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
tasks = [] tasks: list[asyncio.Task[Any]] = []
if warmer.ENABLED: if warmer.ENABLED:
tasks.append(asyncio.create_task(warmer.rewarm_loop())) tasks.append(asyncio.create_task(warmer.rewarm_loop()))
log.info("Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)", warmer.INTERVAL) log.info(
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
warmer.INTERVAL,
)
if sentry.ENABLED: if sentry.ENABLED:
tasks.append(asyncio.create_task(sentry.sentry_loop())) tasks.append(asyncio.create_task(sentry.sentry_loop()))
tasks.append(asyncio.create_task(reminders.reminders_loop())) tasks.append(asyncio.create_task(reminders.reminders_loop()))
if memory_svc.AUTO_DEDUPE_ENABLED: if memory_svc.AUTO_DEDUPE_ENABLED:
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop())) tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)", log.info(
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD) "Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
memory_svc.AUTO_DEDUPE_INTERVAL,
memory_svc.AUTO_DEDUPE_THRESHOLD,
)
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client # Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo). # pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
app.state.gw_client = httpx.AsyncClient( app.state.gw_client = httpx.AsyncClient(
@@ -55,7 +78,7 @@ async def lifespan(app: FastAPI):
yield yield
finally: finally:
for task in tasks: for task in tasks:
task.cancel() _ = task.cancel()
await app.state.gw_client.aclose() await app.state.gw_client.aclose()
@@ -71,7 +94,9 @@ app.add_middleware(
@app.middleware("http") @app.middleware("http")
async def no_cache(request: Request, call_next): async def no_cache(
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
) -> httpx.Response:
resp = await call_next(request) resp = await call_next(request)
if request.url.path.startswith("/api"): if request.url.path.startswith("/api"):
resp.headers["Cache-Control"] = "no-cache" resp.headers["Cache-Control"] = "no-cache"
@@ -85,12 +110,20 @@ app.include_router(system.router)
app.include_router(connect.router) app.include_router(connect.router)
app.include_router(memory.router) app.include_router(memory.router)
app.include_router(agent.router) app.include_router(agent.router)
app.include_router(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab) app.include_router(
app.include_router(reminders_router.router) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten voice.router
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
app.include_router(
reminders_router.router
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto) app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
app.include_router(maintenance.router) app.include_router(maintenance.router)
app.include_router(console.router) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all app.include_router(
app.include_router(hermes_ui.router) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all console.router
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
app.include_router(
hermes_ui.router
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html. # Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
@@ -113,6 +146,7 @@ if FRONTEND_DIST.exists():
index = FRONTEND_DIST / "index.html" index = FRONTEND_DIST / "index.html"
if index.exists(): if index.exists():
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle. # index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
return FileResponse(index, headers={"Cache-Control": "no-cache, must-revalidate"}) return FileResponse(
index, headers={"Cache-Control": "no-cache, must-revalidate"}
)
return {"detail": "frontend not built"} return {"detail": "frontend not built"}
+6
View File
@@ -97,6 +97,10 @@ _HUI_DIST="$HOME/.hermes/hermes-agent/hermes_cli/web_dist"
if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \ if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \
--base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then --base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then
# Dienst VOR dem Tausch stoppen — sonst crasht der laufende `hermes serve`, wenn er index.html
# im Lösch-Fenster (rm→cp) liest (FileNotFoundError → Start-Limit → tot). Der reguläre restart
# weiter unten bringt ihn sauber auf der neuen web_dist hoch.
systemctl --user stop hermes-builtin-ui 2>/dev/null || true
rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut." rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut."
else else
echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt." echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt."
@@ -118,6 +122,8 @@ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor). # Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true [ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
# Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein. # Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein.
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
systemctl --user restart hermes-builtin-ui 2>/dev/null || true systemctl --user restart hermes-builtin-ui 2>/dev/null || true
systemctl --user restart mission-control-2 systemctl --user restart mission-control-2
command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/dev/null || true command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/dev/null || true
+52
View File
@@ -0,0 +1,52 @@
# Antigravity-Review-Prompt — Mission Control 2.0
Prompt zum Einfügen in Antigravity 2.0 (Modell: **Gemini 3.1 Pro, High-Modus**). Projektordner =
dieses Repo. `.aiexclude` hält `.venv`/`node_modules`/`dist` aus dem Kontext.
---
Du bist ein erfahrener Senior-Reviewer und führst ein **vollständiges, ehrliches Code- und
Architektur-Review** dieses Projekts (Mission Control 2.0, kurz MC2) durch.
**Zuerst lesen — das ist der Rahmen, weiche nicht davon ab:**
1. `AGENTS.md` — die verbindlichen Projekt-Regeln.
2. `SAVEPOINT.md` — wo das Projekt gerade steht, Nordstern und Randbedingungen.
3. `README.md` und `docs/` — Kontext.
**Was MC2 ist:** die Web-Admin-Konsole einer **100 % lokalen** KI-Appliance (AMD Strix Halo, 128 GB,
llama.cpp-Vulkan). Backend = FastAPI (`backend/`, Dienst auf :9001), Frontend = React/Vite/Tailwind
(`frontend/`). Sidecars: `mem0_service/`, `voice_service/`, `mcp/`, `hermes/`, `client/`.
**Nordstern:** Das System muss **über Jahre ohne Wartung** laufen, betrieben von einem
**Nicht-Entwickler**. Robustheit, Einfachheit und Selbstheilung schlagen Features.
## Dein Auftrag
Prüfe das gesamte Repo (außer dem, was `.aiexclude` ausschließt). Suche gezielt nach:
- **Korrektheits-Bugs & Race Conditions** — besonders in `backend/services/` und den Routern.
- **Betriebs-Fragilität:** Was bricht bei **Reboot**, **Update** oder **Teil-Ausfall**? Was ist nicht
reboot-/idempotenz-fest? (Der Betreiber kann es nicht selbst debuggen.)
- **Sicherheitslücken** — Auth/Token-Handling, Command-Allowlists, Prompt-Injection-Guards,
Pfad-/Eingabe-Validierung, `sudo -n`-Nutzung. (Nur MELDEN, nichts an der Security-Config ändern.)
- **Ressourcen-Lecks & Fehlerbehandlung** — offene Clients/Dateien, verschluckte Exceptions,
fehlende Timeouts.
- **Toter/redundanter Code, DRY-/KISS-Verstöße, Über-Komplexität** — Vereinfachungs-Chancen.
- **Doku-/Realitäts-Drift** — Widersprüche zwischen `AGENTS.md`/Kommentaren und dem Code
(z. B. Zeitzone UTC vs. Europe/Berlin).
- **Wartbarkeit** — würde ein Fremder das in einem Jahr noch verstehen und sicher anfassen können?
## Harte Leitplanken (nicht empfehlen)
- KEINE Cloud-Migration, KEIN Voll-Rewrite, KEINE schweren neuen Frameworks.
- KEINE Modell-/Engine-Wechsel, die nicht in ~124 GB passen; keine Cloud-LLMs.
- `frontend/dist` bleibt bewusst im git (Box hat kein Node) — nicht als Fehler werten.
- Security-Config nur MELDEN, nie „einfach ändern".
- Bevorzuge **Vereinfachung und Härtung** vor neuen Features.
## Ausgabeformat (auf DEUTSCH)
1. **Gesamturteil** (35 Sätze): Wie gesund ist das Projekt? Trägt es den Nordstern „Jahre ohne Wartung"?
2. **Befunde, nach Schwere sortiert** — je Befund:
- Schweregrad **P0** (bricht/Sicherheit) · **P1** (echter Bug/Fragilität) · **P2** (Wartbarkeit) · **P3** (nice-to-have)
- `datei:zeile` · was ist das Problem · **warum es zählt** (konkretes Fehlszenario) · **konkreter Fix**
3. **Positives** — was bewusst gut gelöst ist (damit es nicht „wegoptimiert" wird).
4. **Was ich bewusst NICHT ändern würde** — und warum (Respekt vor den getroffenen Entscheidungen).
Sei ehrlich und konkret. Erfinde keine Probleme, um die Liste zu füllen. Wenn etwas gut ist, sag das.
-3
View File
@@ -35,8 +35,5 @@
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^6.0.3" "vite": "^6.0.3"
},
"allowScripts": {
"esbuild@0.25.12": true
} }
} }