Feat: UI-Audit — Telegram/MCP/PC-Status, incomplete-Badge, SSH-Karte ersetzt

- backend: PC_EXECUTOR_URL Env-Var + 3 neue agent_status()-Felder
  (telegram_enabled, mcp_server_count, pc_executor_reachable)
- AgentStatusCard: 2x2-Grid (Gateway/WebUI/Gehirn/Verdrahtung) +
  echte Status-Footer-Zeilen fuer Telegram, MCP-Server-Anzahl, PC
- TokenStatsCard: 'Juni 2026' entfernt, Null-Fallback fuer pricing
- AgentView: SSH-Bypass-Karte ersetzt durch PC-Executor-Statuskarte
- Cockpit + RolesCard: Datei-fehlt-Badge + Load-Button disabled fuer incomplete-Modelle
- api.ts: AgentStatus-Interface um 3 neue Felder erweitert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 00:01:36 +02:00
parent 6a464bf653
commit 0afddcdfdc
13 changed files with 546 additions and 455 deletions
+2
View File
@@ -50,6 +50,8 @@ GATEWAY_CONFIG_PATH = Path(os.environ.get(
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("/")
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("/")
# --- Server ------------------------------------------------------------------
HOST = os.environ.get("MC_HOST", "0.0.0.0")
+24 -1
View File
@@ -5,10 +5,11 @@ Hermes' eigener Config verdrahtet (siehe docs/HERMES_SETUP.md).
"""
import logging
import os
import httpx
from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL
from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL, PC_EXECUTOR_URL
log = logging.getLogger(__name__)
@@ -21,6 +22,24 @@ def _reach(url: str, path: str = "") -> bool:
return False
def _count_enabled_mcp_servers() -> int:
config_path = HERMES_HOME / "config.yaml"
if not config_path.exists():
return 0
try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("r", encoding="utf-8") as f:
cfg = r_yaml.load(f) or {}
mcp_servers = cfg.get("mcp_servers", {}) if isinstance(cfg, dict) else {}
if not isinstance(mcp_servers, dict):
return 0
return sum(1 for v in mcp_servers.values() if isinstance(v, dict) and v.get("enabled", True))
except Exception:
log.debug("_count_enabled_mcp_servers: Fehler", exc_info=True)
return 0
def agent_status() -> dict:
"""Erreichbarkeit von Gateway (:8642) + WebUI (:8787) + lokale Hinweise."""
home = HERMES_HOME
@@ -49,6 +68,10 @@ def agent_status() -> dict:
"has_config": (home / "config.yaml").exists() or (home / "config.json").exists(),
"has_skills": (home / "skills").exists(),
"has_memories": (home / "memories").exists(),
# Neue Felder: Telegram, MCP-Server-Anzahl, PC-Executor-Erreichbarkeit.
"telegram_enabled": bool(os.environ.get("TELEGRAM_BOT_TOKEN", "")),
"mcp_server_count": _count_enabled_mcp_servers(),
"pc_executor_reachable": _reach(PC_EXECUTOR_URL, "/health"),
}
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
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-l7I88DA-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1UOWeoZj.css">
<script type="module" crossorigin src="/assets/index-BRFJuGKC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C0dfTDFj.css">
</head>
<body>
<div id="root"></div>
@@ -1,5 +1,5 @@
import { useState } from "react"
import { Bot, ExternalLink, Cpu, Layers, X, Check } from "lucide-react"
import { Bot, ExternalLink, Cpu, Layers, Wrench, X, Check } from "lucide-react"
import { api } from "@/lib/api"
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
@@ -51,37 +51,43 @@ export function AgentStatusCard() {
</div>
{agent ? (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Gateway</div>
<div className="flex items-center gap-2 mt-1">
<span className={cn("h-2 w-2 rounded-full", agent.gateway_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.gateway_reachable ? "Online" : "Offline"}</span>
</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">WebUI</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>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Gateway</div>
<div className="flex items-center gap-2 mt-1">
<span className={cn("h-2 w-2 rounded-full", agent.gateway_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.gateway_reachable ? "Online" : "Offline"}</span>
</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">WebUI</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>
</div>
</div>
<div
onClick={() => setShowBrainSelect(true)}
className="p-3 bg-background/20 rounded-xl border border-border/40 hover:border-primary/45 hover:bg-background/30 transition-all duration-300 cursor-pointer group"
className="p-3 bg-background/20 rounded-xl border border-border/40 hover:border-primary/45 hover:bg-background/30 transition-all duration-300 cursor-pointer"
>
<div className="flex justify-between items-center">
<span className="text-[10px] text-muted-foreground uppercase font-semibold">Aktives Gehirn</span>
<button className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-primary border border-primary/20 bg-primary/10 hover:bg-primary/20 px-1.5 py-0.5 rounded transition-all cursor-pointer font-space">
<Cpu className="h-3 w-3" /> Ändern
</button>
<span className="text-[10px] text-muted-foreground uppercase font-semibold">Gehirn</span>
<Cpu className="h-3 w-3 text-primary" />
</div>
<div className="text-xs font-medium mt-1 font-mono text-primary flex items-center gap-1.5">
<Layers className="h-3.5 w-3.5" />
{agent.brain_model ? `model: ${agent.brain_model}` : "model: auto"}
<div className="text-xs font-medium mt-1 font-mono text-primary truncate flex items-center gap-1">
<Layers className="h-3 w-3 shrink-0" />
{agent.brain_model || "auto"}
</div>
</div>
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
<div className="flex justify-between items-center">
<span className="text-[10px] text-muted-foreground uppercase font-semibold">Verdrahtung</span>
<Wrench className="h-3 w-3 text-primary" />
</div>
<div className="text-[10px] font-mono mt-1 space-y-0.5 text-muted-foreground">
<div>Config: {agent.has_config ? <span className="text-emerald-400"></span> : "—"}</div>
<div>Skills: {agent.has_skills ? <span className="text-emerald-400"></span> : "—"}</div>
<div>Memory: {agent.has_memories ? <span className="text-emerald-400"></span> : "—"}</div>
</div>
</div>
</div>
@@ -89,9 +95,26 @@ export function AgentStatusCard() {
<div className="h-28 flex items-center justify-center text-xs text-muted-foreground">Lade Agenten-Status</div>
)}
</div>
<div className="mt-3 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
Gedächtnis & Stack-Tools via MCP gekoppelt.
</div>
{agent && (
<div className="mt-3 border-t border-border/30 pt-3 space-y-1.5">
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
<span>Telegram</span>
<span className={cn("font-semibold", agent.telegram_enabled ? "text-emerald-400" : "")}>
{agent.telegram_enabled ? "aktiv" : "nicht konfiguriert"}
</span>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
<span>MCP-Server</span>
<span className="font-semibold text-foreground">{agent.mcp_server_count ?? 0} verbunden</span>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
<span>PC Executor</span>
<span className={cn("font-semibold", agent.pc_executor_reachable ? "text-emerald-400" : "")}>
{agent.pc_executor_reachable ? "erreichbar" : "nicht verbunden"}
</span>
</div>
</div>
)}
{agent && showBrainSelect && (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
@@ -68,6 +68,11 @@ export function RolesCard() {
SLOTS: {m.parallel_slots}
</span>
)}
{m.incomplete && (
<span className="text-[7px] leading-none font-mono font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 px-1 py-0.5 rounded" title="GGUF-Datei fehlt">
fehlt
</span>
)}
</div>
)}
</div>
@@ -51,9 +51,9 @@ export function TokenStatsCard() {
)}
</div>
<div className="mt-4 text-[9px] text-muted-foreground/70 border-t border-border/30 pt-2.5 leading-normal">
Berechnet im Vergleich zu Cloud-APIs von Juni 2026
Berechnet ggü. Cloud-APIs
{tokenStats?.pricing?.heavy
? `${tokenStats.pricing.heavy.in.toFixed(2).replace(".", ",")} $ / ${tokenStats.pricing.heavy.out.toFixed(2).replace(".", ",")} $ pro 1M tkn).`
? `${(tokenStats.pricing.heavy.in ?? 0).toFixed(2).replace(".", ",")} $ / ${(tokenStats.pricing.heavy.out ?? 0).toFixed(2).replace(".", ",")} $ pro 1M tkn).`
: "."}
</div>
</div>
+3
View File
@@ -224,6 +224,9 @@ export interface AgentStatus {
has_config: boolean
has_skills: boolean
has_memories: boolean
telegram_enabled?: boolean
mcp_server_count?: number
pc_executor_reachable?: boolean
}
export interface Job {
+36 -22
View File
@@ -1,5 +1,5 @@
import { useState, useRef, useCallback, useMemo } from "react"
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield, Check, X } from "lucide-react"
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
import { api } from "@/lib/api"
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
@@ -327,37 +327,51 @@ export function AgentView() {
</div>
</div>
{/* SSH / Sandbox-Hilfe Card */}
{/* PC Executor Status Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-6 space-y-4 shadow-lg shadow-black/10">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">SSH-Kopplung &amp; Sandbox-Umgehung</h3>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Monitor className="h-5 w-5 text-primary" />
<h3 className="text-sm font-bold uppercase tracking-wider text-foreground">PC Verbindung</h3>
</div>
<div className="flex items-center gap-2">
<span className={cn("h-2 w-2 rounded-full", s.pc_executor_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-semibold text-foreground">
{s.pc_executor_reachable ? "Erreichbar" : "Nicht verbunden"}
</span>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2 text-xs text-muted-foreground leading-relaxed text-left">
<div className="space-y-3">
<h4 className="text-xs font-bold text-foreground">1. Warum verweigert Hermes Aktionen auf dem PC?</h4>
<p>
Hermes ist standardmäßig so ausgerichtet, dass er sich als virtueller Assistent ohne Systemzugriff versteht.
Um dies zu umgehen, musst du Hermes' **System-Prompt** in der Chat-WebUI anpassen.
Der <strong className="text-foreground">Hermes PC Executor</strong> läuft auf <code className="text-foreground font-mono px-1 py-0.5 rounded bg-muted/40 border border-border/30">192.168.178.98:7777</code> und ermöglicht Hermes den direkten Zugriff auf deinen Windows-PC Shell-Befehle, Screenshots, Tastatureingaben.
</p>
<p>
Hermes erreicht den PC über den MCP-Server <code className="text-foreground font-mono px-1 py-0.5 rounded bg-muted/40 border border-border/30">hermes-pc-control</code>, der automatisch beim Gateway-Start verbunden wird.
</p>
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[9px] text-cyan-300">
<span className="text-muted-foreground/60">Beispiel System-Prompt Erweiterung:</span>
<br />
"Du hast über das terminal-Tool vollen SSH-Zugriff auf den Windows-PC des Users (TobisPC). Nutze SSH-Befehle um Aktionen dort auszuführen, wenn danach gefragt wird. Behaupte nie, du könntest das nicht!"
</div>
</div>
<div className="space-y-3">
<h4 className="text-xs font-bold text-foreground">2. SSH-Verbindung zum lokalen Windows-PC einrichten</h4>
<p>
Damit der Agent Befehle auf deinem PC ausführen kann, muss OpenSSH auf Windows aktiv und mit einem Key gekoppelt sein:
</p>
<ul className="list-disc pl-4 space-y-1.5 text-[11px]">
<li><strong>OpenSSH Server auf Windows starten:</strong> In PowerShell als Admin ausführen: <code className="text-foreground font-mono">Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0</code></li>
<li><strong>SSH-Key auf der Box erzeugen:</strong> <code className="text-foreground font-mono">ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_hermes_agent</code></li>
<li><strong>Key autorisieren:</strong> Kopiere den Inhalt von <code className="text-foreground font-mono">~/.ssh/id_ed25519_hermes_agent.pub</code> in deine Windows-Datei <code className="text-foreground font-mono">C:\Users\TobisPC\.ssh\authorized_keys</code></li>
</ul>
{s.pc_executor_reachable ? (
<div className="p-3.5 bg-emerald-500/5 border border-emerald-500/20 rounded-xl space-y-1">
<div className="text-emerald-400 font-semibold text-xs flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" />
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>.
</p>
</div>
) : (
<div className="space-y-2">
<h4 className="text-xs font-bold text-foreground">PC Executor starten</h4>
<p>Starte <code className="text-foreground font-mono">start.bat</code> im Ordner <code className="text-foreground font-mono">client\hermes-pc\</code> auf dem Windows-PC und lasse das Fenster offen.</p>
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[9px] text-cyan-300">
client\hermes-pc\start.bat
</div>
</div>
)}
</div>
</div>
</div>
+24 -8
View File
@@ -754,6 +754,11 @@ export function Cockpit() {
SLOTS: {m.parallel_slots}
</span>
)}
{m.incomplete && (
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-mono text-amber-400 font-bold uppercase" title="GGUF-Datei fehlt — Modell kann nicht geladen werden">
Datei fehlt
</span>
)}
</div>
</div>
</div>
@@ -803,11 +808,14 @@ export function Cockpit() {
<div className="flex items-center gap-1.5 border-t border-border/20 pt-3 mt-auto">
<button
onClick={() => isRunning ? handleUnloadModel(m.name) : handleLoadModel(m.name)}
disabled={m.incomplete && !isRunning}
className={cn(
"flex-1 h-7 rounded-lg text-[9px] font-bold uppercase transition-all cursor-pointer border flex items-center justify-center gap-1",
isRunning
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400"
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary"
"flex-1 h-7 rounded-lg text-[9px] font-bold uppercase transition-all border flex items-center justify-center gap-1",
m.incomplete && !isRunning
? "border-border/30 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
: isRunning
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400 cursor-pointer"
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary cursor-pointer"
)}
>
{isRunning ? "Entladen" : "Laden"}
@@ -889,6 +897,11 @@ export function Cockpit() {
SLOTS: {m.parallel_slots}
</span>
)}
{m.incomplete && (
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[8px] font-mono text-amber-400 font-bold uppercase shrink-0" title="GGUF-Datei fehlt">
Datei fehlt
</span>
)}
{isRunning && (
<span className="flex items-center gap-0.5 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider shrink-0">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> warm
@@ -914,11 +927,14 @@ export function Cockpit() {
<div className="flex items-center gap-1.5">
<button
onClick={() => isRunning ? handleUnloadModel(m.name) : handleLoadModel(m.name)}
disabled={m.incomplete && !isRunning}
className={cn(
"h-7 px-3 rounded-lg text-[9px] font-bold uppercase transition-all cursor-pointer border flex items-center gap-1",
isRunning
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400"
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary"
"h-7 px-3 rounded-lg text-[9px] font-bold uppercase transition-all border flex items-center gap-1",
m.incomplete && !isRunning
? "border-border/30 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
: isRunning
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400 cursor-pointer"
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary cursor-pointer"
)}
>
{isRunning ? "Entladen" : "Laden"}