Skip to content

Commit e86b450

Browse files
committed
Restructure disk charts in monitoring page
Add mountpoint info to get_all_disks Use mountpoints to find usage info for each drive
1 parent 6e5c728 commit e86b450

File tree

4 files changed

+99
-54
lines changed

4 files changed

+99
-54
lines changed

docs/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ <h1>PyNinja - Features<a class="headerlink" href="#pyninja-features" title="Perm
981981

982982
<dl class="py function">
983983
<dt class="sig sig-object py" id="pyninja.features.disks.parse_size">
984-
<span class="sig-prename descclassname"><span class="pre">pyninja.features.disks.</span></span><span class="sig-name descname"><span class="pre">parse_size</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">size_str</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">str</span></span></span><a class="headerlink" href="#pyninja.features.disks.parse_size" title="Permalink to this definition"></a></dt>
984+
<span class="sig-prename descclassname"><span class="pre">pyninja.features.disks.</span></span><span class="sig-name descname"><span class="pre">parse_size</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">size_str</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">str</span></span></span><a class="headerlink" href="#pyninja.features.disks.parse_size" title="Permalink to this definition"></a></dt>
985985
<dd><p>Convert size string with units to a standard size in bytes.</p>
986986
<dl class="field-list simple">
987987
<dt class="field-odd">Parameters<span class="colon">:</span></dt>

pyninja/features/disks.py

+20-21
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def get_partitions_for_disk(device_id: str) -> List[str]:
3232
] or [0]
3333

3434

35-
def parse_size(size_str) -> str:
35+
def parse_size(size_str: str) -> str:
3636
"""Convert size string with units to a standard size in bytes.
3737
3838
Args:
@@ -79,29 +79,28 @@ def _linux(lib_path: FilePath) -> List[Dict[str, str]]:
7979
"""
8080
# Using -d to list only physical disks, and filtering out loop devices
8181
result = subprocess.run(
82-
[lib_path, "-o", "NAME,SIZE,TYPE,MODEL", "-d"],
82+
[lib_path, "-o", "NAME,SIZE,TYPE,MODEL,MOUNTPOINT", "-J"],
8383
capture_output=True,
8484
text=True,
8585
)
86-
disks = result.stdout.strip().splitlines()
87-
filtered_disks = [disk for disk in disks if "loop" not in disk]
88-
keys = (
89-
filtered_disks[0]
90-
.lower()
91-
.replace("name", "DeviceID")
92-
.replace("model", "Name")
93-
.replace("size", "Size")
94-
.split()
95-
)
96-
disk_info = [
97-
dict(zip(keys, line.split(None, len(keys) - 1))) for line in filtered_disks[1:]
98-
]
99-
# Normalize the output, ensuring the correct field names and types
100-
for disk in disk_info:
101-
disk["Size"] = parse_size(disk["Size"])
102-
disk["Partitions"] = len(get_partitions_for_disk(disk["DeviceID"]))
103-
disk.pop("type", None)
104-
return disk_info
86+
data = json.loads(result.stdout)
87+
disks = []
88+
for device in data.get("blockdevices", []):
89+
if device["type"] == "disk":
90+
disk_info = {
91+
"DeviceID": device["name"],
92+
"Size": device["size"],
93+
"Name": device.get("model", "Unknown"),
94+
"Mountpoints": [],
95+
}
96+
# Collect mount points from partitions
97+
if "children" in device:
98+
for partition in device["children"]:
99+
if partition.get("mountpoint"):
100+
disk_info["Mountpoints"].append(partition["mountpoint"])
101+
disk_info["Mountpoints"] = ", ".join(disk_info["Mountpoints"])
102+
disks.append(disk_info)
103+
return disks
105104

106105

107106
def is_physical_disk(lib_path: FilePath, device_id: str) -> bool:

pyninja/monitor/routes.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from fastapi.websockets import WebSocket, WebSocketDisconnect
1111

1212
from pyninja import monitor, version
13+
from pyninja.features import disks
1314
from pyninja.modules import enums, exceptions, models
1415

1516
LOGGER = logging.getLogger("uvicorn.default")
@@ -199,7 +200,21 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
199200
# Base task with a placeholder asyncio sleep to start the task loop
200201
task = asyncio.create_task(asyncio.sleep(0.1))
201202
# Store disk usage information (during startup) to avoid repeated calls
202-
disk_info = shutil.disk_usage("/")._asdict()
203+
all_disks = disks.get_all_disks()
204+
disk_info = []
205+
for disk in all_disks:
206+
total, used, free = (0, 0, 0)
207+
disk_usage = {"name": disk.get("Name"), "id": disk.get("DeviceID")}
208+
mountpoints = (
209+
disk.get("Mountpoints", "").split(", ") if disk.get("Mountpoints") else []
210+
)
211+
for mountpoint in mountpoints:
212+
part_usage = shutil.disk_usage(mountpoint)
213+
total += part_usage.total
214+
used += part_usage.used
215+
free += part_usage.free
216+
disk_usage.update({"total": total, "used": used, "free": free})
217+
disk_info.append(disk_usage)
203218
while True:
204219
# Validate session asynchronously (non-blocking)
205220
# This way of handling session validation is more efficient than using a blocking call

pyninja/monitor/templates/main.html

