Files
mission-control-v2/frontend/src/components/models/LaneEditor.tsx
T
Hitonabi 94444f4dcb Frontend-Erneuerung: Mobile, Diät, Tempo, UI-Politur (10-Punkte-Paket)
- Fix: 'Interaktives Terminal' im Cockpit führte ins Leere (#terminal) → Konsole
- Mobile: Off-Canvas-Sidebar mit Hamburger unter md, Inhalt volle Breite
  (vorher: 240px starre Sidebar, 135px Rest mit Quer-Scroll auf dem Handy)
- Dead Weight raus: src/mc3/ (verworfener Prototyp), DashboardView (alte
  Zentrale) + 6 nur dort genutzte Karten, 8 ungenutzte npm-Pakete
  (three, three-vrm, react-three fiber/drei, sigma, graphology ×2, @types/three)
- Code-Splitting: alle Views außer Cockpit per React.lazy → Haupt-Bundle
  983 → 739 KB, jede View ein eigenes Chunk
- Fonts lokal (@fontsource) statt Google-CDN → UI offline identisch
- Polling harmonisiert: EINE Takt-Tabelle in queries.ts (Graphen 3s, Rest
  8-60s), keine Override-Intervalle mehr in Komponenten; Updates-Check
  (apt/git!) von 4s auf 60s
- CSS-Hack-Layer aufgelöst: !important-Klassen-Sniffing → explizite
  .mc-card/.mc-card-sm/.nav-active-Klassen (Look identisch, ungeschichtete
  Regeln statt Utility-Raten); totes .light-Theme raus; Scrollbars 1× definiert
- Aurora: animierte Fullscreen-Blur-Filter → statische radial-gradients
  (gleicher Look, keine Dauer-GPU-Last)
- Sicherheit: Sudo-Passwort/HF-Token nur noch bei mutierenden Requests,
  nicht mehr auf jedem GET-Poll
- Strg+K statt ⌘K auf Windows; dist neu gebaut

Live gegen die Box verifiziert: alle 10 Views + Mobile-Flow, 0 Konsolen-Fehler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:29:51 +02:00

182 lines
7.9 KiB
TypeScript

import { useEffect, useState } from "react"
import { Sliders, RotateCcw, Check, Loader2, MessageSquare, Code2 } from "lucide-react"
import { updateRoutingPolicy, type RoutingPolicy } from "@/lib/api"
import { useRoutingPolicy, useQueryClient, qk } from "@/lib/queries"
import { cn } from "@/lib/utils"
// Welche Policy-Felder zu welcher Lane gehören (Rest ist global).
const CHAT_FIELDS: (keyof RoutingPolicy)[] = ["fast", "heavy", "heavy_chars"]
const CODING_FIELDS: (keyof RoutingPolicy)[] = ["coder_lite", "coder", "coding_escalate_chars"]
export function LaneEditor() {
const qc = useQueryClient()
const { data: meta, isLoading } = useRoutingPolicy()
const [draft, setDraft] = useState<RoutingPolicy | null>(null)
const [saving, setSaving] = useState(false)
const [err, setErr] = useState("")
const [savedAt, setSavedAt] = useState(0)
// Draft initialisieren, sobald die Policy geladen ist (und nicht überschreiben, wenn schon editiert).
useEffect(() => {
if (meta?.policy && !draft) setDraft({ ...meta.policy })
}, [meta, draft])
if (isLoading || !meta || !draft) {
return (
<div className="mc-card p-5 text-xs text-muted-foreground">
Lade Routing-Policy
</div>
)
}
const fieldSpec = (key: keyof RoutingPolicy) => meta.fields.find((f) => f.key === key)!
const dirty = (Object.keys(draft) as (keyof RoutingPolicy)[]).some((k) => draft[k] !== meta.policy[k])
const set = <K extends keyof RoutingPolicy>(key: K, value: RoutingPolicy[K]) => {
setDraft((d) => (d ? { ...d, [key]: value } : d))
setErr("")
}
const resetField = (key: keyof RoutingPolicy) => set(key, meta.defaults[key])
async function save() {
if (!draft) return
const patch: Partial<RoutingPolicy> = {}
for (const k of Object.keys(draft) as (keyof RoutingPolicy)[]) {
if (draft[k] !== meta!.policy[k]) (patch as any)[k] = draft[k]
}
if (Object.keys(patch).length === 0) return
setSaving(true)
setErr("")
try {
const { policy } = await updateRoutingPolicy(patch)
setDraft({ ...policy })
qc.invalidateQueries({ queryKey: qk.routingPolicy })
qc.invalidateQueries({ queryKey: qk.routing })
setSavedAt(Date.now())
setTimeout(() => setSavedAt(0), 2000)
} catch (e: any) {
setErr(e.message || String(e))
} finally {
setSaving(false)
}
}
function Field({ k }: { k: keyof RoutingPolicy }) {
const spec = fieldSpec(k)
const val = draft![k]
const isDefault = draft![k] === meta!.defaults[k]
return (
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<label className="text-[10px] font-semibold text-muted-foreground">{spec.label}</label>
{!isDefault && (
<button
onClick={() => resetField(k)}
className="flex items-center gap-0.5 text-[9px] text-muted-foreground/60 hover:text-foreground transition-colors cursor-pointer"
title={`Auf Default zurücksetzen (${String(meta!.defaults[k]) || "leer"})`}
>
<RotateCcw className="h-2.5 w-2.5" /> Default
</button>
)}
</div>
{spec.type === "bool" ? (
<button
onClick={() => set(k, !val as any)}
className={cn(
"flex h-8 w-full items-center justify-between rounded-lg border px-3 text-[11px] font-semibold transition-all cursor-pointer",
val ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-300" : "border-border/40 bg-background/40 text-muted-foreground",
)}
>
<span>{val ? "An" : "Aus"}</span>
<span className={cn("h-3.5 w-3.5 rounded-full transition-colors", val ? "bg-emerald-400" : "bg-muted-foreground/40")} />
</button>
) : spec.type === "int" ? (
<input
type="number"
value={val as number}
min={spec.min}
max={spec.max}
aria-label={spec.label}
onChange={(e) => set(k, Number(e.target.value) as any)}
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
/>
) : (
<input
type="text"
value={val as string}
aria-label={spec.label}
spellCheck={false}
placeholder={k === "coder_lite" ? "(leer = aus)" : ""}
onChange={(e) => set(k, e.target.value as any)}
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
/>
)}
</div>
)
}
return (
<div className="mc-card p-5 space-y-4">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2">
<Sliders className="h-4.5 w-4.5 text-primary" />
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Lane-Routing &amp; Policy</span>
<span className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300 border border-emerald-500/20">hot-reload</span>
</div>
<div className="flex items-center gap-2">
{err && <span className="text-[10px] text-red-400 max-w-[280px] truncate" title={err}>{err}</span>}
{savedAt > 0 && (
<span className="flex items-center gap-1 text-[10px] text-emerald-400"><Check className="h-3.5 w-3.5" /> gespeichert</span>
)}
<button
onClick={save}
disabled={!dirty || saving}
className={cn(
"h-8 px-4 rounded-lg text-[10px] font-bold uppercase tracking-wide transition-all flex items-center gap-1.5",
dirty && !saving
? "bg-primary text-primary-foreground hover:opacity-90 cursor-pointer shadow-md shadow-primary/10"
: "bg-background/40 text-muted-foreground/50 border border-border/40 cursor-not-allowed",
)}
>
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
Speichern
</button>
</div>
</div>
<p className="text-[10px] text-muted-foreground/70 leading-relaxed -mt-1">
Welches echte Modell hinter den virtuellen Lanes <code className="text-cyan-300">chat</code> und{" "}
<code className="text-cyan-300">coding</code> steckt. Änderungen greifen sofort (kein Neustart). Die
Keyword-Heuristiken bleiben im Code.
</p>
<div className="grid gap-4 md:grid-cols-2">
{/* chat-Lane */}
<div className="rounded-xl border border-border/40 bg-background/25 p-4 space-y-3">
<div className="flex items-center gap-2 border-b border-border/30 pb-2">
<MessageSquare className="h-4 w-4 text-teal-400" />
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground">chat</span>
<span className="text-[9px] text-muted-foreground/60 font-mono">(= auto)</span>
</div>
{CHAT_FIELDS.map((k) => <Field key={k} k={k} />)}
</div>
{/* coding-Lane */}
<div className="rounded-xl border border-border/40 bg-background/25 p-4 space-y-3">
<div className="flex items-center gap-2 border-b border-border/30 pb-2">
<Code2 className="h-4 w-4 text-indigo-400" />
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground">coding</span>
<span className="text-[9px] text-muted-foreground/60 font-mono">(agentisch immer Coder)</span>
</div>
{CODING_FIELDS.map((k) => <Field key={k} k={k} />)}
</div>
</div>
{/* Globale Schalter */}
<div className="rounded-xl border border-border/40 bg-background/25 p-4">
<div className="max-w-xs"><Field k="fast_no_think" /></div>
</div>
</div>
)
}