From 9d73684ecec30366edd86fc5768521a42a3ac9f7 Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 14 Apr 2026 14:22:31 +0200 Subject: [PATCH] Monitor-Events Backend-Persistierung in SQLite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src-tauri/src/db.rs | 177 ++++++++++++++++++++++++++++++++++++++- src-tauri/src/lib.rs | 6 ++ src/lib/stores/app.ts | 73 +++++++++++++++- src/lib/stores/events.ts | 4 + 4 files changed, 256 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 97ff1d5..d1a5066 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -39,6 +39,20 @@ pub struct ChatMessage { 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, // JSON-String + pub agent_id: Option, + pub session_id: Option, + pub duration_ms: Option, + pub error: Option, +} + /// Datenbank-Wrapper pub struct Database { conn: Connection, @@ -167,7 +181,28 @@ impl Database { ); 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(()) @@ -711,6 +746,96 @@ impl Database { 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> { + 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::>>()?; + + Ok(events) + } + + /// Lädt Monitor-Events nach Typ gefiltert + pub fn load_monitor_events_by_type(&self, event_type: &str, limit: usize) -> SqlResult> { + 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::>>()?; + + Ok(events) + } + + /// Löscht alle Monitor-Events + pub fn clear_monitor_events(&self) -> SqlResult { + 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> { + 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::>>()?; + Ok(counts) + } + // ============ Statistiken ============ /// DB-Statistiken @@ -865,3 +990,53 @@ pub async fn compact_session( 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::(); + 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) -> Result, String> { + let limit = limit.unwrap_or(1000); // Standard: letzte 1000 Events + let state = app.state::(); + 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, +) -> Result, String> { + let limit = limit.unwrap_or(500); + let state = app.state::(); + 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 { + let state = app.state::(); + 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, String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.count_monitor_events_by_type().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 42be33c..d1c42a1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -68,6 +68,12 @@ pub fn run() { db::load_messages, db::clear_messages, 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) knowledge::search_knowledge, knowledge::get_knowledge, diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index 99ff654..2eb5c65 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -1,6 +1,7 @@ // Claude Desktop — App-State import { writable, derived } from 'svelte/store'; +import { invoke } from '@tauri-apps/api/core'; // Typen 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( type: MonitorEventType, summary: string, @@ -352,12 +353,78 @@ export function addMonitorEvent( 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; } -// Monitor leeren -export function clearMonitorEvents() { +// Monitor-Event in DB speichern +async function saveMonitorEventToDb(event: MonitorEvent): Promise { + // 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 { + 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('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 { monitorEvents.set([]); + selectedMonitorEventId.set(null); + + try { + const count = await invoke('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 diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index a9b4c03..7243687 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -21,6 +21,7 @@ import { currentSessionId, messageToDb, addMonitorEvent, + loadMonitorEventsFromDb, type Message, type Agent, type MonitorEventType @@ -102,6 +103,9 @@ export async function initEventListeners(): Promise { console.log('🎧 Initialisiere Event-Listener...'); await cleanupEventListeners(); + // Monitor-Events aus DB laden (letzte Session) + await loadMonitorEventsFromDb(500); + // Bridge bereit listeners.push( await listen('bridge-ready', () => {