Skip to main content

garmin_cli/db/
models.rs

1//! Database models matching schema tables
2
3use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Profile for multi-account support
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Profile {
9    pub profile_id: i32,
10    pub display_name: String,
11    pub user_id: Option<i64>,
12    pub created_at: Option<DateTime<Utc>>,
13    pub last_sync_at: Option<DateTime<Utc>>,
14}
15
16/// Activity summary from Garmin API
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Activity {
19    pub activity_id: i64,
20    pub profile_id: i32,
21    pub activity_name: Option<String>,
22    pub activity_type: Option<String>,
23    pub start_time_local: Option<DateTime<Utc>>,
24    pub start_time_gmt: Option<DateTime<Utc>>,
25    pub duration_sec: Option<f64>,
26    pub distance_m: Option<f64>,
27    pub calories: Option<i32>,
28    pub avg_hr: Option<i32>,
29    pub max_hr: Option<i32>,
30    pub avg_speed: Option<f64>,
31    pub max_speed: Option<f64>,
32    pub elevation_gain: Option<f64>,
33    pub elevation_loss: Option<f64>,
34    pub avg_cadence: Option<f64>,
35    pub avg_power: Option<i32>,
36    pub normalized_power: Option<i32>,
37    pub training_effect: Option<f64>,
38    pub training_load: Option<f64>,
39    pub start_lat: Option<f64>,
40    pub start_lon: Option<f64>,
41    pub end_lat: Option<f64>,
42    pub end_lon: Option<f64>,
43    pub ground_contact_time: Option<f64>,
44    pub vertical_oscillation: Option<f64>,
45    pub stride_length: Option<f64>,
46    pub location_name: Option<String>,
47    pub raw_json: Option<serde_json::Value>,
48}
49
50/// GPS track point with sensor data
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TrackPoint {
53    pub id: Option<i64>,
54    pub activity_id: i64,
55    pub timestamp: DateTime<Utc>,
56    pub lat: Option<f64>,
57    pub lon: Option<f64>,
58    pub elevation: Option<f64>,
59    pub heart_rate: Option<i32>,
60    pub cadence: Option<i32>,
61    pub power: Option<i32>,
62    pub speed: Option<f64>,
63}
64
65/// Daily health metrics
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DailyHealth {
68    pub id: Option<i64>,
69    pub profile_id: i32,
70    pub date: NaiveDate,
71    pub steps: Option<i32>,
72    pub step_goal: Option<i32>,
73    pub total_calories: Option<i32>,
74    pub active_calories: Option<i32>,
75    pub bmr_calories: Option<i32>,
76    pub resting_hr: Option<i32>,
77    pub sleep_seconds: Option<i32>,
78    pub deep_sleep_seconds: Option<i32>,
79    pub light_sleep_seconds: Option<i32>,
80    pub rem_sleep_seconds: Option<i32>,
81    pub sleep_score: Option<i32>,
82    /// Subjective sleep note added in the Garmin mobile app (dailySleepDTO.userNote)
83    pub sleep_note: Option<String>,
84    pub avg_stress: Option<i32>,
85    pub max_stress: Option<i32>,
86    pub body_battery_start: Option<i32>,
87    pub body_battery_end: Option<i32>,
88    pub hrv_weekly_avg: Option<i32>,
89    pub hrv_last_night: Option<i32>,
90    pub hrv_status: Option<String>,
91    pub avg_respiration: Option<f64>,
92    pub avg_spo2: Option<i32>,
93    pub lowest_spo2: Option<i32>,
94    pub hydration_ml: Option<i32>,
95    pub moderate_intensity_min: Option<i32>,
96    pub vigorous_intensity_min: Option<i32>,
97    pub raw_json: Option<serde_json::Value>,
98}
99
100/// Performance metrics
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PerformanceMetrics {
103    pub id: Option<i64>,
104    pub profile_id: i32,
105    pub date: NaiveDate,
106    pub vo2max: Option<f64>,
107    pub fitness_age: Option<i32>,
108    pub training_readiness: Option<i32>,
109    pub training_status: Option<String>,
110    pub lactate_threshold_hr: Option<i32>,
111    pub lactate_threshold_pace: Option<f64>,
112    pub race_5k_sec: Option<i32>,
113    pub race_10k_sec: Option<i32>,
114    pub race_half_sec: Option<i32>,
115    pub race_marathon_sec: Option<i32>,
116    pub endurance_score: Option<i32>,
117    pub hill_score: Option<i32>,
118    pub raw_json: Option<serde_json::Value>,
119}
120
121/// Weight entry
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct WeightEntry {
124    pub id: Option<i64>,
125    pub profile_id: i32,
126    pub date: NaiveDate,
127    pub weight_kg: Option<f64>,
128    pub bmi: Option<f64>,
129    pub body_fat_pct: Option<f64>,
130    pub muscle_mass_kg: Option<f64>,
131}
132
133/// Sync state for incremental updates
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SyncState {
136    pub profile_id: i32,
137    pub data_type: String,
138    pub last_sync_date: Option<NaiveDate>,
139    pub last_activity_id: Option<i64>,
140}
141
142/// Sync task status
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum TaskStatus {
146    Pending,
147    InProgress,
148    Completed,
149    Failed,
150}
151
152impl std::fmt::Display for TaskStatus {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            TaskStatus::Pending => write!(f, "pending"),
156            TaskStatus::InProgress => write!(f, "in_progress"),
157            TaskStatus::Completed => write!(f, "completed"),
158            TaskStatus::Failed => write!(f, "failed"),
159        }
160    }
161}
162
163/// Sync pipeline for task routing
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
165#[serde(rename_all = "snake_case")]
166pub enum SyncPipeline {
167    #[default]
168    Frontier,
169    Backfill,
170}
171
172impl std::fmt::Display for SyncPipeline {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            SyncPipeline::Frontier => write!(f, "frontier"),
176            SyncPipeline::Backfill => write!(f, "backfill"),
177        }
178    }
179}
180
181/// Sync task types
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(tag = "type", rename_all = "snake_case")]
184pub enum SyncTaskType {
185    Activities {
186        start: u32,
187        limit: u32,
188        #[serde(default)]
189        min_date: Option<NaiveDate>,
190        #[serde(default)]
191        max_date: Option<NaiveDate>,
192    },
193    ActivityDetail {
194        activity_id: i64,
195    },
196    DownloadGpx {
197        activity_id: i64,
198        #[serde(default)]
199        activity_name: Option<String>,
200        #[serde(default)]
201        activity_date: Option<String>,
202    },
203    DailyHealth {
204        date: NaiveDate,
205    },
206    Performance {
207        date: NaiveDate,
208    },
209    Weight {
210        from: NaiveDate,
211        to: NaiveDate,
212    },
213    GenerateEmbeddings {
214        activity_ids: Vec<i64>,
215    },
216}
217
218/// Sync task for queue
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct SyncTask {
221    pub id: Option<i64>,
222    pub profile_id: i32,
223    pub task_type: SyncTaskType,
224    #[serde(default)]
225    pub pipeline: SyncPipeline,
226    pub status: TaskStatus,
227    pub attempts: i32,
228    pub last_error: Option<String>,
229    pub created_at: Option<DateTime<Utc>>,
230    pub next_retry_at: Option<DateTime<Utc>>,
231    pub completed_at: Option<DateTime<Utc>>,
232}
233
234impl SyncTask {
235    pub fn new(profile_id: i32, pipeline: SyncPipeline, task_type: SyncTaskType) -> Self {
236        Self {
237            id: None,
238            profile_id,
239            task_type,
240            pipeline,
241            status: TaskStatus::Pending,
242            attempts: 0,
243            last_error: None,
244            created_at: None,
245            next_retry_at: None,
246            completed_at: None,
247        }
248    }
249}