+62-31
Original file line numberDiff line numberDiff line change
@@ -387,13 +387,7 @@ <h3>Swap Usage</h3>
387387
</div>
388388
<p id="swapUsageText">Swap: 0%</p>
389389
{% endif %}
390-
391-
<h3>Disk Usage</h3>
392-
<div class="progress">
393-
<div id="diskUsage" class="progress-bar"></div>
394-
</div>
395-
<p id="diskUsageText">Disk: 0%</p>
396-
390+
<br><br>
397391
<div class="graph">
398392
<h3>CPU Load Averages</h3>
399393
<canvas class="graph-canvas" id="loadChart" width="400" height="200"></canvas>
@@ -413,10 +407,11 @@ <h5 id="swapTotal"></h5>
413407
<canvas id="swapChart"></canvas>
414408
</div>
415409
{% endif %}
410+
</div>
411+
<div class="box">
416412
<h3>Disk Usage</h3>
417-
<h5 id="diskTotal"></h5>
418-
<div class="chart-container">
419-
<canvas id="diskChart"></canvas>
413+
<div id="diskChartsContainer">
414+
<!-- Charts will be dynamically appended here -->
420415
</div>
421416
</div>
422417
</div>
@@ -475,7 +470,6 @@ <h3>PyUdisk Stats</h3>
475470

476471
let memoryChartInstance = null;
477472
let swapChartInstance = null;
478-
let diskChartInstance = null;
479473
let loadChartInstance = null;
480474

481475
ws.onmessage = function (event) {
@@ -610,13 +604,6 @@ <h3>PyUdisk Stats</h3>
610604
updateProgressBar('swapUsage', swapUsage);
611605
}
612606

613-
// Disk Usage Progress Bar
614-
const diskInfo = data.disk_info;
615-
const diskUsage = (diskInfo.used / diskInfo.total) * 100;
616-
document.getElementById('diskUsage').style.width = diskUsage.toFixed(2) + '%';
617-
document.getElementById('diskUsageText').innerText = `Disk: ${diskUsage.toFixed(2)}%`;
618-
updateProgressBar('diskUsage', diskUsage);
619-
620607
// CPU Load Avg Graph
621608
const loadAverages = data.load_averages;
622609
if (loadChartInstance) {
@@ -734,20 +721,64 @@ <h3>PyUdisk Stats</h3>
734721
}
735722

736723
// Disk Chart
737-
document.getElementById("diskTotal").innerText = `Total: ${formatBytes(diskInfo.total)}`;
738-
if (diskChartInstance) {
739-
diskChartInstance.data.datasets[0].data = [diskInfo.used, diskInfo.total - diskInfo.used];
740-
diskChartInstance.update();
741-
} else {
742-
const diskChart = document.getElementById('diskChart').getContext('2d');
743-
diskChartInstance = createChartInstance(
744-
diskChart,
745-
'Disk Usage',
746-
['#63950d', '#ca7b00'],
747-
[diskInfo.used, diskInfo.total - diskInfo.used]
748-
);
749-
}
724+
const diskInfoList = data.disk_info;
725+
const diskChartInstances = {};
726+
const container = document.getElementById("diskChartsContainer");
727+
// Iterate over the list of disk info
728+
diskInfoList.forEach((diskInfo, index) => {
729+
// Check if the chart already exists in the DOM
730+
let chartBox = document.getElementById(`diskBox${index}`);
731+
if (!chartBox) {
732+
// Create new chart container if it doesn't exist
733+
chartBox = document.createElement('div');
734+
chartBox.id = `diskBox${index}`;
735+
736+
const diskLabel = document.createElement('h5');
737+
diskLabel.id = `diskLabel${index}`;
738+
chartBox.appendChild(diskLabel);
739+
740+
const chartContainer = document.createElement('div');
741+
chartContainer.classList.add('chart-container');
742+
chartBox.appendChild(chartContainer);
743+
744+
const canvas = document.createElement('canvas');
745+
canvas.id = `diskChart${index}`;
746+
chartContainer.appendChild(canvas);
747+
748+
container.appendChild(chartBox);
749+
}
750750

751+
// Update the label
752+
const diskLabel = document.getElementById(`diskLabel${index}`);
753+
diskLabel.innerText = `${diskInfo.name} (${diskInfo.id}) - ${formatBytes(diskInfo.total)}`;
754+
755+
// Create or update chart instance
756+
if (diskChartInstances[index]) {
757+
// Update existing chart
758+
const chart = diskChartInstances[index];
759+
chart.data.datasets[0].data = [diskInfo.used, diskInfo.total - diskInfo.used];
760+
chart.update();
761+
} else {
762+
// Create new chart instance
763+
const diskChart = document.getElementById(`diskChart${index}`).getContext('2d');
764+
diskChartInstances[index] = createChartInstance(
765+
diskChart,
766+
`Disk ${index + 1} Usage`,
767+
['#63950d', '#ca7b00'],
768+
[diskInfo.used, diskInfo.total - diskInfo.used]
769+
);
770+
}
771+
});
772+
773+
// Remove old charts that are no longer in the data
774+
Object.keys(diskChartInstances).forEach((key) => {
775+
if (!diskInfoList[key]) {
776+
diskChartInstances[key].destroy();
777+
delete diskChartInstances[key];
778+
const oldBox = document.getElementById(`diskBox${key}`);
779+
if (oldBox) oldBox.remove();
780+
}
781+
});
751782
};
752783

753784
function updateProgressBar(id, percentage) {

0 commit comments

Comments
 (0)