mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-10 21:18:42 -06:00
Merge pull request #30 from bunny-lab-io/codex/fix-storage-usage-display-on-device_details-page
Fix storage usage display and align status icon
This commit is contained in:
@@ -17,6 +17,8 @@ import time # Heartbeat timestamps
|
||||
import subprocess
|
||||
import getpass
|
||||
import datetime
|
||||
import shutil
|
||||
import string
|
||||
|
||||
import requests
|
||||
try:
|
||||
@@ -465,63 +467,83 @@ 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:
|
||||
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
|
||||
free_pct = 100 - usage
|
||||
disks.append({
|
||||
"drive": drive,
|
||||
"disk_type": "Fixed Disk",
|
||||
"usage": usage,
|
||||
"total": total,
|
||||
"free": free_pct,
|
||||
})
|
||||
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
|
||||
usage = (used / total * 100) if total else 0
|
||||
free = 100 - usage
|
||||
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,
|
||||
"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
|
||||
@@ -532,14 +554,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}")
|
||||
|
@@ -283,15 +283,65 @@ 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 toNum = (val) => {
|
||||
if (val === undefined || val === null) return undefined;
|
||||
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;
|
||||
};
|
||||
|
||||
const rows = (details.storage || []).map((d) => {
|
||||
const total = toNum(d.total);
|
||||
let usagePct = toNum(d.usage);
|
||||
let usedBytes = toNum(d.used);
|
||||
let freeBytes = toNum(d.free);
|
||||
let freePct;
|
||||
|
||||
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,
|
||||
used: usedBytes,
|
||||
freePct,
|
||||
freeBytes,
|
||||
total,
|
||||
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 (
|
||||
<Box sx={{ maxHeight: 400, overflowY: "auto" }}>
|
||||
<Table size="small">
|
||||
@@ -299,9 +349,11 @@ export default function DeviceDetails({ device, onBack }) {
|
||||
<TableRow>
|
||||
<TableCell>Drive Letter</TableCell>
|
||||
<TableCell>Disk Type</TableCell>
|
||||
<TableCell>Usage</TableCell>
|
||||
<TableCell>Total Size</TableCell>
|
||||
<TableCell>Used</TableCell>
|
||||
<TableCell>Free %</TableCell>
|
||||
<TableCell>Free GB</TableCell>
|
||||
<TableCell>Total Size</TableCell>
|
||||
<TableCell>Usage</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -309,6 +361,26 @@ export default function DeviceDetails({ device, onBack }) {
|
||||
<TableRow key={`${d.drive}-${i}`}>
|
||||
<TableCell>{d.drive}</TableCell>
|
||||
<TableCell>{d.disk_type}</TableCell>
|
||||
<TableCell>
|
||||
{d.used !== undefined && !Number.isNaN(d.used)
|
||||
? formatBytes(d.used)
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{d.freePct !== undefined && !Number.isNaN(d.freePct)
|
||||
? `${d.freePct.toFixed(1)}%`
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{d.freeBytes !== undefined && !Number.isNaN(d.freeBytes)
|
||||
? formatBytes(d.freeBytes)
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{d.total !== undefined && !Number.isNaN(d.total)
|
||||
? formatBytes(d.total)
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ flexGrow: 1, mr: 1 }}>
|
||||
@@ -318,7 +390,7 @@ export default function DeviceDetails({ device, onBack }) {
|
||||
sx={{
|
||||
height: 10,
|
||||
bgcolor: "#333",
|
||||
"& .MuiLinearProgress-bar": { bgcolor: "#58a6ff" }
|
||||
"& .MuiLinearProgress-bar": { bgcolor: "#00d18c" }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -329,16 +401,6 @@ export default function DeviceDetails({ device, onBack }) {
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{d.total !== undefined && !Number.isNaN(d.total)
|
||||
? formatBytes(d.total)
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{d.free !== undefined && !Number.isNaN(d.free)
|
||||
? `${d.free.toFixed(1)}%`
|
||||
: "unknown"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
@@ -165,33 +165,44 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sorted.map((r, i) => (
|
||||
<TableRow
|
||||
key={r.id || i}
|
||||
hover
|
||||
onClick={() => onSelectDevice && onSelectDevice(r)}
|
||||
sx={{ cursor: onSelectDevice ? "pointer" : "default" }}
|
||||
>
|
||||
<TableRow key={r.id || i} hover>
|
||||
<TableCell>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 10,
|
||||
background: statusColor(r.status),
|
||||
marginRight: 8,
|
||||
verticalAlign: "middle"
|
||||
}}
|
||||
/>
|
||||
{r.status}
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 10,
|
||||
bgcolor: statusColor(r.status),
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
{r.status}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={() => onSelectDevice && onSelectDevice(r)}
|
||||
sx={{
|
||||
color: "#58a6ff",
|
||||
"&:hover": {
|
||||
cursor: onSelectDevice ? "pointer" : "default",
|
||||
textDecoration: onSelectDevice ? "underline" : "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{r.hostname}
|
||||
</TableCell>
|
||||
<TableCell>{r.hostname}</TableCell>
|
||||
<TableCell>{timeSince(r.lastSeen)}</TableCell>
|
||||
<TableCell>{r.os}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => openMenu(e, r)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openMenu(e, r);
|
||||
}}
|
||||
sx={{ color: "#ccc" }}
|
||||
>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
|
@@ -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/<agent_id>", 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
|
||||
|
||||
|
Reference in New Issue
Block a user