From 7a48fc2a28e3d7e20c106561dbd390bd0489654b Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 01:24:35 -0600 Subject: [PATCH 1/6] Fix drive usage display and center status icon --- Data/Server/WebUI/src/Device_Details.jsx | 34 +++++++++++++++++++----- Data/Server/WebUI/src/Device_List.jsx | 26 +++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 5309b8a..6c61e2b 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -283,13 +283,33 @@ export default function DeviceDetails({ device, onBack }) { }; const renderStorage = () => { - const rows = (details.storage || []).map((d) => ({ - drive: d.drive, - disk_type: d.disk_type, - usage: d.usage !== undefined ? Number(d.usage) : undefined, - total: d.total !== undefined ? Number(d.total) : undefined, - free: d.free !== undefined ? Number(d.free) : undefined, - })); + const parseNum = (val) => { + if (val === undefined || val === null) return undefined; + const n = Number(String(val).replace(/[^0-9.]/g, "")); + return Number.isNaN(n) ? undefined : n; + }; + const rows = (details.storage || []).map((d) => { + const total = parseNum(d.total); + const rawFree = parseNum(d.free); + const freePct = rawFree !== undefined + ? rawFree <= 100 + ? rawFree + : total + ? (rawFree / total) * 100 + : undefined + : undefined; + let usage = parseNum(d.usage); + if ((usage === undefined || Number.isNaN(usage)) && freePct !== undefined) { + usage = 100 - freePct; + } + return { + drive: d.drive, + disk_type: d.disk_type, + usage, + total, + free: freePct, + }; + }); if (!rows.length) return placeholderTable(["Drive Letter", "Disk Type", "Usage", "Total Size", "Free %"]); return ( diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index 0799c4c..adf87c7 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -172,18 +172,20 @@ export default function DeviceList({ onSelectDevice }) { sx={{ cursor: onSelectDevice ? "pointer" : "default" }} > - - {r.status} + + + {r.status} + {r.hostname} {timeSince(r.lastSeen)} From 7f5e0c69da9fb15b0b9595f2d577230aae0cf31c Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 01:50:08 -0600 Subject: [PATCH 2/6] Load device records on startup and improve storage display --- Data/Server/WebUI/src/Device_Details.jsx | 45 +++++++++++++++------- Data/Server/WebUI/src/Device_List.jsx | 19 ++++++---- Data/Server/server.py | 48 ++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 6c61e2b..32aa907 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -283,25 +283,40 @@ export default function DeviceDetails({ device, onBack }) { }; const renderStorage = () => { - const parseNum = (val) => { + const toNum = (val) => { if (val === undefined || val === null) return undefined; - const n = Number(String(val).replace(/[^0-9.]/g, "")); + const n = Number(val); return Number.isNaN(n) ? undefined : n; }; + const rows = (details.storage || []).map((d) => { - const total = parseNum(d.total); - const rawFree = parseNum(d.free); - const freePct = rawFree !== undefined - ? rawFree <= 100 - ? rawFree - : total - ? (rawFree / total) * 100 - : undefined - : undefined; - let usage = parseNum(d.usage); - if ((usage === undefined || Number.isNaN(usage)) && freePct !== undefined) { - usage = 100 - freePct; + const total = toNum(d.total); + let usage = toNum(d.usage); + let freePct; + + if (usage !== undefined) { + if (usage <= 1) usage *= 100; + freePct = 100 - usage; + } else { + const freeRaw = toNum(d.free); + if (freeRaw !== undefined) { + if (freeRaw > 1 && freeRaw > 100 && total) { + freePct = (freeRaw / total) * 100; + } else if (freeRaw <= 1) { + freePct = freeRaw * 100; + } else { + freePct = freeRaw; + } + usage = freePct !== undefined ? 100 - freePct : undefined; + } else { + const usedRaw = toNum(d.used); + if (usedRaw !== undefined && total) { + usage = (usedRaw / total) * 100; + freePct = 100 - usage; + } + } } + return { drive: d.drive, disk_type: d.disk_type, @@ -310,8 +325,10 @@ export default function DeviceDetails({ device, onBack }) { free: freePct, }; }); + if (!rows.length) return placeholderTable(["Drive Letter", "Disk Type", "Usage", "Total Size", "Free %"]); + return ( diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index adf87c7..07984d8 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -165,12 +165,7 @@ export default function DeviceList({ onSelectDevice }) { {sorted.map((r, i) => ( - onSelectDevice && onSelectDevice(r)} - sx={{ cursor: onSelectDevice ? "pointer" : "default" }} - > + - {r.hostname} + onSelectDevice && onSelectDevice(r)} + sx={{ cursor: onSelectDevice ? "pointer" : "default" }} + > + {r.hostname} + {timeSince(r.lastSeen)} {r.os} openMenu(e, r)} + onClick={(e) => { + e.stopPropagation(); + openMenu(e, r); + }} sx={{ color: "#ccc" }} > diff --git a/Data/Server/server.py b/Data/Server/server.py index 9738f97..6ac27c1 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -398,6 +398,36 @@ def init_db(): init_db() + +def load_agents_from_db(): + """Populate registered_agents with any devices stored in the database.""" + try: + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute("SELECT hostname, details FROM device_details") + for hostname, details_json in cur.fetchall(): + try: + details = json.loads(details_json or "{}") + except Exception: + details = {} + summary = details.get("summary", {}) + agent_id = summary.get("agent_id") or hostname + registered_agents[agent_id] = { + "agent_id": agent_id, + "hostname": summary.get("hostname") or hostname, + "agent_operating_system": summary.get("operating_system") + or summary.get("agent_operating_system") + or "-", + "last_seen": summary.get("last_seen") or 0, + "status": "Offline", + } + conn.close() + except Exception as e: + print(f"[WARN] Failed to load agents from DB: {e}") + + +load_agents_from_db() + @app.route("/api/agents") def get_agents(): """ @@ -481,10 +511,20 @@ def set_device_description(hostname: str): @app.route("/api/agent/", methods=["DELETE"]) def delete_agent(agent_id: str): - """Remove an agent from the in-memory registry.""" - if agent_id in registered_agents: - registered_agents.pop(agent_id, None) - agent_configurations.pop(agent_id, None) + """Remove an agent from the registry and database.""" + info = registered_agents.pop(agent_id, None) + agent_configurations.pop(agent_id, None) + hostname = info.get("hostname") if info else None + if hostname: + try: + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute("DELETE FROM device_details WHERE hostname = ?", (hostname,)) + conn.commit() + conn.close() + except Exception as e: + return jsonify({"error": str(e)}), 500 + if info: return jsonify({"status": "removed"}) return jsonify({"error": "agent not found"}), 404 From 76bd4fb33bb7751ce3b820441e0d42a23d7a7786 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 02:33:13 -0600 Subject: [PATCH 3/6] Refine storage metrics and highlight device links --- Data/Agent/borealis-agent.py | 17 ++++++++++------- Data/Server/WebUI/src/Device_List.jsx | 8 +++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 00df6b1..6071d38 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -465,7 +465,8 @@ def collect_storage(): "disk_type": "Removable" if "removable" in part.opts.lower() else "Fixed Disk", "usage": usage.percent, "total": usage.total, - "free": 100 - usage.percent, + "free": usage.free, + "used": usage.used, }) elif plat == "windows": try: @@ -485,13 +486,13 @@ def collect_storage(): free_bytes = float(free) used = total - free_bytes usage = (used / total * 100) if total else 0 - free_pct = 100 - usage disks.append({ "drive": drive, "disk_type": "Fixed Disk", "usage": usage, "total": total, - "free": free_pct, + "free": free_bytes, + "used": used, }) except Exception: pass @@ -512,15 +513,16 @@ def collect_storage(): for d in data: total = d.get("Capacity") or 0 used = d.get("Used") or 0 + free_bytes = d.get("Free") or max(total - used, 0) usage = (used / total * 100) if total else 0 - free = 100 - usage drive = d.get("Root") or f"{d.get('Name','')}:" disks.append({ "drive": drive, "disk_type": "Fixed Disk", "usage": usage, "total": total, - "free": free, + "free": free_bytes, + "used": used, }) else: out = subprocess.run( @@ -532,14 +534,15 @@ def collect_storage(): if len(parts) >= 6: total = int(parts[1]) * 1024 used = int(parts[2]) * 1024 + free_bytes = int(parts[3]) * 1024 usage = float(parts[4].rstrip("%")) - free = 100 - usage disks.append({ "drive": parts[5], "disk_type": "Fixed Disk", "usage": usage, "total": total, - "free": free, + "free": free_bytes, + "used": used, }) except Exception as e: print(f"[WARN] collect_storage failed: {e}") diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index 07984d8..fe28a0a 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -184,7 +184,13 @@ export default function DeviceList({ onSelectDevice }) { onSelectDevice && onSelectDevice(r)} - sx={{ cursor: onSelectDevice ? "pointer" : "default" }} + sx={{ + color: "#58a6ff", + "&:hover": { + cursor: onSelectDevice ? "pointer" : "default", + textDecoration: onSelectDevice ? "underline" : "none", + }, + }} > {r.hostname} From 00792c5b10d1619dc2dc7fb3205e3cdd679f6c67 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 02:47:00 -0600 Subject: [PATCH 4/6] fix: parse numeric storage values --- Data/Server/WebUI/src/Device_Details.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 32aa907..6d9bdc6 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -285,7 +285,10 @@ export default function DeviceDetails({ device, onBack }) { const renderStorage = () => { const toNum = (val) => { if (val === undefined || val === null) return undefined; - const n = Number(val); + if (typeof val === "number") { + return Number.isNaN(val) ? undefined : val; + } + const n = parseFloat(String(val).replace(/[^0-9.]+/g, "")); return Number.isNaN(n) ? undefined : n; }; From 396b15aa4ce00b8aed80727909c53b3674371b5a Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 03:13:32 -0600 Subject: [PATCH 5/6] Use Python disk_usage for Windows storage metrics --- Data/Agent/borealis-agent.py | 122 ++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 6071d38..6eff17a 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -17,6 +17,8 @@ import time # Heartbeat timestamps import subprocess import getpass import datetime +import shutil +import string import requests try: @@ -469,61 +471,79 @@ def collect_storage(): "used": usage.used, }) elif plat == "windows": - try: - out = subprocess.run( - ["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"], - capture_output=True, - text=True, - timeout=60, - ) - lines = [l for l in out.stdout.splitlines() if l.strip()][1:] - for line in lines: - parts = line.split() - if len(parts) >= 3: - drive, free, size = parts[0], parts[1], parts[2] - try: - total = float(size) - free_bytes = float(free) - used = total - free_bytes - usage = (used / total * 100) if total else 0 - disks.append({ - "drive": drive, - "disk_type": "Fixed Disk", - "usage": usage, - "total": total, - "free": free_bytes, - "used": used, - }) - except Exception: - pass - except FileNotFoundError: - ps_cmd = ( - "Get-PSDrive -PSProvider FileSystem | " - "Select-Object Name,Free,Used,Capacity,Root | ConvertTo-Json" - ) - out = subprocess.run( - ["powershell", "-NoProfile", "-Command", ps_cmd], - capture_output=True, - text=True, - timeout=60, - ) - data = json.loads(out.stdout or "[]") - if isinstance(data, dict): - data = [data] - for d in data: - total = d.get("Capacity") or 0 - used = d.get("Used") or 0 - free_bytes = d.get("Free") or max(total - used, 0) - usage = (used / total * 100) if total else 0 - drive = d.get("Root") or f"{d.get('Name','')}:" + found = False + for letter in string.ascii_uppercase: + drive = f"{letter}:\\" + if os.path.exists(drive): + try: + usage = shutil.disk_usage(drive) + except Exception: + continue disks.append({ "drive": drive, "disk_type": "Fixed Disk", - "usage": usage, - "total": total, - "free": free_bytes, - "used": used, + "usage": (usage.used / usage.total * 100) if usage.total else 0, + "total": usage.total, + "free": usage.free, + "used": usage.used, }) + found = True + if not found: + try: + out = subprocess.run( + ["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"], + capture_output=True, + text=True, + timeout=60, + ) + lines = [l for l in out.stdout.splitlines() if l.strip()][1:] + for line in lines: + parts = line.split() + if len(parts) >= 3: + drive, free, size = parts[0], parts[1], parts[2] + try: + total = float(size) + free_bytes = float(free) + used = total - free_bytes + usage = (used / total * 100) if total else 0 + disks.append({ + "drive": drive, + "disk_type": "Fixed Disk", + "usage": usage, + "total": total, + "free": free_bytes, + "used": used, + }) + except Exception: + pass + except FileNotFoundError: + ps_cmd = ( + "Get-PSDrive -PSProvider FileSystem | " + "Select-Object Name,Free,Used,Capacity,Root | ConvertTo-Json" + ) + out = subprocess.run( + ["powershell", "-NoProfile", "-Command", ps_cmd], + capture_output=True, + text=True, + timeout=60, + ) + data = json.loads(out.stdout or "[]") + if isinstance(data, dict): + data = [data] + for d in data: + total = d.get("Capacity") or 0 + used = d.get("Used") or 0 + free_bytes = d.get("Free") or max(total - used, 0) + usage = (used / total * 100) if total else 0 + drive = d.get("Root") or f"{d.get('Name','')}:" + disks.append({ + "drive": drive, + "disk_type": "Fixed Disk", + "usage": usage, + "total": total, + "free": free_bytes, + "used": used, + }) else: out = subprocess.run( ["df", "-kP"], capture_output=True, text=True, timeout=60 From 199f233ad67ff7a0b40a650d67334c7064ec5c2b Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 13 Aug 2025 03:23:02 -0600 Subject: [PATCH 6/6] Show used storage and green usage bars --- Data/Server/WebUI/src/Device_Details.jsx | 98 +++++++++++++++--------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 6d9bdc6..b6cfffa 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -294,43 +294,53 @@ export default function DeviceDetails({ device, onBack }) { const rows = (details.storage || []).map((d) => { const total = toNum(d.total); - let usage = toNum(d.usage); + let usagePct = toNum(d.usage); + let usedBytes = toNum(d.used); + let freeBytes = toNum(d.free); let freePct; - if (usage !== undefined) { - if (usage <= 1) usage *= 100; - freePct = 100 - usage; - } else { - const freeRaw = toNum(d.free); - if (freeRaw !== undefined) { - if (freeRaw > 1 && freeRaw > 100 && total) { - freePct = (freeRaw / total) * 100; - } else if (freeRaw <= 1) { - freePct = freeRaw * 100; - } else { - freePct = freeRaw; - } - usage = freePct !== undefined ? 100 - freePct : undefined; - } else { - const usedRaw = toNum(d.used); - if (usedRaw !== undefined && total) { - usage = (usedRaw / total) * 100; - freePct = 100 - usage; - } - } + if (usagePct !== undefined) { + if (usagePct <= 1) usagePct *= 100; + freePct = 100 - usagePct; + } + + if (usedBytes === undefined && total !== undefined && usagePct !== undefined) { + usedBytes = (usagePct / 100) * total; + } + + if (freeBytes === undefined && total !== undefined && usedBytes !== undefined) { + freeBytes = total - usedBytes; + } + + if (freePct === undefined && total !== undefined && freeBytes !== undefined) { + freePct = (freeBytes / total) * 100; + } + + if (usagePct === undefined && freePct !== undefined) { + usagePct = 100 - freePct; } return { drive: d.drive, disk_type: d.disk_type, - usage, + used: usedBytes, + freePct, + freeBytes, total, - free: freePct, + usage: usagePct, }; }); if (!rows.length) - return placeholderTable(["Drive Letter", "Disk Type", "Usage", "Total Size", "Free %"]); + return placeholderTable([ + "Drive Letter", + "Disk Type", + "Used", + "Free %", + "Free GB", + "Total Size", + "Usage", + ]); return ( @@ -339,9 +349,11 @@ export default function DeviceDetails({ device, onBack }) { Drive Letter Disk Type - Usage - Total Size + Used Free % + Free GB + Total Size + Usage @@ -349,6 +361,26 @@ export default function DeviceDetails({ device, onBack }) { {d.drive} {d.disk_type} + + {d.used !== undefined && !Number.isNaN(d.used) + ? formatBytes(d.used) + : "unknown"} + + + {d.freePct !== undefined && !Number.isNaN(d.freePct) + ? `${d.freePct.toFixed(1)}%` + : "unknown"} + + + {d.freeBytes !== undefined && !Number.isNaN(d.freeBytes) + ? formatBytes(d.freeBytes) + : "unknown"} + + + {d.total !== undefined && !Number.isNaN(d.total) + ? formatBytes(d.total) + : "unknown"} + @@ -358,7 +390,7 @@ export default function DeviceDetails({ device, onBack }) { sx={{ height: 10, bgcolor: "#333", - "& .MuiLinearProgress-bar": { bgcolor: "#58a6ff" } + "& .MuiLinearProgress-bar": { bgcolor: "#00d18c" } }} /> @@ -369,16 +401,6 @@ export default function DeviceDetails({ device, onBack }) { - - {d.total !== undefined && !Number.isNaN(d.total) - ? formatBytes(d.total) - : "unknown"} - - - {d.free !== undefined && !Number.isNaN(d.free) - ? `${d.free.toFixed(1)}%` - : "unknown"} - ))}