Compare commits

...

2 commits

Author SHA1 Message Date
Barely Removable
1aa01ed4e8 Bump to v1.6.2 for deploy
All checks were successful
Deploy / deploy (push) Successful in 26s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:15:42 -07:00
Barely Removable
5ef2502d4e Report disk sizes in the right unit and drop parity from the card
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 <noreply@anthropic.com>
2026-08-27 13:15:42 -07:00
3 changed files with 25 additions and 32 deletions

41
app.py
View file

@ -22,7 +22,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Docker tab caches stale container IDs, so the footer is the reliable way # Docker tab caches stale container IDs, so the footer is the reliable way
# to confirm a `docker compose up -d --build` actually took effect. # to confirm a `docker compose up -d --build` actually took effect.
# Both are env-overridable if the deploy pipeline ever injects them. # Both are env-overridable if the deploy pipeline ever injects them.
APP_VERSION = os.environ.get("APP_VERSION", "1.6.1") APP_VERSION = os.environ.get("APP_VERSION", "1.6.2")
APP_VERSION_DATE = os.environ.get("APP_VERSION_DATE", "2026-08-27") APP_VERSION_DATE = os.environ.get("APP_VERSION_DATE", "2026-08-27")
# Must point at a path that survives `docker compose up --build` — the # Must point at a path that survives `docker compose up --build` — the
@ -876,17 +876,27 @@ def get_unraid_stats():
if b >= 1024**2: return f"{b/1024**2:.1f} MB" if b >= 1024**2: return f"{b/1024**2:.1f} MB"
return f"{b/1024:.0f} KB" 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: if UNRAID_HOST and UNRAID_API_KEY:
# HTTPS on 443 with SSL verification disabled (Unraid uses a self-signed cert) # HTTPS on 443 with SSL verification disabled (Unraid uses a self-signed cert)
url = f"https://{UNRAID_HOST}:443/graphql" url = f"https://{UNRAID_HOST}:443/graphql"
headers = {"x-api-key": UNRAID_API_KEY, "Content-Type": "application/json"} 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 = """{ query = """{
vars { version } vars { version }
array { array {
state state
disks { name device fsSize fsFree type temp isSpinning } disks { name device fsSize fsFree type temp isSpinning }
caches { name device fsSize fsFree type temp isSpinning } caches { name device fsSize fsFree type temp isSpinning }
parities { name device temp isSpinning }
} }
}""" }"""
try: try:
@ -898,29 +908,25 @@ def get_unraid_stats():
arr = d.get("array", {}) arr = d.get("array", {})
disks = arr.get("disks", []) disks = arr.get("disks", [])
data_disks = [dk for dk in disks if dk.get("type") == "DATA"] 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) total_bytes = sum(_fs_bytes(dk.get("fsSize")) for dk in data_disks)
free_bytes = sum(int(dk.get("fsFree", 0) or 0) 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_bytes = total_bytes - free_bytes
used_pct = round(used_bytes / total_bytes * 100) if total_bytes else 0 used_pct = round(used_bytes / total_bytes * 100) if total_bytes else 0
def disk_row(dk, with_usage=True): def disk_row(dk):
row = { 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", "name": dk.get("name") or dk.get("device") or "disk",
"device": dk.get("device", ""), "device": dk.get("device", ""),
"temp": dk.get("temp"), "temp": dk.get("temp"),
"spinning": dk.get("isSpinning"), "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 { return {
"status": "ok", "status": "ok",
@ -934,7 +940,6 @@ def get_unraid_stats():
"array_used_pct": used_pct, "array_used_pct": used_pct,
"disks": [disk_row(dk) for dk in data_disks], "disks": [disk_row(dk) for dk in data_disks],
"caches": [disk_row(dk) for dk in arr.get("caches", [])], "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: except Exception:
pass pass

View file

@ -630,8 +630,8 @@ dd.is-accent { color: var(--accent); }
text-align: right; text-align: right;
flex-shrink: 0; flex-shrink: 0;
} }
/* Still used by the disk_state() macro when a disk reports no temperature. */
.disk-null { color: var(--text-3); } .disk-null { color: var(--text-3); }
.disk-table tr.is-parity th { color: var(--text-2); }
@media (max-width: 720px) { @media (max-width: 720px) {
/* Below this the six columns stop fitting; the table scrolls in its own /* Below this the six columns stop fitting; the table scrolls in its own
container rather than squeezing the sizes into unreadable wraps. */ container rather than squeezing the sizes into unreadable wraps. */

View file

@ -187,7 +187,7 @@
<div class="card-body"> <div class="card-body">
{% if unraid.status == 'ok' %} {% 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. #} {# Array total leads, then the disks that make it up. #}
{% set pct = unraid.array_used_pct %} {% set pct = unraid.array_used_pct %}
<div class="meter array-meter"> <div class="meter array-meter">
@ -230,18 +230,6 @@
<td class="num">{{ disk_state(dk) }}</td> <td class="num">{{ disk_state(dk) }}</td>
</tr> </tr>
{% endfor %} {% 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. #}
<tr class="is-parity">
<th scope="row" title="{{ dk.device }}">{{ dk.name }}</th>
<td class="num"><span class="disk-null">&mdash;</span></td>
<td class="num"><span class="disk-null">&mdash;</span></td>
<td class="num"><span class="disk-null">&mdash;</span></td>
<td class="usage"><span class="disk-null">Parity · no filesystem</span></td>
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>