Compare commits
2 commits
6f4958ba89
...
1aa01ed4e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa01ed4e8 | ||
|
|
5ef2502d4e |
3 changed files with 25 additions and 32 deletions
41
app.py
41
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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
|
|
@ -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">—</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue