diff --git a/src-tauri/src/btmsg.rs b/src-tauri/src/btmsg.rs index 86e6b42..3c140ec 100644 --- a/src-tauri/src/btmsg.rs +++ b/src-tauri/src/btmsg.rs @@ -4,6 +4,7 @@ use rusqlite::{params, Connection, OpenFlags}; use serde::{Deserialize, Serialize}; +use crate::error::AppError; use std::path::PathBuf; use std::sync::OnceLock; @@ -24,19 +25,19 @@ fn db_path() -> PathBuf { }) } -fn open_db() -> Result { +fn open_db() -> Result { let path = db_path(); if !path.exists() { - return Err("btmsg database not found. Run 'btmsg register' first.".into()); + return Err(AppError::database("btmsg database not found. Run 'btmsg register' first.")); } let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE) - .map_err(|e| format!("Failed to open btmsg.db: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to open btmsg.db: {e}")))?; conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())) - .map_err(|e| format!("Failed to set WAL mode: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set WAL mode: {e}")))?; conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(())) - .map_err(|e| format!("Failed to set busy_timeout: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set busy_timeout: {e}")))?; conn.execute_batch("PRAGMA foreign_keys = ON") - .map_err(|e| format!("Failed to enable foreign keys: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to enable foreign keys: {e}")))?; // Migration: add seen_messages table if not present conn.execute_batch( @@ -122,12 +123,12 @@ pub struct BtmsgChannelMessage { pub sender_role: String, } -pub fn get_agents(group_id: &str) -> Result, String> { +pub fn get_agents(group_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT a.*, (SELECT COUNT(*) FROM messages m WHERE m.to_agent = a.id AND m.read = 0) as unread_count \ FROM agents a WHERE a.group_id = ? ORDER BY a.tier, a.role, a.name" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let agents = stmt.query_map(params![group_id], |row| { Ok(BtmsgAgent { @@ -140,28 +141,28 @@ pub fn get_agents(group_id: &str) -> Result, String> { status: row.get::<_, Option>("status")?.unwrap_or_else(|| "stopped".into()), unread_count: row.get("unread_count")?, }) - }).map_err(|e| format!("Query error: {e}"))?; + }).map_err(|e| AppError::database(format!("Query error: {e}")))?; - agents.collect::, _>>().map_err(|e| format!("Row error: {e}")) + agents.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn unread_count(agent_id: &str) -> Result { +pub fn unread_count(agent_id: &str) -> Result { let db = open_db()?; db.query_row( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", params![agent_id], |row| row.get(0), - ).map_err(|e| format!("Query error: {e}")) + ).map_err(|e| AppError::database(format!("Query error: {e}"))) } -pub fn unread_messages(agent_id: &str) -> Result, String> { +pub fn unread_messages(agent_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \ a.name AS sender_name, a.role AS sender_role \ FROM messages m JOIN agents a ON m.from_agent = a.id \ WHERE m.to_agent = ? AND m.read = 0 ORDER BY m.created_at ASC" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let msgs = stmt.query_map(params![agent_id], |row| { Ok(BtmsgMessage { @@ -175,15 +176,15 @@ pub fn unread_messages(agent_id: &str) -> Result, String> { sender_name: row.get("sender_name")?, sender_role: row.get("sender_role")?, }) - }).map_err(|e| format!("Query error: {e}"))?; + }).map_err(|e| AppError::database(format!("Query error: {e}")))?; - msgs.collect::, _>>().map_err(|e| format!("Row error: {e}")) + msgs.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } /// Get messages that have not been seen by this session. /// Unlike unread_messages (which uses the global `read` flag), /// this tracks per-session acknowledgment via the seen_messages table. -pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result, String> { +pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \ @@ -193,7 +194,7 @@ pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result Result, _>>().map_err(|e| format!("Row error: {e}")) + rows.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } /// Mark specific message IDs as seen by this session. -pub fn mark_messages_seen(session_id: &str, message_ids: &[String]) -> Result<(), String> { +pub fn mark_messages_seen(session_id: &str, message_ids: &[String]) -> Result<(), AppError> { if message_ids.is_empty() { return Ok(()); } let db = open_db()?; let mut stmt = db.prepare( "INSERT OR IGNORE INTO seen_messages (session_id, message_id) VALUES (?1, ?2)" - ).map_err(|e| format!("Prepare mark_seen: {e}"))?; + ).map_err(|e| AppError::database(format!("Prepare mark_seen: {e}")))?; for id in message_ids { stmt.execute(params![session_id, id]) - .map_err(|e| format!("Insert seen: {e}"))?; + .map_err(|e| AppError::database(format!("Insert seen: {e}")))?; } Ok(()) } /// Prune seen_messages entries older than the given threshold. /// Uses emergency aggressive pruning (3 days) when row count exceeds the threshold. -pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Result { +pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Result { let db = open_db()?; let count: i64 = db.query_row( "SELECT COUNT(*) FROM seen_messages", [], |row| row.get(0) - ).map_err(|e| format!("Count seen: {e}"))?; + ).map_err(|e| AppError::database(format!("Count seen: {e}")))?; let threshold_secs = if count > emergency_threshold { // Emergency: prune more aggressively (3 days instead of configured max) @@ -248,12 +249,12 @@ pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Resul let deleted = db.execute( "DELETE FROM seen_messages WHERE seen_at < unixepoch() - ?1", params![threshold_secs], - ).map_err(|e| format!("Prune seen: {e}"))?; + ).map_err(|e| AppError::database(format!("Prune seen: {e}")))?; Ok(deleted as u64) } -pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result, String> { +pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \ @@ -261,7 +262,7 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result Result, _>>().map_err(|e| format!("Row error: {e}")) + msgs.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } /// Default heartbeat staleness threshold: 5 minutes const STALE_HEARTBEAT_SECS: i64 = 300; -pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result { +pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result { let db = open_db()?; // Get sender's group and tier @@ -291,7 +292,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result 0 { @@ -299,10 +300,10 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result 0 FROM contacts WHERE agent_id = ? AND contact_id = ?", params![from_agent, to_agent], |row| row.get(0), - ).map_err(|e| format!("Contact check error: {e}"))?; + ).map_err(|e| AppError::database(format!("Contact check error: {e}")))?; if !allowed { - return Err(format!("Not allowed to message '{to_agent}'")); + return Err(AppError::database(format!("Not allowed to message '{to_agent}'"))); } } @@ -332,7 +333,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result Result Result Result<(), String> { +pub fn set_status(agent_id: &str, status: &str) -> Result<(), AppError> { let db = open_db()?; db.execute( "UPDATE agents SET status = ?, last_active_at = datetime('now') WHERE id = ?", params![status, agent_id], - ).map_err(|e| format!("Update error: {e}"))?; + ).map_err(|e| AppError::database(format!("Update error: {e}")))?; Ok(()) } -pub fn ensure_admin(group_id: &str) -> Result<(), String> { +pub fn ensure_admin(group_id: &str) -> Result<(), AppError> { let db = open_db()?; let exists: bool = db.query_row( "SELECT COUNT(*) > 0 FROM agents WHERE id = 'admin'", [], |row| row.get(0), - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; if !exists { db.execute( "INSERT INTO agents (id, name, role, group_id, tier, status) \ VALUES ('admin', 'Operator', 'admin', ?, 0, 'active')", params![group_id], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; } // Ensure admin has bidirectional contacts with ALL agents in the group let mut stmt = db.prepare( "SELECT id FROM agents WHERE group_id = ? AND id != 'admin'" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let agent_ids: Vec = stmt.query_map(params![group_id], |row| row.get(0)) - .map_err(|e| format!("Query error: {e}"))? + .map_err(|e| AppError::database(format!("Query error: {e}")))? .collect::, _>>() - .map_err(|e| format!("Row error: {e}"))?; + .map_err(|e| AppError::database(format!("Row error: {e}")))?; drop(stmt); for aid in &agent_ids { db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES ('admin', ?)", params![aid], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?, 'admin')", params![aid], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; } Ok(()) @@ -410,7 +411,7 @@ pub fn ensure_admin(group_id: &str) -> Result<(), String> { /// - Manager gets contacts with ALL agents (Tier 1 + Tier 2) /// - Other Tier 1 agents get contacts with Manager /// Also ensures admin agent and review channels exist. -pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result<(), String> { +pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result<(), AppError> { for group in &groups.groups { register_group_agents(group)?; } @@ -418,7 +419,7 @@ pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result } /// Register all agents for a single group. -fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), String> { +fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), AppError> { let db = open_db_or_create()?; let group_id = &group.id; @@ -478,11 +479,11 @@ fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), Strin db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)", params![tier1_ids[i], tier1_ids[j]], - ).map_err(|e| format!("Contact insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?; db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)", params![tier1_ids[j], tier1_ids[i]], - ).map_err(|e| format!("Contact insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?; } } @@ -492,11 +493,11 @@ fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), Strin db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)", params![mgr_id, t2_id], - ).map_err(|e| format!("Contact insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?; db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)", params![t2_id, mgr_id], - ).map_err(|e| format!("Contact insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?; } } @@ -522,7 +523,7 @@ fn upsert_agent( model: Option<&str>, cwd: Option<&str>, system_prompt: Option<&str>, -) -> Result<(), String> { +) -> Result<(), AppError> { db.execute( "INSERT INTO agents (id, name, role, group_id, tier, model, cwd, system_prompt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) @@ -536,7 +537,7 @@ fn upsert_agent( system_prompt = excluded.system_prompt", params![id, name, role, group_id, tier, model, cwd, system_prompt], ) - .map_err(|e| format!("Upsert agent error: {e}"))?; + .map_err(|e| AppError::database(format!("Upsert agent error: {e}")))?; Ok(()) } @@ -561,23 +562,23 @@ fn ensure_review_channels_for_group(db: &Connection, group_id: &str) { } /// Open btmsg database, creating it with schema if it doesn't exist. -fn open_db_or_create() -> Result { +fn open_db_or_create() -> Result { let path = db_path(); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create data dir: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to create data dir: {e}")))?; } let conn = Connection::open_with_flags( &path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, ) - .map_err(|e| format!("Failed to open/create btmsg.db: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to open/create btmsg.db: {e}")))?; conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())) - .map_err(|e| format!("Failed to set WAL mode: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set WAL mode: {e}")))?; conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(())) - .map_err(|e| format!("Failed to set busy_timeout: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set busy_timeout: {e}")))?; // Create tables if they don't exist (same schema as Python btmsg CLI) conn.execute_batch( @@ -723,11 +724,11 @@ fn open_db_or_create() -> Result { FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_seen_messages_session ON seen_messages(session_id);" - ).map_err(|e| format!("Schema creation error: {e}"))?; + ).map_err(|e| AppError::database(format!("Schema creation error: {e}")))?; // Enable foreign keys for ON DELETE CASCADE support conn.execute_batch("PRAGMA foreign_keys = ON") - .map_err(|e| format!("Failed to enable foreign keys: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to enable foreign keys: {e}")))?; Ok(conn) } @@ -744,23 +745,23 @@ pub struct AgentHeartbeat { pub timestamp: i64, } -pub fn record_heartbeat(agent_id: &str) -> Result<(), String> { +pub fn record_heartbeat(agent_id: &str) -> Result<(), AppError> { let db = open_db()?; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| format!("Time error: {e}"))? + .map_err(|e| AppError::database(format!("Time error: {e}")))? .as_secs() as i64; db.execute( "INSERT INTO heartbeats (agent_id, timestamp) VALUES (?1, ?2) \ ON CONFLICT(agent_id) DO UPDATE SET timestamp = excluded.timestamp", params![agent_id, now], ) - .map_err(|e| format!("Heartbeat upsert error: {e}"))?; + .map_err(|e| AppError::database(format!("Heartbeat upsert error: {e}")))?; Ok(()) } #[allow(dead_code)] // Called via Tauri IPC command btmsg_get_agent_heartbeats -pub fn get_agent_heartbeats(group_id: &str) -> Result, String> { +pub fn get_agent_heartbeats(group_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( @@ -768,7 +769,7 @@ pub fn get_agent_heartbeats(group_id: &str) -> Result, Strin FROM heartbeats h JOIN agents a ON h.agent_id = a.id \ WHERE a.group_id = ? ORDER BY h.timestamp DESC", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let rows = stmt .query_map(params![group_id], |row| { @@ -779,17 +780,17 @@ pub fn get_agent_heartbeats(group_id: &str) -> Result, Strin timestamp: row.get("timestamp")?, }) }) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; rows.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result, String> { +pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result, AppError> { let db = open_db()?; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| format!("Time error: {e}"))? + .map_err(|e| AppError::database(format!("Time error: {e}")))? .as_secs() as i64; let cutoff = now - threshold_secs; @@ -799,14 +800,14 @@ pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result 0 AND (h.timestamp IS NULL OR h.timestamp < ?)", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let ids = stmt .query_map(params![group_id, cutoff], |row| row.get::<_, String>(0)) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; ids.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } // ---- Dead letter queue ---- @@ -822,7 +823,7 @@ pub struct DeadLetter { pub created_at: String, } -pub fn get_dead_letters(group_id: &str, limit: i32) -> Result, String> { +pub fn get_dead_letters(group_id: &str, limit: i32) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( @@ -832,7 +833,7 @@ pub fn get_dead_letters(group_id: &str, limit: i32) -> Result, S WHERE a.group_id = ? \ ORDER BY d.created_at DESC LIMIT ?", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let rows = stmt .query_map(params![group_id, limit], |row| { @@ -845,10 +846,10 @@ pub fn get_dead_letters(group_id: &str, limit: i32) -> Result, S created_at: row.get("created_at")?, }) }) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; rows.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } #[allow(dead_code)] // Called via Tauri IPC command btmsg_queue_dead_letter @@ -857,28 +858,28 @@ pub fn queue_dead_letter( to_agent: &str, content: &str, error: &str, -) -> Result<(), String> { +) -> Result<(), AppError> { let db = open_db()?; db.execute( "INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)", params![from_agent, to_agent, content, error], ) - .map_err(|e| format!("Dead letter insert error: {e}"))?; + .map_err(|e| AppError::database(format!("Dead letter insert error: {e}")))?; Ok(()) } -pub fn clear_dead_letters(group_id: &str) -> Result<(), String> { +pub fn clear_dead_letters(group_id: &str) -> Result<(), AppError> { let db = open_db()?; db.execute( "DELETE FROM dead_letter_queue WHERE to_agent IN (SELECT id FROM agents WHERE group_id = ?)", params![group_id], ) - .map_err(|e| format!("Delete error: {e}"))?; + .map_err(|e| AppError::database(format!("Delete error: {e}")))?; Ok(()) } /// Clear all communications for a group: messages, channel messages, seen tracking, dead letters -pub fn clear_all_communications(group_id: &str) -> Result<(), String> { +pub fn clear_all_communications(group_id: &str) -> Result<(), AppError> { let db = open_db()?; let agent_ids_clause = "(SELECT id FROM agents WHERE group_id = ?1)"; @@ -886,22 +887,22 @@ pub fn clear_all_communications(group_id: &str) -> Result<(), String> { db.execute( &format!("DELETE FROM seen_messages WHERE message_id IN (SELECT id FROM messages WHERE group_id = ?1 OR from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause})"), params![group_id], - ).map_err(|e| format!("Clear seen_messages error: {e}"))?; + ).map_err(|e| AppError::database(format!("Clear seen_messages error: {e}")))?; db.execute( &format!("DELETE FROM messages WHERE group_id = ?1 OR from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause}"), params![group_id], - ).map_err(|e| format!("Clear messages error: {e}"))?; + ).map_err(|e| AppError::database(format!("Clear messages error: {e}")))?; db.execute( &format!("DELETE FROM channel_messages WHERE channel_id IN (SELECT id FROM channels WHERE group_id = ?1) OR from_agent IN {agent_ids_clause}"), params![group_id], - ).map_err(|e| format!("Clear channel_messages error: {e}"))?; + ).map_err(|e| AppError::database(format!("Clear channel_messages error: {e}")))?; db.execute( &format!("DELETE FROM dead_letter_queue WHERE from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause}"), params![group_id], - ).map_err(|e| format!("Clear dead_letter_queue error: {e}"))?; + ).map_err(|e| AppError::database(format!("Clear dead_letter_queue error: {e}")))?; Ok(()) } @@ -918,17 +919,17 @@ pub struct AuditEntry { pub created_at: String, } -pub fn log_audit_event(agent_id: &str, event_type: &str, detail: &str) -> Result<(), String> { +pub fn log_audit_event(agent_id: &str, event_type: &str, detail: &str) -> Result<(), AppError> { let db = open_db_or_create()?; db.execute( "INSERT INTO audit_log (agent_id, event_type, detail) VALUES (?1, ?2, ?3)", params![agent_id, event_type, detail], ) - .map_err(|e| format!("Audit log insert error: {e}"))?; + .map_err(|e| AppError::database(format!("Audit log insert error: {e}")))?; Ok(()) } -pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result, String> { +pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( @@ -938,7 +939,7 @@ pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result Result, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } pub fn get_audit_log_for_agent( agent_id: &str, limit: i32, -) -> Result, String> { +) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( "SELECT id, agent_id, event_type, detail, created_at \ FROM audit_log WHERE agent_id = ? ORDER BY created_at DESC, id DESC LIMIT ?", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let rows = stmt .query_map(params![agent_id, limit], |row| { @@ -978,13 +979,13 @@ pub fn get_audit_log_for_agent( created_at: row.get("created_at")?, }) }) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; rows.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn all_feed(group_id: &str, limit: i32) -> Result, String> { +pub fn all_feed(group_id: &str, limit: i32) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT m.id, m.from_agent, m.to_agent, m.content, m.created_at, m.reply_to, \ @@ -995,7 +996,7 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result, Str JOIN agents a2 ON m.to_agent = a2.id \ WHERE m.group_id = ? \ ORDER BY m.created_at DESC LIMIT ?" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let msgs = stmt.query_map(params![group_id, limit], |row| { Ok(BtmsgFeedMessage { @@ -1010,28 +1011,28 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result, Str recipient_name: row.get("recipient_name")?, recipient_role: row.get("recipient_role")?, }) - }).map_err(|e| format!("Query error: {e}"))?; + }).map_err(|e| AppError::database(format!("Query error: {e}")))?; - msgs.collect::, _>>().map_err(|e| format!("Row error: {e}")) + msgs.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn mark_read_conversation(reader_id: &str, sender_id: &str) -> Result<(), String> { +pub fn mark_read_conversation(reader_id: &str, sender_id: &str) -> Result<(), AppError> { let db = open_db()?; db.execute( "UPDATE messages SET read = 1 WHERE to_agent = ? AND from_agent = ? AND read = 0", params![reader_id, sender_id], - ).map_err(|e| format!("Update error: {e}"))?; + ).map_err(|e| AppError::database(format!("Update error: {e}")))?; Ok(()) } -pub fn get_channels(group_id: &str) -> Result, String> { +pub fn get_channels(group_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT c.id, c.name, c.group_id, c.created_by, \ (SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id) AS member_count, \ c.created_at \ FROM channels c WHERE c.group_id = ? ORDER BY c.name" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let channels = stmt.query_map(params![group_id], |row| { Ok(BtmsgChannel { @@ -1042,19 +1043,19 @@ pub fn get_channels(group_id: &str) -> Result, String> { member_count: row.get("member_count")?, created_at: row.get("created_at")?, }) - }).map_err(|e| format!("Query error: {e}"))?; + }).map_err(|e| AppError::database(format!("Query error: {e}")))?; - channels.collect::, _>>().map_err(|e| format!("Row error: {e}")) + channels.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result, String> { +pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result, AppError> { let db = open_db()?; let mut stmt = db.prepare( "SELECT cm.id, cm.channel_id, cm.from_agent, cm.content, cm.created_at, \ a.name AS sender_name, a.role AS sender_role \ FROM channel_messages cm JOIN agents a ON cm.from_agent = a.id \ WHERE cm.channel_id = ? ORDER BY cm.created_at ASC LIMIT ?" - ).map_err(|e| format!("Query error: {e}"))?; + ).map_err(|e| AppError::database(format!("Query error: {e}")))?; let msgs = stmt.query_map(params![channel_id, limit], |row| { Ok(BtmsgChannelMessage { @@ -1066,12 +1067,12 @@ pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result, _>>().map_err(|e| format!("Row error: {e}")) + msgs.collect::, _>>().map_err(|e| AppError::database(format!("Row error: {e}"))) } -pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result { +pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result { let db = open_db()?; // Verify channel exists @@ -1079,24 +1080,24 @@ pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) - "SELECT id FROM channels WHERE id = ?", params![channel_id], |row| row.get(0), - ).map_err(|e| format!("Channel not found: {e}"))?; + ).map_err(|e| AppError::database(format!("Channel not found: {e}")))?; // Check membership (admin bypasses) let sender_tier: i32 = db.query_row( "SELECT tier FROM agents WHERE id = ?", params![from_agent], |row| row.get(0), - ).map_err(|e| format!("Sender not found: {e}"))?; + ).map_err(|e| AppError::database(format!("Sender not found: {e}")))?; if sender_tier > 0 { let is_member: bool = db.query_row( "SELECT COUNT(*) > 0 FROM channel_members WHERE channel_id = ? AND agent_id = ?", params![channel_id, from_agent], |row| row.get(0), - ).map_err(|e| format!("Membership check error: {e}"))?; + ).map_err(|e| AppError::database(format!("Membership check error: {e}")))?; if !is_member { - return Err("Not a member of this channel".into()); + return Err(AppError::database("Not a member of this channel")); } } @@ -1104,35 +1105,35 @@ pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) - db.execute( "INSERT INTO channel_messages (id, channel_id, from_agent, content) VALUES (?1, ?2, ?3, ?4)", params![msg_id, channel_id, from_agent, content], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; Ok(msg_id) } -pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result { +pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result { let db = open_db()?; let channel_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); db.execute( "INSERT INTO channels (id, name, group_id, created_by) VALUES (?1, ?2, ?3, ?4)", params![channel_id, name, group_id, created_by], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; // Auto-add creator as member db.execute( "INSERT INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)", params![channel_id, created_by], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; Ok(channel_id) } -pub fn add_channel_member(channel_id: &str, agent_id: &str) -> Result<(), String> { +pub fn add_channel_member(channel_id: &str, agent_id: &str) -> Result<(), AppError> { let db = open_db()?; db.execute( "INSERT OR IGNORE INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)", params![channel_id, agent_id], - ).map_err(|e| format!("Insert error: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert error: {e}")))?; Ok(()) } diff --git a/src-tauri/src/bttask.rs b/src-tauri/src/bttask.rs index 2df05c7..ea52057 100644 --- a/src-tauri/src/bttask.rs +++ b/src-tauri/src/bttask.rs @@ -4,6 +4,7 @@ use rusqlite::{params, Connection, OpenFlags}; use serde::{Deserialize, Serialize}; +use crate::error::AppError; use std::path::PathBuf; use std::sync::OnceLock; @@ -24,17 +25,17 @@ fn db_path() -> PathBuf { }) } -fn open_db() -> Result { +fn open_db() -> Result { let path = db_path(); if !path.exists() { - return Err("btmsg database not found".into()); + return Err(AppError::database("btmsg database not found")); } let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE) - .map_err(|e| format!("Failed to open btmsg.db: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to open btmsg.db: {e}")))?; conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())) - .map_err(|e| format!("Failed to set WAL mode: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set WAL mode: {e}")))?; conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(())) - .map_err(|e| format!("Failed to set busy_timeout: {e}"))?; + .map_err(|e| AppError::database(format!("Failed to set busy_timeout: {e}")))?; // Migration: add version column if missing let has_version: i64 = conn @@ -81,7 +82,7 @@ pub struct TaskComment { } /// Get all tasks for a group -pub fn list_tasks(group_id: &str) -> Result, String> { +pub fn list_tasks(group_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( @@ -91,7 +92,7 @@ pub fn list_tasks(group_id: &str) -> Result, String> { FROM tasks WHERE group_id = ?1 ORDER BY sort_order ASC, created_at DESC", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let rows = stmt .query_map(params![group_id], |row| { @@ -111,14 +112,14 @@ pub fn list_tasks(group_id: &str) -> Result, String> { version: row.get::<_, i64>("version").unwrap_or(1), }) }) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; rows.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } /// Get comments for a task -pub fn task_comments(task_id: &str) -> Result, String> { +pub fn task_comments(task_id: &str) -> Result, AppError> { let db = open_db()?; let mut stmt = db .prepare( @@ -126,7 +127,7 @@ pub fn task_comments(task_id: &str) -> Result, String> { FROM task_comments WHERE task_id = ?1 ORDER BY created_at ASC", ) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; let rows = stmt .query_map(params![task_id], |row| { @@ -138,20 +139,20 @@ pub fn task_comments(task_id: &str) -> Result, String> { created_at: row.get::<_, String>("created_at").unwrap_or_default(), }) }) - .map_err(|e| format!("Query error: {e}"))?; + .map_err(|e| AppError::database(format!("Query error: {e}")))?; rows.collect::, _>>() - .map_err(|e| format!("Row error: {e}")) + .map_err(|e| AppError::database(format!("Row error: {e}"))) } /// Update task status with optimistic locking. /// `expected_version` must match the current version in the database. /// Returns the new version on success. /// When transitioning to 'review', auto-posts to #review-queue channel if it exists. -pub fn update_task_status(task_id: &str, status: &str, expected_version: i64) -> Result { +pub fn update_task_status(task_id: &str, status: &str, expected_version: i64) -> Result { let valid = ["todo", "progress", "review", "done", "blocked"]; if !valid.contains(&status) { - return Err(format!("Invalid status '{}'. Valid: {:?}", status, valid)); + return Err(AppError::database(format!("Invalid status '{}'. Valid: {:?}", status, valid))); } let db = open_db()?; @@ -171,10 +172,10 @@ pub fn update_task_status(task_id: &str, status: &str, expected_version: i64) -> WHERE id = ?2 AND version = ?3", params![status, task_id, expected_version], ) - .map_err(|e| format!("Update error: {e}"))?; + .map_err(|e| AppError::database(format!("Update error: {e}")))?; if rows_affected == 0 { - return Err("Task was modified by another agent (version conflict)".into()); + return Err(AppError::database("Task was modified by another agent (version conflict)")); } let new_version = expected_version + 1; @@ -248,25 +249,25 @@ fn ensure_review_channels(db: &Connection, group_id: &str) -> Option { } /// Count tasks in 'review' status for a group -pub fn review_queue_count(group_id: &str) -> Result { +pub fn review_queue_count(group_id: &str) -> Result { let db = open_db()?; db.query_row( "SELECT COUNT(*) FROM tasks WHERE group_id = ?1 AND status = 'review'", params![group_id], |row| row.get(0), ) - .map_err(|e| format!("Query error: {e}")) + .map_err(|e| AppError::database(format!("Query error: {e}"))) } /// Add a comment to a task -pub fn add_comment(task_id: &str, agent_id: &str, content: &str) -> Result { +pub fn add_comment(task_id: &str, agent_id: &str, content: &str) -> Result { let db = open_db()?; let id = uuid::Uuid::new_v4().to_string(); db.execute( "INSERT INTO task_comments (id, task_id, agent_id, content) VALUES (?1, ?2, ?3, ?4)", params![id, task_id, agent_id, content], ) - .map_err(|e| format!("Insert error: {e}"))?; + .map_err(|e| AppError::database(format!("Insert error: {e}")))?; Ok(id) } @@ -278,7 +279,7 @@ pub fn create_task( group_id: &str, created_by: &str, assigned_to: Option<&str>, -) -> Result { +) -> Result { let db = open_db()?; let id = uuid::Uuid::new_v4().to_string(); db.execute( @@ -286,17 +287,17 @@ pub fn create_task( VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![id, title, description, priority, group_id, created_by, assigned_to], ) - .map_err(|e| format!("Insert error: {e}"))?; + .map_err(|e| AppError::database(format!("Insert error: {e}")))?; Ok(id) } /// Delete a task -pub fn delete_task(task_id: &str) -> Result<(), String> { +pub fn delete_task(task_id: &str) -> Result<(), AppError> { let db = open_db()?; db.execute("DELETE FROM task_comments WHERE task_id = ?1", params![task_id]) - .map_err(|e| format!("Delete comments error: {e}"))?; + .map_err(|e| AppError::database(format!("Delete comments error: {e}")))?; db.execute("DELETE FROM tasks WHERE id = ?1", params![task_id]) - .map_err(|e| format!("Delete task error: {e}"))?; + .map_err(|e| AppError::database(format!("Delete task error: {e}")))?; Ok(()) } diff --git a/src-tauri/src/commands/btmsg.rs b/src-tauri/src/commands/btmsg.rs index ae98a14..65fbed6 100644 --- a/src-tauri/src/commands/btmsg.rs +++ b/src-tauri/src/commands/btmsg.rs @@ -4,130 +4,130 @@ use crate::groups; #[tauri::command] pub fn btmsg_get_agents(group_id: String) -> Result, AppError> { - btmsg::get_agents(&group_id).map_err(AppError::database) + btmsg::get_agents(&group_id) } #[tauri::command] pub fn btmsg_unread_count(agent_id: String) -> Result { - btmsg::unread_count(&agent_id).map_err(AppError::database) + btmsg::unread_count(&agent_id) } #[tauri::command] pub fn btmsg_unread_messages(agent_id: String) -> Result, AppError> { - btmsg::unread_messages(&agent_id).map_err(AppError::database) + btmsg::unread_messages(&agent_id) } #[tauri::command] pub fn btmsg_history(agent_id: String, other_id: String, limit: i32) -> Result, AppError> { - btmsg::history(&agent_id, &other_id, limit).map_err(AppError::database) + btmsg::history(&agent_id, &other_id, limit) } #[tauri::command] pub fn btmsg_send(from_agent: String, to_agent: String, content: String) -> Result { - btmsg::send_message(&from_agent, &to_agent, &content).map_err(AppError::database) + btmsg::send_message(&from_agent, &to_agent, &content) } #[tauri::command] pub fn btmsg_set_status(agent_id: String, status: String) -> Result<(), AppError> { - btmsg::set_status(&agent_id, &status).map_err(AppError::database) + btmsg::set_status(&agent_id, &status) } #[tauri::command] pub fn btmsg_ensure_admin(group_id: String) -> Result<(), AppError> { - btmsg::ensure_admin(&group_id).map_err(AppError::database) + btmsg::ensure_admin(&group_id) } #[tauri::command] pub fn btmsg_all_feed(group_id: String, limit: i32) -> Result, AppError> { - btmsg::all_feed(&group_id, limit).map_err(AppError::database) + btmsg::all_feed(&group_id, limit) } #[tauri::command] pub fn btmsg_mark_read(reader_id: String, sender_id: String) -> Result<(), AppError> { - btmsg::mark_read_conversation(&reader_id, &sender_id).map_err(AppError::database) + btmsg::mark_read_conversation(&reader_id, &sender_id) } #[tauri::command] pub fn btmsg_get_channels(group_id: String) -> Result, AppError> { - btmsg::get_channels(&group_id).map_err(AppError::database) + btmsg::get_channels(&group_id) } #[tauri::command] pub fn btmsg_channel_messages(channel_id: String, limit: i32) -> Result, AppError> { - btmsg::get_channel_messages(&channel_id, limit).map_err(AppError::database) + btmsg::get_channel_messages(&channel_id, limit) } #[tauri::command] pub fn btmsg_channel_send(channel_id: String, from_agent: String, content: String) -> Result { - btmsg::send_channel_message(&channel_id, &from_agent, &content).map_err(AppError::database) + btmsg::send_channel_message(&channel_id, &from_agent, &content) } #[tauri::command] pub fn btmsg_create_channel(name: String, group_id: String, created_by: String) -> Result { - btmsg::create_channel(&name, &group_id, &created_by).map_err(AppError::database) + btmsg::create_channel(&name, &group_id, &created_by) } #[tauri::command] pub fn btmsg_add_channel_member(channel_id: String, agent_id: String) -> Result<(), AppError> { - btmsg::add_channel_member(&channel_id, &agent_id).map_err(AppError::database) + btmsg::add_channel_member(&channel_id, &agent_id) } /// Register all agents from a GroupsFile into the btmsg database. /// Creates/updates agent records, sets up contact permissions, ensures review channels. #[tauri::command] pub fn btmsg_register_agents(config: groups::GroupsFile) -> Result<(), AppError> { - btmsg::register_agents_from_groups(&config).map_err(AppError::database) + btmsg::register_agents_from_groups(&config) } // ---- Per-message acknowledgment (seen_messages) ---- #[tauri::command] pub fn btmsg_unseen_messages(agent_id: String, session_id: String) -> Result, AppError> { - btmsg::unseen_messages(&agent_id, &session_id).map_err(AppError::database) + btmsg::unseen_messages(&agent_id, &session_id) } #[tauri::command] pub fn btmsg_mark_seen(session_id: String, message_ids: Vec) -> Result<(), AppError> { - btmsg::mark_messages_seen(&session_id, &message_ids).map_err(AppError::database) + btmsg::mark_messages_seen(&session_id, &message_ids) } #[tauri::command] pub fn btmsg_prune_seen() -> Result { - btmsg::prune_seen_messages(7 * 24 * 3600, 200_000).map_err(AppError::database) + btmsg::prune_seen_messages(7 * 24 * 3600, 200_000) } // ---- Heartbeat monitoring ---- #[tauri::command] pub fn btmsg_record_heartbeat(agent_id: String) -> Result<(), AppError> { - btmsg::record_heartbeat(&agent_id).map_err(AppError::database) + btmsg::record_heartbeat(&agent_id) } #[tauri::command] pub fn btmsg_get_stale_agents(group_id: String, threshold_secs: i64) -> Result, AppError> { - btmsg::get_stale_agents(&group_id, threshold_secs).map_err(AppError::database) + btmsg::get_stale_agents(&group_id, threshold_secs) } #[tauri::command] pub fn btmsg_get_agent_heartbeats(group_id: String) -> Result, AppError> { - btmsg::get_agent_heartbeats(&group_id).map_err(AppError::database) + btmsg::get_agent_heartbeats(&group_id) } // ---- Dead letter queue ---- #[tauri::command] pub fn btmsg_get_dead_letters(group_id: String, limit: i32) -> Result, AppError> { - btmsg::get_dead_letters(&group_id, limit).map_err(AppError::database) + btmsg::get_dead_letters(&group_id, limit) } #[tauri::command] pub fn btmsg_clear_dead_letters(group_id: String) -> Result<(), AppError> { - btmsg::clear_dead_letters(&group_id).map_err(AppError::database) + btmsg::clear_dead_letters(&group_id) } #[tauri::command] pub fn btmsg_clear_all_comms(group_id: String) -> Result<(), AppError> { - btmsg::clear_all_communications(&group_id).map_err(AppError::database) + btmsg::clear_all_communications(&group_id) } #[tauri::command] @@ -138,22 +138,22 @@ pub fn btmsg_queue_dead_letter( error: String, ) -> Result<(), AppError> { btmsg::queue_dead_letter(&from_agent, &to_agent, &content, &error) - .map_err(AppError::database) + } // ---- Audit log ---- #[tauri::command] pub fn audit_log_event(agent_id: String, event_type: String, detail: String) -> Result<(), AppError> { - btmsg::log_audit_event(&agent_id, &event_type, &detail).map_err(AppError::database) + btmsg::log_audit_event(&agent_id, &event_type, &detail) } #[tauri::command] pub fn audit_log_list(group_id: String, limit: i32, offset: i32) -> Result, AppError> { - btmsg::get_audit_log(&group_id, limit, offset).map_err(AppError::database) + btmsg::get_audit_log(&group_id, limit, offset) } #[tauri::command] pub fn audit_log_for_agent(agent_id: String, limit: i32) -> Result, AppError> { - btmsg::get_audit_log_for_agent(&agent_id, limit).map_err(AppError::database) + btmsg::get_audit_log_for_agent(&agent_id, limit) } diff --git a/src-tauri/src/commands/bttask.rs b/src-tauri/src/commands/bttask.rs index 0a5742f..22067cf 100644 --- a/src-tauri/src/commands/bttask.rs +++ b/src-tauri/src/commands/bttask.rs @@ -3,22 +3,22 @@ use crate::error::AppError; #[tauri::command] pub fn bttask_list(group_id: String) -> Result, AppError> { - bttask::list_tasks(&group_id).map_err(AppError::database) + bttask::list_tasks(&group_id) } #[tauri::command] pub fn bttask_comments(task_id: String) -> Result, AppError> { - bttask::task_comments(&task_id).map_err(AppError::database) + bttask::task_comments(&task_id) } #[tauri::command] pub fn bttask_update_status(task_id: String, status: String, version: i64) -> Result { - bttask::update_task_status(&task_id, &status, version).map_err(AppError::database) + bttask::update_task_status(&task_id, &status, version) } #[tauri::command] pub fn bttask_add_comment(task_id: String, agent_id: String, content: String) -> Result { - bttask::add_comment(&task_id, &agent_id, &content).map_err(AppError::database) + bttask::add_comment(&task_id, &agent_id, &content) } #[tauri::command] @@ -31,15 +31,15 @@ pub fn bttask_create( assigned_to: Option, ) -> Result { bttask::create_task(&title, &description, &priority, &group_id, &created_by, assigned_to.as_deref()) - .map_err(AppError::database) + } #[tauri::command] pub fn bttask_delete(task_id: String) -> Result<(), AppError> { - bttask::delete_task(&task_id).map_err(AppError::database) + bttask::delete_task(&task_id) } #[tauri::command] pub fn bttask_review_queue_count(group_id: String) -> Result { - bttask::review_queue_count(&group_id).map_err(AppError::database) + bttask::review_queue_count(&group_id) } diff --git a/src-tauri/src/session/agents.rs b/src-tauri/src/session/agents.rs index 8ffd7fd..4acb026 100644 --- a/src-tauri/src/session/agents.rs +++ b/src-tauri/src/session/agents.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentMessageRecord { @@ -37,21 +38,21 @@ impl SessionDb { project_id: &str, sdk_session_id: Option<&str>, messages: &[AgentMessageRecord], - ) -> Result<(), String> { + ) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); // Wrap DELETE+INSERTs in a transaction to prevent partial writes on crash let tx = conn.unchecked_transaction() - .map_err(|e| format!("Begin transaction failed: {e}"))?; + .map_err(|e| AppError::database(format!("Begin transaction failed: {e}")))?; // Clear previous messages for this session tx.execute( "DELETE FROM agent_messages WHERE session_id = ?1", params![session_id], - ).map_err(|e| format!("Delete old messages failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Delete old messages failed: {e}")))?; let mut stmt = tx.prepare( "INSERT INTO agent_messages (session_id, project_id, sdk_session_id, message_type, content, parent_id, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" - ).map_err(|e| format!("Prepare insert failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Prepare insert failed: {e}")))?; for msg in messages { stmt.execute(params![ @@ -62,21 +63,21 @@ impl SessionDb { msg.content, msg.parent_id, msg.created_at, - ]).map_err(|e| format!("Insert message failed: {e}"))?; + ]).map_err(|e| AppError::database(format!("Insert message failed: {e}")))?; } drop(stmt); - tx.commit().map_err(|e| format!("Commit failed: {e}"))?; + tx.commit().map_err(|e| AppError::database(format!("Commit failed: {e}")))?; Ok(()) } - pub fn load_agent_messages(&self, project_id: &str) -> Result, String> { + pub fn load_agent_messages(&self, project_id: &str) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "SELECT id, session_id, project_id, sdk_session_id, message_type, content, parent_id, created_at FROM agent_messages WHERE project_id = ?1 ORDER BY created_at ASC" - ).map_err(|e| format!("Query prepare failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Query prepare failed: {e}")))?; let messages = stmt.query_map(params![project_id], |row| { Ok(AgentMessageRecord { @@ -89,14 +90,14 @@ impl SessionDb { parent_id: row.get(6)?, created_at: row.get(7)?, }) - }).map_err(|e| format!("Query failed: {e}"))? + }).map_err(|e| AppError::database(format!("Query failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("Row read failed: {e}"))?; + .map_err(|e| AppError::database(format!("Row read failed: {e}")))?; Ok(messages) } - pub fn save_project_agent_state(&self, state: &ProjectAgentState) -> Result<(), String> { + pub fn save_project_agent_state(&self, state: &ProjectAgentState) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "INSERT OR REPLACE INTO project_agent_state (project_id, last_session_id, sdk_session_id, status, cost_usd, input_tokens, output_tokens, last_prompt, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", @@ -111,15 +112,15 @@ impl SessionDb { state.last_prompt, state.updated_at, ], - ).map_err(|e| format!("Save project agent state failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Save project agent state failed: {e}")))?; Ok(()) } - pub fn load_project_agent_state(&self, project_id: &str) -> Result, String> { + pub fn load_project_agent_state(&self, project_id: &str) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "SELECT project_id, last_session_id, sdk_session_id, status, cost_usd, input_tokens, output_tokens, last_prompt, updated_at FROM project_agent_state WHERE project_id = ?1" - ).map_err(|e| format!("Query prepare failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Query prepare failed: {e}")))?; let result = stmt.query_row(params![project_id], |row| { Ok(ProjectAgentState { @@ -138,7 +139,7 @@ impl SessionDb { match result { Ok(state) => Ok(Some(state)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(format!("Load project agent state failed: {e}")), + Err(e) => Err(AppError::database(format!("Load project agent state failed: {e}"))), } } } diff --git a/src-tauri/src/session/anchors.rs b/src-tauri/src/session/anchors.rs index 1936feb..78240bb 100644 --- a/src-tauri/src/session/anchors.rs +++ b/src-tauri/src/session/anchors.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionAnchorRecord { @@ -17,14 +18,14 @@ pub struct SessionAnchorRecord { } impl SessionDb { - pub fn save_session_anchors(&self, anchors: &[SessionAnchorRecord]) -> Result<(), String> { + pub fn save_session_anchors(&self, anchors: &[SessionAnchorRecord]) -> Result<(), AppError> { if anchors.is_empty() { return Ok(()); } let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "INSERT OR REPLACE INTO session_anchors (id, project_id, message_id, anchor_type, content, estimated_tokens, turn_index, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)" - ).map_err(|e| format!("Prepare anchor insert failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Prepare anchor insert failed: {e}")))?; for anchor in anchors { stmt.execute(params![ @@ -36,16 +37,16 @@ impl SessionDb { anchor.estimated_tokens, anchor.turn_index, anchor.created_at, - ]).map_err(|e| format!("Insert anchor failed: {e}"))?; + ]).map_err(|e| AppError::database(format!("Insert anchor failed: {e}")))?; } Ok(()) } - pub fn load_session_anchors(&self, project_id: &str) -> Result, String> { + pub fn load_session_anchors(&self, project_id: &str) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "SELECT id, project_id, message_id, anchor_type, content, estimated_tokens, turn_index, created_at FROM session_anchors WHERE project_id = ?1 ORDER BY turn_index ASC" - ).map_err(|e| format!("Query anchors failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Query anchors failed: {e}")))?; let anchors = stmt.query_map(params![project_id], |row| { Ok(SessionAnchorRecord { @@ -58,33 +59,33 @@ impl SessionDb { turn_index: row.get(6)?, created_at: row.get(7)?, }) - }).map_err(|e| format!("Query anchors failed: {e}"))? + }).map_err(|e| AppError::database(format!("Query anchors failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("Read anchor row failed: {e}"))?; + .map_err(|e| AppError::database(format!("Read anchor row failed: {e}")))?; Ok(anchors) } - pub fn delete_session_anchor(&self, id: &str) -> Result<(), String> { + pub fn delete_session_anchor(&self, id: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute("DELETE FROM session_anchors WHERE id = ?1", params![id]) - .map_err(|e| format!("Delete anchor failed: {e}"))?; + .map_err(|e| AppError::database(format!("Delete anchor failed: {e}")))?; Ok(()) } - pub fn delete_project_anchors(&self, project_id: &str) -> Result<(), String> { + pub fn delete_project_anchors(&self, project_id: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute("DELETE FROM session_anchors WHERE project_id = ?1", params![project_id]) - .map_err(|e| format!("Delete project anchors failed: {e}"))?; + .map_err(|e| AppError::database(format!("Delete project anchors failed: {e}")))?; Ok(()) } - pub fn update_anchor_type(&self, id: &str, anchor_type: &str) -> Result<(), String> { + pub fn update_anchor_type(&self, id: &str, anchor_type: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE session_anchors SET anchor_type = ?2 WHERE id = ?1", params![id, anchor_type], - ).map_err(|e| format!("Update anchor type failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Update anchor type failed: {e}")))?; Ok(()) } } diff --git a/src-tauri/src/session/layout.rs b/src-tauri/src/session/layout.rs index d134521..3f2d734 100644 --- a/src-tauri/src/session/layout.rs +++ b/src-tauri/src/session/layout.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LayoutState { @@ -11,29 +12,29 @@ pub struct LayoutState { } impl SessionDb { - pub fn save_layout(&self, layout: &LayoutState) -> Result<(), String> { + pub fn save_layout(&self, layout: &LayoutState) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); let pane_ids_json = serde_json::to_string(&layout.pane_ids) - .map_err(|e| format!("Serialize pane_ids failed: {e}"))?; + .map_err(|e| AppError::validation(format!("Serialize pane_ids failed: {e}")))?; conn.execute( "UPDATE layout_state SET preset = ?1, pane_ids = ?2 WHERE id = 1", params![layout.preset, pane_ids_json], - ).map_err(|e| format!("Layout save failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Layout save failed: {e}")))?; Ok(()) } - pub fn load_layout(&self) -> Result { + pub fn load_layout(&self) -> Result { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare("SELECT preset, pane_ids FROM layout_state WHERE id = 1") - .map_err(|e| format!("Layout query failed: {e}"))?; + .map_err(|e| AppError::database(format!("Layout query failed: {e}")))?; stmt.query_row([], |row| { let preset: String = row.get(0)?; let pane_ids_json: String = row.get(1)?; let pane_ids: Vec = serde_json::from_str(&pane_ids_json).unwrap_or_default(); Ok(LayoutState { preset, pane_ids }) - }).map_err(|e| format!("Layout read failed: {e}")) + }).map_err(|e| AppError::database(format!("Layout read failed: {e}"))) } } diff --git a/src-tauri/src/session/metrics.rs b/src-tauri/src/session/metrics.rs index 60f6660..ccc12cc 100644 --- a/src-tauri/src/session/metrics.rs +++ b/src-tauri/src/session/metrics.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionMetric { @@ -22,7 +23,7 @@ pub struct SessionMetric { } impl SessionDb { - pub fn save_session_metric(&self, metric: &SessionMetric) -> Result<(), String> { + pub fn save_session_metric(&self, metric: &SessionMetric) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "INSERT INTO session_metrics (project_id, session_id, start_time, end_time, peak_tokens, turn_count, tool_call_count, cost_usd, model, status, error_message) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", @@ -39,22 +40,22 @@ impl SessionDb { metric.status, metric.error_message, ], - ).map_err(|e| format!("Save session metric failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Save session metric failed: {e}")))?; // Enforce retention: keep last 100 per project conn.execute( "DELETE FROM session_metrics WHERE project_id = ?1 AND id NOT IN (SELECT id FROM session_metrics WHERE project_id = ?1 ORDER BY end_time DESC LIMIT 100)", params![metric.project_id], - ).map_err(|e| format!("Prune session metrics failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Prune session metrics failed: {e}")))?; Ok(()) } - pub fn load_session_metrics(&self, project_id: &str, limit: i64) -> Result, String> { + pub fn load_session_metrics(&self, project_id: &str, limit: i64) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "SELECT id, project_id, session_id, start_time, end_time, peak_tokens, turn_count, tool_call_count, cost_usd, model, status, error_message FROM session_metrics WHERE project_id = ?1 ORDER BY end_time DESC LIMIT ?2" - ).map_err(|e| format!("Query prepare failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Query prepare failed: {e}")))?; let metrics = stmt.query_map(params![project_id, limit], |row| { Ok(SessionMetric { @@ -71,9 +72,9 @@ impl SessionDb { status: row.get(10)?, error_message: row.get(11)?, }) - }).map_err(|e| format!("Query failed: {e}"))? + }).map_err(|e| AppError::database(format!("Query failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("Row read failed: {e}"))?; + .map_err(|e| AppError::database(format!("Row read failed: {e}")))?; Ok(metrics) } diff --git a/src-tauri/src/session/sessions.rs b/src-tauri/src/session/sessions.rs index 1d2b330..20d1be4 100644 --- a/src-tauri/src/session/sessions.rs +++ b/src-tauri/src/session/sessions.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { @@ -20,11 +21,11 @@ pub struct Session { } impl SessionDb { - pub fn list_sessions(&self) -> Result, String> { + pub fn list_sessions(&self) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare("SELECT id, type, title, shell, cwd, args, group_name, created_at, last_used_at FROM sessions ORDER BY last_used_at DESC") - .map_err(|e| format!("Query prepare failed: {e}"))?; + .map_err(|e| AppError::database(format!("Query prepare failed: {e}")))?; let sessions = stmt .query_map([], |row| { @@ -42,14 +43,14 @@ impl SessionDb { last_used_at: row.get(8)?, }) }) - .map_err(|e| format!("Query failed: {e}"))? + .map_err(|e| AppError::database(format!("Query failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("Row read failed: {e}"))?; + .map_err(|e| AppError::database(format!("Row read failed: {e}")))?; Ok(sessions) } - pub fn save_session(&self, session: &Session) -> Result<(), String> { + pub fn save_session(&self, session: &Session) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); let args_json = session.args.as_ref().map(|a| serde_json::to_string(a).unwrap_or_default()); conn.execute( @@ -65,36 +66,36 @@ impl SessionDb { session.created_at, session.last_used_at, ], - ).map_err(|e| format!("Insert failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Insert failed: {e}")))?; Ok(()) } - pub fn delete_session(&self, id: &str) -> Result<(), String> { + pub fn delete_session(&self, id: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute("DELETE FROM sessions WHERE id = ?1", params![id]) - .map_err(|e| format!("Delete failed: {e}"))?; + .map_err(|e| AppError::database(format!("Delete failed: {e}")))?; Ok(()) } - pub fn update_title(&self, id: &str, title: &str) -> Result<(), String> { + pub fn update_title(&self, id: &str, title: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE sessions SET title = ?1 WHERE id = ?2", params![title, id], - ).map_err(|e| format!("Update failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Update failed: {e}")))?; Ok(()) } - pub fn update_group(&self, id: &str, group_name: &str) -> Result<(), String> { + pub fn update_group(&self, id: &str, group_name: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE sessions SET group_name = ?1 WHERE id = ?2", params![group_name, id], - ).map_err(|e| format!("Update group failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Update group failed: {e}")))?; Ok(()) } - pub fn touch_session(&self, id: &str) -> Result<(), String> { + pub fn touch_session(&self, id: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -103,7 +104,7 @@ impl SessionDb { conn.execute( "UPDATE sessions SET last_used_at = ?1 WHERE id = ?2", params![now, id], - ).map_err(|e| format!("Touch failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Touch failed: {e}")))?; Ok(()) } } diff --git a/src-tauri/src/session/settings.rs b/src-tauri/src/session/settings.rs index b295753..4268275 100644 --- a/src-tauri/src/session/settings.rs +++ b/src-tauri/src/session/settings.rs @@ -2,40 +2,41 @@ use rusqlite::params; use super::SessionDb; +use crate::error::AppError; impl SessionDb { - pub fn get_setting(&self, key: &str) -> Result, String> { + pub fn get_setting(&self, key: &str) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare("SELECT value FROM settings WHERE key = ?1") - .map_err(|e| format!("Settings query failed: {e}"))?; + .map_err(|e| AppError::database(format!("Settings query failed: {e}")))?; let result = stmt.query_row(params![key], |row| row.get(0)); match result { Ok(val) => Ok(Some(val)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(format!("Settings read failed: {e}")), + Err(e) => Err(AppError::database(format!("Settings read failed: {e}"))), } } - pub fn set_setting(&self, key: &str, value: &str) -> Result<(), String> { + pub fn set_setting(&self, key: &str, value: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", params![key, value], - ).map_err(|e| format!("Settings write failed: {e}"))?; + ).map_err(|e| AppError::database(format!("Settings write failed: {e}")))?; Ok(()) } - pub fn get_all_settings(&self) -> Result, String> { + pub fn get_all_settings(&self) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare("SELECT key, value FROM settings ORDER BY key") - .map_err(|e| format!("Settings query failed: {e}"))?; + .map_err(|e| AppError::database(format!("Settings query failed: {e}")))?; let settings = stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) - .map_err(|e| format!("Settings query failed: {e}"))? + .map_err(|e| AppError::database(format!("Settings query failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("Settings read failed: {e}"))?; + .map_err(|e| AppError::database(format!("Settings read failed: {e}")))?; Ok(settings) } } diff --git a/src-tauri/src/session/ssh.rs b/src-tauri/src/session/ssh.rs index 226c48d..537dbd4 100644 --- a/src-tauri/src/session/ssh.rs +++ b/src-tauri/src/session/ssh.rs @@ -3,6 +3,7 @@ use rusqlite::params; use serde::{Deserialize, Serialize}; use super::SessionDb; +use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SshSession { @@ -19,11 +20,11 @@ pub struct SshSession { } impl SessionDb { - pub fn list_ssh_sessions(&self) -> Result, String> { + pub fn list_ssh_sessions(&self) -> Result, AppError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn .prepare("SELECT id, name, host, port, username, key_file, folder, color, created_at, last_used_at FROM ssh_sessions ORDER BY last_used_at DESC") - .map_err(|e| format!("SSH query prepare failed: {e}"))?; + .map_err(|e| AppError::database(format!("SSH query prepare failed: {e}")))?; let sessions = stmt .query_map([], |row| { @@ -40,14 +41,14 @@ impl SessionDb { last_used_at: row.get(9)?, }) }) - .map_err(|e| format!("SSH query failed: {e}"))? + .map_err(|e| AppError::database(format!("SSH query failed: {e}")))? .collect::, _>>() - .map_err(|e| format!("SSH row read failed: {e}"))?; + .map_err(|e| AppError::database(format!("SSH row read failed: {e}")))?; Ok(sessions) } - pub fn save_ssh_session(&self, session: &SshSession) -> Result<(), String> { + pub fn save_ssh_session(&self, session: &SshSession) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute( "INSERT OR REPLACE INTO ssh_sessions (id, name, host, port, username, key_file, folder, color, created_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", @@ -63,14 +64,14 @@ impl SessionDb { session.created_at, session.last_used_at, ], - ).map_err(|e| format!("SSH insert failed: {e}"))?; + ).map_err(|e| AppError::database(format!("SSH insert failed: {e}")))?; Ok(()) } - pub fn delete_ssh_session(&self, id: &str) -> Result<(), String> { + pub fn delete_ssh_session(&self, id: &str) -> Result<(), AppError> { let conn = self.conn.lock().unwrap(); conn.execute("DELETE FROM ssh_sessions WHERE id = ?1", params![id]) - .map_err(|e| format!("SSH delete failed: {e}"))?; + .map_err(|e| AppError::database(format!("SSH delete failed: {e}")))?; Ok(()) } }