Monitor-Events Backend-Persistierung in SQLite

- MonitorEvent Struct + CRUD-Methoden in db.rs
- monitor_events Tabelle mit Auto-Cleanup (7 Tage)
- Tauri Commands: save/load/clear_monitor_events
- Frontend: Events beim Start laden, beim Hinzufügen speichern
- Async clearMonitorEvents löscht auch DB-Einträge

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 14:22:31 +02:00
parent af663c6eee
commit 9d73684ece
4 changed files with 256 additions and 4 deletions

View file

@ -39,6 +39,20 @@ pub struct ChatMessage {
pub timestamp: String, pub timestamp: String,
} }
/// Ein Monitor-Event (System-Log)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MonitorEvent {
pub id: String,
pub timestamp: String,
pub event_type: String, // "api", "tool", "agent", "hook", "mcp", "error", "debug"
pub summary: String,
pub details: Option<String>, // JSON-String
pub agent_id: Option<String>,
pub session_id: Option<String>,
pub duration_ms: Option<i64>,
pub error: Option<String>,
}
/// Datenbank-Wrapper /// Datenbank-Wrapper
pub struct Database { pub struct Database {
conn: Connection, conn: Connection,
@ -167,7 +181,28 @@ impl Database {
); );
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
" -- Monitor-Events (System-Log)
CREATE TABLE IF NOT EXISTS monitor_events (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
event_type TEXT NOT NULL,
summary TEXT NOT NULL,
details TEXT,
agent_id TEXT,
session_id TEXT,
duration_ms INTEGER,
error TEXT
);
CREATE INDEX IF NOT EXISTS idx_monitor_timestamp ON monitor_events(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_monitor_type ON monitor_events(event_type);
-- Automatisch alte Monitor-Events löschen (älter als 7 Tage)
CREATE TRIGGER IF NOT EXISTS cleanup_old_monitor_events
AFTER INSERT ON monitor_events
BEGIN
DELETE FROM monitor_events
WHERE timestamp < datetime('now', '-7 days');
END;
", ",
)?; )?;
Ok(()) Ok(())
@ -711,6 +746,96 @@ impl Database {
Ok(settings) Ok(settings)
} }
// ============ Monitor-Events ============
/// Speichert ein Monitor-Event
pub fn save_monitor_event(&self, event: &MonitorEvent) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO monitor_events (id, timestamp, event_type, summary, details, agent_id, session_id, duration_ms, error)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
event.id,
event.timestamp,
event.event_type,
event.summary,
event.details,
event.agent_id,
event.session_id,
event.duration_ms,
event.error,
],
)?;
Ok(())
}
/// Lädt die letzten N Monitor-Events
pub fn load_monitor_events(&self, limit: usize) -> SqlResult<Vec<MonitorEvent>> {
let mut stmt = self.conn.prepare(
"SELECT id, timestamp, event_type, summary, details, agent_id, session_id, duration_ms, error
FROM monitor_events ORDER BY timestamp DESC LIMIT ?1"
)?;
let events = stmt.query_map(params![limit as i64], |row| {
Ok(MonitorEvent {
id: row.get(0)?,
timestamp: row.get(1)?,
event_type: row.get(2)?,
summary: row.get(3)?,
details: row.get(4)?,
agent_id: row.get(5)?,
session_id: row.get(6)?,
duration_ms: row.get(7)?,
error: row.get(8)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(events)
}
/// Lädt Monitor-Events nach Typ gefiltert
pub fn load_monitor_events_by_type(&self, event_type: &str, limit: usize) -> SqlResult<Vec<MonitorEvent>> {
let mut stmt = self.conn.prepare(
"SELECT id, timestamp, event_type, summary, details, agent_id, session_id, duration_ms, error
FROM monitor_events WHERE event_type = ?1 ORDER BY timestamp DESC LIMIT ?2"
)?;
let events = stmt.query_map(params![event_type, limit as i64], |row| {
Ok(MonitorEvent {
id: row.get(0)?,
timestamp: row.get(1)?,
event_type: row.get(2)?,
summary: row.get(3)?,
details: row.get(4)?,
agent_id: row.get(5)?,
session_id: row.get(6)?,
duration_ms: row.get(7)?,
error: row.get(8)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(events)
}
/// Löscht alle Monitor-Events
pub fn clear_monitor_events(&self) -> SqlResult<usize> {
let count: usize = self.conn.query_row(
"SELECT COUNT(*) FROM monitor_events", [], |row| row.get(0)
)?;
self.conn.execute("DELETE FROM monitor_events", [])?;
Ok(count)
}
/// Zählt Monitor-Events nach Typ
pub fn count_monitor_events_by_type(&self) -> SqlResult<Vec<(String, usize)>> {
let mut stmt = self.conn.prepare(
"SELECT event_type, COUNT(*) FROM monitor_events GROUP BY event_type"
)?;
let counts = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(counts)
}
// ============ Statistiken ============ // ============ Statistiken ============
/// DB-Statistiken /// DB-Statistiken
@ -865,3 +990,53 @@ pub async fn compact_session(
Ok(compacted) Ok(compacted)
} }
// ============ Monitor-Events Commands ============
/// Monitor-Event speichern
#[tauri::command]
pub async fn save_monitor_event(app: AppHandle, event: MonitorEvent) -> Result<(), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.save_monitor_event(&event).map_err(|e| e.to_string())
}
/// Monitor-Events laden (neueste zuerst)
#[tauri::command]
pub async fn load_monitor_events(app: AppHandle, limit: Option<usize>) -> Result<Vec<MonitorEvent>, String> {
let limit = limit.unwrap_or(1000); // Standard: letzte 1000 Events
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.load_monitor_events(limit).map_err(|e| e.to_string())
}
/// Monitor-Events nach Typ laden
#[tauri::command]
pub async fn load_monitor_events_by_type(
app: AppHandle,
event_type: String,
limit: Option<usize>,
) -> Result<Vec<MonitorEvent>, String> {
let limit = limit.unwrap_or(500);
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.load_monitor_events_by_type(&event_type, limit).map_err(|e| e.to_string())
}
/// Alle Monitor-Events löschen
#[tauri::command]
pub async fn clear_all_monitor_events(app: AppHandle) -> Result<usize, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
let count = db.clear_monitor_events().map_err(|e| e.to_string())?;
println!("🗑️ {} Monitor-Events gelöscht", count);
Ok(count)
}
/// Monitor-Event Statistiken
#[tauri::command]
pub async fn get_monitor_stats(app: AppHandle) -> Result<Vec<(String, usize)>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.count_monitor_events_by_type().map_err(|e| e.to_string())
}

View file

@ -68,6 +68,12 @@ pub fn run() {
db::load_messages, db::load_messages,
db::clear_messages, db::clear_messages,
db::compact_session, db::compact_session,
// Monitor-Events
db::save_monitor_event,
db::load_monitor_events,
db::load_monitor_events_by_type,
db::clear_all_monitor_events,
db::get_monitor_stats,
// Wissensbasis (claude-db) // Wissensbasis (claude-db)
knowledge::search_knowledge, knowledge::search_knowledge,
knowledge::get_knowledge, knowledge::get_knowledge,

View file

@ -1,6 +1,7 @@
// Claude Desktop — App-State // Claude Desktop — App-State
import { writable, derived } from 'svelte/store'; import { writable, derived } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';
// Typen // Typen
export interface Agent { export interface Agent {
@ -327,7 +328,7 @@ export const monitorStats = derived(monitorEvents, ($events) => {
}; };
}); });
// Monitor-Event hinzufügen // Monitor-Event hinzufügen (mit Persistierung)
export function addMonitorEvent( export function addMonitorEvent(
type: MonitorEventType, type: MonitorEventType,
summary: string, summary: string,
@ -352,12 +353,78 @@ export function addMonitorEvent(
return updated; return updated;
}); });
// Asynchron in DB speichern (fire-and-forget)
saveMonitorEventToDb(event).catch((err) => {
console.warn('Monitor-Event konnte nicht gespeichert werden:', err);
});
return event.id; return event.id;
} }
// Monitor leeren // Monitor-Event in DB speichern
export function clearMonitorEvents() { async function saveMonitorEventToDb(event: MonitorEvent): Promise<void> {
// Für DB-Speicherung: Timestamp als ISO-String, Details als JSON-String
const dbEvent = {
id: event.id,
timestamp: event.timestamp.toISOString(),
event_type: event.type,
summary: event.summary,
details: JSON.stringify(event.details),
agent_id: event.agentId ?? null,
session_id: null, // TODO: Aktuelle Session-ID übergeben
duration_ms: event.durationMs ?? null,
error: event.error ?? null,
};
await invoke('save_monitor_event', { event: dbEvent });
}
// Monitor-Events aus DB laden
export async function loadMonitorEventsFromDb(limit = 500): Promise<void> {
try {
interface DbMonitorEvent {
id: string;
timestamp: string;
event_type: string;
summary: string;
details: string | null;
agent_id: string | null;
session_id: string | null;
duration_ms: number | null;
error: string | null;
}
const dbEvents = await invoke<DbMonitorEvent[]>('load_monitor_events', { limit });
// DB-Events in Frontend-Format umwandeln (neueste zuerst → umkehren für chronologische Reihenfolge)
const events: MonitorEvent[] = dbEvents.reverse().map((e) => ({
id: e.id,
timestamp: new Date(e.timestamp),
type: e.event_type as MonitorEventType,
summary: e.summary,
details: e.details ? JSON.parse(e.details) : {},
agentId: e.agent_id ?? undefined,
durationMs: e.duration_ms ?? undefined,
error: e.error ?? undefined,
}));
monitorEvents.set(events);
console.log(`📊 ${events.length} Monitor-Events aus DB geladen`);
} catch (err) {
console.error('Fehler beim Laden der Monitor-Events:', err);
}
}
// Monitor leeren (auch in DB)
export async function clearMonitorEvents(): Promise<void> {
monitorEvents.set([]); monitorEvents.set([]);
selectedMonitorEventId.set(null);
try {
const count = await invoke<number>('clear_all_monitor_events');
console.log(`🗑️ ${count} Monitor-Events aus DB gelöscht`);
} catch (err) {
console.warn('Monitor-Events konnten nicht aus DB gelöscht werden:', err);
}
} }
// Sensitive Daten maskieren // Sensitive Daten maskieren

View file

@ -21,6 +21,7 @@ import {
currentSessionId, currentSessionId,
messageToDb, messageToDb,
addMonitorEvent, addMonitorEvent,
loadMonitorEventsFromDb,
type Message, type Message,
type Agent, type Agent,
type MonitorEventType type MonitorEventType
@ -102,6 +103,9 @@ export async function initEventListeners(): Promise<void> {
console.log('🎧 Initialisiere Event-Listener...'); console.log('🎧 Initialisiere Event-Listener...');
await cleanupEventListeners(); await cleanupEventListeners();
// Monitor-Events aus DB laden (letzte Session)
await loadMonitorEventsFromDb(500);
// Bridge bereit // Bridge bereit
listeners.push( listeners.push(
await listen('bridge-ready', () => { await listen('bridge-ready', () => {