Feat: Add manual updates check, Settings tab, direct model upgrades, logs password redirection, fix engine version detection, and expand AI agent Guide tab

This commit is contained in:
Hitonabi
2026-06-26 13:09:50 +02:00
parent 563e7837b9
commit 0c7b0b19af
11 changed files with 1122 additions and 599 deletions
+7
View File
@@ -21,6 +21,13 @@ class RestartReq(BaseModel):
def updates() -> dict:
return maintenance.updates()
@router.post("/maintenance/check-updates")
def check_updates(body: SudoReq) -> dict:
res = maintenance.check_updates_job(body.sudo_password)
if isinstance(res, dict) and not res.get("ok", True):
return res
return res
@router.post("/maintenance/os-update")
def os_update(body: SudoReq) -> dict:
+11
View File
@@ -168,6 +168,17 @@ def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> di
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
return {"ok": r["ok"], "text": r["out"] or r["err"]}
def check_updates_job(sudo_password: str | None = None) -> dict:
if err := check_sudo_needs_password(sudo_password):
return err
def on_done():
_engine_cache.update(ts=0.0, avail=False)
cmd = "sudo apt-get update"
job_id = jobengine.start_job(["bash", "-c", cmd], "Nach Updates suchen", on_done=on_done, sudo_password=sudo_password)
return {"ok": True, "job_id": job_id}
def os_update_job(sudo_password: str | None = None) -> dict:
if err := check_sudo_needs_password(sudo_password):
+15 -8
View File
@@ -132,20 +132,27 @@ def get_engine_version() -> dict:
if git_info:
return {**git_info, "type": "git"}
try:
binary = os.path.join(engine_path, "llama-server")
if not os.path.exists(binary):
binary = os.path.join(engine_path, "bin", "llama-server")
if not os.path.exists(binary):
binary = "llama-server"
candidates = [
os.path.join(engine_path, "llama-server"),
os.path.join(engine_path, "bin", "llama-server"),
"/usr/local/bin/llama-server",
"/usr/bin/llama-server",
"llama-server"
]
for binary in candidates:
if binary != "llama-server" and not os.path.exists(binary):
continue
try:
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
if res.returncode == 0:
lines = res.stdout.strip().splitlines()
output = (res.stdout or "").strip() or (res.stderr or "").strip()
if output:
lines = output.splitlines()
ver = lines[0] if lines else "unknown"
return {"version_text": ver, "type": "binary"}
except Exception:
pass
return {"type": "unknown"}
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-Dk6KzQ7B.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nR6_L3dg.css">
<script type="module" crossorigin src="/assets/index-oiIMDlga.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-WcZJ9RJs.css">
</head>
<body>
<div id="root"></div>
+210 -7
View File
@@ -1,12 +1,12 @@
import { useEffect, useState, useRef } from "react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power } from "lucide-react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle } from "lucide-react"
import { api, type Job, type UpdatesResp } from "@/lib/api"
import { cn } from "@/lib/utils"
interface SystemDrawerProps {
open: boolean
onClose: () => void
defaultTab?: "maintenance" | "logs"
defaultTab?: "maintenance" | "logs" | "settings"
}
const SERVICES = [
@@ -29,8 +29,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
const [selectedService, setSelectedService] = useState("llama-swap")
const [logs, setLogs] = useState("")
const [loadingLogs, setLoadingLogs] = useState(false)
const [logStatus, setLogStatus] = useState<string | null>(null)
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
const [activeTab, setActiveTab] = useState<"maintenance" | "logs">("maintenance")
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
const [checkingUpdates, setCheckingUpdates] = useState(false)
// Credentials State
const [sudoPasswordInput, setSudoPasswordInput] = useState("")
const [hfTokenInput, setHfTokenInput] = useState("")
const [showSudo, setShowSudo] = useState(false)
const [showHf, setShowHf] = useState(false)
useEffect(() => {
if (open) {
setSudoPasswordInput(localStorage.getItem("mc_sudo_password") || "")
setHfTokenInput(localStorage.getItem("mc_hf_token") || "")
}
}, [open])
useEffect(() => {
if (open && defaultTab) {
@@ -54,12 +69,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
function loadServiceLogs(service: string) {
setLoadingLogs(true)
api<{ ok: boolean; text: string; err?: string }>(`/api/maintenance/logs?service=${service}&lines=150`)
setLogStatus(null)
api<{ ok: boolean; text: string; err?: string; status?: string }>(`/api/maintenance/logs?service=${service}&lines=150`)
.then((res) => {
if (res.ok) {
setLogs(res.text)
} else {
setLogs(`Fehler beim Laden der Logs: ${res.err || "Unbekannter Fehler"}`)
if (res.status === "incorrect_password" || res.status === "password_required") {
setLogStatus(res.status)
}
}
})
.catch((e) => setLogs(`Fehler: ${e.message}`))
@@ -112,6 +131,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
}
}
async function triggerCheckUpdates() {
setCheckingUpdates(true)
try {
await api("/api/maintenance/check-updates", { method: "POST" })
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
alert(`Fehler bei der Update-Suche: ${e.message}`)
} finally {
setCheckingUpdates(false)
}
}
async function triggerModelUpgrade(repo: string, role: string) {
try {
await api("/api/models/install", {
method: "POST",
body: JSON.stringify({ repo, role })
})
alert(`Modell-Upgrade für '${role}' (${repo}) gestartet.`)
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
alert(`Fehler beim Starten des Modell-Upgrades: ${e.message}`)
}
}
async function triggerReboot() {
if (!confirm("Bist du sicher, dass du das gesamte Host-System neu starten willst?")) return
try {
@@ -210,15 +256,36 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
>
System-Logs
</button>
<button
onClick={() => setActiveTab("settings")}
className={cn(
"flex-1 py-3 text-xs font-semibold uppercase tracking-wider border-b-2 text-center transition-all",
activeTab === "settings"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
Einstellungen
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{activeTab === "maintenance" ? (
{activeTab === "maintenance" && (
<>
{/* Quick Actions */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
<button
onClick={triggerCheckUpdates}
disabled={checkingUpdates}
className="flex items-center gap-1 text-[10px] font-semibold text-primary hover:text-primary/80 transition-colors disabled:opacity-50"
>
<RefreshCw className={cn("h-3 w-3", checkingUpdates && "animate-spin")} />
Nach Updates suchen
</button>
</div>
<div className="grid grid-cols-2 gap-3">
<button
@@ -256,6 +323,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</button>
</div>
{/* Modell Upgrades */}
{updates?.model_list && updates.model_list.length > 0 && (
<div className="space-y-3">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Verfügbare Modell-Upgrades</h3>
<div className="space-y-2">
{updates.model_list.map((m) => (
<div key={m.role} className="p-3 rounded-xl border border-border/60 bg-background/20 flex flex-col gap-2">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-xs font-semibold">{m.title}</div>
<div className="text-[10px] font-mono text-muted-foreground">{m.repo}</div>
<div className="text-[10px] text-primary font-semibold uppercase mt-0.5">Rolle: {m.role}</div>
</div>
<button
onClick={() => triggerModelUpgrade(m.repo, m.role)}
className="flex items-center gap-1.5 text-[10px] font-semibold text-emerald-400 hover:text-emerald-300 border border-emerald-500/20 bg-emerald-500/5 hover:bg-emerald-500/10 px-2 py-1 rounded-lg transition-colors shrink-0"
>
<Download className="h-3.5 w-3.5" />
Upgrade
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Active Jobs */}
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -347,7 +441,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</div>
</div>
</>
) : (
)}
{activeTab === "logs" && (
// Logs View
<div className="flex flex-col h-full space-y-4">
{/* Service Select & Restart */}
@@ -397,7 +493,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
ref={logContainerRef}
className="flex-1 p-4 overflow-y-auto text-[10px] font-mono text-cyan-300/90 whitespace-pre-wrap select-text leading-relaxed scrollbar-thin"
>
{loadingLogs && !logs ? (
{logStatus === "password_required" || logStatus === "incorrect_password" ? (
<div className="flex flex-col items-center justify-center p-6 text-center h-full space-y-3">
<AlertTriangle className="h-8 w-8 text-amber-400 animate-pulse animate-duration-1000" />
<div className="text-xs font-semibold text-amber-300">
{logStatus === "incorrect_password" ? "Falsches Sudo-Passwort hinterlegt." : "Sudo-Passwort für systemd-Dienste erforderlich."}
</div>
<p className="text-[10px] text-muted-foreground max-w-xs leading-normal">
Für das Auslesen der systemd-Logs von {selectedService} werden Root-Rechte benötigt. Hinterlege dein Passwort in den Einstellungen.
</p>
<button
onClick={() => setActiveTab("settings")}
className="px-3 py-1.5 text-[10px] font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-md transition-colors mt-2"
>
Sudo-Passwort eintragen
</button>
</div>
) : loadingLogs && !logs ? (
<span className="text-muted-foreground">Lade Logs...</span>
) : (
logs || <span className="text-muted-foreground">Keine Logeinträge vorhanden.</span>
@@ -406,6 +518,97 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</div>
</div>
)}
{activeTab === "settings" && (
<div className="space-y-6">
<div className="space-y-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Zugangsdaten & Schlüssel</h3>
<p className="text-[10px] text-muted-foreground leading-normal">
Diese Daten werden ausschließlich lokal in deinem Browser (localStorage) gespeichert und bei Bedarf an das Backend übertragen.
</p>
</div>
{/* Sudo Passwort */}
<div className="space-y-2">
<label className="text-xs font-semibold flex items-center gap-1.5">
<Shield className="h-4 w-4 text-amber-400" />
Host Sudo-Passwort
</label>
<div className="relative">
<input
type={showSudo ? "text" : "password"}
value={sudoPasswordInput}
onChange={(e) => setSudoPasswordInput(e.target.value)}
placeholder="Sudo-Passwort für System-Operationen"
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/>
<button
type="button"
onClick={() => setShowSudo(!showSudo)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showSudo ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
Wird benötigt für systemd-Dienste (Llama Swap logs/restart), OS-Update (apt) und Reboot.
</p>
</div>
{/* HuggingFace Token */}
<div className="space-y-2">
<label className="text-xs font-semibold flex items-center gap-1.5">
<Key className="h-4 w-4 text-violet-400" />
HuggingFace API Token
</label>
<div className="relative">
<input
type={showHf ? "text" : "password"}
value={hfTokenInput}
onChange={(e) => setHfTokenInput(e.target.value)}
placeholder="hf_..."
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/>
<button
type="button"
onClick={() => setShowHf(!showHf)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showHf ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
Erlaubt das Herunterladen von geschützten oder Gate-restricted Modellen direkt über Mission Control.
</p>
</div>
{/* Buttons */}
<div className="flex gap-3 pt-2">
<button
onClick={() => {
localStorage.setItem("mc_sudo_password", sudoPasswordInput);
localStorage.setItem("mc_hf_token", hfTokenInput);
alert("Einstellungen erfolgreich lokal gespeichert.");
}}
className="flex-1 h-9 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors"
>
Speichern
</button>
<button
onClick={() => {
setSudoPasswordInput("");
setHfTokenInput("");
localStorage.removeItem("mc_sudo_password");
localStorage.removeItem("mc_hf_token");
alert("Zugangsdaten gelöscht.");
}}
className="h-9 px-4 flex items-center justify-center text-xs font-semibold text-muted-foreground border border-border/60 bg-background/20 hover:bg-accent rounded-lg transition-colors"
>
Zurücksetzen
</button>
</div>
</div>
)}
</div>
</div>
</>
+45 -1
View File
@@ -1,9 +1,53 @@
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const headers = {
"Content-Type": "application/json",
...init?.headers,
} as Record<string, string>
const sudoPassword = localStorage.getItem("mc_sudo_password")
const hfToken = localStorage.getItem("mc_hf_token")
if (sudoPassword) {
headers["X-Sudo-Password"] = sudoPassword
}
let body = init?.body
const method = init?.method?.toUpperCase() || "GET"
if (method === "POST") {
if (typeof body === "string") {
try {
const data = JSON.parse(body)
let changed = false
if (sudoPassword && !("sudo_password" in data)) {
data["sudo_password"] = sudoPassword
changed = true
}
if (hfToken && !("hf_token" in data)) {
data["hf_token"] = hfToken
changed = true
}
if (changed) {
body = JSON.stringify(data)
}
} catch (e) {
// ignore JSON parse errors
}
} else if (!body) {
const data: Record<string, any> = {}
if (sudoPassword) data["sudo_password"] = sudoPassword
if (hfToken) data["hf_token"] = hfToken
if (Object.keys(data).length > 0) {
body = JSON.stringify(data)
}
}
}
const res = await fetch(path, {
headers: { "Content-Type": "application/json" },
...init,
headers,
body,
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return res.json() as Promise<T>
+228 -2
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from "react"
import { BookOpen, Layers, Brain, Terminal, Code, RefreshCw, Cpu, Star } from "lucide-react"
import { BookOpen, Layers, Brain, Terminal, Code, RefreshCw, Cpu, Star, Compass, Wrench, Shield, ArrowRight } from "lucide-react"
import { api, type Health } from "@/lib/api"
import { cn } from "@/lib/utils"
export function GuideView() {
const [mainTab, setMainTab] = useState<"connect" | "concepts">("connect")
const [activeTab, setActiveTab] = useState<"roocode" | "cursor" | "opencode">("roocode")
const [health, setHealth] = useState<Health | null>(null)
@@ -42,6 +43,34 @@ export function GuideView() {
</p>
</div>
{/* Main Mode Tabs */}
<div className="flex gap-4 border-b border-border/40 pb-px">
<button
onClick={() => setMainTab("connect")}
className={cn(
"pb-3 text-xs font-bold uppercase tracking-wider border-b-2 px-1 transition-all",
mainTab === "connect"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
Editor-Anbindung
</button>
<button
onClick={() => setMainTab("concepts")}
className={cn(
"pb-3 text-xs font-bold uppercase tracking-wider border-b-2 px-1 transition-all",
mainTab === "concepts"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
KI-Wissensdatenbank (Juni 2026)
</button>
</div>
{mainTab === "connect" ? (
<>
{/* Connection HUD Panel */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col sm:flex-row justify-between sm:items-center gap-4">
<div className="flex items-center gap-3">
@@ -299,10 +328,207 @@ 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) ist. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</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 der Hermes WebUI einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
</ul>
</div>
</>
) : (
/* KI-Wissensdatenbank Concepts Tab */
<div className="space-y-6">
{/* Introduction */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex items-start gap-4">
<Compass className="h-8 w-8 text-primary shrink-0 mt-0.5" />
<div className="space-y-1">
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">Entwickler-Guide: Modernes Agentic Coding (2026)</h3>
<p className="text-xs text-muted-foreground leading-normal">
Willkommen im Wissenszentrum für dein Mission Control 2 Setup. Hier erfährst du, wie die verschiedenen Technologien (MoE, MCP, Skills, Hermes) zusammenarbeiten und wie du das Maximum aus deinen AI-Prozessabläufen herausholst.
</p>
</div>
</div>
{/* Concepts Grid */}
<div className="grid gap-6 md:grid-cols-2">
{/* MoE Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Layers className="h-5 w-5 text-cyan-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">1. Mixture of Experts (MoE)</h3>
<span className="text-[9px] text-cyan-400 font-mono">Effizienz durch Spezialisierung</span>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
<p>
<strong>Was ist das?</strong> Bei traditionellen LLMs wird für jedes Wort das gesamte neuronale Netz aktiviert. Bei MoE besteht das Modell aus mehreren spezialisierten Teilnetzwerken (den <em>Experts</em>). Ein intelligenter <em>Router</em> entscheidet pro Token (Wortteil), welche Experten zur Berechnung herangezogen werden.
</p>
<p>
<strong>Warum in MC2?</strong> So können extrem leistungsstarke Modelle (wie DeepSeek-V3, Mixtral oder Command R+) mit wesentlich geringeren Hardwarekosten ausgeführt werden. Es wird nur ein Bruchteil der Parameter geladen und aktiv berechnet, was Speicherplatz spart und die Inferenz beschleunigt.
</p>
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] text-foreground">
<span className="text-cyan-400">Vorteil:</span> GPT-4-Klasse Performance bei einem Bruchteil der aktiven VRAM-Last auf deinem Homelab!
</div>
</div>
</div>
{/* MCP Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Compass className="h-5 w-5 text-violet-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">2. Model Context Protocol (MCP)</h3>
<span className="text-[9px] text-violet-400 font-mono">Standardisierte Agenten-Tools</span>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
<p>
<strong>Was ist das?</strong> MCP ist ein offenes Protokoll (initiiert von Anthropic), das festlegt, wie ein KI-Client (z.B. Roo Code auf deinem PC) mit externen Datenquellen und Tools kommuniziert. Es funktioniert wie ein USB-Standard für KI.
</p>
<p>
<strong>Warum in MC2?</strong> MCP trennt den AI-Kern von der Umgebung. Statt für jeden Editor eigene Tools zu schreiben, binden deine Agenten (Roo Code, Hermes) einfach MCP-Server an. Diese Server können Dateien lesen, Websuchen durchführen, Git bedienen oder mit deiner App interagieren.
</p>
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] text-foreground">
<span className="text-violet-400">Prinzip:</span> Agent MCP Gateway Lokale Tools (Dateien, Terminal, Web)
</div>
</div>
</div>
{/* Skills Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Brain className="h-5 w-5 text-emerald-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">3. Agent Skills</h3>
<span className="text-[9px] text-emerald-400 font-mono">Modulbasierte Fähigkeiten</span>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
<p>
<strong>Was ist das?</strong> Ein Skill ist ein Verzeichnis mit standardisierten Anweisungen, Scripten und Beispielen, das deine Agenten für spezifische Aufgaben trainiert (z.B. Test-Driven Development, Code-Vereinfachung, API-Design).
</p>
<p>
<strong>Wie benutzt man sie?</strong> Lege einen Skill-Ordner unter <code>.agents/skills/&lt;name&gt;</code> in deinem Projekt an. Das Herzstück ist die Datei <code>SKILL.md</code> mit folgendem Aufbau:
</p>
<pre className="p-2.5 bg-background/25 rounded-lg border border-border/30 font-mono text-[9px] text-foreground overflow-x-auto whitespace-pre">
{`---
name: tdd-pro
description: Drive development with strict TDD practices
---
# Instructions
...`}
</pre>
<p>
Der Agent scannt diese Ordner automatisch und erweitert seine Systembefehle bei passenden Workflows. Gute Quellen sind dein lokales Mission Control Repository oder GitHub.
</p>
</div>
</div>
{/* Hermes Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Cpu className="h-5 w-5 text-indigo-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">4. Arbeiten mit Hermes</h3>
<span className="text-[9px] text-indigo-400 font-mono">Autonomer Box-Agent</span>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
<p>
<strong>Was ist das?</strong> Hermes ist der auf der Box installierte, autonome Hintergrund-Agent. Er verwaltet das Dateisystem und kann über REST (Port 8642) oder eine interaktive ChatUI (Port 8787) gesteuert werden.
</p>
<p>
<strong>Best Practices für Hermes:</strong>
</p>
<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 (WebUI-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
</ul>
</div>
</div>
{/* Tools Best Practices */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4 md:col-span-2">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Wrench className="h-5 w-5 text-amber-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">5. Richtiges Arbeiten mit Tools (Dateien, Terminal, DevTools)</h3>
<span className="text-[9px] text-amber-400 font-mono">Fehler vermeiden &amp; Kosten senken</span>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4 text-xs text-muted-foreground leading-normal">
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
<h4 className="font-bold text-foreground flex items-center gap-1">
<Terminal className="h-3.5 w-3.5 text-primary" /> Terminal
</h4>
<p className="text-[11px]">
Verwende nur non-interaktive Befehle. Hänge bei langen Tasks (wie dev-Servern) ein <code>&amp;</code> an oder nutze die integrierte Job-Verwaltung. Verwende niemals interaktive Eingabeaufforderungen.
</p>
</div>
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
<h4 className="font-bold text-foreground flex items-center gap-1">
<Code className="h-3.5 w-3.5 text-cyan-400" /> Dateimanager
</h4>
<p className="text-[11px]">
Überschreibe keine ganzen Dateien, wenn du nur eine Zeile ändern willst. Verwende gezielte Ersetzungs-Tools (wie <code>replace_file_content</code>). Das spart massiv Token-Kosten und beugt Fehlern vor.
</p>
</div>
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
<h4 className="font-bold text-foreground flex items-center gap-1">
<Wrench className="h-3.5 w-3.5 text-violet-400" /> Browser DevTools
</h4>
<p className="text-[11px]">
Koppele deine Debug-Dienste mit dem Chrome-DevTools-Plugin. So kann der Agent Fehler in der Konsole live analysieren und das DOM verifizieren, anstatt blind zu raten.
</p>
</div>
</div>
</div>
{/* Autonomie Erklärung */}
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4 md:col-span-2">
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
<Shield className="h-5 w-5 text-emerald-400" />
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">Wie autonom ist Mission Control 2 wirklich?</h3>
<span className="text-[9px] text-emerald-400 font-mono">Die Grenze zwischen Automatisierung und Kontrolle</span>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-3 leading-normal">
<p>
Mission Control 2 ist als <strong>semi-autonomes Gateway</strong> konzipiert. Es besitzt die Fähigkeit, komplexe Workflows komplett selbstständig durchzuführen, unterliegt jedoch strikten Sicherheitsplanken:
</p>
<div className="grid sm:grid-cols-2 gap-4 pt-1">
<div className="space-y-1.5">
<h4 className="font-bold text-foreground flex items-center gap-1 text-[11px]">
<ArrowRight className="h-3 w-3 text-emerald-400" /> Was läuft vollautomatisch?
</h4>
<ul className="list-disc pl-4 space-y-1 text-[11px]">
<li>Modellaustausch (Routing) per llama-swap je nach Anfragetyp (z.B. Code vs. Reasoning).</li>
<li>Hintergrund-Synchronisation und Speicherung von Gedächtniseinträgen (Memory).</li>
<li>Modell-Installation und API-Key-Injektionen in Gateway-Verbindungen.</li>
</ul>
</div>
<div className="space-y-1.5">
<h4 className="font-bold text-foreground flex items-center gap-1 text-[11px]">
<ArrowRight className="h-3 w-3 text-amber-400" /> Wo ist menschliche Freigabe nötig?
</h4>
<ul className="list-disc pl-4 space-y-1 text-[11px]">
<li><strong>Systembefehle:</strong> Das Ausführen von Terminal-Kommandos auf deinem PC erfordert standardmäßig deine Bestätigung.</li>
<li><strong>Kritische Systemeingriffe:</strong> OS-Updates, Engine-Rebuilds und Host-Reboots müssen manuell über das Dashboard ausgelöst werden.</li>
<li><strong>Gedächtnis-Löschung:</strong> Permanentes Verwerfen von gesammelten Memorys wird von dir freigegeben.</li>
</ul>
</div>
</div>
<p className="text-[11px] bg-background/25 p-3 rounded-xl border border-border/30 mt-2">
<strong>Fazit:</strong> Der Stack erledigt die Kärrnerarbeit (Modelle tauschen, API-Adapter bereitstellen, Sandbox-Verbindungen herstellen) komplett im Hintergrund. Er agiert als dein persönlicher, treuer Copilot, ohne jemals ungefragt schädliche Operationen auf deinem Hauptsystem auszuführen.
</p>
</div>
</div>
</div>
</div>
)}
</div>
)
}