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 @@