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:
@@ -21,6 +21,13 @@ class RestartReq(BaseModel):
|
|||||||
def updates() -> dict:
|
def updates() -> dict:
|
||||||
return maintenance.updates()
|
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")
|
@router.post("/maintenance/os-update")
|
||||||
def os_update(body: SudoReq) -> dict:
|
def os_update(body: SudoReq) -> dict:
|
||||||
|
|||||||
@@ -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": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
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:
|
def os_update_job(sudo_password: str | None = None) -> dict:
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password(sudo_password):
|
||||||
|
|||||||
@@ -132,20 +132,27 @@ def get_engine_version() -> dict:
|
|||||||
if git_info:
|
if git_info:
|
||||||
return {**git_info, "type": "git"}
|
return {**git_info, "type": "git"}
|
||||||
|
|
||||||
try:
|
candidates = [
|
||||||
binary = os.path.join(engine_path, "llama-server")
|
os.path.join(engine_path, "llama-server"),
|
||||||
if not os.path.exists(binary):
|
os.path.join(engine_path, "bin", "llama-server"),
|
||||||
binary = os.path.join(engine_path, "bin", "llama-server")
|
"/usr/local/bin/llama-server",
|
||||||
if not os.path.exists(binary):
|
"/usr/bin/llama-server",
|
||||||
binary = "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)
|
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
|
||||||
if res.returncode == 0:
|
output = (res.stdout or "").strip() or (res.stderr or "").strip()
|
||||||
lines = res.stdout.strip().splitlines()
|
if output:
|
||||||
|
lines = output.splitlines()
|
||||||
ver = lines[0] if lines else "unknown"
|
ver = lines[0] if lines else "unknown"
|
||||||
return {"version_text": ver, "type": "binary"}
|
return {"version_text": ver, "type": "binary"}
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return {"type": "unknown"}
|
return {"type": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
-350
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
+375
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-Dk6KzQ7B.js"></script>
|
<script type="module" crossorigin src="/assets/index-oiIMDlga.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-nR6_L3dg.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-WcZJ9RJs.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useState, useRef } from "react"
|
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 { api, type Job, type UpdatesResp } from "@/lib/api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
interface SystemDrawerProps {
|
interface SystemDrawerProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
defaultTab?: "maintenance" | "logs"
|
defaultTab?: "maintenance" | "logs" | "settings"
|
||||||
}
|
}
|
||||||
|
|
||||||
const SERVICES = [
|
const SERVICES = [
|
||||||
@@ -29,8 +29,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
const [selectedService, setSelectedService] = useState("llama-swap")
|
const [selectedService, setSelectedService] = useState("llama-swap")
|
||||||
const [logs, setLogs] = useState("")
|
const [logs, setLogs] = useState("")
|
||||||
const [loadingLogs, setLoadingLogs] = useState(false)
|
const [loadingLogs, setLoadingLogs] = useState(false)
|
||||||
|
const [logStatus, setLogStatus] = useState<string | null>(null)
|
||||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
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(() => {
|
useEffect(() => {
|
||||||
if (open && defaultTab) {
|
if (open && defaultTab) {
|
||||||
@@ -54,12 +69,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
|
|
||||||
function loadServiceLogs(service: string) {
|
function loadServiceLogs(service: string) {
|
||||||
setLoadingLogs(true)
|
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) => {
|
.then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setLogs(res.text)
|
setLogs(res.text)
|
||||||
} else {
|
} else {
|
||||||
setLogs(`Fehler beim Laden der Logs: ${res.err || "Unbekannter Fehler"}`)
|
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}`))
|
.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() {
|
async function triggerReboot() {
|
||||||
if (!confirm("Bist du sicher, dass du das gesamte Host-System neu starten willst?")) return
|
if (!confirm("Bist du sicher, dass du das gesamte Host-System neu starten willst?")) return
|
||||||
try {
|
try {
|
||||||
@@ -210,15 +256,36 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
>
|
>
|
||||||
System-Logs
|
System-Logs
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
{activeTab === "maintenance" ? (
|
{activeTab === "maintenance" && (
|
||||||
<>
|
<>
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="space-y-3">
|
<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>
|
<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">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -256,6 +323,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Active Jobs */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -347,7 +441,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{activeTab === "logs" && (
|
||||||
// Logs View
|
// Logs View
|
||||||
<div className="flex flex-col h-full space-y-4">
|
<div className="flex flex-col h-full space-y-4">
|
||||||
{/* Service Select & Restart */}
|
{/* Service Select & Restart */}
|
||||||
@@ -397,7 +493,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
ref={logContainerRef}
|
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"
|
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>
|
<span className="text-muted-foreground">Lade Logs...</span>
|
||||||
) : (
|
) : (
|
||||||
logs || <span className="text-muted-foreground">Keine Logeinträge vorhanden.</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>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+45
-1
@@ -1,9 +1,53 @@
|
|||||||
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
|
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
|
||||||
|
|
||||||
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
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, {
|
const res = await fetch(path, {
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
...init,
|
...init,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return res.json() as Promise<T>
|
return res.json() as Promise<T>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect } from "react"
|
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 { api, type Health } from "@/lib/api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export function GuideView() {
|
export function GuideView() {
|
||||||
|
const [mainTab, setMainTab] = useState<"connect" | "concepts">("connect")
|
||||||
const [activeTab, setActiveTab] = useState<"roocode" | "cursor" | "opencode">("roocode")
|
const [activeTab, setActiveTab] = useState<"roocode" | "cursor" | "opencode">("roocode")
|
||||||
const [health, setHealth] = useState<Health | null>(null)
|
const [health, setHealth] = useState<Health | null>(null)
|
||||||
|
|
||||||
@@ -42,6 +43,34 @@ export function GuideView() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* 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="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">
|
<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">
|
<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) 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>
|
<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>
|
</ul>
|
||||||
</div>
|
</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/<name></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 & 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>&</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user