Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src-tauri/src/cloud/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn open_queue() -> Result<Connection, String> {
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,
Expand All @@ -34,23 +34,24 @@ pub fn open_queue() -> Result<Connection, String> {
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'",
[],
|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,
Expand Down
12 changes: 11 additions & 1 deletion src-tauri/src/cloud/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ async fn handle_message<S>(
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) => {
Expand All @@ -215,7 +221,11 @@ async fn handle_message<S>(
);
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,
Expand Down
27 changes: 19 additions & 8 deletions src-tauri/src/commands/alerts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ static SHARE_TRACKER: Mutex<Option<HashMap<String, (f64, Instant)>>> = 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<Option<Instant>> = 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() {
Expand Down Expand Up @@ -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)?;

Expand All @@ -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),
}
}
}
Expand Down
Loading