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>
This commit is contained in:
Hitonabi
2026-07-03 20:11:59 +02:00
parent 5ee3f8f73d
commit 8a2db70f3c
10 changed files with 271 additions and 161 deletions
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-D0kUzkZ8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-_Rap01H_.css">
<script type="module" crossorigin src="/assets/index-B0tHcMp_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQJ4LntK.css">
</head>
<body>
<div id="root"></div>
+12 -57
View File
@@ -1,9 +1,10 @@
import { useEffect, useState, useRef } from "react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, AlertTriangle, Camera, Bot, Box, Shuffle } from "lucide-react"
import { useEffect, useState } from "react"
import { X, RefreshCw, Cpu, Server, Shield, Power, Camera, Bot, Box, Shuffle } from "lucide-react"
import { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api"
import { cn } from "@/lib/utils"
import { CustomDialog } from "./CustomDialog"
import { SERVICES, ServiceRow, UpdateRow, formatBytes } from "./system/rows"
import { LogConsole } from "./system/LogConsole"
import { SettingsTab } from "./system/SettingsTab"
import { UpdateDetailModal, type UpdateDetailState } from "./system/UpdateDetailModal"
@@ -86,7 +87,6 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
}
}, [open, defaultTab])
const logContainerRef = useRef<HTMLPreElement | null>(null)
function loadUpdates() {
api<UpdatesResp>("/api/maintenance/updates")
@@ -124,15 +124,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
}
})
.catch((e) => setLogs(`Fehler: ${e.message}`))
.finally(() => {
setLoadingLogs(false)
// Scroll to bottom
setTimeout(() => {
if (logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight
}
}, 50)
})
.finally(() => setLoadingLogs(false))
}
// Poll jobs & updates when open
@@ -593,51 +585,14 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</button>
</div>
{/* Log Console Container */}
<div className="flex-1 flex flex-col min-h-[300px] border border-border/60 bg-black/50 rounded-xl overflow-hidden shadow-inner">
{/* Console Header */}
<div className="flex h-9 items-center justify-between px-4 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">
<Terminal className="h-3 w-3 text-primary" />
<span>stdout/stderr - {selectedService}</span>
</div>
<button
onClick={() => loadServiceLogs(selectedService)}
disabled={loadingLogs}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<RefreshCw className={cn("h-3 w-3", loadingLogs && "animate-spin")} />
</button>
</div>
{/* Console Log Area */}
<pre
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"
>
{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>
)}
</pre>
</div>
<LogConsole
service={selectedService}
text={logs}
loading={loadingLogs}
logStatus={logStatus}
onRefresh={() => loadServiceLogs(selectedService)}
onOpenSettings={() => setActiveTab("settings")}
/>
</div>
)}
@@ -0,0 +1,119 @@
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>
)
}
+1
View File
@@ -8,6 +8,7 @@ export const SERVICES = [
{ id: "mission-control-2", label: "Mission Control", type: "user", reach: "gateway (integr" },
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user", reach: "hermes-gateway" },
{ id: "mem0-service", label: "Mem0 (Gedächtnis)", type: "user", reach: "mem0" },
{ id: "voice-service", label: "Voice (Sprache)", type: "user", reach: "voice" },
{ id: "hermes-terminal", label: "Hermes Terminal", type: "user", reach: "hermes-terminal" },
{ id: "llama-swap", label: "Llama Swap", type: "system", reach: "llama-swap" },
]