mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 02:48:43 -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 subprocess
|
||||||
import getpass
|
import getpass
|
||||||
import datetime
|
import datetime
|
||||||
|
import shutil
|
||||||
|
import string
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
try:
|
try:
|
||||||
@@ -465,63 +467,83 @@ def collect_storage():
|
|||||||
"disk_type": "Removable" if "removable" in part.opts.lower() else "Fixed Disk",
|
"disk_type": "Removable" if "removable" in part.opts.lower() else "Fixed Disk",
|
||||||
"usage": usage.percent,
|
"usage": usage.percent,
|
||||||
"total": usage.total,
|
"total": usage.total,
|
||||||
"free": 100 - usage.percent,
|
"free": usage.free,
|
||||||
|
"used": usage.used,
|
||||||
})
|
})
|
||||||
elif plat == "windows":
|
elif plat == "windows":
|
||||||
try:
|
found = False
|
||||||
out = subprocess.run(
|
for letter in string.ascii_uppercase:
|
||||||
["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"],
|
drive = f"{letter}:\\"
|
||||||
capture_output=True,
|
if os.path.exists(drive):
|
||||||
text=True,
|
try:
|
||||||
timeout=60,
|
usage = shutil.disk_usage(drive)
|
||||||
)
|
except Exception:
|
||||||
lines = [l for l in out.stdout.splitlines() if l.strip()][1:]
|
continue
|
||||||
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','')}:"
|
|
||||||
disks.append({
|
disks.append({
|
||||||
"drive": drive,
|
"drive": drive,
|
||||||
"disk_type": "Fixed Disk",
|
"disk_type": "Fixed Disk",
|
||||||
"usage": usage,
|
"usage": (usage.used / usage.total * 100) if usage.total else 0,
|
||||||
"total": total,
|
"total": usage.total,
|
||||||
"free": free,
|
"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:
|
else:
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
["df", "-kP"], capture_output=True, text=True, timeout=60
|
["df", "-kP"], capture_output=True, text=True, timeout=60
|
||||||
@@ -532,14 +554,15 @@ def collect_storage():
|
|||||||
if len(parts) >= 6:
|
if len(parts) >= 6:
|
||||||
total = int(parts[1]) * 1024
|
total = int(parts[1]) * 1024
|
||||||
used = int(parts[2]) * 1024
|
used = int(parts[2]) * 1024
|
||||||
|
free_bytes = int(parts[3]) * 1024
|
||||||
usage = float(parts[4].rstrip("%"))
|
usage = float(parts[4].rstrip("%"))
|
||||||
free = 100 - usage
|
|
||||||
disks.append({
|
disks.append({
|
||||||
"drive": parts[5],
|
"drive": parts[5],
|
||||||
"disk_type": "Fixed Disk",
|
"disk_type": "Fixed Disk",
|
||||||
"usage": usage,
|
"usage": usage,
|
||||||
"total": total,
|
"total": total,
|
||||||
"free": free,
|
"free": free_bytes,
|
||||||
|
"used": used,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WARN] collect_storage failed: {e}")
|
print(f"[WARN] collect_storage failed: {e}")
|
||||||
|
@@ -283,15 +283,65 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStorage = () => {
|
const renderStorage = () => {
|
||||||
const rows = (details.storage || []).map((d) => ({
|
const toNum = (val) => {
|
||||||
drive: d.drive,
|
if (val === undefined || val === null) return undefined;
|
||||||
disk_type: d.disk_type,
|
if (typeof val === "number") {
|
||||||
usage: d.usage !== undefined ? Number(d.usage) : undefined,
|
return Number.isNaN(val) ? undefined : val;
|
||||||
total: d.total !== undefined ? Number(d.total) : undefined,
|
}
|
||||||
free: d.free !== undefined ? Number(d.free) : undefined,
|
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)
|
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 (
|
return (
|
||||||
<Box sx={{ maxHeight: 400, overflowY: "auto" }}>
|
<Box sx={{ maxHeight: 400, overflowY: "auto" }}>
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
@@ -299,9 +349,11 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Drive Letter</TableCell>
|
<TableCell>Drive Letter</TableCell>
|
||||||
<TableCell>Disk Type</TableCell>
|
<TableCell>Disk Type</TableCell>
|
||||||
<TableCell>Usage</TableCell>
|
<TableCell>Used</TableCell>
|
||||||
<TableCell>Total Size</TableCell>
|
|
||||||
<TableCell>Free %</TableCell>
|
<TableCell>Free %</TableCell>
|
||||||
|
<TableCell>Free GB</TableCell>
|
||||||
|
<TableCell>Total Size</TableCell>
|
||||||
|
<TableCell>Usage</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -309,6 +361,26 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
<TableRow key={`${d.drive}-${i}`}>
|
<TableRow key={`${d.drive}-${i}`}>
|
||||||
<TableCell>{d.drive}</TableCell>
|
<TableCell>{d.drive}</TableCell>
|
||||||
<TableCell>{d.disk_type}</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>
|
<TableCell>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Box sx={{ flexGrow: 1, mr: 1 }}>
|
<Box sx={{ flexGrow: 1, mr: 1 }}>
|
||||||
@@ -318,7 +390,7 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
sx={{
|
sx={{
|
||||||
height: 10,
|
height: 10,
|
||||||
bgcolor: "#333",
|
bgcolor: "#333",
|
||||||
"& .MuiLinearProgress-bar": { bgcolor: "#58a6ff" }
|
"& .MuiLinearProgress-bar": { bgcolor: "#00d18c" }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -329,16 +401,6 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
@@ -165,33 +165,44 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{sorted.map((r, i) => (
|
{sorted.map((r, i) => (
|
||||||
<TableRow
|
<TableRow key={r.id || i} hover>
|
||||||
key={r.id || i}
|
|
||||||
hover
|
|
||||||
onClick={() => onSelectDevice && onSelectDevice(r)}
|
|
||||||
sx={{ cursor: onSelectDevice ? "pointer" : "default" }}
|
|
||||||
>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
style={{
|
<Box
|
||||||
display: "inline-block",
|
component="span"
|
||||||
width: 10,
|
sx={{
|
||||||
height: 10,
|
display: "inline-block",
|
||||||
borderRadius: 10,
|
width: 10,
|
||||||
background: statusColor(r.status),
|
height: 10,
|
||||||
marginRight: 8,
|
borderRadius: 10,
|
||||||
verticalAlign: "middle"
|
bgcolor: statusColor(r.status),
|
||||||
}}
|
mr: 1,
|
||||||
/>
|
}}
|
||||||
{r.status}
|
/>
|
||||||
|
{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>
|
||||||
<TableCell>{r.hostname}</TableCell>
|
|
||||||
<TableCell>{timeSince(r.lastSeen)}</TableCell>
|
<TableCell>{timeSince(r.lastSeen)}</TableCell>
|
||||||
<TableCell>{r.os}</TableCell>
|
<TableCell>{r.os}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => openMenu(e, r)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openMenu(e, r);
|
||||||
|
}}
|
||||||
sx={{ color: "#ccc" }}
|
sx={{ color: "#ccc" }}
|
||||||
>
|
>
|
||||||
<MoreVertIcon fontSize="small" />
|
<MoreVertIcon fontSize="small" />
|
||||||
|
@@ -398,6 +398,36 @@ def init_db():
|
|||||||
|
|
||||||
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")
|
@app.route("/api/agents")
|
||||||
def get_agents():
|
def get_agents():
|
||||||
"""
|
"""
|
||||||
@@ -481,10 +511,20 @@ def set_device_description(hostname: str):
|
|||||||
|
|
||||||
@app.route("/api/agent/<agent_id>", methods=["DELETE"])
|
@app.route("/api/agent/<agent_id>", methods=["DELETE"])
|
||||||
def delete_agent(agent_id: str):
|
def delete_agent(agent_id: str):
|
||||||
"""Remove an agent from the in-memory registry."""
|
"""Remove an agent from the registry and database."""
|
||||||
if agent_id in registered_agents:
|
info = registered_agents.pop(agent_id, None)
|
||||||
registered_agents.pop(agent_id, None)
|
agent_configurations.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({"status": "removed"})
|
||||||
return jsonify({"error": "agent not found"}), 404
|
return jsonify({"error": "agent not found"}), 404
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user