diff --git a/src-tauri/src/cloud/queue.rs b/src-tauri/src/cloud/queue.rs index 64e50a5..df6833f 100644 --- a/src-tauri/src/cloud/queue.rs +++ b/src-tauri/src/cloud/queue.rs @@ -23,7 +23,7 @@ pub fn open_queue() -> Result { conn.execute_batch( "CREATE TABLE IF NOT EXISTS cloud_sync_queue ( id INTEGER PRIMARY KEY, - kind TEXT NOT NULL CHECK (kind IN ('snapshot', 'alert', 'miners')), + kind TEXT NOT NULL CHECK (kind IN ('snapshot', 'alert', 'miners', 'alert-read', 'alerts-read-all')), payload_json TEXT NOT NULL, created_at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, @@ -34,9 +34,10 @@ pub fn open_queue() -> Result { ON cloud_sync_queue(created_at);" ).map_err(|e| format!("Failed to create queue table: {}", e))?; - // Migration: older installs created the table with a CHECK that allowed - // only ('snapshot', 'alert'). SQLite can't ALTER a CHECK constraint, so - // rebuild the table when the existing definition doesn't include 'miners'. + // Migration: older installs created the table with a CHECK that didn't + // allow every kind the sync loop can now enqueue. SQLite can't ALTER a + // CHECK constraint, so rebuild the table when the existing definition is + // missing any of the current kinds. let existing_sql: String = conn .query_row( "SELECT sql FROM sqlite_master WHERE type='table' AND name='cloud_sync_queue'", @@ -44,13 +45,13 @@ pub fn open_queue() -> Result { |row| row.get(0), ) .unwrap_or_default(); - if !existing_sql.is_empty() && !existing_sql.contains("'miners'") { - log::info!("Cloud queue: migrating CHECK constraint to allow 'miners' kind"); + if !existing_sql.is_empty() && !existing_sql.contains("'alert-read'") { + log::info!("Cloud queue: migrating CHECK constraint to allow 'alert-read'/'alerts-read-all' kinds"); conn.execute_batch( "BEGIN; CREATE TABLE cloud_sync_queue_new ( id INTEGER PRIMARY KEY, - kind TEXT NOT NULL CHECK (kind IN ('snapshot', 'alert', 'miners')), + kind TEXT NOT NULL CHECK (kind IN ('snapshot', 'alert', 'miners', 'alert-read', 'alerts-read-all')), payload_json TEXT NOT NULL, created_at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, diff --git a/src-tauri/src/cloud/ws.rs b/src-tauri/src/cloud/ws.rs index 60cf95f..81d8698 100644 --- a/src-tauri/src/cloud/ws.rs +++ b/src-tauri/src/cloud/ws.rs @@ -205,6 +205,12 @@ async fn handle_message( let rule_name = data.get("ruleName").and_then(|v| v.as_str()).unwrap_or(""); let miner_id = data.get("minerId").and_then(|v| v.as_str()).unwrap_or(""); let timestamp = data.get("timestamp").and_then(|v| v.as_str()).unwrap_or(""); + log::info!( + "Cloud WS: received alert-read (rule='{}', miner='{}', ts='{}')", + rule_name, + miner_id, + timestamp + ); match crate::commands::alerts::mark_alert_read(rule_name, miner_id, timestamp) { Ok(true) => { @@ -215,7 +221,11 @@ async fn handle_message( ); let _ = app_handle.emit("alerts-updated", ()); } - Ok(false) => log::debug!( + // Was debug-level — invisible at the default log level, which made + // this branch indistinguishable from "message never arrived" when + // debugging individual-read sync. Bump to warn with the full tuple + // so a mismatch (e.g. minerId/timestamp drift) is diagnosable. + Ok(false) => log::warn!( "Cloud WS: alert-read had no local match (rule='{}', miner='{}', ts='{}')", rule_name, miner_id, diff --git a/src-tauri/src/commands/alerts.rs b/src-tauri/src/commands/alerts.rs index 827e92b..4a2bbfc 100644 --- a/src-tauri/src/commands/alerts.rs +++ b/src-tauri/src/commands/alerts.rs @@ -102,9 +102,11 @@ static SHARE_TRACKER: Mutex>> = Mutex::ne // Startup grace period — alerts are suppressed for the first N seconds after // the process starts so that stateful checks (NoShares, MinerOffline, etc.) // have time to warm up from a cold boot. Prevents alert storms when PoPManager -// is restarted while miners are running normally. +// is restarted while miners are running normally. Kept short (30s) so a user +// who just launched the app isn't left wondering why nothing fires — that's +// long enough for one or two poll cycles to establish a baseline. static STARTUP_INSTANT: Mutex> = Mutex::new(None); -const STARTUP_GRACE_SECONDS: u64 = 300; // 5 minutes +const STARTUP_GRACE_SECONDS: u64 = 30; fn within_startup_grace() -> (bool, u64) { let mut guard = match STARTUP_INSTANT.lock() { @@ -322,11 +324,13 @@ pub async fn acknowledge_alert( // Only newly-acked alerts need to be pushed. let mut history = load_history(); let mut tuple: Option<(String, String, String)> = None; - if let Some(e) = history.iter_mut().find(|e| e.id == id) { - if !e.acknowledged { + match history.iter_mut().find(|e| e.id == id) { + Some(e) if !e.acknowledged => { e.acknowledged = true; tuple = Some((e.rule_name.clone(), e.miner_ip.clone(), e.timestamp.clone())); } + Some(_) => log::debug!("Cloud: alert {} already acknowledged locally, no-op", id), + None => log::warn!("Cloud: acknowledge_alert called with unknown id {}", id), } save_history(&history)?; @@ -343,17 +347,24 @@ pub async fn acknowledge_alert( let api_key = state.api_key.lock().unwrap().clone(); let pushed = match api_key { Some(key) => match crate::cloud::client::push_alert_read(&key, &payload).await { - Ok(()) => true, + Ok(()) => { + log::info!("Cloud: alert-read pushed successfully ({})", rule_name); + true + } Err(e) => { log::warn!("Cloud: alert-read push failed, queueing — {}", e); false } }, - None => false, + None => { + log::warn!("Cloud: no api_key set, queueing alert-read instead of pushing"); + false + } }; if !pushed { - if let Err(qe) = crate::cloud::queue::enqueue("alert-read", &payload) { - log::warn!("Cloud: failed to enqueue alert-read: {}", qe); + match crate::cloud::queue::enqueue("alert-read", &payload) { + Ok(()) => log::info!("Cloud: enqueued alert-read for sync"), + Err(qe) => log::warn!("Cloud: failed to enqueue alert-read: {}", qe), } } }