agentmux_srv/backend/storage/
history.rs1use rusqlite::params;
15use serde::{Deserialize, Serialize};
16
17use super::error::StoreError;
18use super::store::Store;
19
20#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
22#[ts(export, export_to = "../../../frontend/types/rpc/")]
23pub struct AgentHistory {
24 #[ts(type = "number")]
25 pub id: i64,
26 pub agent_id: String,
27 pub session_date: String,
30 pub entry: String,
31 #[ts(type = "number")]
32 pub timestamp: i64,
33}
34
35impl Store {
36 pub fn agent_history_append(
38 &self,
39 agent_id: &str,
40 entry: &str,
41 ) -> Result<AgentHistory, StoreError> {
42 let now = agentmux_common::time::now_ms();
43 let secs = (now / 1000) as u64;
45 let days = secs / 86400;
46 let session_date = format_epoch_date(days);
48 let conn = self.conn.lock().unwrap();
49 conn.execute(
50 "INSERT INTO db_agent_history (agent_id, session_date, entry, timestamp) VALUES (?1, ?2, ?3, ?4)",
51 params![agent_id, session_date, entry, now],
52 )?;
53 let id = conn.last_insert_rowid();
54 Ok(AgentHistory {
55 id,
56 agent_id: agent_id.to_string(),
57 session_date,
58 entry: entry.to_string(),
59 timestamp: now,
60 })
61 }
62
63 pub fn agent_history_list(
65 &self,
66 agent_id: &str,
67 session_date: Option<&str>,
68 limit: i64,
69 offset: i64,
70 ) -> Result<Vec<AgentHistory>, StoreError> {
71 let conn = self.conn.lock().unwrap();
72 let (sql, params_vec): (String, Vec<Box<dyn rusqlite::types::ToSql>>) = match session_date {
73 Some(date) => (
74 "SELECT id, agent_id, session_date, entry, timestamp
75 FROM db_agent_history WHERE agent_id=?1 AND session_date=?2
76 ORDER BY timestamp DESC LIMIT ?3 OFFSET ?4"
77 .to_string(),
78 vec![
79 Box::new(agent_id.to_string()),
80 Box::new(date.to_string()),
81 Box::new(limit),
82 Box::new(offset),
83 ],
84 ),
85 None => (
86 "SELECT id, agent_id, session_date, entry, timestamp
87 FROM db_agent_history WHERE agent_id=?1
88 ORDER BY timestamp DESC LIMIT ?2 OFFSET ?3"
89 .to_string(),
90 vec![
91 Box::new(agent_id.to_string()),
92 Box::new(limit),
93 Box::new(offset),
94 ],
95 ),
96 };
97 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
98 params_vec.iter().map(|b| b.as_ref()).collect();
99 let mut stmt = conn.prepare(&sql)?;
100 let rows = stmt.query_map(params_refs.as_slice(), |row| {
101 Ok(AgentHistory {
102 id: row.get(0)?,
103 agent_id: row.get(1)?,
104 session_date: row.get(2)?,
105 entry: row.get(3)?,
106 timestamp: row.get(4)?,
107 })
108 })?;
109 let mut entries = Vec::new();
110 for row in rows {
111 entries.push(row?);
112 }
113 Ok(entries)
114 }
115
116 pub fn agent_history_search(
118 &self,
119 agent_id: &str,
120 query: &str,
121 limit: i64,
122 ) -> Result<Vec<AgentHistory>, StoreError> {
123 let conn = self.conn.lock().unwrap();
124 let pattern = format!("%{}%", query);
125 let mut stmt = conn.prepare(
126 "SELECT id, agent_id, session_date, entry, timestamp
127 FROM db_agent_history WHERE agent_id=?1 AND entry LIKE ?2
128 ORDER BY timestamp DESC LIMIT ?3",
129 )?;
130 let rows = stmt.query_map(params![agent_id, pattern, limit], |row| {
131 Ok(AgentHistory {
132 id: row.get(0)?,
133 agent_id: row.get(1)?,
134 session_date: row.get(2)?,
135 entry: row.get(3)?,
136 timestamp: row.get(4)?,
137 })
138 })?;
139 let mut entries = Vec::new();
140 for row in rows {
141 entries.push(row?);
142 }
143 Ok(entries)
144 }
145}
146
147fn format_epoch_date(days_since_epoch: u64) -> String {
150 let z = days_since_epoch + 719468;
152 let era = z / 146097;
153 let doe = z - era * 146097;
154 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
155 let y = yoe + era * 400;
156 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
157 let mp = (5 * doy + 2) / 153;
158 let d = doy - (153 * mp + 2) / 5 + 1;
159 let m = if mp < 10 { mp + 3 } else { mp - 9 };
160 let y = if m <= 2 { y + 1 } else { y };
161 format!("{:04}-{:02}-{:02}", y, m, d)
162}