claude-desktop/src-tauri/src/db.rs
Eddy 433e2de2b6 Modell-Auswahl in Settings implementiert
- Neues SettingsPanel mit Modell-Auswahl (Haiku/Sonnet/Opus)
- Modell wird in SQLite persistiert (claude_model Setting)
- Bridge unterstützt set-model und get-models Commands
- Modell kann zur Laufzeit gewechselt werden
- Preisanzeige pro Modell im Settings-Panel
- Aktuelles Modell wird beim App-Start geladen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 09:32:26 +02:00

639 lines
22 KiB
Rust

// Claude Desktop — SQLite Datenbankschicht
// Persistiert Guard-Rails, Audit-Log, Memory und Einstellungen
use rusqlite::{params, Connection, Result as SqlResult};
use std::path::Path;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use crate::audit::{AuditAction, AuditCategory, AuditEntry, AuditStats};
use crate::guard::{Permission, PermissionAction, PermissionType};
use crate::memory::{ContextCategory, MemoryEntry, Pattern};
/// Eine Claude-Session
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Session {
pub id: String,
pub claude_session_id: Option<String>,
pub title: String,
pub working_dir: Option<String>,
pub message_count: i64,
pub token_input: i64,
pub token_output: i64,
pub cost_usd: f64,
pub status: String,
pub created_at: String,
pub updated_at: String,
pub last_message: Option<String>,
}
/// Datenbank-Wrapper
pub struct Database {
conn: Connection,
}
/// Datenbank-Statistiken
#[derive(Debug, serde::Serialize)]
pub struct DbStats {
pub permissions: usize,
pub audit_entries: usize,
pub memory_entries: usize,
pub patterns: usize,
pub db_size_kb: u64,
}
impl Database {
/// Öffnet oder erstellt die Datenbank
pub fn open(path: &Path) -> SqlResult<Self> {
let conn = Connection::open(path)?;
// WAL-Modus für bessere Performance
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
let db = Self { conn };
db.create_tables()?;
Ok(db)
}
/// Schema erstellen
fn create_tables(&self) -> SqlResult<()> {
self.conn.execute_batch(
"
-- Guard-Rails Permissions
CREATE TABLE IF NOT EXISTS permissions (
id TEXT PRIMARY KEY,
pattern TEXT NOT NULL,
tool TEXT,
path_pattern TEXT,
action TEXT NOT NULL DEFAULT 'allow',
created_at TEXT NOT NULL,
use_count INTEGER DEFAULT 0,
last_used TEXT
);
-- Audit-Log
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
category TEXT NOT NULL,
action TEXT NOT NULL,
item_id TEXT NOT NULL,
item_name TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
reason TEXT,
auto_corrected INTEGER DEFAULT 0,
session_id TEXT
);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_audit_category ON audit_log(category);
-- Memory-Einträge
CREATE TABLE IF NOT EXISTS memory (
id TEXT PRIMARY KEY,
category TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
sticky INTEGER DEFAULT 0,
auto_load INTEGER DEFAULT 0,
last_used TEXT,
use_count INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category);
CREATE INDEX IF NOT EXISTS idx_memory_sticky ON memory(sticky) WHERE sticky = 1;
-- Patterns (Vorgehensweisen)
CREATE TABLE IF NOT EXISTS patterns (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
trigger_text TEXT,
old_approach TEXT,
new_approach TEXT,
reason TEXT,
occurrence_count INTEGER DEFAULT 1,
auto_corrected INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Sessions (Claude-Konversationen)
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
claude_session_id TEXT,
title TEXT NOT NULL,
working_dir TEXT,
message_count INTEGER DEFAULT 0,
token_input INTEGER DEFAULT 0,
token_output INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_message TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
-- Einstellungen (Key-Value)
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
",
)?;
Ok(())
}
// ============ Permissions ============
/// Speichert eine Permission
pub fn save_permission(&self, perm: &Permission) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO permissions (id, pattern, tool, path_pattern, action, created_at, use_count, last_used)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
perm.id,
perm.pattern,
perm.tool,
perm.path_pattern,
format!("{:?}", perm.action).to_lowercase(),
perm.created_at,
perm.use_count,
perm.last_used,
],
)?;
Ok(())
}
/// Lädt alle permanenten Permissions
pub fn load_permissions(&self) -> SqlResult<Vec<Permission>> {
let mut stmt = self.conn.prepare(
"SELECT id, pattern, tool, path_pattern, action, created_at, use_count, last_used FROM permissions"
)?;
let perms = stmt.query_map([], |row| {
let action_str: String = row.get(4)?;
let action = match action_str.as_str() {
"deny" => PermissionAction::Deny,
_ => PermissionAction::Allow,
};
Ok(Permission {
id: row.get(0)?,
pattern: row.get(1)?,
tool: row.get(2)?,
path_pattern: row.get(3)?,
permission_type: PermissionType::Permanent,
action,
created_at: row.get(5)?,
use_count: row.get(6)?,
last_used: row.get(7)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(perms)
}
/// Löscht eine Permission
pub fn delete_permission(&self, id: &str) -> SqlResult<()> {
self.conn.execute("DELETE FROM permissions WHERE id = ?1", params![id])?;
Ok(())
}
// ============ Audit-Log ============
/// Speichert einen Audit-Eintrag
pub fn save_audit_entry(&self, entry: &AuditEntry) -> SqlResult<()> {
self.conn.execute(
"INSERT INTO audit_log (id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
entry.id,
entry.timestamp,
format!("{:?}", entry.category).to_lowercase(),
format!("{:?}", entry.action).to_lowercase(),
entry.item_id,
entry.item_name,
entry.old_value.as_ref().map(|v| v.to_string()),
entry.new_value.as_ref().map(|v| v.to_string()),
entry.reason,
entry.auto_corrected as i32,
entry.session_id,
],
)?;
Ok(())
}
/// Lädt die letzten N Audit-Einträge
pub fn load_audit_log(&self, limit: usize) -> SqlResult<Vec<AuditEntry>> {
let mut stmt = self.conn.prepare(
"SELECT id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id
FROM audit_log ORDER BY timestamp DESC LIMIT ?1"
)?;
let entries = stmt.query_map(params![limit as i64], |row| {
let cat_str: String = row.get(2)?;
let act_str: String = row.get(3)?;
let old_val: Option<String> = row.get(6)?;
let new_val: Option<String> = row.get(7)?;
let auto_corr: i32 = row.get(9)?;
Ok(AuditEntry {
id: row.get(0)?,
timestamp: row.get(1)?,
category: parse_audit_category(&cat_str),
action: parse_audit_action(&act_str),
item_id: row.get(4)?,
item_name: row.get(5)?,
old_value: old_val.and_then(|s| serde_json::from_str(&s).ok()),
new_value: new_val.and_then(|s| serde_json::from_str(&s).ok()),
reason: row.get(8)?,
auto_corrected: auto_corr != 0,
session_id: row.get(10)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(entries)
}
/// Audit-Statistiken
pub fn audit_stats(&self) -> SqlResult<AuditStats> {
let total: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
)?;
let auto_corrected: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log WHERE auto_corrected = 1", [], |row| row.get(0)
)?;
let today: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log WHERE timestamp LIKE ?1 || '%'",
params![chrono::Local::now().format("%Y-%m-%d").to_string()],
|row| row.get(0),
)?;
Ok(AuditStats { total, auto_corrected, today })
}
// ============ Memory ============
/// Speichert einen Memory-Eintrag
pub fn save_memory_entry(&self, entry: &MemoryEntry) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO memory (id, category, key, value, sticky, auto_load, last_used, use_count)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
entry.id,
format!("{:?}", entry.category),
entry.key,
entry.value.to_string(),
entry.sticky as i32,
entry.auto_load as i32,
entry.last_used,
entry.use_count,
],
)?;
Ok(())
}
/// Lädt alle Memory-Einträge
pub fn load_memory_entries(&self) -> SqlResult<Vec<MemoryEntry>> {
let mut stmt = self.conn.prepare(
"SELECT id, category, key, value, sticky, auto_load, last_used, use_count FROM memory"
)?;
let entries = stmt.query_map([], |row| {
let cat_str: String = row.get(1)?;
let val_str: String = row.get(3)?;
let sticky: i32 = row.get(4)?;
let auto_load: i32 = row.get(5)?;
Ok(MemoryEntry {
id: row.get(0)?,
category: parse_context_category(&cat_str),
key: row.get(2)?,
value: serde_json::from_str(&val_str).unwrap_or(serde_json::Value::String(val_str)),
sticky: sticky != 0,
auto_load: auto_load != 0,
last_used: row.get(6)?,
use_count: row.get(7)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(entries)
}
/// Löscht einen Memory-Eintrag
pub fn delete_memory_entry(&self, id: &str) -> SqlResult<()> {
self.conn.execute("DELETE FROM memory WHERE id = ?1", params![id])?;
Ok(())
}
// ============ Patterns ============
/// Speichert ein Pattern
pub fn save_pattern(&self, pattern: &Pattern) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO patterns (id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
pattern.id,
pattern.name,
pattern.description,
pattern.trigger,
pattern.old_approach,
pattern.new_approach,
pattern.reason,
pattern.occurrence_count,
pattern.auto_corrected as i32,
pattern.created_at,
pattern.updated_at,
],
)?;
Ok(())
}
/// Lädt alle Patterns
pub fn load_patterns(&self) -> SqlResult<Vec<Pattern>> {
let mut stmt = self.conn.prepare(
"SELECT id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at FROM patterns"
)?;
let patterns = stmt.query_map([], |row| {
let auto_corr: i32 = row.get(8)?;
Ok(Pattern {
id: row.get(0)?,
name: row.get(1)?,
description: row.get(2)?,
trigger: row.get(3)?,
old_approach: row.get(4)?,
new_approach: row.get(5)?,
reason: row.get(6)?,
occurrence_count: row.get(7)?,
auto_corrected: auto_corr != 0,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(patterns)
}
// ============ Sessions ============
/// Erstellt eine neue Session
pub fn create_session(&self, session: &Session) -> SqlResult<()> {
self.conn.execute(
"INSERT INTO sessions (id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
session.id,
session.claude_session_id,
session.title,
session.working_dir,
session.message_count,
session.token_input,
session.token_output,
session.cost_usd,
session.status,
session.created_at,
session.updated_at,
session.last_message,
],
)?;
Ok(())
}
/// Aktualisiert eine Session
pub fn update_session(&self, session: &Session) -> SqlResult<()> {
self.conn.execute(
"UPDATE sessions SET claude_session_id = ?2, title = ?3, message_count = ?4,
token_input = ?5, token_output = ?6, cost_usd = ?7, status = ?8,
updated_at = ?9, last_message = ?10
WHERE id = ?1",
params![
session.id,
session.claude_session_id,
session.title,
session.message_count,
session.token_input,
session.token_output,
session.cost_usd,
session.status,
chrono::Local::now().to_rfc3339(),
session.last_message,
],
)?;
Ok(())
}
/// Lädt alle Sessions (neueste zuerst)
pub fn load_sessions(&self, limit: usize) -> SqlResult<Vec<Session>> {
let mut stmt = self.conn.prepare(
"SELECT id, claude_session_id, title, working_dir, message_count,
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
FROM sessions ORDER BY updated_at DESC LIMIT ?1"
)?;
let sessions = stmt.query_map(params![limit as i64], |row| {
Ok(Session {
id: row.get(0)?,
claude_session_id: row.get(1)?,
title: row.get(2)?,
working_dir: row.get(3)?,
message_count: row.get(4)?,
token_input: row.get(5)?,
token_output: row.get(6)?,
cost_usd: row.get(7)?,
status: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
last_message: row.get(11)?,
})
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(sessions)
}
/// Holt eine Session nach ID
pub fn get_session(&self, id: &str) -> SqlResult<Option<Session>> {
let result = self.conn.query_row(
"SELECT id, claude_session_id, title, working_dir, message_count,
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
FROM sessions WHERE id = ?1",
params![id],
|row| {
Ok(Session {
id: row.get(0)?,
claude_session_id: row.get(1)?,
title: row.get(2)?,
working_dir: row.get(3)?,
message_count: row.get(4)?,
token_input: row.get(5)?,
token_output: row.get(6)?,
cost_usd: row.get(7)?,
status: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
last_message: row.get(11)?,
})
},
);
match result {
Ok(s) => Ok(Some(s)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
/// Löscht eine Session
pub fn delete_session(&self, id: &str) -> SqlResult<()> {
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
Ok(())
}
// ============ Settings ============
/// Speichert eine Einstellung
pub fn set_setting(&self, key: &str, value: &str) -> SqlResult<()> {
self.conn.execute(
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)",
params![key, value, chrono::Local::now().to_rfc3339()],
)?;
Ok(())
}
/// Liest eine Einstellung
pub fn get_setting(&self, key: &str) -> SqlResult<Option<String>> {
let result = self.conn.query_row(
"SELECT value FROM settings WHERE key = ?1",
params![key],
|row| row.get(0),
);
match result {
Ok(val) => Ok(Some(val)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
/// Lädt alle Einstellungen
pub fn get_all_settings(&self) -> SqlResult<Vec<(String, String)>> {
let mut stmt = self.conn.prepare("SELECT key, value FROM settings")?;
let settings = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(settings)
}
// ============ Statistiken ============
/// DB-Statistiken
pub fn stats(&self) -> SqlResult<DbStats> {
let permissions: usize = self.conn.query_row(
"SELECT COUNT(*) FROM permissions", [], |row| row.get(0)
)?;
let audit_entries: usize = self.conn.query_row(
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
)?;
let memory_entries: usize = self.conn.query_row(
"SELECT COUNT(*) FROM memory", [], |row| row.get(0)
)?;
let patterns: usize = self.conn.query_row(
"SELECT COUNT(*) FROM patterns", [], |row| row.get(0)
)?;
// DB-Größe ermitteln
let page_count: u64 = self.conn.query_row(
"PRAGMA page_count", [], |row| row.get(0)
)?;
let page_size: u64 = self.conn.query_row(
"PRAGMA page_size", [], |row| row.get(0)
)?;
let db_size_kb = (page_count * page_size) / 1024;
Ok(DbStats { permissions, audit_entries, memory_entries, patterns, db_size_kb })
}
}
// ============ Hilfsfunktionen ============
fn parse_audit_category(s: &str) -> AuditCategory {
match s {
"guardrail" | "guard_rail" => AuditCategory::GuardRail,
"pattern" => AuditCategory::Pattern,
"hook" => AuditCategory::Hook,
"skill" => AuditCategory::Skill,
"setting" => AuditCategory::Setting,
"mcp" => AuditCategory::MCP,
"memory" => AuditCategory::Memory,
_ => AuditCategory::Setting,
}
}
fn parse_audit_action(s: &str) -> AuditAction {
match s {
"create" => AuditAction::Create,
"update" => AuditAction::Update,
"delete" => AuditAction::Delete,
"enable" => AuditAction::Enable,
"disable" => AuditAction::Disable,
_ => AuditAction::Update,
}
}
fn parse_context_category(s: &str) -> ContextCategory {
match s {
"Critical" => ContextCategory::Critical,
"Pattern" => ContextCategory::Pattern,
"Preference" => ContextCategory::Preference,
"GuardRail" => ContextCategory::GuardRail,
"Hook" => ContextCategory::Hook,
"Skill" => ContextCategory::Skill,
_ => ContextCategory::Pattern,
}
}
// ============ Tauri Commands ============
pub type DbState = Arc<Mutex<Database>>;
/// DB initialisieren (falls Frontend es auslösen will)
#[tauri::command]
pub async fn init_database(app: AppHandle) -> Result<DbStats, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string())
}
/// DB-Statistiken abrufen
#[tauri::command]
pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string())
}
/// Einstellung lesen
#[tauri::command]
pub async fn get_setting(app: AppHandle, key: String) -> Result<Option<String>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_setting(&key).map_err(|e| e.to_string())
}
/// Einstellung speichern
#[tauri::command]
pub async fn set_setting(app: AppHandle, key: String, value: String) -> Result<(), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.set_setting(&key, &value).map_err(|e| e.to_string())
}
/// Alle Einstellungen laden
#[tauri::command]
pub async fn get_all_settings(app: AppHandle) -> Result<Vec<(String, String)>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_all_settings().map_err(|e| e.to_string())
}