*mm}-@x}c|5G`@_rTTSx#wA|rsTIR=eRGd~
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
literal 0
HcmV?d00001
diff --git a/browser-extension/icons/icon16.png b/browser-extension/icons/icon16.png
new file mode 100644
index 0000000000000000000000000000000000000000..39c96db33f4f7870c8ee55633638c531dd63582e
GIT binary patch
literal 539
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/"
+ ]
+}
\ No newline at end of file
diff --git a/browser-extension/native-host.bat b/browser-extension/native-host.bat
new file mode 100644
index 0000000..fb757d6
--- /dev/null
+++ b/browser-extension/native-host.bat
@@ -0,0 +1,6 @@
+@echo off
+REM Soterios Native Messaging Host
+REM This batch file launches the Node.js native host that communicates with the desktop app
+
+set NODE_PATH=%~dp0..\..\node_modules
+node "%~dp0native-host.js" %*
\ No newline at end of file
diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js
new file mode 100644
index 0000000..7b93fca
--- /dev/null
+++ b/browser-extension/native-host.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+/**
+ * Soterios Native Messaging Host
+ * Bridges browser extension <-> desktop Electron app via stdin/stdout JSON messages
+ */
+
+const { spawn } = require('child_process');
+const readline = require('readline');
+
+const DESKTOP_APP = process.env.SOTERIOS_APP_PATH || 'soterios://';
+
+function log(...args) {
+ console.error('[Soterios Native Host]', new Date().toISOString(), ...args);
+}
+
+function send(msg) {
+ const json = JSON.stringify(msg);
+ const len = Buffer.byteLength(json);
+ const buf = Buffer.alloc(4 + len);
+ buf.writeUInt32LE(len, 0);
+ buf.write(json, 4);
+ process.stdout.write(buf);
+}
+
+function readMessages() {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ terminal: false
+ });
+
+ let buffer = Buffer.alloc(0);
+
+ process.stdin.on('data', chunk => {
+ buffer = Buffer.concat([buffer, chunk]);
+
+ while (buffer.length >= 4) {
+ const len = buffer.readUInt32LE(0);
+ if (buffer.length < 4 + len) break;
+
+ const json = buffer.subarray(4, 4 + len).toString();
+ buffer = buffer.subarray(4 + len);
+
+ try {
+ const msg = JSON.parse(json);
+ handleMessage(msg);
+ } catch (e) {
+ log('Parse error:', e.message);
+ }
+ }
+ });
+}
+
+let desktopProc = null;
+const pending = new Map();
+let msgId = 0;
+
+function launchDesktopApp() {
+ if (desktopProc) return Promise.resolve();
+
+ return new Promise((resolve, reject) => {
+ const url = process.platform === 'win32'
+ ? `cmd /c start "" "${DESKTOP_APP}"`
+ : process.platform === 'darwin'
+ ? `open -a "Soterios"`
+ : `xdg-open "${DESKTOP_APP}"`;
+
+ desktopProc = spawn(url, { shell: true, detached: true });
+ desktopProc.unref();
+
+ desktopProc.on('error', e => {
+ log('Desktop app launch error:', e.message);
+ desktopProc = null;
+ });
+
+ setTimeout(resolve, 1500);
+ });
+}
+
+async function handleMessage(msg) {
+ log('Received:', msg.type);
+
+ switch (msg.type) {
+ case 'CREDENTIAL_LEAK': {
+ await launchDesktopApp();
+ send({ type: 'LEAK_NOTIFIED', ok: true, original: msg });
+ break;
+ }
+ case 'PING': {
+ send({ type: 'PONG', ok: true });
+ break;
+ }
+ case 'OPEN_APP': {
+ await launchDesktopApp();
+ send({ type: 'APP_OPENED', ok: true });
+ break;
+ }
+ default: {
+ send({ type: 'ERROR', error: 'Unknown message type', original: msg });
+ }
+ }
+}
+
+process.on('uncaughtException', e => {
+ log('Uncaught:', e);
+ send({ type: 'ERROR', error: e.message });
+});
+
+process.on('unhandledRejection', e => {
+ log('Unhandled rejection:', e);
+});
+
+log('Starting native messaging host');
+readMessages();
\ No newline at end of file
diff --git a/browser-extension/options.html b/browser-extension/options.html
new file mode 100644
index 0000000..3c53605
--- /dev/null
+++ b/browser-extension/options.html
@@ -0,0 +1,75 @@
+
+
+
+
+ Soterios Options
+
+
+
+ Soterios Credential Safety
+ Configure breach monitoring and desktop integration
+
+
+
Breach Monitoring
+
+
+
Check passwords against Have I Been Pwned
+
Uses k-anonymity (only first 5 hash chars sent). Never sends full password.
+
+
+
+
+
+
Auto-check on password fields
+
Show breach indicator automatically when you enter a password
+
+
+
+
+
+
Show Soterios icon in password fields
+
Click to manually check any password
+
+
+
+
+
+
+
Desktop App Integration
+
+
+
Notify Soterios desktop app of leaks
+
Sends breach alerts to the desktop app via native messaging
+
+
+
+
+
+ Settings saved
+
+
+ Note: Desktop integration requires the Soterios native messaging host installed. The installer sets this up automatically. If desktop notifications don't work, reinstall Soterios or run node tools/install-native-host.js as admin.
+
+
+
+
+
\ No newline at end of file
diff --git a/browser-extension/options.js b/browser-extension/options.js
new file mode 100644
index 0000000..562e5bc
--- /dev/null
+++ b/browser-extension/options.js
@@ -0,0 +1,50 @@
+const DEFAULTS = {
+ hibpEnabled: true,
+ autoCheck: true,
+ showIcon: true,
+ notifyDesktop: true
+};
+
+function loadSettings() {
+ chrome.storage.sync.get(DEFAULTS, settings => {
+ Object.keys(DEFAULTS).forEach(key => {
+ const el = document.getElementById(key);
+ if (el) el.setAttribute('aria-checked', settings[key]);
+ });
+ });
+}
+
+function saveSettings() {
+ const settings = {};
+ Object.keys(DEFAULTS).forEach(key => {
+ const el = document.getElementById(key);
+ if (el) settings[key] = el.getAttribute('aria-checked') === 'true';
+ });
+ chrome.storage.sync.set(settings, () => {
+ const msg = document.getElementById('savedMsg');
+ msg.classList.add('show');
+ setTimeout(() => msg.classList.remove('show'), 1500);
+ chrome.runtime.sendMessage({ type: 'SETTINGS_UPDATED', settings });
+ });
+}
+
+function setupToggles() {
+ document.querySelectorAll('.toggle').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const checked = btn.getAttribute('aria-checked') === 'true';
+ btn.setAttribute('aria-checked', !checked);
+ saveSettings();
+ });
+ btn.addEventListener('keydown', e => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ btn.click();
+ }
+ });
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ loadSettings();
+ setupToggles();
+});
\ No newline at end of file
diff --git a/browser-extension/package.json b/browser-extension/package.json
new file mode 100644
index 0000000..5466349
--- /dev/null
+++ b/browser-extension/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "soterios-browser-extension",
+ "version": "1.0.0",
+ "description": "Soterios Credential Safety Browser Extension",
+ "private": true,
+ "scripts": {
+ "build:icons": "node tools/build-icons.js",
+ "package": "npm run build:icons && cd browser-extension && zip -r ../soterios-extension.zip . -x '*.DS_Store' 'icons/*.svg' 'tools/*'",
+ "install:host": "node tools/install-native-host.js"
+ },
+ "devDependencies": {
+ "svgexport": "^0.4.2"
+ }
+}
\ No newline at end of file
diff --git a/browser-extension/popup.html b/browser-extension/popup.html
new file mode 100644
index 0000000..1ea5687
--- /dev/null
+++ b/browser-extension/popup.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
Check a Password
+
+
+ Check
+
+
+
+
+
+
Desktop App
+
+
+ Checking connection...
+
+
+
+ Settings
+
+
+
+
\ No newline at end of file
diff --git a/browser-extension/popup.js b/browser-extension/popup.js
new file mode 100644
index 0000000..8400b6f
--- /dev/null
+++ b/browser-extension/popup.js
@@ -0,0 +1,85 @@
+const HIBP_API = 'https://api.pwnedpasswords.com/range/';
+
+async function sha1(str) {
+ const buf = new TextEncoder().encode(str);
+ const hash = await crypto.subtle.digest('SHA-1', buf);
+ return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
+}
+
+async function checkPwned(password) {
+ const hash = await sha1(password);
+ const prefix = hash.slice(0, 5);
+ const suffix = hash.slice(5);
+ const resp = await fetch(`${HIBP_API}${prefix}`);
+ const text = await resp.text();
+ const lines = text.trim().split('\n');
+ for (const line of lines) {
+ const [suf, count] = line.split(':');
+ if (suf === suffix) return parseInt(count, 10);
+ }
+ return 0;
+}
+
+function showResult(count) {
+ const result = document.getElementById('result');
+ if (count === 0) {
+ result.className = 'result safe';
+ result.innerHTML = `
+ ✓ Not found in breaches
+ This password was not found in the HIBP database (${count} occurrences).
+ `;
+ } else {
+ result.className = 'result pwned';
+ result.innerHTML = `
+ ⚠ Found in ${count} breach${count > 1 ? 'es' : ''}
+ This password appears in known data breaches. Do not use it. Generate a new one in the Soterios app.
+ `;
+ }
+ result.style.display = 'block';
+}
+
+async function checkConnection() {
+ try {
+ const resp = await fetch('http://localhost:17234/api/health', { method: 'GET', timeout: 1000 });
+ if (resp.ok) {
+ document.getElementById('statusDot').classList.remove('offline');
+ document.getElementById('statusText').textContent = 'Soterios app connected';
+ } else throw new Error();
+ } catch {
+ document.getElementById('statusDot').classList.add('offline');
+ document.getElementById('statusText').textContent = 'Soterios app not running';
+ }
+}
+
+document.getElementById('checkBtn').addEventListener('click', async () => {
+ const input = document.getElementById('passwordInput');
+ const btn = document.getElementById('checkBtn');
+ const loader = document.getElementById('loader');
+ const pwd = input.value;
+
+ if (!pwd) return;
+
+ btn.disabled = true;
+ loader.classList.add('active');
+ document.getElementById('result').style.display = 'none';
+
+ try {
+ const count = await checkPwned(pwd);
+ showResult(count);
+ } catch (e) {
+ document.getElementById('result').className = 'result pwned';
+ document.getElementById('result').innerHTML = 'Error
Could not check password.
';
+ document.getElementById('result').style.display = 'block';
+ } finally {
+ btn.disabled = false;
+ loader.classList.remove('active');
+ }
+});
+
+document.getElementById('openOptions').addEventListener('click', (e) => {
+ e.preventDefault();
+ chrome.runtime.openOptionsPage();
+});
+
+checkConnection();
+setInterval(checkConnection, 30000);
\ No newline at end of file
diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js
index 68cbfec..fb5e891 100644
--- a/src/main/ipcHandlers.js
+++ b/src/main/ipcHandlers.js
@@ -766,6 +766,23 @@ function registerIpcHandlers(mainWindow, services) {
return { found: count > 0, count };
});
+ ipcMain.handle('credential-leak:notify', async (_event, payload) => {
+ if (!payload?.password) return { ok: false, error: 'Missing password' };
+ const sha = crypto.createHash('sha1').update(payload.password).digest('hex').toUpperCase();
+ const alert = {
+ level: 'danger',
+ source: 'Browser Extension',
+ title: 'Credential Leak Detected',
+ message: `Password found in ${payload.count} breach${payload.count > 1 ? 'es' : ''} via browser extension`,
+ detail: `SHA-1 prefix: ${sha.slice(0, 5)}... | Breaches: ${payload.count}`,
+ timestamp: new Date().toISOString(),
+ metadata: { source: 'browser-extension', hashPrefix: sha.slice(0, 5), count: payload.count }
+ };
+ db.addAlert(alert);
+ if (services.eventBus) services.eventBus.emit('alert:new', alert);
+ return { ok: true };
+ });
+
ipcMain.handle('xon:email', async (_event, email) => {
if (!email) return { found: false, breaches: [] };
if (!db.getSetting('feature.externalLookups', true)) throw new Error('External lookups are disabled in Settings.');
diff --git a/src/main/main.js b/src/main/main.js
index 35f7826..b425bd8 100644
--- a/src/main/main.js
+++ b/src/main/main.js
@@ -580,7 +580,27 @@ function buildAppMenu() {
app.setAppUserModelId('com.soterios.app');
+const gotTheLock = app.requestSingleInstanceLock();
+if (!gotTheLock) {
+ app.quit();
+ process.exit(0);
+}
+
+app.on('second-instance', (_event, commandLine) => {
+ if (mainWindow) {
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ mainWindow.focus();
+ const url = commandLine.find(arg => arg.startsWith('soterios://'));
+ if (url) mainWindow.webContents.send('protocol-url', url);
+ }
+});
+
app.whenReady().then(async () => {
+ // Register custom protocol for browser extension communication
+ if (process.platform === 'win32') {
+ app.setAsDefaultProtocolClient('soterios');
+ }
+
const dbPath = path.join(app.getPath('userData'), 'soterios.db');
// File logging is opt-in via SOTERIOS_LOG_FILE (path or "1" for the default log file).
const logConfig = { level: process.env.SOTERIOS_LOG_LEVEL || 'info' };
diff --git a/tools/build-icons.js b/tools/build-icons.js
new file mode 100644
index 0000000..35cea89
--- /dev/null
+++ b/tools/build-icons.js
@@ -0,0 +1,22 @@
+const { execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const sizes = [16, 32, 48, 128];
+const svgPath = path.join(__dirname, '../browser-extension/icons/icon.svg');
+const iconsDir = path.join(__dirname, '../browser-extension/icons');
+
+if (!fs.existsSync(svgPath)) {
+ console.error('icon.svg not found');
+ process.exit(1);
+}
+
+for (const size of sizes) {
+ const outPath = path.join(iconsDir, `icon${size}.png`);
+ try {
+ execSync(`npx -y svgexport "${svgPath}" "${outPath}" ${size}:${size}`, { stdio: 'inherit' });
+ console.log(`Generated ${outPath}`);
+ } catch (e) {
+ console.error(`Failed to generate ${size}px icon:`, e.message);
+ }
+}
\ No newline at end of file
diff --git a/tools/install-native-host.js b/tools/install-native-host.js
new file mode 100644
index 0000000..d55afee
--- /dev/null
+++ b/tools/install-native-host.js
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+/**
+ * Install Soterios Native Messaging Host
+ * Run as Administrator on Windows
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const EXTENSION_ID = process.env.EXTENSION_ID || 'YOUR_EXTENSION_ID_HERE';
+const IS_WIN = process.platform === 'win32';
+
+function main() {
+ const extDir = path.resolve(__dirname, '..', 'browser-extension');
+ const manifestPath = path.join(extDir, 'native-host-manifest.json');
+ const batPath = path.join(extDir, 'native-host.bat');
+ const jsPath = path.join(extDir, 'native-host.js');
+
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(batPath) || !fs.existsSync(jsPath)) {
+ console.error('Extension files not found. Run from project root.');
+ process.exit(1);
+ }
+
+ let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ manifest.allowed_origins = [manifest.allowed_origins[0].replace('', EXTENSION_ID)];
+
+ if (IS_WIN) {
+ const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`;
+ const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`;
+ try {
+ execSync(regCmd, { stdio: 'inherit' });
+ console.log('Registered native host for Chrome (Current User)');
+ } catch (e) {
+ console.error('Failed to register (run as Administrator):', e.message);
+ process.exit(1);
+ }
+
+ const regPathEdge = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${manifest.name}`;
+ const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`;
+ try {
+ execSync(regCmdEdge, { stdio: 'inherit' });
+ console.log('Registered native host for Edge (Current User)');
+ } catch (e) {
+ console.warn('Edge registration failed:', e.message);
+ }
+ } else {
+ const dir = process.platform === 'darwin'
+ ? path.join(process.env.HOME, 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts')
+ : path.join(process.env.HOME, '.config', 'google-chrome', 'NativeMessagingHosts');
+
+ fs.mkdirSync(dir, { recursive: true });
+ const target = path.join(dir, `${manifest.name}.json`);
+ fs.writeFileSync(target, JSON.stringify(manifest, null, 2));
+ console.log('Installed manifest to:', target);
+ }
+
+ console.log('\nDone! Reload the extension in chrome://extensions');
+}
+
+main();
\ No newline at end of file
From 75173dc3fb9355913e6e164d96f1b4b39e5fd4c7 Mon Sep 17 00:00:00 2001
From: Chris <185133702+chrisriv10@users.noreply.github.com>
Date: Tue, 21 Jul 2026 18:54:30 -0500
Subject: [PATCH 2/8] Add browser extension integration toggle to Settings
- Add 'Browser Extension Integration' feature toggle in Settings
- IPC handler 'browserExtension:installNativeHost' installs native messaging host (Windows)
- When enabled, installs native host for Chrome/Edge
- i18n strings for install status
---
src/i18n/locales/en.json | 6 ++++++
src/main/ipcHandlers.js | 31 +++++++++++++++++++++++++++
src/ui/js/pages/settings.js | 42 +++++++++++++++++++++++++++++++++++++
3 files changed, 79 insertions(+)
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 1592cef..9021e79 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -117,6 +117,12 @@
"settings.geoLookup.desc": "Resolve IP addresses to display a world map of active connections",
"settings.networkPerimeterMap.label": "Network Perimeter Map",
"settings.networkPerimeterMap.desc": "Show the live connection visualization on the Firewall page",
+"settings.browserExtension.label": "Browser Extension Integration",
+ "settings.browserExtension.desc": "Receive credential leak alerts from the Soterios browser extension (requires native messaging host)",
+ "settings.browserExtension.installing": "Installing native messaging host...",
+ "settings.browserExtension.installed": "Native messaging host installed. Install the extension from Chrome Web Store.",
+ "settings.browserExtension.installFailed": "Failed to install native host: {error}",
+ "settings.browserExtension.disabled": "Browser extension integration disabled",
"settings.colorScheme": "Color Scheme",
"settings.theme.dark": "Dark",
"settings.theme.light": "Light",
diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js
index fb5e891..b370670 100644
--- a/src/main/ipcHandlers.js
+++ b/src/main/ipcHandlers.js
@@ -918,6 +918,37 @@ function registerIpcHandlers(mainWindow, services) {
});
ipcMain.handle('tray:quit', () => app.quit());
+
+ // -- Browser Extension Native Host --
+ ipcMain.handle('browserExtension:installNativeHost', async () => {
+ if (process.platform !== 'win32') {
+ return { ok: false, error: 'Native host install only supported on Windows' };
+ }
+ const { execSync } = require('child_process');
+ const fs = require('fs');
+ const path = require('path');
+ const extDir = path.join(__dirname, '..', '..', 'browser-extension');
+ const manifestPath = path.join(extDir, 'native-host-manifest.json');
+ const batPath = path.join(extDir, 'native-host.bat');
+ const jsPath = path.join(extDir, 'native-host.js');
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(batPath) || !fs.existsSync(jsPath)) {
+ return { ok: false, error: 'Extension files not found. Reinstall Soterios.' };
+ }
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ const extId = process.env.SOTERIOS_EXT_ID || 'YOUR_EXTENSION_ID_HERE';
+ manifest.allowed_origins = [manifest.allowed_origins[0].replace('', extId)];
+ const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`;
+ const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`;
+ try {
+ execSync(regCmd, { stdio: 'ignore' });
+ const regPathEdge = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${manifest.name}`;
+ const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`;
+ try { execSync(regCmdEdge, { stdio: 'ignore' }); } catch (_) {}
+ return { ok: true };
+ } catch (e) {
+ return { ok: false, error: e.message || String(e) };
+ }
+ });
}
module.exports = { registerIpcHandlers };
\ No newline at end of file
diff --git a/src/ui/js/pages/settings.js b/src/ui/js/pages/settings.js
index b7499c9..9a895b5 100644
--- a/src/ui/js/pages/settings.js
+++ b/src/ui/js/pages/settings.js
@@ -119,6 +119,21 @@ window.Pages.settings = {
+
+
+
${escapeHtml(t('settings.networkPerimeterMap.label'))}
+
${escapeHtml(t('settings.networkPerimeterMap.desc'))}
+
+
+
+
+
+
+
${escapeHtml(t('settings.browserExtension.label'))}
+
${escapeHtml(t('settings.browserExtension.desc'))}
+
+
+
@@ -384,6 +399,33 @@ window.Pages.settings = {
container.querySelector('#externalLookupsToggle').addEventListener('change', (event) => saveFeature('externalLookups', event.target.checked, event.target));
container.querySelector('#geoLookupToggle').addEventListener('change', (event) => saveFeature('geoLookup', event.target.checked, event.target));
container.querySelector('#networkPerimeterMapToggle').addEventListener('change', (event) => saveFeature('networkPerimeterMap', event.target.checked, event.target));
+ container.querySelector('#browserExtensionToggle').addEventListener('change', async (event) => {
+ const checked = event.target.checked;
+ const statusEl = container.querySelector('#featureToggleStatus');
+ statusEl.textContent = '';
+ event.target.disabled = true;
+ try {
+ await Api.updateSettings({ features: { browserExtension: checked } });
+ if (checked) {
+ statusEl.textContent = t('settings.browserExtension.installing');
+ const result = await window.api.invoke('browserExtension:installNativeHost');
+ if (result.ok) {
+ statusEl.textContent = t('settings.browserExtension.installed');
+ } else {
+ event.target.checked = false;
+ await Api.updateSettings({ features: { browserExtension: false } });
+ statusEl.textContent = result.error || t('settings.browserExtension.installFailed');
+ }
+ } else {
+ statusEl.textContent = t('settings.featureSaved');
+ }
+ } catch (err) {
+ event.target.checked = !checked;
+ statusEl.textContent = err.message || String(err);
+ } finally {
+ event.target.disabled = false;
+ }
+ });
container.querySelector('#notificationsToggle').addEventListener('change', async (event) => {
const checked = event.target.checked;
const statusEl = container.querySelector('#notificationStatus');
From 4fe4ec22e2b5428d7aef3157892e745e94220647 Mon Sep 17 00:00:00 2001
From: Chris <185133702+chrisriv10@users.noreply.github.com>
Date: Tue, 21 Jul 2026 19:17:12 -0500
Subject: [PATCH 3/8] Enhance tray dashboard: health score, RTP status, quick
scan, network sparkline
- healthSummary.js: Return RTP, firewall, network stats, last scan
- trayDashboard.html: Health score badge, RTP indicator, sparkline, quick scan button
- trayDashboard.js: Already passes enhanced summary
---
src/main/healthSummary.js | 50 +++++++-
src/ui/pages/trayDashboard.html | 208 +++++++++++++++++++++++++++++---
src/ui/pages/trayDashboard.js | 166 +++++++++++++++++++++++++
3 files changed, 406 insertions(+), 18 deletions(-)
create mode 100644 src/ui/pages/trayDashboard.js
diff --git a/src/main/healthSummary.js b/src/main/healthSummary.js
index df30525..ad64678 100644
--- a/src/main/healthSummary.js
+++ b/src/main/healthSummary.js
@@ -19,10 +19,56 @@ async function getTrayHealthSummary(db, toolRegistry) {
}
const disk = result.data.breakdown?.disk;
+
+ // RTP status
+ let rtp = { enabled: false };
+ try {
+ const { RealTimeWatcher } = require('../security/RealTimeWatcher');
+ // Check if RTP is enabled in settings
+ const rtpEnabled = db.getSetting('feature.realtimeProtection', false);
+ rtp = { enabled: rtpEnabled };
+ } catch (_) {}
+
+ // Firewall status
+ let firewall = { active: false };
+ try {
+ const { execFile } = require('child_process');
+ const { promisify } = require('util');
+ const execFileAsync = promisify(execFile);
+ const { stdout } = await execFileAsync('netsh', ['advfirewall', 'show', 'allprofiles', 'state'], { timeout: 5000 });
+ firewall = { active: /ON|ENABLED/i.test(stdout) };
+ } catch (_) {}
+
+ // Network traffic history (last 24h)
+ let network = { rxKBs: 0, txKBs: 0, history: [] };
+ try {
+ const history = db.getNetworkHistory ? db.getNetworkHistory(24 * 60) : []; // last 24h, 1 sample per min
+ if (history.length) {
+ const latest = history[history.length - 1];
+ network.rxKBs = Math.round((latest.rx_bytes || 0) / 1024);
+ network.txKBs = Math.round((latest.tx_bytes || 0) / 1024);
+ network.history = history.slice(-60).map(h => (h.tx_bytes + h.rx_bytes) / 1024); // last 60 samples for sparkline
+ }
+ } catch (_) {}
+
+ // Last scan info
+ let lastScan = null;
+ if (latest) {
+ lastScan = {
+ timestamp: latest.timestamp,
+ filesScanned: latest.files_scanned,
+ threatsFound: latest.threats_found
+ };
+ }
+
return {
score: result.data.score,
- detail: disk?.reason || 'Protection and resource summary ready.'
+ detail: disk?.reason || 'Protection and resource summary ready.',
+ rtp,
+ firewall,
+ network,
+ lastScan
};
}
-module.exports = { getTrayHealthSummary };
+module.exports = { getTrayHealthSummary };
\ No newline at end of file
diff --git a/src/ui/pages/trayDashboard.html b/src/ui/pages/trayDashboard.html
index e4b198c..1a6e3e0 100644
--- a/src/ui/pages/trayDashboard.html
+++ b/src/ui/pages/trayDashboard.html
@@ -11,6 +11,13 @@
--text: #f2f5f8;
--muted: #aab4bf;
--accent: #58a6ff;
+ --accent-bg: rgba(88,166,255,0.15);
+ --danger: #f85149;
+ --danger-bg: rgba(248,81,73,0.15);
+ --ok: #3fb950;
+ --ok-bg: rgba(63,185,80,0.15);
+ --spark-rx: #58a6ff;
+ --spark-tx: #f85149;
}
html, body {
margin: 0;
@@ -27,15 +34,46 @@
box-shadow: 0 12px 32px rgba(0,0,0,0.35);
color: var(--text);
}
- .title { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
+ .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
+ .title { font-size: 14px; font-weight: 700; }
+ .status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ }
+ .status-badge.active { background: var(--ok-bg); color: #3fb950; }
+ .status-badge.inactive { background: var(--danger-bg); color: #f85149; }
+ .status-dot {
+ width: 6px; height: 6px; border-radius: 50%;
+ background: currentColor;
+ }
+ .score-row { display: flex; align-items: baseline; gap: 12px; margin: 8px 0 4px; }
.score {
font-size: 42px;
font-weight: 700;
color: var(--accent);
line-height: 1;
+ }
+ .score-detail { font-size: 12px; color: var(--muted); line-height: 1.5; }
+ .rtp-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-top: 1px solid var(--border); }
+ .rtp-label { font-size: 12px; color: var(--muted); flex: 1; }
+ .sparkline {
+ height: 40px;
margin: 8px 0;
+ position: relative;
+ }
+ .sparkline canvas { width: 100%; height: 100%; }
+ .sparkline-labels {
+ display: flex;
+ justify-content: space-between;
+ font-size: 9px;
+ color: var(--muted);
+ margin-top: 2px;
}
- .meta { font-size: 12px; color: var(--muted); line-height: 1.5; }
.actions { display: flex; gap: 8px; margin-top: 14px; }
button {
flex: 1;
@@ -46,40 +84,178 @@
padding: 8px 10px;
font-size: 12px;
cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
}
+ button:hover { background: rgba(255,255,255,0.08); }
button.primary {
background: var(--accent);
border-color: transparent;
color: #0b0e14;
font-weight: 600;
}
+ button.primary:hover { background: #4a9eff; }
+ button.secondary:hover { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
-
System Health
-
--
-
Loading summary...
+
+
+
+
Loading summary...
+
+
+
Network
+
+
+
+ 0 KB/s
+ 0 KB/s
+
+
+
+
- Open Soterios
- Quit
+ Quick Scan
+ Open Soterios
+ Quit
+
-