From b56979f9b9370a77a17f01bb7d7471e69a35b81f Mon Sep 17 00:00:00 2001 From: Proof of Prints Date: Sun, 7 Jun 2026 00:11:18 -0500 Subject: [PATCH] feat(cloud): immediate sync on login + live Syncing status After login the 'Last sync' time stayed blank for up to ~60s (login only enqueued; the sync loop is a 60s ticker) and the Syncing status enum was never set. - Add login_notify (tokio Notify) on CloudState; cloud_login notifies it so the sync loop runs an immediate cycle (mirrors poller force_poll). - Sync loop select!s on the 60s tick or the login signal. - Set status=Syncing while a snapshot/miners push is in flight; success flips to Connected. Login sets Connecting; an end-of-cycle guard settles any leftover Connecting/Syncing to Connected (auth/error preserved). Frontend already maps syncing -> pulsing dot, so no UI change. --- src-tauri/src/cloud/mod.rs | 15 +++++++++++++-- src-tauri/src/cloud/sync.rs | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/cloud/mod.rs b/src-tauri/src/cloud/mod.rs index 50efdac..0010dc1 100644 --- a/src-tauri/src/cloud/mod.rs +++ b/src-tauri/src/cloud/mod.rs @@ -1,6 +1,7 @@ use serde::Serialize; use std::sync::{Arc, Mutex}; use tauri::Manager; +use tokio::sync::Notify; pub mod auth; pub mod client; @@ -32,6 +33,9 @@ pub struct CloudState { pub queue_size: Mutex, // number of pending items pub latest_snapshot: Mutex>, pub latest_miners: Mutex>, + /// Notified on login so the sync loop runs an immediate cycle instead of + /// waiting up to 60s for the next tick. Mirrors the poller's force_poll. + pub login_notify: Notify, } impl CloudState { @@ -46,6 +50,7 @@ impl CloudState { queue_size: Mutex::new(0), latest_snapshot: Mutex::new(None), latest_miners: Mutex::new(None), + login_notify: Notify::new(), } } } @@ -85,9 +90,11 @@ pub async fn cloud_login( auth::store_instance_id(&instance.id)?; auth::store_instance_name(&instance.name)?; - // 4. Update state + // 4. Update state. Mark Connecting (not Connected yet) so the UI reflects + // that the first sync cycle is about to run; the sync loop flips it to + // Syncing during the push and Connected on success. { - *state.status.lock().unwrap() = CloudSyncStatus::Connected; + *state.status.lock().unwrap() = CloudSyncStatus::Connecting; *state.email.lock().unwrap() = Some(email.clone()); *state.instance_name.lock().unwrap() = Some(instance.name.clone()); *state.instance_id.lock().unwrap() = Some(instance.id.clone()); @@ -120,6 +127,10 @@ pub async fn cloud_login( } } + // 6. Wake the sync loop so it pushes now instead of waiting for the next + // 60s tick — gives the user immediate "Last sync" feedback after login. + state.login_notify.notify_one(); + Ok(cloud_status_from_state(&state)) } diff --git a/src-tauri/src/cloud/sync.rs b/src-tauri/src/cloud/sync.rs index fb53fc4..f3c75e7 100644 --- a/src-tauri/src/cloud/sync.rs +++ b/src-tauri/src/cloud/sync.rs @@ -14,7 +14,13 @@ pub async fn start_sync_loop( let mut cycle: u64 = 0; loop { - tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + // Wait for the regular interval or an immediate-sync signal (login). + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => {} + _ = cloud_state.login_notify.notified() => { + log::info!("Cloud sync: immediate cycle triggered by login"); + } + } // Check if logged in let api_key = { @@ -38,6 +44,7 @@ pub async fn start_sync_loop( if let Some(payload) = snapshot { log::info!("Cloud sync: pushing snapshot to cloud"); + *cloud_state.status.lock().unwrap() = CloudSyncStatus::Syncing; match client::push_snapshot(&api_key, &payload).await { Ok(()) => { let now_ms = chrono::Utc::now().timestamp_millis(); @@ -65,6 +72,7 @@ pub async fn start_sync_loop( if let Some(payload) = miners { log::info!("Cloud sync: pushing miner state to cloud"); + *cloud_state.status.lock().unwrap() = CloudSyncStatus::Syncing; match client::push_miners(&api_key, &payload).await { Ok(()) => { let now_ms = chrono::Utc::now().timestamp_millis(); @@ -146,6 +154,15 @@ pub async fn start_sync_loop( *cloud_state.queue_size.lock().unwrap() = count; } + // Settle the status: any leftover Connecting/Syncing (e.g. login with no + // data to push) resolves to Connected. Auth/Error states are preserved. + { + let mut st = cloud_state.status.lock().unwrap(); + if matches!(*st, CloudSyncStatus::Connecting | CloudSyncStatus::Syncing) { + *st = CloudSyncStatus::Connected; + } + } + // --- 5. Periodic prune (every ~100 cycles ≈ 100 minutes) --- if cycle % 100 == 0 { if let Err(e) = queue::prune() {