garmin_cli/cli/commands/
sync.rs

1//! Sync commands for garmin-cli
2
3use chrono::NaiveDate;
4
5use crate::client::GarminClient;
6use crate::config::CredentialStore;
7use crate::db::default_db_path;
8use crate::error::Result;
9use crate::sync::{SyncEngine, SyncOptions, TaskQueue};
10use crate::Database;
11
12use super::auth::refresh_token;
13
14/// Run sync operation
15#[allow(clippy::too_many_arguments)]
16pub async fn run(
17    profile: Option<String>,
18    db_path: Option<String>,
19    activities: bool,
20    health: bool,
21    performance: bool,
22    from: Option<String>,
23    to: Option<String>,
24    dry_run: bool,
25    simple: bool,
26) -> Result<()> {
27    let store = CredentialStore::new(profile.clone())?;
28    let (oauth1, oauth2) = refresh_token(&store).await?;
29
30    // Open database
31    let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
32    println!("Using database: {}", db_path);
33
34    let db = Database::open(&db_path)?;
35
36    // Get or create profile
37    let profile_id = get_or_create_profile(&db, profile.as_deref())?;
38
39    // Build sync options
40    let sync_all = !activities && !health && !performance;
41    let opts = SyncOptions {
42        sync_activities: activities || sync_all,
43        sync_health: health || sync_all,
44        sync_performance: performance || sync_all,
45        from_date: from.as_ref().and_then(|s| parse_date(s)),
46        to_date: to.as_ref().and_then(|s| parse_date(s)),
47        dry_run,
48        force: false,
49        fancy_ui: !simple, // Use fancy TUI unless --simple is specified
50        concurrency: 3,
51    };
52
53    if dry_run {
54        println!("Dry run mode - no changes will be made");
55    }
56
57    // Create sync engine
58    let client = GarminClient::new(&oauth1.domain);
59    let mut engine = SyncEngine::new(db, client, oauth2, profile_id);
60
61    if !simple {
62        // TUI mode - less initial output
63        let stats = engine.run(opts).await?;
64        // Stats printed by TUI
65        let _ = stats; // Suppress unused warning
66    } else {
67        // Simple mode - traditional output
68        println!("Starting sync...");
69        let stats = engine.run(opts).await?;
70        println!("\nSync complete: {}", stats);
71    }
72
73    Ok(())
74}
75
76/// Show sync status
77pub async fn status(profile: Option<String>, db_path: Option<String>) -> Result<()> {
78    let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
79
80    if !std::path::Path::new(&db_path).exists() {
81        println!("No database found at: {}", db_path);
82        println!("Run 'garmin sync' to create one.");
83        return Ok(());
84    }
85
86    let db = Database::open(&db_path)?;
87    let conn = db.connection();
88    let conn = conn.lock().unwrap();
89
90    // Get profile info
91    let profile_name = profile.as_deref().unwrap_or("default");
92
93    // Count activities
94    let activity_count: i64 = conn
95        .query_row("SELECT COUNT(*) FROM activities", [], |row| row.get(0))
96        .unwrap_or(0);
97
98    // Count health days
99    let health_count: i64 = conn
100        .query_row("SELECT COUNT(*) FROM daily_health", [], |row| row.get(0))
101        .unwrap_or(0);
102
103    // Count track points
104    let trackpoint_count: i64 = conn
105        .query_row("SELECT COUNT(*) FROM track_points", [], |row| row.get(0))
106        .unwrap_or(0);
107
108    // Get pending tasks
109    let pending_count: i64 = conn
110        .query_row(
111            "SELECT COUNT(*) FROM sync_tasks WHERE status IN ('pending', 'failed')",
112            [],
113            |row| row.get(0),
114        )
115        .unwrap_or(0);
116
117    println!("Database: {}", db_path);
118    println!("Profile: {}", profile_name);
119    println!();
120    println!("Data stored:");
121    println!("  Activities:    {:>8}", activity_count);
122    println!("  Health days:   {:>8}", health_count);
123    println!("  Track points:  {:>8}", trackpoint_count);
124    println!();
125    if pending_count > 0 {
126        println!("Pending sync tasks: {}", pending_count);
127    }
128
129    Ok(())
130}
131
132/// Get or create profile in database
133fn get_or_create_profile(db: &Database, profile_name: Option<&str>) -> Result<i32> {
134    let conn = db.connection();
135    let conn = conn.lock().unwrap();
136
137    let name = profile_name.unwrap_or("default");
138
139    // Try to get existing profile
140    let existing = conn.query_row(
141        "SELECT profile_id FROM profiles WHERE display_name = ?",
142        duckdb::params![name],
143        |row| row.get::<_, i32>(0),
144    );
145
146    match existing {
147        Ok(id) => Ok(id),
148        Err(duckdb::Error::QueryReturnedNoRows) => {
149            // Create new profile with RETURNING to get the generated ID
150            let id: i32 = conn
151                .query_row(
152                    "INSERT INTO profiles (display_name) VALUES (?) RETURNING profile_id",
153                    duckdb::params![name],
154                    |row| row.get(0),
155                )
156                .map_err(|e| crate::GarminError::Database(e.to_string()))?;
157
158            Ok(id)
159        }
160        Err(e) => Err(crate::GarminError::Database(e.to_string())),
161    }
162}
163
164/// Parse date string to NaiveDate
165fn parse_date(s: &str) -> Option<NaiveDate> {
166    NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
167}
168
169/// Reset failed tasks to pending
170pub async fn reset(db_path: Option<String>) -> Result<()> {
171    let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
172
173    if !std::path::Path::new(&db_path).exists() {
174        println!("No database found at: {}", db_path);
175        return Ok(());
176    }
177
178    let db = Database::open(&db_path)?;
179    let queue = TaskQueue::new(db);
180
181    let reset_count = queue.reset_failed()?;
182    println!("Reset {} failed tasks to pending", reset_count);
183
184    Ok(())
185}
186
187/// Clear all pending tasks
188pub async fn clear(db_path: Option<String>) -> Result<()> {
189    let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
190
191    if !std::path::Path::new(&db_path).exists() {
192        println!("No database found at: {}", db_path);
193        return Ok(());
194    }
195
196    let db = Database::open(&db_path)?;
197    let queue = TaskQueue::new(db);
198
199    let cleared = queue.clear_pending()?;
200    println!("Cleared {} pending tasks", cleared);
201
202    Ok(())
203}