Feat: Hermes-Terminal (ttyd) statt AnythingLLM + AnythingLLM komplett raus

Paket B des Plans. AnythingLLM war nur ein Fallback-Chat zu Hermes; ersetzt durch das
echte interaktive Agent-Terminal (`hermes chat`, mit Tools/PC) als eingebettetes
Web-Terminal.

Box: ttyd (apt) wrappt `hermes chat`; systemd-User-Unit deploy/hermes-terminal.service
(LAN-Bind eno1:7681, apt-Default-ttyd-Dienst deaktiviert). In deploy.sh verankert.

Backend: config HERMES_TERMINAL_URL statt ANYTHINGLLM_URL/_REPO; agent_status liefert
terminal_url/terminal_reachable; maintenance ohne _anythingllm_update; system.py Dienst-Liste
zeigt "Hermes-Terminal".

Frontend: neue Terminal-Seite (iframe auf ttyd) + Nav-Tab; AgentView/AgentStatusCard/nav/api
auf Terminal umgestellt; SystemDrawer toter hermes-dashboard raus, hermes-webui -> hermes-terminal;
Guide-Texte aktualisiert.

Cleanup: deploy/hermes-webui.service + deploy/lobechat/ entfernt (LobeChat-Migration hinfaellig),
HERMES_WEBUI_URL-Env raus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 18:23:37 +02:00
parent cf65589b93
commit dcfede7e69
23 changed files with 246 additions and 281 deletions
+5 -6
View File
@@ -53,14 +53,13 @@ HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
# Gateway (OpenAI-API des Agenten) + Chat-WebUI = AnythingLLM (eigener Host).
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
HERMES_WEBUI_URL = os.environ.get("HERMES_WEBUI_URL", "http://127.0.0.1:8787").rstrip("/")
# AnythingLLM = die Chat-WebUI (löst nesquena/hermes-webui ab). Eigener Host im LAN.
ANYTHINGLLM_URL = os.environ.get("MC_ANYTHINGLLM_URL", "http://192.168.178.155:3001").rstrip("/")
# GitHub-Repos für Update-Checks.
# Hermes-Terminal: ttyd-Web-Terminal der interaktiven Agent-CLI (Ersatz für AnythingLLM-Chat).
# Wird in MC2 per iframe eingebettet (Terminal-Seite). Siehe deploy/hermes-terminal.service.
HERMES_TERMINAL_URL = os.environ.get("MC_HERMES_TERMINAL_URL", "http://192.168.178.151:7681").rstrip("/")
# GitHub-Repo für Update-Checks.
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
ANYTHINGLLM_REPO = os.environ.get("MC_ANYTHINGLLM_REPO", "Mintplex-Labs/anything-llm")
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/")
+2 -2
View File
@@ -46,12 +46,12 @@ def services() -> dict:
{"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
{"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
{"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
{"name": "AnythingLLM", "url": a["webui_url"], "ok": a["webui_reachable"]},
{"name": "Hermes-Terminal", "url": a["terminal_url"], "ok": a["terminal_reachable"]},
],
"links": {
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
"gateway": gw_url,
"hermes_webui": a["webui_url"],
"hermes_terminal": a["terminal_url"],
},
}
+4 -4
View File
@@ -11,7 +11,7 @@ import re
import httpx
import psutil
from config import ANYTHINGLLM_URL, HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL
from config import HERMES_TERMINAL_URL, HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL
log = logging.getLogger(__name__)
@@ -162,10 +162,10 @@ def agent_status() -> dict:
return {
"gateway_url": HERMES_API_URL,
# Chat-WebUI ist jetzt AnythingLLM (eigener Host), nicht mehr hermes-webui :8787.
"webui_url": ANYTHINGLLM_URL,
# Interaktives Web-Terminal (ttyd → `hermes chat`), eingebettet in MC2.
"terminal_url": HERMES_TERMINAL_URL,
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
"webui_reachable": _reach(ANYTHINGLLM_URL, "/api/ping"),
"terminal_reachable": _reach(HERMES_TERMINAL_URL, "/"),
"home_exists": home.exists(),
"brain_model": brain_model,
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
+3 -19
View File
@@ -15,7 +15,7 @@ from datetime import datetime
import httpx
import psutil
from config import ANYTHINGLLM_REPO, ANYTHINGLLM_URL, HERMES_AGENT_REPO
from config import HERMES_AGENT_REPO
from services import catalog, discover, jobengine, llamaswap, system
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
@@ -128,28 +128,12 @@ def _hermes_agent_update() -> dict:
return info
def _anythingllm_update() -> dict:
"""AnythingLLM: erreichbar? + neueste verfügbare Version. Installierte Version ist
remote nicht unauth. abfragbar → update=None (nur Info: neueste verfügbar)."""
reachable = False
try:
reachable = httpx.get(f"{ANYTHINGLLM_URL}/api/ping", timeout=4).status_code == 200
except Exception:
reachable = False
info = {"key": "anythingllm", "name": "AnythingLLM", "current": None,
"latest": None, "update": None, "reachable": reachable}
gh = _gh_latest(ANYTHINGLLM_REPO)
if gh:
info["latest"] = gh["tag"]
return info
def _components_cached() -> list[dict]:
"""Update-Status von Hermes-Agent + AnythingLLM (1h-Cache → GitHub schonen)."""
"""Update-Status von Hermes-Agent (1h-Cache → GitHub schonen)."""
now = time.time()
if now - _comp_cache["ts"] < 3600 and _comp_cache["data"]:
return _comp_cache["data"]
data = [_hermes_agent_update(), _anythingllm_update()]
data = [_hermes_agent_update()]
_comp_cache.update(ts=now, data=data)
return data
+6 -1
View File
@@ -19,13 +19,18 @@ fi
"$SRC/backend/.venv/bin/python" -m pip install -q --upgrade pip
"$SRC/backend/.venv/bin/python" -m pip install -q -r "$SRC/backend/requirements.txt"
# systemd-USER-Unit installieren/aktualisieren
# systemd-USER-Units installieren/aktualisieren
mkdir -p "$HOME/.config/systemd/user"
cp "$SRC/deploy/mission-control-2.service" "$HOME/.config/systemd/user/mission-control-2.service"
# Hermes-Terminal (ttyd -> `hermes chat`), in MC2 als Terminal-Seite eingebettet.
# Einmalig manuell noetig: `sudo apt install -y ttyd` + `sudo systemctl disable --now ttyd` (apt-Default-Dienst).
cp "$SRC/deploy/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-terminal.service"
systemctl --user daemon-reload
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
systemctl --user enable hermes-terminal >/dev/null 2>&1 || true
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
systemctl --user restart mission-control-2
command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/dev/null || true
sleep 2
echo "--- Health ---"
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=Hermes Terminal — ttyd web terminal wrapping `hermes chat` (interaktiver Agent)
Documentation=https://github.com/tsl0922/ttyd
After=network.target
[Service]
# Exponiert die interaktive Hermes-Agent-CLI (`hermes chat`, voller Agent mit Tools/PC) als
# Web-Terminal. Wird in MC2 per iframe eingebettet (Terminal-Seite) — Ersatz fuer AnythingLLM.
#
# SICHERHEIT: --writable + LAN-Bind ohne Auth = dasselbe Trust-Modell wie das MC2-Dashboard
# (vertrautes Heim-LAN, kein Internet-Exposure). Fuer Basic-Auth: am ExecStart
# --credential <user>:<pass>
# ergaenzen (Browser fragt dann einmalig im iframe nach).
Type=simple
# --interface eno1 = LAN-Bind (box-spezifisch; eno1 traegt 192.168.178.151).
ExecStart=/usr/bin/ttyd --writable --interface eno1 --port 7681 --max-clients 2 --cwd %h/.hermes %h/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main chat
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
-27
View File
@@ -1,27 +0,0 @@
# systemd-USER-Unit für das standalone Hermes-WebUI (nesquena/hermes-webui).
# Eigene Oberfläche des Agenten (Chat/Sessions/Workspace/Cron/Skills/Memory).
# An 0.0.0.0:8787 gebunden, damit der Windows-PC im LAN direkt zugreifen kann
# → Passwort-Schutz aktivieren (HERMES_WEBUI_PASSWORD)!
# Ablage: ~/.config/systemd/user/hermes-webui.service ; dann:
# systemctl --user enable --now hermes-webui
# loginctl enable-linger hitonabi # läuft auch ohne aktive Session
[Unit]
Description=Hermes WebUI (standalone)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/hermes-webui
# Start über das mitgelieferte Control-Skript (siehe docs/HERMES_SETUP.md).
ExecStart=%h/hermes-webui/ctl.sh start --foreground
Environment=HERMES_WEBUI_HOST=0.0.0.0
Environment=HERMES_WEBUI_PORT=8787
Environment=HERMES_HOME=%h/.hermes
# Environment=HERMES_WEBUI_PASSWORD=... # in ~/.config/environment.d/ setzen, NICHT hier committen
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
-13
View File
@@ -1,13 +0,0 @@
# LobeChat-Konfiguration (auf dem Proxmox-MiniPC). Kopiere zu .env und anpassen.
# OpenAI-kompatibler Endpoint der AI-Box. Optionen:
# MC2-Gateway (empfohlen, model:auto + alle Modelle): http://192.168.178.151:9001/v1
# llama-swap direkt (nur Modell-Liste): http://192.168.178.151:8080/v1
# hermes-agent (Agent mit Tools/MCP, falls gewünscht): http://192.168.178.151:8642/v1
AI_BOX_GATEWAY=http://192.168.178.151:9001/v1
# Dummy-Key (lokale Endpoints prüfen ihn nicht):
OPENAI_API_KEY=mc2-local
# Zugangscode fürs Web-UI (PFLICHT — frei wählen):
ACCESS_CODE=bitte-aendern
-42
View File
@@ -1,42 +0,0 @@
# LobeChat — Hermes-Chat-Frontend (ersetzt nesquena/hermes-webui)
Läuft auf dem **Proxmox-MiniPC** (nicht auf der AI-Box). Schlanke client-only Edition:
kein Postgres/Auth/S3, Verlauf liegt im Browser. Spricht über LAN den OpenAI-kompatiblen
**MC2-Gateway** der AI-Box an (`:9001/v1`, liefert `model:auto` + alle llama-swap-Modelle).
## Installation (auf dem Proxmox-Host bzw. einem LXC/VM mit Docker)
```bash
# In einem LXC/VM mit Docker + compose-Plugin:
mkdir -p ~/lobechat && cd ~/lobechat
# docker-compose.yml + .env.example aus diesem Ordner herkopieren
cp .env.example .env
nano .env # ACCESS_CODE setzen, AI_BOX_GATEWAY prüfen
docker compose up -d
```
Öffnen: `http://<proxmox-ip>:3210` → Zugangscode = `ACCESS_CODE`.
## Was ist vorkonfiguriert
- **Modell-Liste** auf `Hermes-4-14B`, `Qwen3.6-35B-A3B` (fast), `Qwen3-Coder-Next`,
`Qwen3.5-122B-A10B` (heavy) und `auto` fixiert (`OPENAI_MODEL_LIST`).
- **Hermes-Standardassistent** (Modell `Hermes-4-14B` + System-Prompt) via
`DEFAULT_AGENT_CONFIG`.
## Endpoint-Wahl (in `.env`)
- **MC2-Gateway `:9001/v1`** *(Default, empfohlen)* — OpenAI-kompatibel, `model:auto`,
Token-Tracking, alle Modelle.
- **llama-swap `:8080/v1`** — direkt, ohne Routing/Token-Tracking.
- **hermes-agent `:8642/v1`** — nur falls die Chat-UI den **Agenten** (Tools/MCP/Memory)
nutzen soll; API-Kompatibilität vorher prüfen.
## Cutover (alte hermes-webui ablösen)
Auf der **AI-Box**:
```bash
systemctl --user disable --now hermes-webui
```
In MC2 den „Hermes öffnen"-Link auf LobeChat zeigen lassen:
`HERMES_WEBUI_URL=http://<proxmox-ip>:3210` (env der `mission-control-2.service`).
## Hinweis
Tool-/Function-Calls streamt llama.cpp nicht nativ — für reine Chat-Nutzung irrelevant.
Bei Bedarf an persistenter History/RAG später auf die DB-Edition (Postgres+pgvector)
umstellen.
-25
View File
@@ -1,25 +0,0 @@
# LobeChat als Hermes-Chat-Frontend — läuft auf dem Proxmox-MiniPC (Docker/LXC),
# NICHT auf der AI-Box. Spricht über LAN den OpenAI-kompatiblen MC2-Gateway der Box an.
# Client-only Edition (kein Postgres/Auth/S3) — bewusst schlank ("nicht overengineeren").
#
# Start: cp .env.example .env && $EDITOR .env && docker compose up -d
# Öffnen: http://<proxmox-ip>:3210 (Zugangscode = ACCESS_CODE)
services:
lobe-chat:
image: lobehub/lobe-chat:latest
container_name: lobe-chat
restart: unless-stopped
ports:
- "3210:3210"
environment:
# --- Provider: MC2-Gateway der AI-Box (OpenAI-kompatibel, liefert model:auto + alle Modelle) ---
- OPENAI_API_KEY=${OPENAI_API_KEY:-mc2-local}
- OPENAI_PROXY_URL=${AI_BOX_GATEWAY:-http://192.168.178.151:9001/v1}
# Modell-Auswahl auf unsere llama-swap-Modelle fixieren (-all entfernt Defaults):
- "OPENAI_MODEL_LIST=-all,+Hermes-4-14B,+Qwen3.6-35B-A3B,+Qwen3-Coder-Next,+Qwen3.5-122B-A10B,+auto"
# --- Hermes-Standardassistent (Modell + System-Prompt) ---
- "DEFAULT_AGENT_CONFIG=model=Hermes-4-14B;provider=openai;params.temperature=0.7;systemRole=Du bist Hermes, der lokale KI-Assistent dieses Homelabs. Antworte präzise, hilfsbereit und auf Deutsch, sofern nicht anders gewünscht."
# --- Zugriffsschutz im LAN (einfacher Code statt voller Auth) ---
- ACCESS_CODE=${ACCESS_CODE:?bitte in .env setzen}
- ACCESS_CODE_ENABLED=1
-1
View File
@@ -21,7 +21,6 @@ Environment=MC_MODELS_DIR=/srv/models
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
Environment=MC_ENGINE_UPDATE_CMD=/usr/local/bin/update-llamacpp
Environment=HERMES_WEBUI_URL=http://192.168.178.155:3001
Restart=on-failure
RestartSec=3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BpalJKIe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CeszaZ7k.css">
<script type="module" crossorigin src="/assets/index-BtXN9NH2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SuFuJOVE.css">
</head>
<body>
<div id="root"></div>
+3 -1
View File
@@ -7,6 +7,7 @@ import { ModelsView } from "@/views/ModelsView"
import { ConnectView } from "@/views/ConnectView"
import { MemoryView } from "@/views/MemoryView"
import { AgentView } from "@/views/AgentView"
import { TerminalView } from "@/views/TerminalView"
import { GuideView } from "@/views/GuideView"
import { Placeholder } from "@/views/Placeholder"
import { SystemDrawer } from "@/components/SystemDrawer"
@@ -180,8 +181,9 @@ export default function App() {
{view === "connect" && <ConnectView />}
{view === "memory" && <MemoryView />}
{view === "agent" && <AgentView />}
{view === "terminal" && <TerminalView />}
{view === "guide" && <GuideView />}
{!["dashboard", "models", "connect", "memory", "agent", "guide"].includes(view) && (
{!["dashboard", "models", "connect", "memory", "agent", "terminal", "guide"].includes(view) && (
<Placeholder title={active.label} hint={active.hint} />
)}
</main>
+1 -2
View File
@@ -15,8 +15,7 @@ const SERVICES = [
{ id: "llama-swap", label: "Llama Swap", type: "system" },
{ id: "mission-control-2", label: "Mission Control 2", type: "user" },
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user" },
{ id: "hermes-dashboard", label: "Hermes Dashboard", type: "user" },
{ id: "hermes-webui", label: "Hermes WebUI", type: "user" }
{ id: "hermes-terminal", label: "Hermes Terminal", type: "user" }
]
function formatBytes(bytes?: number) {
@@ -33,19 +33,19 @@ export function AgentStatusCard() {
<Bot className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Hermes Agent</h2>
</div>
{agent?.webui_url && (
{agent?.terminal_url && (
<a
href={resolveExternalUrl(agent.webui_url)}
href={resolveExternalUrl(agent.terminal_url)}
target="_blank"
rel="noopener"
className={cn(
"flex items-center gap-1 px-3 py-1 rounded-lg text-xs font-medium transition-all",
agent.webui_reachable
agent.terminal_reachable
? "bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-primary/20"
: "border border-border text-muted-foreground"
)}
>
<ExternalLink className="h-3 w-3" /> AnythingLLM öffnen
<ExternalLink className="h-3 w-3" /> Terminal öffnen
</a>
)}
</div>
@@ -60,10 +60,10 @@ export function AgentStatusCard() {
</div>
</div>
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
<div className="text-[10px] text-muted-foreground uppercase font-semibold">AnythingLLM</div>
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Terminal</div>
<div className="flex items-center gap-2 mt-1">
<span className={cn("h-2 w-2 rounded-full", agent.webui_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.webui_reachable ? "Online" : "Offline"}</span>
<span className={cn("h-2 w-2 rounded-full", agent.terminal_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.terminal_reachable ? "Online" : "Offline"}</span>
</div>
</div>
<div
+5 -5
View File
@@ -227,15 +227,15 @@ export interface SystemStatus {
export interface ServicesResp {
services: { name: string; url: string; ok: boolean }[]
links: { engine_ui: string; gateway: string; hermes_webui: string }
links: { engine_ui: string; gateway: string; hermes_terminal: string }
}
export interface ComponentUpdate {
key: string // "hermes_agent" | "anythingllm"
key: string // z.B. "hermes_agent"
name: string
current: string | null
latest: string | null
update: boolean | null // null = unbestimmbar (z.B. AnythingLLM-Version remote nicht abfragbar)
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
reachable: boolean | null
}
@@ -278,9 +278,9 @@ export interface DedupeResult {
export interface AgentStatus {
gateway_url: string
webui_url: string
terminal_url: string
gateway_reachable: boolean
webui_reachable: boolean
terminal_reachable: boolean
home_exists: boolean
brain_model?: string
has_config: boolean
+4 -2
View File
@@ -4,11 +4,12 @@ import {
Brain,
Plug,
Bot,
TerminalSquare,
HelpCircle,
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "models" | "memory" | "connect" | "agent" | "guide"
export type ViewId = "dashboard" | "models" | "memory" | "connect" | "agent" | "terminal" | "guide"
export interface NavItem {
id: ViewId
@@ -23,7 +24,8 @@ export const NAV: NavItem[] = [
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
{ id: "agent", label: "Hermes", hint: "Agent-Status & AnythingLLM öffnen", icon: Bot },
{ id: "agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot },
{ id: "terminal", label: "Terminal", hint: "Interaktives Hermes-Agent-Terminal", icon: TerminalSquare },
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
]
+17 -17
View File
@@ -135,20 +135,20 @@ export function AgentView() {
</p>
</div>
{s?.webui_url && (
{s?.terminal_url && (
<a
href={resolveExternalUrl(s.webui_url)}
href={resolveExternalUrl(s.terminal_url)}
target="_blank"
rel="noopener"
className={cn(
"flex h-9 px-4 items-center gap-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer shadow-md shrink-0 self-start",
s.webui_reachable
s.terminal_reachable
? "bg-primary text-primary-foreground hover:opacity-90 shadow-primary/10"
: "border border-border/60 text-muted-foreground bg-background/20"
)}
>
<ExternalLink className="h-4 w-4" />
<span>AnythingLLM öffnen</span>
<span>Terminal öffnen</span>
</a>
)}
</div>
@@ -170,9 +170,9 @@ export function AgentView() {
icon={Bot}
/>
<Tile
label="AnythingLLM"
ok={s.webui_reachable}
detail="Chat-UI (AnythingLLM)"
label="Terminal"
ok={s.terminal_reachable}
detail="Web-Terminal (hermes chat)"
icon={Activity}
/>
<Tile
@@ -213,9 +213,9 @@ export function AgentView() {
</linearGradient>
</defs>
{/* Connection: WebUI to Gateway */}
{/* Connection: Terminal to Gateway */}
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "webui" || s.webui_reachable) && (
{(hoveredNode === "terminal" || s.terminal_reachable) && (
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
@@ -236,14 +236,14 @@ export function AgentView() {
<div
className="absolute cursor-pointer select-none z-10 w-36 h-9 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2.5 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "15%", top: "50%" }}
onMouseEnter={() => setHoveredNode("webui")}
onMouseEnter={() => setHoveredNode("terminal")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
title={s.webui_reachable ? "Klicken um AnythingLLM zu öffnen" : "AnythingLLM offline"}
onClick={() => s.terminal_reachable && window.open(resolveExternalUrl(s.terminal_url), "_blank")}
title={s.terminal_reachable ? "Klicken um das Hermes-Terminal zu öffnen" : "Terminal offline"}
>
<Activity className={cn("h-3.5 w-3.5", s.webui_reachable ? "text-emerald-400" : "text-amber-500")} />
<span>AnythingLLM</span>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.webui_reachable ? "bg-emerald-500" : "bg-amber-500")} />
<Activity className={cn("h-3.5 w-3.5", s.terminal_reachable ? "text-emerald-400" : "text-amber-500")} />
<span>Terminal</span>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.terminal_reachable ? "bg-emerald-500" : "bg-amber-500")} />
</div>
{/* COLUMN 2: Gateway */}
@@ -360,7 +360,7 @@ export function AgentView() {
PC Executor verbunden
</div>
<p className="text-[10px]">
Hermes kann jetzt per Telegram oder WebUI Befehle auf TobisNicerPC ausführen. Nutze <code className="text-foreground">pc_shell</code>, <code className="text-foreground">pc_screenshot</code> oder <code className="text-foreground">pc_open</code>.
Hermes kann jetzt per Telegram oder Terminal Befehle auf TobisNicerPC ausführen. Nutze <code className="text-foreground">pc_shell</code>, <code className="text-foreground">pc_screenshot</code> oder <code className="text-foreground">pc_open</code>.
</p>
</div>
) : (
@@ -395,7 +395,7 @@ export function AgentView() {
<div className="p-3.5 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1">
<div className="text-muted-foreground/60"># Dienste manuell auf der Box prüfen:</div>
<div className="text-primary">systemctl --user status hermes-gateway</div>
<div className="text-primary">systemctl --user status hermes-webui</div>
<div className="text-primary">systemctl --user status hermes-terminal</div>
</div>
<p>
+3 -3
View File
@@ -328,8 +328,8 @@ export function GuideView() {
<ul className="text-[11px] text-muted-foreground space-y-2 list-disc pl-4 leading-relaxed">
<li><strong>Keine Verbindung?</strong> Überprüfe im Live-Verbindungsprüfer oben, ob die Box-IP erreichbar ist. Stelle sicher, dass dein lokaler PC im selben WLAN/LAN-Netzwerk wie die Box eingeloggt ist.</li>
<li><strong>Modell antwortet nicht?</strong> Schaue unter <strong>Diagnose</strong>, ob der Dienst `llama-swap` aktiv (grün) is. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte in AnythingLLM einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
<li><strong>Modell antwortet nicht?</strong> Schaue in der <strong>Zentrale</strong> unter <strong>Dienste</strong>, ob `llama-swap` aktiv (grün) ist. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte im <strong>Terminal</strong> eine frische Session (neuer `hermes chat`). Ältere Sessions sammeln Kontext-Müll an.</li>
</ul>
</div>
</>
@@ -474,7 +474,7 @@ description: Drive development with strict TDD practices
<ul className="list-disc pl-4 space-y-1">
<li><strong>Chat-Kontext sauber halten:</strong> Starte regelmäßig neue Chats. Zu große Historien verlangsamen den Agenten und führen zu Halluzinationen.</li>
<li><strong>Gehirn festlegen:</strong> Konfiguriere im Gateway die Modell-Rolle <code>brain</code> für Hermes, damit er automatisch das passende Modell per Llama Swap lädt.</li>
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (AnythingLLM-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (`~/.hermes/SOUL.md` / Config), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
</ul>
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
import { TerminalSquare, ExternalLink, AlertTriangle } from "lucide-react"
import { useAgentStatus } from "@/lib/queries"
import { resolveExternalUrl } from "@/lib/utils"
export function TerminalView() {
const { data: agent } = useAgentStatus(5_000)
const url = agent?.terminal_url ? resolveExternalUrl(agent.terminal_url) : undefined
const ok = agent?.terminal_reachable
return (
<div className="flex h-full flex-col gap-4">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
Hermes Terminal
</h1>
<p className="flex items-center gap-2 text-sm text-muted-foreground">
Interaktiver Agent (<code className="rounded bg-background/40 px-1 font-mono text-[11px] text-primary">hermes chat</code>) mit Tools &amp; PC-Steuerung mehr als Chatten".
</p>
</div>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
<span className={`h-2 w-2 rounded-full ${ok ? "bg-emerald-500 animate-pulse" : "bg-amber-500"}`} />
{ok ? "online" : "offline"}
</span>
{url && (
<a href={url} target="_blank" rel="noopener"
className="flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground">
<ExternalLink className="h-3.5 w-3.5" /> In neuem Tab
</a>
)}
</div>
</div>
{url ? (
<div className="relative min-h-[68vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25">
{ok === false && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center">
<AlertTriangle className="h-8 w-8 text-amber-400" />
<div className="text-sm font-semibold text-amber-300">Terminal nicht erreichbar</div>
<p className="max-w-sm text-[11px] leading-normal text-muted-foreground">
Der <code className="font-mono text-primary">hermes-terminal</code>-Dienst (ttyd) läuft nicht.
Auf der Box: <code className="font-mono">systemctl --user restart hermes-terminal</code>.
</p>
</div>
)}
<iframe src={url} title="Hermes Terminal" className="h-full w-full border-0" style={{ minHeight: "68vh" }} />
</div>
) : (
<div className="flex min-h-[68vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground">
<TerminalSquare className="mr-2 h-4 w-4" /> Lade Terminal
</div>
)}
</div>
)
}