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:
+5
-6
@@ -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("/")
|
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) -------------------------------
|
# --- 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_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-Terminal: ttyd-Web-Terminal der interaktiven Agent-CLI (Ersatz für AnythingLLM-Chat).
|
||||||
# AnythingLLM = die Chat-WebUI (löst nesquena/hermes-webui ab). Eigener Host im LAN.
|
# Wird in MC2 per iframe eingebettet (Terminal-Seite). Siehe deploy/hermes-terminal.service.
|
||||||
ANYTHINGLLM_URL = os.environ.get("MC_ANYTHINGLLM_URL", "http://192.168.178.155:3001").rstrip("/")
|
HERMES_TERMINAL_URL = os.environ.get("MC_HERMES_TERMINAL_URL", "http://192.168.178.151:7681").rstrip("/")
|
||||||
# GitHub-Repos für Update-Checks.
|
# GitHub-Repo für Update-Checks.
|
||||||
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
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")))
|
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 — 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("/")
|
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/")
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ def services() -> dict:
|
|||||||
{"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
|
{"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
|
||||||
{"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
|
{"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
|
||||||
{"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["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": {
|
"links": {
|
||||||
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
|
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
|
||||||
"gateway": gw_url,
|
"gateway": gw_url,
|
||||||
"hermes_webui": a["webui_url"],
|
"hermes_terminal": a["terminal_url"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import re
|
|||||||
import httpx
|
import httpx
|
||||||
import psutil
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -162,10 +162,10 @@ def agent_status() -> dict:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"gateway_url": HERMES_API_URL,
|
"gateway_url": HERMES_API_URL,
|
||||||
# Chat-WebUI ist jetzt AnythingLLM (eigener Host), nicht mehr hermes-webui :8787.
|
# Interaktives Web-Terminal (ttyd → `hermes chat`), eingebettet in MC2.
|
||||||
"webui_url": ANYTHINGLLM_URL,
|
"terminal_url": HERMES_TERMINAL_URL,
|
||||||
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
|
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
|
||||||
"webui_reachable": _reach(ANYTHINGLLM_URL, "/api/ping"),
|
"terminal_reachable": _reach(HERMES_TERMINAL_URL, "/"),
|
||||||
"home_exists": home.exists(),
|
"home_exists": home.exists(),
|
||||||
"brain_model": brain_model,
|
"brain_model": brain_model,
|
||||||
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
|
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from datetime import datetime
|
|||||||
import httpx
|
import httpx
|
||||||
import psutil
|
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
|
from services import catalog, discover, jobengine, llamaswap, system
|
||||||
|
|
||||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||||
@@ -128,28 +128,12 @@ def _hermes_agent_update() -> dict:
|
|||||||
return info
|
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]:
|
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()
|
now = time.time()
|
||||||
if now - _comp_cache["ts"] < 3600 and _comp_cache["data"]:
|
if now - _comp_cache["ts"] < 3600 and _comp_cache["data"]:
|
||||||
return _comp_cache["data"]
|
return _comp_cache["data"]
|
||||||
data = [_hermes_agent_update(), _anythingllm_update()]
|
data = [_hermes_agent_update()]
|
||||||
_comp_cache.update(ts=now, data=data)
|
_comp_cache.update(ts=now, data=data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -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 --upgrade pip
|
||||||
"$SRC/backend/.venv/bin/python" -m pip install -q -r "$SRC/backend/requirements.txt"
|
"$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"
|
mkdir -p "$HOME/.config/systemd/user"
|
||||||
cp "$SRC/deploy/mission-control-2.service" "$HOME/.config/systemd/user/mission-control-2.service"
|
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 daemon-reload
|
||||||
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
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
|
loginctl enable-linger "$USER" >/dev/null 2>&1 || 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
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
echo "--- Health ---"
|
echo "--- Health ---"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -21,7 +21,6 @@ Environment=MC_MODELS_DIR=/srv/models
|
|||||||
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
||||||
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
||||||
Environment=MC_ENGINE_UPDATE_CMD=/usr/local/bin/update-llamacpp
|
Environment=MC_ENGINE_UPDATE_CMD=/usr/local/bin/update-llamacpp
|
||||||
Environment=HERMES_WEBUI_URL=http://192.168.178.155:3001
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|||||||
+106
-101
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BpalJKIe.js"></script>
|
<script type="module" crossorigin src="/assets/index-BtXN9NH2.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CeszaZ7k.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-SuFuJOVE.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ModelsView } from "@/views/ModelsView"
|
|||||||
import { ConnectView } from "@/views/ConnectView"
|
import { ConnectView } from "@/views/ConnectView"
|
||||||
import { MemoryView } from "@/views/MemoryView"
|
import { MemoryView } from "@/views/MemoryView"
|
||||||
import { AgentView } from "@/views/AgentView"
|
import { AgentView } from "@/views/AgentView"
|
||||||
|
import { TerminalView } from "@/views/TerminalView"
|
||||||
import { GuideView } from "@/views/GuideView"
|
import { GuideView } from "@/views/GuideView"
|
||||||
import { Placeholder } from "@/views/Placeholder"
|
import { Placeholder } from "@/views/Placeholder"
|
||||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||||
@@ -180,8 +181,9 @@ export default function App() {
|
|||||||
{view === "connect" && <ConnectView />}
|
{view === "connect" && <ConnectView />}
|
||||||
{view === "memory" && <MemoryView />}
|
{view === "memory" && <MemoryView />}
|
||||||
{view === "agent" && <AgentView />}
|
{view === "agent" && <AgentView />}
|
||||||
|
{view === "terminal" && <TerminalView />}
|
||||||
{view === "guide" && <GuideView />}
|
{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} />
|
<Placeholder title={active.label} hint={active.hint} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ const SERVICES = [
|
|||||||
{ id: "llama-swap", label: "Llama Swap", type: "system" },
|
{ id: "llama-swap", label: "Llama Swap", type: "system" },
|
||||||
{ id: "mission-control-2", label: "Mission Control 2", type: "user" },
|
{ id: "mission-control-2", label: "Mission Control 2", type: "user" },
|
||||||
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user" },
|
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user" },
|
||||||
{ id: "hermes-dashboard", label: "Hermes Dashboard", type: "user" },
|
{ id: "hermes-terminal", label: "Hermes Terminal", type: "user" }
|
||||||
{ id: "hermes-webui", label: "Hermes WebUI", type: "user" }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
function formatBytes(bytes?: number) {
|
function formatBytes(bytes?: number) {
|
||||||
|
|||||||
@@ -33,19 +33,19 @@ export function AgentStatusCard() {
|
|||||||
<Bot className="h-4.5 w-4.5 text-primary" />
|
<Bot className="h-4.5 w-4.5 text-primary" />
|
||||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Hermes Agent</h2>
|
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Hermes Agent</h2>
|
||||||
</div>
|
</div>
|
||||||
{agent?.webui_url && (
|
{agent?.terminal_url && (
|
||||||
<a
|
<a
|
||||||
href={resolveExternalUrl(agent.webui_url)}
|
href={resolveExternalUrl(agent.terminal_url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 px-3 py-1 rounded-lg text-xs font-medium transition-all",
|
"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"
|
? "bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-primary/20"
|
||||||
: "border border-border text-muted-foreground"
|
: "border border-border text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-3 w-3" /> AnythingLLM öffnen
|
<ExternalLink className="h-3 w-3" /> Terminal öffnen
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,10 +60,10 @@ export function AgentStatusCard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
<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">
|
<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={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.webui_reachable ? "Online" : "Offline"}</span>
|
<span className="text-xs font-medium">{agent.terminal_reachable ? "Online" : "Offline"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -227,15 +227,15 @@ export interface SystemStatus {
|
|||||||
|
|
||||||
export interface ServicesResp {
|
export interface ServicesResp {
|
||||||
services: { name: string; url: string; ok: boolean }[]
|
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 {
|
export interface ComponentUpdate {
|
||||||
key: string // "hermes_agent" | "anythingllm"
|
key: string // z.B. "hermes_agent"
|
||||||
name: string
|
name: string
|
||||||
current: string | null
|
current: string | null
|
||||||
latest: 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
|
reachable: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,9 +278,9 @@ export interface DedupeResult {
|
|||||||
|
|
||||||
export interface AgentStatus {
|
export interface AgentStatus {
|
||||||
gateway_url: string
|
gateway_url: string
|
||||||
webui_url: string
|
terminal_url: string
|
||||||
gateway_reachable: boolean
|
gateway_reachable: boolean
|
||||||
webui_reachable: boolean
|
terminal_reachable: boolean
|
||||||
home_exists: boolean
|
home_exists: boolean
|
||||||
brain_model?: string
|
brain_model?: string
|
||||||
has_config: boolean
|
has_config: boolean
|
||||||
|
|||||||
+4
-2
@@ -4,11 +4,12 @@ import {
|
|||||||
Brain,
|
Brain,
|
||||||
Plug,
|
Plug,
|
||||||
Bot,
|
Bot,
|
||||||
|
TerminalSquare,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react"
|
} 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 {
|
export interface NavItem {
|
||||||
id: ViewId
|
id: ViewId
|
||||||
@@ -23,7 +24,8 @@ export const NAV: NavItem[] = [
|
|||||||
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
|
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
|
||||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
||||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
{ 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 },
|
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -135,20 +135,20 @@ export function AgentView() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{s?.webui_url && (
|
{s?.terminal_url && (
|
||||||
<a
|
<a
|
||||||
href={resolveExternalUrl(s.webui_url)}
|
href={resolveExternalUrl(s.terminal_url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
className={cn(
|
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",
|
"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"
|
? "bg-primary text-primary-foreground hover:opacity-90 shadow-primary/10"
|
||||||
: "border border-border/60 text-muted-foreground bg-background/20"
|
: "border border-border/60 text-muted-foreground bg-background/20"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
<span>AnythingLLM öffnen</span>
|
<span>Terminal öffnen</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,9 +170,9 @@ export function AgentView() {
|
|||||||
icon={Bot}
|
icon={Bot}
|
||||||
/>
|
/>
|
||||||
<Tile
|
<Tile
|
||||||
label="AnythingLLM"
|
label="Terminal"
|
||||||
ok={s.webui_reachable}
|
ok={s.terminal_reachable}
|
||||||
detail="Chat-UI (AnythingLLM)"
|
detail="Web-Terminal (hermes chat)"
|
||||||
icon={Activity}
|
icon={Activity}
|
||||||
/>
|
/>
|
||||||
<Tile
|
<Tile
|
||||||
@@ -213,9 +213,9 @@ export function AgentView() {
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</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" />
|
<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" />
|
<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
|
<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"
|
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%" }}
|
style={{ left: "15%", top: "50%" }}
|
||||||
onMouseEnter={() => setHoveredNode("webui")}
|
onMouseEnter={() => setHoveredNode("terminal")}
|
||||||
onMouseLeave={() => setHoveredNode(null)}
|
onMouseLeave={() => setHoveredNode(null)}
|
||||||
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
|
onClick={() => s.terminal_reachable && window.open(resolveExternalUrl(s.terminal_url), "_blank")}
|
||||||
title={s.webui_reachable ? "Klicken um AnythingLLM zu öffnen" : "AnythingLLM offline"}
|
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")} />
|
<Activity className={cn("h-3.5 w-3.5", s.terminal_reachable ? "text-emerald-400" : "text-amber-500")} />
|
||||||
<span>AnythingLLM</span>
|
<span>Terminal</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")} />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* COLUMN 2: Gateway */}
|
{/* COLUMN 2: Gateway */}
|
||||||
@@ -360,7 +360,7 @@ export function AgentView() {
|
|||||||
PC Executor verbunden
|
PC Executor verbunden
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px]">
|
<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>
|
</p>
|
||||||
</div>
|
</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="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-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-gateway</div>
|
||||||
<div className="text-primary">systemctl --user status hermes-webui</div>
|
<div className="text-primary">systemctl --user status hermes-terminal</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -328,8 +328,8 @@ export function GuideView() {
|
|||||||
|
|
||||||
<ul className="text-[11px] text-muted-foreground space-y-2 list-disc pl-4 leading-relaxed">
|
<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>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>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 in AnythingLLM einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -474,7 +474,7 @@ description: Drive development with strict TDD practices
|
|||||||
<ul className="list-disc pl-4 space-y-1">
|
<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>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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 & 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user