UI v3: Cockpit-Startseite + Modelle-Werkbank als Default, neue Box-Konsole
Kompletter Frontend-Umbau auf das abgenommene v3-Konzept, dazu zwei neue Funktionen.
Frontend (Neuordnung bestehender IST-Views, kein Backend-Umbau):
- Cockpit ist die neue Startseite: ruhige Bereichs-Kacheln (je 1 Live-Zahl +
Status-Punkt), ehrliche Status-Zeile "Box gesund" + Speicher-Pille, "Braucht dich"
(kritische Probleme mit 1-Klick-Reparatur + bereitliegende Updates).
- Modelle-Werkbank: Maschinenraum-Speicherleiste als Hero (Arbeitsspeicher-Balken,
nach Rolle eingefaerbt + frei), darunter Master/Detail. Eingebettet als Haupt-Tab
im Modell-Manager ("Werkbank"), "Modelle finden" + JobsBar bleiben.
- Sidebar/Nav neu strukturiert; alle Aktionen ueber die bestehenden /api-Endpoints.
Neue Features:
- Box-Konsole: zweites ttyd-Web-Terminal mit echter Login-Shell auf :7682 (direkter,
SSH-artiger Box-Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie hermes-terminal).
Neuer Dienst deploy/box-console.service + agent_status-Felder box_console_url/-reachable.
- Box-Zugang: das Host-Sudo-Passwort laesst sich jetzt direkt in der Konsole-Seite
setzen/aendern/loeschen (lokal im Browser), statt nur versteckt im System-Drawer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { SquareTerminal, ExternalLink, AlertTriangle, Shield, Eye, EyeOff, Check, Trash2 } from "lucide-react"
|
||||
import { useAgentStatus } from "@/lib/queries"
|
||||
import { resolveExternalUrl, cn } from "@/lib/utils"
|
||||
|
||||
// Konsole = direkter, SSH-artiger Box-Zugriff (ttyd → Login-Shell auf :7682), eingebettet
|
||||
// per iframe — Schwester der Hermes-Terminal-Seite. Darunter „Box-Zugang": das Host-Sudo-
|
||||
// Passwort, das MC2 für System-Operationen (Neustarts, apt, Reboot) braucht, hier direkt
|
||||
// änder-/löschbar (nur lokal im Browser gespeichert, wie in den Einstellungen).
|
||||
export function KonsoleView() {
|
||||
const { data: agent } = useAgentStatus(5_000)
|
||||
const url = agent?.box_console_url ? resolveExternalUrl(agent.box_console_url) : undefined
|
||||
const ok = agent?.box_console_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">
|
||||
Konsole
|
||||
</h1>
|
||||
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser.
|
||||
</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={cn("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-[58vh] 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">Konsole nicht erreichbar</div>
|
||||
<p className="max-w-sm text-[11px] leading-normal text-muted-foreground">
|
||||
Der <code className="font-mono text-primary">box-console</code>-Dienst (ttyd) läuft nicht.
|
||||
Auf der Box: <code className="font-mono">systemctl --user restart box-console</code>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<iframe src={url} title="Box-Konsole" className="h-full w-full border-0" style={{ minHeight: "58vh" }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground">
|
||||
<SquareTerminal className="mr-2 h-4 w-4" /> Lade Konsole…
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BoxAccessCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// „Box-Zugang" — das Host-Sudo-Passwort direkt setzen/ändern/löschen. Reine Frontend-
|
||||
// Verwaltung des lokal gespeicherten Werts (localStorage `mc_sudo_password`), den api.ts
|
||||
// bei Bedarf mitschickt. KEINE Änderung des echten Linux-Kontopassworts.
|
||||
function BoxAccessCard() {
|
||||
const [pw, setPw] = useState("")
|
||||
const [stored, setStored] = useState(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const v = localStorage.getItem("mc_sudo_password") || ""
|
||||
setPw(v); setStored(!!v)
|
||||
}, [])
|
||||
|
||||
function save() {
|
||||
if (pw) localStorage.setItem("mc_sudo_password", pw)
|
||||
else localStorage.removeItem("mc_sudo_password")
|
||||
setStored(!!pw)
|
||||
setSaved(true); setTimeout(() => setSaved(false), 1800)
|
||||
}
|
||||
function clear() {
|
||||
localStorage.removeItem("mc_sudo_password"); setPw(""); setStored(false)
|
||||
setSaved(true); setTimeout(() => setSaved(false), 1800)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-5 shadow-lg shadow-black/15 backdrop-blur-md">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Shield className="h-4.5 w-4.5 text-amber-400" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Box-Zugang</h2>
|
||||
<span className={cn("ml-1 rounded-full border px-2 py-0.5 text-[10px] font-semibold",
|
||||
stored ? "border-emerald-500/30 bg-emerald-500/5 text-emerald-400" : "border-border/40 bg-background/30 text-muted-foreground")}>
|
||||
{stored ? "gesetzt" : "nicht gesetzt"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mb-3 text-[11px] leading-normal text-muted-foreground">
|
||||
Das Box-Passwort (Sudo) braucht MC2 für System-Aktionen wie Dienst-Neustarts, OS-Updates und Neustart der Box.
|
||||
Es wird nur lokal in deinem Browser gespeichert. Hier kannst du es setzen, ändern oder löschen.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type={show ? "text" : "password"} value={pw} onChange={(e) => setPw(e.target.value)}
|
||||
placeholder="Box-Passwort" autoComplete="off" spellCheck={false} name="box-password"
|
||||
className="h-10 w-full rounded-lg border border-border/60 bg-background/40 pl-3 pr-10 text-xs text-foreground outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
<button type="button" onClick={() => setShow(!show)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label={show ? "Passwort verbergen" : "Passwort anzeigen"}>
|
||||
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={save}
|
||||
className="flex h-10 items-center justify-center gap-1.5 rounded-lg bg-primary px-4 text-xs font-semibold text-primary-foreground shadow shadow-primary/10 transition-colors hover:bg-primary/90 cursor-pointer">
|
||||
{saved ? <><Check className="h-4 w-4" /> gespeichert</> : "Speichern"}
|
||||
</button>
|
||||
<button onClick={clear} disabled={!stored && !pw}
|
||||
className="flex h-10 items-center justify-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent cursor-pointer disabled:opacity-40">
|
||||
<Trash2 className="h-3.5 w-3.5" /> Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user