1use rusqlite::params;
2
3use crate::error::{Error, Result};
4use crate::store::Store;
5use crate::types::Session;
6
7impl Store {
8 pub fn session_start(&self, project: &str, directory: Option<&str>) -> Result<Session> {
9 let existing: Option<Session> = self
11 .conn()
12 .query_row(
13 "SELECT id, project, directory, started_at, ended_at, summary, status
14 FROM sessions WHERE project = ?1 AND status = 'active'
15 ORDER BY started_at DESC LIMIT 1",
16 params![project],
17 |row| {
18 Ok(Session {
19 id: row.get(0)?,
20 project: row.get(1)?,
21 directory: row.get(2)?,
22 started_at: row.get(3)?,
23 ended_at: row.get(4)?,
24 summary: row.get(5)?,
25 status: row.get(6)?,
26 })
27 },
28 )
29 .ok();
30
31 if let Some(session) = existing {
32 return Ok(session);
33 }
34
35 let id = generate_session_id();
36 self.conn().execute(
37 "INSERT INTO sessions (id, project, directory) VALUES (?1, ?2, ?3)",
38 params![id, project, directory],
39 )?;
40
41 self.conn()
42 .query_row(
43 "SELECT id, project, directory, started_at, ended_at, summary, status
44 FROM sessions WHERE id = ?1",
45 params![id],
46 |row| {
47 Ok(Session {
48 id: row.get(0)?,
49 project: row.get(1)?,
50 directory: row.get(2)?,
51 started_at: row.get(3)?,
52 ended_at: row.get(4)?,
53 summary: row.get(5)?,
54 status: row.get(6)?,
55 })
56 },
57 )
58 .map_err(Error::Database)
59 }
60
61 pub fn session_end(&self, session_id: &str, summary: Option<&str>) -> Result<Session> {
62 let session = self
63 .conn()
64 .query_row(
65 "SELECT id, status FROM sessions WHERE id = ?1",
66 params![session_id],
67 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
68 )
69 .map_err(|_| Error::SessionNotFound(session_id.to_string()))?;
70
71 if session.1 != "active" {
72 return Err(Error::SessionAlreadyEnded(session_id.to_string()));
73 }
74
75 self.conn().execute(
76 "UPDATE sessions SET ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
77 summary = ?1, status = 'completed'
78 WHERE id = ?2",
79 params![summary, session_id],
80 )?;
81
82 self.conn()
83 .query_row(
84 "SELECT id, project, directory, started_at, ended_at, summary, status
85 FROM sessions WHERE id = ?1",
86 params![session_id],
87 |row| {
88 Ok(Session {
89 id: row.get(0)?,
90 project: row.get(1)?,
91 directory: row.get(2)?,
92 started_at: row.get(3)?,
93 ended_at: row.get(4)?,
94 summary: row.get(5)?,
95 status: row.get(6)?,
96 })
97 },
98 )
99 .map_err(Error::Database)
100 }
101
102 pub fn recent_sessions(&self, limit: i32) -> Result<Vec<Session>> {
103 let mut stmt = self.conn().prepare(
104 "SELECT id, project, directory, started_at, ended_at, summary, status
105 FROM sessions ORDER BY started_at DESC LIMIT ?1",
106 )?;
107
108 let results = stmt
109 .query_map(params![limit], |row| {
110 Ok(Session {
111 id: row.get(0)?,
112 project: row.get(1)?,
113 directory: row.get(2)?,
114 started_at: row.get(3)?,
115 ended_at: row.get(4)?,
116 summary: row.get(5)?,
117 status: row.get(6)?,
118 })
119 })?
120 .collect::<std::result::Result<Vec<_>, _>>()?;
121
122 Ok(results)
123 }
124
125 pub fn end_active_sessions(
128 &self,
129 project: Option<&str>,
130 summary: Option<&str>,
131 ) -> Result<usize> {
132 let active = self.recent_sessions(10000)?;
133 let mut ended = 0;
134 for session in &active {
135 if session.status != "active" {
136 continue;
137 }
138 if let Some(proj) = project {
139 if session.project != proj {
140 continue;
141 }
142 }
143 self.session_end(&session.id, summary)?;
144 ended += 1;
145 }
146 Ok(ended)
147 }
148}
149
150fn generate_session_id() -> String {
151 use std::time::{SystemTime, UNIX_EPOCH};
152 let ts = SystemTime::now()
153 .duration_since(UNIX_EPOCH)
154 .unwrap()
155 .as_nanos();
156 format!("ses_{ts:x}")
157}