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:
+5
-1
@@ -19,7 +19,7 @@ from starlette.requests import Request
|
|||||||
|
|
||||||
from config import FRONTEND_DIST, VERSION
|
from config import FRONTEND_DIST, VERSION
|
||||||
from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, reminders as reminders_router, routing, system, voice
|
from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, reminders as reminders_router, routing, system, voice
|
||||||
from services import reminders, sentry, warmer
|
from services import memory as memory_svc, reminders, sentry, warmer
|
||||||
|
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||||
# für alle Module (logging.getLogger(__name__)).
|
# für alle Module (logging.getLogger(__name__)).
|
||||||
@@ -40,6 +40,10 @@ async def lifespan(app: FastAPI):
|
|||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||||
|
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||||
|
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||||
|
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||||
|
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ UI und MCP-Server kompatibel bleiben. Neu gegenüber der alten flachen SQLite:
|
|||||||
5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
|
5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
@@ -19,6 +22,8 @@ import httpx
|
|||||||
|
|
||||||
from config import MEM0_SERVICE_URL
|
from config import MEM0_SERVICE_URL
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
||||||
|
|
||||||
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
||||||
@@ -145,3 +150,28 @@ def dedupe(apply: bool = False, threshold: float = 0.85) -> dict:
|
|||||||
if delete_memory(d["id"]):
|
if delete_memory(d["id"]):
|
||||||
removed += 1
|
removed += 1
|
||||||
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}
|
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Auto-Dedupe (D16e): Dubletten von selbst wegräumen, kein Knopf-Zwang ─────────────
|
||||||
|
# Deterministisch in UNSERER Schicht (wie reminders, Verdikt 11c) statt am Hermes-cron —
|
||||||
|
# läuft als Hintergrund-Task. Höhere Schwelle als der manuelle Knopf (0.9 statt 0.85), weil
|
||||||
|
# OHNE Mensch-Review angewandt wird: lieber eine Dublette stehen lassen als etwas Distinktes
|
||||||
|
# löschen. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier fängt den Rest
|
||||||
|
# (Verbatim-Importe, wiederholte Fakten) ab, damit das Gedächtnis nicht ohne Zutun aufbläht.
|
||||||
|
AUTO_DEDUPE_ENABLED = os.environ.get("MC_MEM_DEDUPE_ENABLED", "1") != "0"
|
||||||
|
AUTO_DEDUPE_INTERVAL = int(os.environ.get("MC_MEM_DEDUPE_INTERVAL", str(24 * 3600))) # täglich
|
||||||
|
AUTO_DEDUPE_START_DELAY = int(os.environ.get("MC_MEM_DEDUPE_START_DELAY", "300")) # 5 min nach Start
|
||||||
|
AUTO_DEDUPE_THRESHOLD = float(os.environ.get("MC_MEM_DEDUPE_THRESHOLD", "0.9"))
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_dedupe_loop() -> None:
|
||||||
|
"""Hintergrund-Task: räumt periodisch Gedächtnis-Dubletten weg (apply=True)."""
|
||||||
|
await asyncio.sleep(AUTO_DEDUPE_START_DELAY) # Mem0-Sidecar nach Start hochkommen lassen
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
res = await asyncio.to_thread(dedupe, True, AUTO_DEDUPE_THRESHOLD) # sync HTTP → Thread
|
||||||
|
if res.get("removed"):
|
||||||
|
log.info("mem-dedupe: %d Dublette(n) automatisch entfernt", res["removed"])
|
||||||
|
except Exception:
|
||||||
|
log.debug("mem-dedupe: Lauf fehlgeschlagen", exc_info=True)
|
||||||
|
await asyncio.sleep(AUTO_DEDUPE_INTERVAL)
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+100
-99
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
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-D0kUzkZ8.js"></script>
|
<script type="module" crossorigin src="/assets/index-B0tHcMp_.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-_Rap01H_.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DQJ4LntK.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState, useRef } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, AlertTriangle, Camera, Bot, Box, Shuffle } from "lucide-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 { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { CustomDialog } from "./CustomDialog"
|
import { CustomDialog } from "./CustomDialog"
|
||||||
import { SERVICES, ServiceRow, UpdateRow, formatBytes } from "./system/rows"
|
import { SERVICES, ServiceRow, UpdateRow, formatBytes } from "./system/rows"
|
||||||
|
import { LogConsole } from "./system/LogConsole"
|
||||||
import { SettingsTab } from "./system/SettingsTab"
|
import { SettingsTab } from "./system/SettingsTab"
|
||||||
import { UpdateDetailModal, type UpdateDetailState } from "./system/UpdateDetailModal"
|
import { UpdateDetailModal, type UpdateDetailState } from "./system/UpdateDetailModal"
|
||||||
|
|
||||||
@@ -86,7 +87,6 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
}
|
}
|
||||||
}, [open, defaultTab])
|
}, [open, defaultTab])
|
||||||
|
|
||||||
const logContainerRef = useRef<HTMLPreElement | null>(null)
|
|
||||||
|
|
||||||
function loadUpdates() {
|
function loadUpdates() {
|
||||||
api<UpdatesResp>("/api/maintenance/updates")
|
api<UpdatesResp>("/api/maintenance/updates")
|
||||||
@@ -124,15 +124,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => setLogs(`Fehler: ${e.message}`))
|
.catch((e) => setLogs(`Fehler: ${e.message}`))
|
||||||
.finally(() => {
|
.finally(() => setLoadingLogs(false))
|
||||||
setLoadingLogs(false)
|
|
||||||
// Scroll to bottom
|
|
||||||
setTimeout(() => {
|
|
||||||
if (logContainerRef.current) {
|
|
||||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll jobs & updates when open
|
// Poll jobs & updates when open
|
||||||
@@ -593,51 +585,14 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Log Console Container */}
|
<LogConsole
|
||||||
<div className="flex-1 flex flex-col min-h-[300px] border border-border/60 bg-black/50 rounded-xl overflow-hidden shadow-inner">
|
service={selectedService}
|
||||||
{/* Console Header */}
|
text={logs}
|
||||||
<div className="flex h-9 items-center justify-between px-4 border-b border-border/40 bg-black/30 shrink-0">
|
loading={loadingLogs}
|
||||||
<div className="flex items-center gap-1.5 text-[10px] font-mono text-muted-foreground">
|
logStatus={logStatus}
|
||||||
<Terminal className="h-3 w-3 text-primary" />
|
onRefresh={() => loadServiceLogs(selectedService)}
|
||||||
<span>stdout/stderr - {selectedService}</span>
|
onOpenSettings={() => setActiveTab("settings")}
|
||||||
</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>
|
|
||||||
</div>
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export const SERVICES = [
|
|||||||
{ id: "mission-control-2", label: "Mission Control", type: "user", reach: "gateway (integr" },
|
{ id: "mission-control-2", label: "Mission Control", type: "user", reach: "gateway (integr" },
|
||||||
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user", reach: "hermes-gateway" },
|
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user", reach: "hermes-gateway" },
|
||||||
{ id: "mem0-service", label: "Mem0 (Gedächtnis)", type: "user", reach: "mem0" },
|
{ 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: "hermes-terminal", label: "Hermes Terminal", type: "user", reach: "hermes-terminal" },
|
||||||
{ id: "llama-swap", label: "Llama Swap", type: "system", reach: "llama-swap" },
|
{ id: "llama-swap", label: "Llama Swap", type: "system", reach: "llama-swap" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user