Compare commits

..

No commits in common. "1aa01ed4e8167f3d1e825a75c126846a3b7690a3" and "6f4958ba89b9d7b3160272a6c037aa6d4ee89352" have entirely different histories.

3 changed files with 32 additions and 25 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
# to confirm a `docker compose up -d --build` actually took effect.
# Both are env-overridable if the deploy pipeline ever injects them.
APP_VERSION = os.environ.get("APP_VERSION", "1.6.2")
APP_VERSION = os.environ.get("APP_VERSION", "1.6.1")
APP_VERSION_DATE = os.environ.get("APP_VERSION_DATE", "2026-08-27")
# Must point at a path that survives `docker compose up --build` — the
@ -876,27 +876,17 @@ 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:
@ -908,25 +898,29 @@ 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(_fs_bytes(dk.get("fsSize")) for dk in data_disks)
free_bytes = sum(_fs_bytes(dk.get("fsFree")) for dk in data_disks)
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)
used_bytes = total_bytes - free_bytes
used_pct = round(used_bytes / total_bytes * 100) if total_bytes else 0
def disk_row(dk):
dtotal = _fs_bytes(dk.get("fsSize"))
dfree = _fs_bytes(dk.get("fsFree"))
dused = dtotal - dfree
return {
def disk_row(dk, with_usage=True):
row = {
"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",
@ -940,6 +934,7 @@ 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

View file

@ -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. */

View file

@ -187,7 +187,7 @@
<div class="card-body">
{% if unraid.status == 'ok' %}
{% if unraid.disks or unraid.caches %}
{% if unraid.disks or unraid.caches or unraid.parities %}
{# Array total leads, then the disks that make it up. #}
{% set pct = unraid.array_used_pct %}
<div class="meter array-meter">
@ -230,6 +230,18 @@
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% 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>
</table>
</div>