From 5ef2502d4e97e2d9ef12f69a694a8aa53ae55989 Mon Sep 17 00:00:00 2001 From: Barely Removable Date: Thu, 27 Aug 2026 13:15:42 -0700 Subject: [PATCH] Report disk sizes in the right unit and drop parity from the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the Storage card. Unraid's GraphQL reports fsSize/fsFree in KB, not bytes — the schema is explicit about it ("(KB) Total Size of the FS"). We were passing those straight to fmt_bytes as if they were bytes, so every figure came out 1024x too small and picked the unit below the right one: a 13 TB disk rendered as "13.0 GB" and a 64.8 TB array as "64.8 GB". _fs_bytes() now does the conversion at the point of reading, so everything downstream still deals in bytes. The shutil fallback already returns real bytes and deliberately doesn't go through it. Parity is gone from the table and is no longer requested at all. It mirrors the array rather than holding a filesystem — the schema notes fsSize is "Not present on Parity type drive" — so it could only ever render as a name, three em-dashes and a temperature, which earned little for a whole row. Worth knowing: its temperature is no longer surfaced anywhere, so a hot parity drive won't show up here. Verified against the raw KB shape the API actually returns: an array of 3.7/11.2/13.0 TB disks now totals 27.9 TB rather than 27.9 GB, the sub-TB cache still correctly reads in GB, and the local fallback still reports this machine's real 228.3 GB rather than a 1024x-inflated figure. Co-Authored-By: Claude Opus 5 --- app.py | 39 ++++++++++++++++++++++----------------- static/css/dashboard.css | 2 +- templates/dashboard.html | 14 +------------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/app.py b/app.py index 8349d33..4991607 100644 --- a/app.py +++ b/app.py @@ -876,17 +876,27 @@ def get_unraid_stats(): if b >= 1024**2: return f"{b/1024**2:.1f} MB" return f"{b/1024:.0f} KB" + def _fs_bytes(v): + """Unraid's GraphQL reports fsSize/fsFree/fsUsed/size in KB, not bytes + — the schema says so explicitly ('(KB) Total Size of the FS'). Reading + them as bytes understated every figure by 1024x, so a 13 TB disk + rendered as '13.0 GB'. The shutil fallback below already returns real + bytes and must not go through this.""" + return int(v or 0) * 1024 + if UNRAID_HOST and UNRAID_API_KEY: # HTTPS on 443 with SSL verification disabled (Unraid uses a self-signed cert) url = f"https://{UNRAID_HOST}:443/graphql" headers = {"x-api-key": UNRAID_API_KEY, "Content-Type": "application/json"} + # Parity is deliberately not requested: it mirrors the array rather + # than holding a filesystem, so it has no size or usage to report and + # only ever rendered as a name and a temperature. query = """{ vars { version } array { state disks { name device fsSize fsFree type temp isSpinning } caches { name device fsSize fsFree type temp isSpinning } - parities { name device temp isSpinning } } }""" try: @@ -898,29 +908,25 @@ def get_unraid_stats(): arr = d.get("array", {}) disks = arr.get("disks", []) data_disks = [dk for dk in disks if dk.get("type") == "DATA"] - total_bytes = sum(int(dk.get("fsSize", 0) or 0) for dk in data_disks) - free_bytes = sum(int(dk.get("fsFree", 0) or 0) for dk in data_disks) + total_bytes = sum(_fs_bytes(dk.get("fsSize")) for dk in data_disks) + free_bytes = sum(_fs_bytes(dk.get("fsFree")) for dk in data_disks) used_bytes = total_bytes - free_bytes used_pct = round(used_bytes / total_bytes * 100) if total_bytes else 0 - def disk_row(dk, with_usage=True): - row = { + def disk_row(dk): + dtotal = _fs_bytes(dk.get("fsSize")) + dfree = _fs_bytes(dk.get("fsFree")) + dused = dtotal - dfree + return { "name": dk.get("name") or dk.get("device") or "disk", "device": dk.get("device", ""), "temp": dk.get("temp"), "spinning": dk.get("isSpinning"), + "total": fmt_bytes(dtotal), + "used": fmt_bytes(dused), + "free": fmt_bytes(dfree), + "used_pct": round(dused / dtotal * 100) if dtotal else 0, } - if with_usage: - dtotal = int(dk.get("fsSize", 0) or 0) - dfree = int(dk.get("fsFree", 0) or 0) - dused = dtotal - dfree - row.update({ - "total": fmt_bytes(dtotal), - "used": fmt_bytes(dused), - "free": fmt_bytes(dfree), - "used_pct": round(dused / dtotal * 100) if dtotal else 0, - }) - return row return { "status": "ok", @@ -934,7 +940,6 @@ def get_unraid_stats(): "array_used_pct": used_pct, "disks": [disk_row(dk) for dk in data_disks], "caches": [disk_row(dk) for dk in arr.get("caches", [])], - "parities": [disk_row(dk, with_usage=False) for dk in arr.get("parities", [])], } except Exception: pass diff --git a/static/css/dashboard.css b/static/css/dashboard.css index ed19e0d..4478a90 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -630,8 +630,8 @@ dd.is-accent { color: var(--accent); } text-align: right; flex-shrink: 0; } +/* Still used by the disk_state() macro when a disk reports no temperature. */ .disk-null { color: var(--text-3); } -.disk-table tr.is-parity th { color: var(--text-2); } @media (max-width: 720px) { /* Below this the six columns stop fitting; the table scrolls in its own container rather than squeezing the sizes into unreadable wraps. */ diff --git a/templates/dashboard.html b/templates/dashboard.html index 0fefef7..e62702c 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -187,7 +187,7 @@
{% if unraid.status == 'ok' %} - {% if unraid.disks or unraid.caches or unraid.parities %} + {% if unraid.disks or unraid.caches %} {# Array total leads, then the disks that make it up. #} {% set pct = unraid.array_used_pct %}
@@ -230,18 +230,6 @@ {{ disk_state(dk) }} {% endfor %} - {% for dk in unraid.parities %} - {# Parity mirrors the array rather than holding a filesystem, - so it has no size or usage of its own to report. #} - - {{ dk.name }} - — - — - — - Parity · no filesystem - {{ disk_state(dk) }} - - {% endfor %}