Files
mission-control-v2/frontend/src/components/system/LogConsole.tsx
T
Hitonabi 8a2db70f3c MC2 Final (D16 c+e): System-Logs mit Fehler-Farben/Filter + Auto-Dedupe
c) System-Logs ausgebaut: neue LogConsole-Komponente färbt Fehler (rot) und
   Warnungen (amber) ein, "Nur Probleme"-Filter mit Zähler, Zeilen-Suche;
   voice-service in die Dienst-Liste aufgenommen (war nur backend-seitig).
e) Mem0-Dubletten automatisch: deterministischer Auto-Dedupe-Loop (täglich,
   apply=True, Schwelle 0.9 > manueller 0.85 da ohne Review) als Backend-
   Hintergrund-Task — kein Memory-Bloat mehr ohne Zutun. Knopf bleibt on-demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:12:11 +02:00

120 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useRef, useState } from "react"
import { AlertTriangle, RefreshCw, Search, Terminal } from "lucide-react"
import { cn } from "@/lib/utils"
// System-Log-Konsole (Paket D16c): färbt Fehler/Warnungen ein, filtert auf „nur Probleme"
// und durchsucht die Zeilen. Ersetzt den früheren monochromen <pre>-Dump („zu dünn").
type Level = "error" | "warn" | "info"
// Wortgrenzen-Regex, damit „error" nicht in jeder URL/jedem Pfad anschlägt. Deutsch + Englisch,
// weil unsere Dienste beides loggen (Hermes englisch, MC2/Voice teils deutsch).
const _ERR = /\b(error|fehler|fail(ed|ure)?|fatal|critical|crit|panic|traceback|exception|denied|refused|unable|emerg|alert|oom|killed)\b/i
const _WARN = /\b(warn(ing)?|deprecat\w*|retry(ing)?|timeout|timed out|degraded|missing|not found|nicht gefunden)\b/i
function classify(line: string): Level {
if (_ERR.test(line)) return "error"
if (_WARN.test(line)) return "warn"
return "info"
}
const LEVEL_CLS: Record<Level, string> = {
error: "text-red-400",
warn: "text-amber-300",
info: "text-cyan-300/80",
}
export function LogConsole({ service, text, loading, logStatus, onRefresh, onOpenSettings }: {
service: string
text: string
loading: boolean
logStatus: string | null
onRefresh: () => void
onOpenSettings: () => void
}) {
const [problemsOnly, setProblemsOnly] = useState(false)
const [query, setQuery] = useState("")
const containerRef = useRef<HTMLDivElement | null>(null)
const lines = useMemo(
() => (text ? text.split("\n").map((t) => ({ t, level: classify(t) })) : []),
[text],
)
const problemCount = useMemo(() => lines.filter((l) => l.level !== "info").length, [lines])
const q = query.trim().toLowerCase()
const shown = lines.filter(
(l) => (!problemsOnly || l.level !== "info") && (!q || l.t.toLowerCase().includes(q)),
)
const needsPw = logStatus === "password_required" || logStatus === "incorrect_password"
return (
<div className="flex-1 flex flex-col min-h-[300px] border border-border/60 bg-black/50 rounded-xl overflow-hidden shadow-inner">
{/* Konsolen-Kopf: Titel + Problem-Filter + Suche + Neu laden */}
<div className="flex h-10 items-center justify-between gap-2 px-3 border-b border-border/40 bg-black/30 shrink-0">
<div className="flex items-center gap-1.5 text-[10px] font-mono text-muted-foreground shrink-0">
<Terminal className="h-3 w-3 text-primary" />
<span className="hidden sm:inline">stdout/stderr </span> {service}
</div>
<div className="flex items-center gap-1.5">
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="filtern…"
className="h-7 w-24 sm:w-36 rounded-md border border-border/50 bg-background/40 pl-6 pr-2 text-[10px] text-foreground outline-none focus:border-primary/50"
/>
</div>
<button
onClick={() => setProblemsOnly((v) => !v)}
title="Nur Fehler und Warnungen zeigen"
className={cn("flex h-7 items-center gap-1 rounded-md border px-2 text-[10px] font-semibold transition-colors",
problemsOnly ? "border-amber-500/50 bg-amber-500/15 text-amber-300"
: "border-border/50 bg-background/20 text-muted-foreground hover:text-foreground")}
>
<AlertTriangle className="h-3 w-3" />
Nur Probleme{problemCount > 0 ? ` (${problemCount})` : ""}
</button>
<button onClick={onRefresh} disabled={loading} title="Neu laden"
className="flex h-7 w-7 items-center justify-center rounded-md border border-border/50 bg-background/20 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50">
<RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
</button>
</div>
</div>
{/* Zeilen-Bereich */}
<div ref={containerRef} className="flex-1 overflow-y-auto p-3 text-[10px] font-mono leading-relaxed scrollbar-thin select-text">
{needsPw ? (
<div className="flex h-full flex-col items-center justify-center space-y-3 p-6 text-center">
<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="max-w-xs text-[10px] leading-normal text-muted-foreground">
Für das Auslesen der systemd-Logs von {service} werden Root-Rechte benötigt. Hinterlege dein Passwort in den Einstellungen.
</p>
<button onClick={onOpenSettings}
className="mt-2 rounded-md bg-primary px-3 py-1.5 text-[10px] font-semibold text-primary-foreground transition-colors hover:bg-primary/95">
Sudo-Passwort eintragen
</button>
</div>
) : loading && !text ? (
<span className="text-muted-foreground">Lade Logs</span>
) : lines.length === 0 ? (
<span className="text-muted-foreground">Keine Logeinträge vorhanden.</span>
) : shown.length === 0 ? (
<span className="text-muted-foreground">
{problemsOnly ? "Keine Fehler oder Warnungen — der Dienst läuft sauber. ✓" : "Kein Treffer für den Filter."}
</span>
) : (
shown.map((l, i) => (
<div key={i} className={cn("whitespace-pre-wrap", LEVEL_CLS[l.level])}>{l.t || " "}</div>
))
)}
</div>
</div>
)
}