fix(error): migrate session submodules + btmsg/bttask backends to AppError

- session/*.rs (sessions, layout, settings, ssh, agents, metrics, anchors)
  now return Result<T, AppError> internally, not just at command boundary
- btmsg.rs and bttask.rs backends migrated to AppError::Database
- 116 cargo tests passing
This commit is contained in:
Hibryda 2026-03-18 01:32:07 +01:00
parent eb04e7e5b5
commit f19b69f018
11 changed files with 264 additions and 255 deletions

View file

@ -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<Connection, String> {
fn open_db() -> Result<Connection, AppError> {
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<Vec<BtmsgAgent>, String> {
pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, 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<Vec<BtmsgAgent>, String> {
status: row.get::<_, Option<String>>("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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
agents.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
}
pub fn unread_count(agent_id: &str) -> Result<i32, String> {
pub fn unread_count(agent_id: &str) -> Result<i32, AppError> {
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<Vec<BtmsgMessage>, String> {
pub fn unread_messages(agent_id: &str) -> Result<Vec<BtmsgMessage>, 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<Vec<BtmsgMessage>, 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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
msgs.collect::<Result<Vec<_>, _>>().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<Vec<BtmsgMessage>, String> {
pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMessage>, 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<Vec<BtmsgMess
WHERE m.to_agent = ?1 \
AND m.id NOT IN (SELECT message_id FROM seen_messages WHERE session_id = ?2) \
ORDER BY m.created_at ASC"
).map_err(|e| format!("Prepare unseen query: {e}"))?;
).map_err(|e| AppError::database(format!("Prepare unseen query: {e}")))?;
let rows = stmt.query_map(params![agent_id, session_id], |row| {
Ok(BtmsgMessage {
@ -207,36 +208,36 @@ pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMess
sender_name: row.get("sender_name")?,
sender_role: row.get("sender_role")?,
})
}).map_err(|e| format!("Query unseen: {e}"))?;
}).map_err(|e| AppError::database(format!("Query unseen: {e}")))?;
rows.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
rows.collect::<Result<Vec<_>, _>>().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<u64, String> {
pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Result<u64, AppError> {
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<Vec<BtmsgMessage>, String> {
pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMessage>, 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<Vec<BtmsgMe
FROM messages m JOIN agents a ON m.from_agent = a.id \
WHERE (m.from_agent = ?1 AND m.to_agent = ?2) OR (m.from_agent = ?2 AND m.to_agent = ?1) \
ORDER BY m.created_at ASC LIMIT ?3"
).map_err(|e| format!("Query error: {e}"))?;
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
let msgs = stmt.query_map(params![agent_id, other_id, limit], |row| {
Ok(BtmsgMessage {
@ -275,15 +276,15 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
msgs.collect::<Result<Vec<_>, _>>().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<String, String> {
pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<String, AppError> {
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<S
"SELECT group_id, tier FROM agents WHERE id = ?",
params![from_agent],
|row| Ok((row.get(0)?, row.get(1)?)),
).map_err(|e| format!("Sender not found: {e}"))?;
).map_err(|e| AppError::database(format!("Sender not found: {e}")))?;
// Admin (tier 0) bypasses contact restrictions
if sender_tier > 0 {
@ -299,10 +300,10 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
"SELECT COUNT(*) > 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<S
"INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)",
params![from_agent, to_agent, content, error_msg],
)
.map_err(|e| format!("Dead letter insert error: {e}"))?;
.map_err(|e| AppError::database(format!("Dead letter insert error: {e}")))?;
// Also log audit event
let _ = db.execute(
@ -340,7 +341,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
params![from_agent, format!("Message to '{}' routed to dead letter queue: {}", to_agent, error_msg)],
);
return Err(error_msg);
return Err(AppError::database(error_msg));
}
}
@ -349,56 +350,56 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
"INSERT INTO messages (id, from_agent, to_agent, content, group_id, sender_group_id) \
VALUES (?1, ?2, ?3, ?4, ?5, (SELECT group_id FROM agents WHERE id = ?2))",
params![msg_id, from_agent, to_agent, content, group_id],
).map_err(|e| format!("Insert error: {e}"))?;
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
Ok(msg_id)
}
pub fn set_status(agent_id: &str, status: &str) -> 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<String> = 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::<Result<Vec<_>, _>>()
.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<Connection, String> {
fn open_db_or_create() -> Result<Connection, AppError> {
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<Connection, String> {
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<Vec<AgentHeartbeat>, String> {
pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, AppError> {
let db = open_db()?;
let mut stmt = db
.prepare(
@ -768,7 +769,7 @@ pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, 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<Vec<AgentHeartbeat>, Strin
timestamp: row.get("timestamp")?,
})
})
.map_err(|e| format!("Query error: {e}"))?;
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
rows.collect::<Result<Vec<_>, _>>()
.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<Vec<String>, String> {
pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result<Vec<String>, 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<Vec<Strin
"SELECT a.id FROM agents a LEFT JOIN heartbeats h ON a.id = h.agent_id \
WHERE a.group_id = ? AND a.tier > 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::<Result<Vec<_>, _>>()
.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<Vec<DeadLetter>, String> {
pub fn get_dead_letters(group_id: &str, limit: i32) -> Result<Vec<DeadLetter>, 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<Vec<DeadLetter>, 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<Vec<DeadLetter>, 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::<Result<Vec<_>, _>>()
.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<Vec<AuditEntry>, String> {
pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<AuditEntry>, 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<Vec<Audi
WHERE a.group_id = ? \
ORDER BY al.created_at DESC, al.id DESC LIMIT ? OFFSET ?",
)
.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, offset], |row| {
@ -950,23 +951,23 @@ pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<Audi
created_at: row.get("created_at")?,
})
})
.map_err(|e| format!("Query error: {e}"))?;
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
rows.collect::<Result<Vec<_>, _>>()
.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<Vec<AuditEntry>, String> {
) -> Result<Vec<AuditEntry>, 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::<Result<Vec<_>, _>>()
.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<Vec<BtmsgFeedMessage>, String> {
pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, 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<Vec<BtmsgFeedMessage>, 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<Vec<BtmsgFeedMessage>, 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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
msgs.collect::<Result<Vec<_>, _>>().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<Vec<BtmsgChannel>, String> {
pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, 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<Vec<BtmsgChannel>, 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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
channels.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
}
pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgChannelMessage>, String> {
pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgChannelMessage>, 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<Vec<BtmsgCha
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::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
}
pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result<String, String> {
pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result<String, AppError> {
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<String, String> {
pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result<String, AppError> {
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(())
}

View file

@ -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<Connection, String> {
fn open_db() -> Result<Connection, AppError> {
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<Vec<Task>, String> {
pub fn list_tasks(group_id: &str) -> Result<Vec<Task>, AppError> {
let db = open_db()?;
let mut stmt = db
.prepare(
@ -91,7 +92,7 @@ pub fn list_tasks(group_id: &str) -> Result<Vec<Task>, 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<Vec<Task>, 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::<Result<Vec<_>, _>>()
.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<Vec<TaskComment>, String> {
pub fn task_comments(task_id: &str) -> Result<Vec<TaskComment>, AppError> {
let db = open_db()?;
let mut stmt = db
.prepare(
@ -126,7 +127,7 @@ pub fn task_comments(task_id: &str) -> Result<Vec<TaskComment>, 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<Vec<TaskComment>, 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::<Result<Vec<_>, _>>()
.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<i64, String> {
pub fn update_task_status(task_id: &str, status: &str, expected_version: i64) -> Result<i64, AppError> {
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<String> {
}
/// Count tasks in 'review' status for a group
pub fn review_queue_count(group_id: &str) -> Result<i64, String> {
pub fn review_queue_count(group_id: &str) -> Result<i64, AppError> {
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<String, String> {
pub fn add_comment(task_id: &str, agent_id: &str, content: &str) -> Result<String, AppError> {
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<String, String> {
) -> Result<String, AppError> {
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(())
}

View file

@ -4,130 +4,130 @@ use crate::groups;
#[tauri::command]
pub fn btmsg_get_agents(group_id: String) -> Result<Vec<btmsg::BtmsgAgent>, 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<i32, AppError> {
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<Vec<btmsg::BtmsgMessage>, 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<Vec<btmsg::BtmsgMessage>, 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<String, AppError> {
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<Vec<btmsg::BtmsgFeedMessage>, 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<Vec<btmsg::BtmsgChannel>, 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<Vec<btmsg::BtmsgChannelMessage>, 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<String, AppError> {
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<String, AppError> {
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<Vec<btmsg::BtmsgMessage>, 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<String>) -> 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<u64, AppError> {
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<Vec<String>, 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<Vec<btmsg::AgentHeartbeat>, 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<Vec<btmsg::DeadLetter>, 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<Vec<btmsg::AuditEntry>, 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<Vec<btmsg::AuditEntry>, AppError> {
btmsg::get_audit_log_for_agent(&agent_id, limit).map_err(AppError::database)
btmsg::get_audit_log_for_agent(&agent_id, limit)
}

View file

@ -3,22 +3,22 @@ use crate::error::AppError;
#[tauri::command]
pub fn bttask_list(group_id: String) -> Result<Vec<bttask::Task>, 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<Vec<bttask::TaskComment>, 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<i64, AppError> {
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<String, AppError> {
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<String>,
) -> Result<String, AppError> {
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<i64, AppError> {
bttask::review_queue_count(&group_id).map_err(AppError::database)
bttask::review_queue_count(&group_id)
}

View file

@ -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<Vec<AgentMessageRecord>, String> {
pub fn load_agent_messages(&self, project_id: &str) -> Result<Vec<AgentMessageRecord>, 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::<Result<Vec<_>, _>>()
.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<Option<ProjectAgentState>, String> {
pub fn load_project_agent_state(&self, project_id: &str) -> Result<Option<ProjectAgentState>, 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}"))),
}
}
}

View file

@ -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<Vec<SessionAnchorRecord>, String> {
pub fn load_session_anchors(&self, project_id: &str) -> Result<Vec<SessionAnchorRecord>, 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::<Result<Vec<_>, _>>()
.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(())
}
}

View file

@ -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<LayoutState, String> {
pub fn load_layout(&self) -> Result<LayoutState, AppError> {
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<String> = 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}")))
}
}

View file

@ -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<Vec<SessionMetric>, String> {
pub fn load_session_metrics(&self, project_id: &str, limit: i64) -> Result<Vec<SessionMetric>, 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::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row read failed: {e}"))?;
.map_err(|e| AppError::database(format!("Row read failed: {e}")))?;
Ok(metrics)
}

View file

@ -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<Vec<Session>, String> {
pub fn list_sessions(&self) -> Result<Vec<Session>, 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::<Result<Vec<_>, _>>()
.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(())
}
}

View file

@ -2,40 +2,41 @@
use rusqlite::params;
use super::SessionDb;
use crate::error::AppError;
impl SessionDb {
pub fn get_setting(&self, key: &str) -> Result<Option<String>, String> {
pub fn get_setting(&self, key: &str) -> Result<Option<String>, 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<Vec<(String, String)>, String> {
pub fn get_all_settings(&self) -> Result<Vec<(String, String)>, 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::<Result<Vec<_>, _>>()
.map_err(|e| format!("Settings read failed: {e}"))?;
.map_err(|e| AppError::database(format!("Settings read failed: {e}")))?;
Ok(settings)
}
}

View file

@ -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<Vec<SshSession>, String> {
pub fn list_ssh_sessions(&self) -> Result<Vec<SshSession>, 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::<Result<Vec<_>, _>>()
.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(())
}
}