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,
|
||||
}
|
||||
|
||||
/// 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
|
||||
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<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 ============
|
||||
|
||||
/// 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::<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::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,
|
||||
|
|
|
|||
|
|
@ -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<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([]);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
currentSessionId,
|
||||
messageToDb,
|
||||
addMonitorEvent,
|
||||
loadMonitorEventsFromDb,
|
||||
type Message,
|
||||
type Agent,
|
||||
type MonitorEventType
|
||||
|
|
@ -102,6 +103,9 @@ export async function initEventListeners(): Promise<void> {
|
|||
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', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue