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:
parent
af663c6eee
commit
9d73684ece
4 changed files with 256 additions and 4 deletions
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue