Compare commits
2 commits
6f4958ba89
...
1aa01ed4e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa01ed4e8 | ||
|
|
5ef2502d4e |
3 changed files with 25 additions and 32 deletions
35
app.py
35
app.py
|
|
@ -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.1")
|
||||
APP_VERSION = os.environ.get("APP_VERSION", "1.6.2")
|
||||
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,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"),
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@
|
|||
<div class="card-body">
|
||||
{% 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 %}
|
||||
<div class="meter array-meter">
|
||||
|
|
@ -230,18 +230,6 @@
|
|||
<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">—</span></td>
|
||||
<td class="num"><span class="disk-null">—</span></td>
|
||||
<td class="num"><span class="disk-null">—</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue