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
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ keyring = { version = "3", features = ["windows-native", "apple-native", "sync-s

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Registry", "Win32_Security"] }
windows = { version = "0.61", features = ["UI_Notifications", "Foundation"] }
tauri-winrt-notification = "0.7"

[profile.release]
Expand Down
43 changes: 43 additions & 0 deletions src-tauri/src/commands/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,49 @@ pub fn setup_windows_aumid(identifier: &str, display_name: &str) {
}
}

/// Report whether Windows will actually surface our toasts. When the per-app or
/// system notification toggle is OFF, `Toast::show()` returns `Ok` but nothing
/// appears — users have no way to tell. The frontend uses this to show a warning
/// banner that deep-links to `ms-settings:notifications`.
///
/// Returns one of: `enabled`, `disabledForApplication`, `disabledForUser`,
/// `disabledByGroupPolicy`, `disabledByManifest`, `unknown`.
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn get_notification_status(app: tauri::AppHandle) -> Result<String, String> {
use windows::core::HSTRING;
use windows::UI::Notifications::{NotificationSetting, ToastNotificationManager};

let identifier = app.config().identifier.clone();
let aumid = HSTRING::from(identifier);
let notifier = ToastNotificationManager::CreateToastNotifierWithId(&aumid)
.map_err(|e| format!("Failed to create toast notifier: {}", e))?;
let setting = notifier
.Setting()
.map_err(|e| format!("Failed to read notification setting: {}", e))?;

let s = if setting == NotificationSetting::Enabled {
"enabled"
} else if setting == NotificationSetting::DisabledForApplication {
"disabledForApplication"
} else if setting == NotificationSetting::DisabledForUser {
"disabledForUser"
} else if setting == NotificationSetting::DisabledByGroupPolicy {
"disabledByGroupPolicy"
} else if setting == NotificationSetting::DisabledByManifest {
"disabledByManifest"
} else {
"unknown"
};
Ok(s.to_string())
}

#[cfg(not(target_os = "windows"))]
#[tauri::command]
pub fn get_notification_status(_app: tauri::AppHandle) -> Result<String, String> {
Ok("enabled".to_string())
}

#[tauri::command]
pub fn send_desktop_notification(
app: tauri::AppHandle,
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use commands::alerts::{
};
use commands::coins::{get_coins, add_coin, remove_coin};
use commands::email::{get_smtp_config, save_smtp_config, test_smtp_config, send_alert_email};
use commands::notifications::send_desktop_notification;
use commands::notifications::{send_desktop_notification, get_notification_status};
use commands::mute::{get_muted_devices, set_device_mute, clear_device_mute};
use commands::uptime::{record_uptime, get_uptime_stats, get_all_uptime_stats, clear_uptime_data};
use commands::export::{export_miners_csv, export_alert_history_csv, export_profitability_csv, export_farm_history_csv};
Expand Down Expand Up @@ -322,6 +322,7 @@ pub fn run() {
test_smtp_config,
send_alert_email,
send_desktop_notification,
get_notification_status,
add_farm_snapshot,
get_farm_history,
clear_farm_history,
Expand Down
58 changes: 58 additions & 0 deletions src/components/NotificationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState, useEffect, useCallback } from "react";
import { invoke } from "@tauri-apps/api/core";
import { open as openUrl } from "@tauri-apps/plugin-shell";

/**
* Warns when Windows notifications are turned off for OverManager. When the
* per-app or system toggle is OFF, toasts silently never appear — this banner
* surfaces that and deep-links to the Windows notification settings page.
*
* Renders nothing while notifications are enabled (or status can't be read).
*/
export default function NotificationBanner() {
const [status, setStatus] = useState<string>("enabled");

const check = useCallback(async () => {
try {
const s = await invoke<string>("get_notification_status");
setStatus(s);
} catch (err) {
console.error("Failed to query notification status:", err);
setStatus("enabled"); // fail open — don't nag on an unexpected error
}
}, []);

useEffect(() => {
check();
// Re-check when the user returns to the window (e.g. after toggling the
// setting in the Windows Settings app).
window.addEventListener("focus", check);
return () => window.removeEventListener("focus", check);
}, [check]);

// "unknown" is treated as enabled to avoid false warnings.
if (status === "enabled" || status === "unknown") return null;

return (
<div className="flex items-start gap-3 px-4 py-3 mb-4 bg-amber-900/20 border border-amber-700/40 rounded-lg">
<svg className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div className="min-w-0">
<p className="text-sm font-medium text-amber-300">
Windows notifications are off for OverManager — desktop alerts won't appear
</p>
<p className="text-xs text-amber-400/80 mt-1">
Turn notifications back on in Windows Settings so alert toasts are delivered.
</p>
<button
onClick={() => openUrl("ms-settings:notifications")}
className="mt-2 px-3 py-1.5 bg-amber-600/20 hover:bg-amber-600/30 border border-amber-600/40 text-amber-200 text-xs font-medium rounded-lg transition-colors"
>
Open Windows notification settings
</button>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions src/pages/Alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { writeTextFile } from "@tauri-apps/plugin-fs";
import type { AlertEvent, AlertRule, RuleType } from "../types/alerts";
import type { SavedMiner, MobileMiner } from "../types/miner";
import { useAlerts } from "../context/AlertContext";
import NotificationBanner from "../components/NotificationBanner";

// ─── Alert Rules helpers ──────────────────────────────────────────────────────

Expand Down Expand Up @@ -297,6 +298,8 @@ export default function Alerts() {
<p className="text-slate-400 mt-1">Monitor alert history and manage alert rules</p>
</div>

<NotificationBanner />

{/* Tab toggle */}
<div className="flex items-center gap-1 mb-6 bg-dark-800 border border-slate-700/50 rounded-lg p-1 w-fit">
<button
Expand Down
46 changes: 46 additions & 0 deletions src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { SmtpConfig } from "../types/alerts";
import obLogo from "../assets/icon.png";
import { useProfitability } from "../context/ProfitabilityContext";
import CloudSyncPanel from "../components/CloudSyncPanel";
import NotificationBanner from "../components/NotificationBanner";

const EMPTY_SMTP: SmtpConfig = {
smtpHost: "",
Expand Down Expand Up @@ -63,6 +64,10 @@ export default function Settings() {
const [smtpLoaded, setSmtpLoaded] = useState(false);
const [testingEmail, setTestingEmail] = useState(false);

// Desktop notification test state
const [testingNotif, setTestingNotif] = useState(false);
const [notifMsg, setNotifMsg] = useState<string | null>(null);

useEffect(() => {
getVersion().then(setCurrentVersion).catch(console.error);
}, []);
Expand Down Expand Up @@ -190,6 +195,22 @@ export default function Settings() {
}
}

async function handleTestNotification() {
setTestingNotif(true);
setNotifMsg(null);
try {
await invoke("send_desktop_notification", {
title: "OverManager test notification",
body: "If you can see this, desktop alerts are working.",
});
setNotifMsg("Test notification sent — check your Windows notifications.");
} catch (err) {
setNotifMsg(`Error: ${err}`);
} finally {
setTestingNotif(false);
}
}

async function handleTestEmail() {
setTestingEmail(true);
setSmtpError(null);
Expand Down Expand Up @@ -488,6 +509,31 @@ export default function Settings() {
</div>
)}

{/* Desktop Notifications */}
<div className="bg-dark-800 rounded-xl border border-slate-700/50 p-6">
<div className="mb-5">
<h3 className="text-lg font-semibold text-white">Desktop Notifications</h3>
<p className="text-xs text-slate-500 mt-0.5">
Alert toasts are delivered through Windows notifications.
</p>
</div>
<NotificationBanner />
<div className="flex items-center gap-3">
<button
onClick={handleTestNotification}
disabled={testingNotif}
className="px-5 py-2 bg-primary-600 hover:bg-primary-700 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
{testingNotif ? "Sending..." : "Send test notification"}
</button>
{notifMsg && (
<span className={`text-xs ${notifMsg.startsWith("Error") ? "text-red-400" : "text-emerald-400"}`}>
{notifMsg}
</span>
)}
</div>
</div>

{/* Data Export */}
<div className="bg-dark-800 rounded-xl border border-slate-700/50 p-6">
<div className="mb-5">
Expand Down
Loading