1use crate::Result;
2use fjall::{Keyspace, PartitionHandle, PersistMode};
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use tracing::{debug, info, warn};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum SyncStatus {
10 InProgress,
11 Completed,
12 Failed,
13 Paused,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SyncStatusEntry {
18 pub id: String,
19 pub sync_source: String,
20 pub linear_issue_id: String,
21 pub motion_task_id: Option<String>,
22 pub status: SyncStatus,
23 pub last_sync_attempt: chrono::DateTime<chrono::Utc>,
24 pub error_message: Option<String>,
25 pub retry_count: u32,
26 pub created_at: chrono::DateTime<chrono::Utc>,
27 pub updated_at: chrono::DateTime<chrono::Utc>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SyncSourceStatus {
32 pub source_name: String,
33 pub last_sync: Option<chrono::DateTime<chrono::Utc>>,
34 pub total_issues_processed: u64,
35 pub successful_syncs: u64,
36 pub failed_syncs: u64,
37 pub errors: Vec<String>,
38}
39
40pub struct StatusStore {
41 keyspace: Keyspace,
42 statuses: PartitionHandle,
43 source_stats: PartitionHandle,
44}
45
46impl StatusStore {
47 pub async fn new<P: AsRef<Path>>(db_path: P) -> Result<Self> {
48 let keyspace = fjall::Config::new(db_path).open()?;
49 let statuses =
50 keyspace.open_partition("sync_statuses", fjall::PartitionCreateOptions::default())?;
51 let source_stats =
52 keyspace.open_partition("source_stats", fjall::PartitionCreateOptions::default())?;
53
54 debug!("status tracking store initialized");
55
56 Ok(Self {
57 keyspace,
58 statuses,
59 source_stats,
60 })
61 }
62
63 pub async fn create_status_entry(
64 &self,
65 sync_source: String,
66 linear_issue_id: String,
67 ) -> Result<SyncStatusEntry> {
68 let entry = SyncStatusEntry::new(sync_source, linear_issue_id);
69 self.store_status_entry(&entry).await?;
70 Ok(entry)
71 }
72
73 pub async fn store_status_entry(&self, entry: &SyncStatusEntry) -> Result<()> {
74 let key = &entry.id;
75 let value = serde_json::to_vec(entry)?;
76
77 self.statuses.insert(key, &value)?;
78 debug!(
79 "Stored status entry: {} ({})",
80 entry.id, entry.linear_issue_id
81 );
82
83 Ok(())
84 }
85
86 pub async fn get_status_entry(&self, id: &str) -> Result<Option<SyncStatusEntry>> {
87 match self.statuses.get(id)? {
88 Some(value) => {
89 let entry: SyncStatusEntry = serde_json::from_slice(&value)?;
90 Ok(Some(entry))
91 }
92 None => Ok(None),
93 }
94 }
95
96 pub async fn update_status(
97 &self,
98 id: &str,
99 status: SyncStatus,
100 error_message: Option<String>,
101 ) -> Result<()> {
102 if let Some(mut entry) = self.get_status_entry(id).await? {
103 entry.status = status;
104 entry.error_message = error_message.clone();
105 entry.last_sync_attempt = chrono::Utc::now();
106 entry.updated_at = chrono::Utc::now();
107
108 if error_message.is_some() {
109 entry.retry_count += 1;
110 }
111
112 self.store_status_entry(&entry).await?;
113 debug!("Updated status for {}: {:?}", id, entry.status);
114 } else {
115 warn!("Attempted to update non-existent status entry: {}", id);
116 }
117
118 Ok(())
119 }
120
121 pub async fn mark_completed(&self, id: &str, motion_task_id: String) -> Result<()> {
122 if let Some(mut entry) = self.get_status_entry(id).await? {
123 entry.status = SyncStatus::Completed;
124 entry.motion_task_id = Some(motion_task_id);
125 entry.last_sync_attempt = chrono::Utc::now();
126 entry.updated_at = chrono::Utc::now();
127 entry.error_message = None;
128
129 self.store_status_entry(&entry).await?;
130 debug!("Marked {} as completed", id);
131 }
132
133 Ok(())
134 }
135
136 pub async fn mark_failed(&self, id: &str, error: String) -> Result<()> {
137 self.update_status(id, SyncStatus::Failed, Some(error))
138 .await
139 }
140
141 pub async fn list_statuses_by_source(&self, sync_source: &str) -> Result<Vec<SyncStatusEntry>> {
142 let mut entries = Vec::new();
143
144 for item in self.statuses.iter() {
145 let (_, value) = item?;
146 let entry: SyncStatusEntry = serde_json::from_slice(&value)?;
147
148 if entry.sync_source == sync_source {
149 entries.push(entry);
150 }
151 }
152
153 debug!(
154 "Found {} status entries for source: {}",
155 entries.len(),
156 sync_source
157 );
158 Ok(entries)
159 }
160
161 pub async fn list_failed_entries(&self) -> Result<Vec<SyncStatusEntry>> {
162 let mut failed_entries = Vec::new();
163
164 for item in self.statuses.iter() {
165 let (_, value) = item?;
166 let entry: SyncStatusEntry = serde_json::from_slice(&value)?;
167
168 if matches!(entry.status, SyncStatus::Failed) {
169 failed_entries.push(entry);
170 }
171 }
172
173 debug!("Found {} failed status entries", failed_entries.len());
174 Ok(failed_entries)
175 }
176
177 pub async fn get_source_status(&self, source_name: &str) -> Result<Option<SyncSourceStatus>> {
178 match self.source_stats.get(source_name)? {
179 Some(value) => {
180 let status: SyncSourceStatus = serde_json::from_slice(&value)?;
181 Ok(Some(status))
182 }
183 None => Ok(None),
184 }
185 }
186
187 pub async fn update_source_stats(
188 &self,
189 source_name: &str,
190 success: bool,
191 error: Option<String>,
192 ) -> Result<()> {
193 let mut status = self
194 .get_source_status(source_name)
195 .await?
196 .unwrap_or_else(|| SyncSourceStatus::new(source_name.to_string()));
197
198 status.last_sync = Some(chrono::Utc::now());
199 status.total_issues_processed += 1;
200
201 if success {
202 status.successful_syncs += 1;
203 } else {
204 status.failed_syncs += 1;
205 if let Some(err) = error {
206 status.errors.push(err);
207 if status.errors.len() > 10 {
209 status.errors.remove(0);
210 }
211 }
212 }
213
214 let value = serde_json::to_vec(&status)?;
215 self.source_stats.insert(source_name, &value)?;
216
217 debug!(
218 "Updated stats for source {}: {} successful, {} failed",
219 source_name, status.successful_syncs, status.failed_syncs
220 );
221
222 Ok(())
223 }
224
225 pub async fn list_all_source_stats(&self) -> Result<Vec<SyncSourceStatus>> {
226 let mut stats = Vec::new();
227
228 for item in self.source_stats.iter() {
229 let (_, value) = item?;
230 let status: SyncSourceStatus = serde_json::from_slice(&value)?;
231 stats.push(status);
232 }
233
234 Ok(stats)
235 }
236
237 pub async fn cleanup_old_entries(&self, older_than_days: u64) -> Result<u64> {
238 let cutoff = chrono::Utc::now() - chrono::Duration::days(older_than_days as i64);
239 let mut deleted_count = 0;
240
241 let mut keys_to_delete = Vec::new();
242
243 for item in self.statuses.iter() {
244 let (key, value) = item?;
245 let entry: SyncStatusEntry = serde_json::from_slice(&value)?;
246
247 if entry.created_at < cutoff && matches!(entry.status, SyncStatus::Completed) {
248 keys_to_delete.push(key.to_vec());
249 }
250 }
251
252 for key in keys_to_delete {
253 self.statuses.remove(&key)?;
254 deleted_count += 1;
255 }
256
257 if deleted_count > 0 {
258 info!("Cleaned up {} old status entries", deleted_count);
259 }
260
261 Ok(deleted_count)
262 }
263
264 pub async fn flush(&self) -> Result<()> {
265 self.keyspace.persist(PersistMode::SyncAll)?;
266 Ok(())
267 }
268}
269
270impl SyncStatusEntry {
271 pub fn new(sync_source: String, linear_issue_id: String) -> Self {
272 let now = chrono::Utc::now();
273
274 Self {
275 id: Uuid::new_v4().to_string(),
276 sync_source,
277 linear_issue_id,
278 motion_task_id: None,
279 status: SyncStatus::InProgress,
280 last_sync_attempt: now,
281 error_message: None,
282 retry_count: 0,
283 created_at: now,
284 updated_at: now,
285 }
286 }
287}
288
289impl SyncSourceStatus {
290 pub fn new(source_name: String) -> Self {
291 Self {
292 source_name,
293 last_sync: None,
294 total_issues_processed: 0,
295 successful_syncs: 0,
296 failed_syncs: 0,
297 errors: Vec::new(),
298 }
299 }
300}