Add a manual refresh button to the Array section on System
Some checks failed
Deploy / deploy (push) Has been cancelled

Unraid's stats were already fetched live on every page load (no
server-side cache), so this doesn't bypass anything — it just lets the
section be re-fetched without a full page reload, for confirming it's
current without waiting on a reload.

Array section moves into its own partial (_array_section.html) so a new
/api/array/refresh route can re-render just that fragment and swap it
into #array-section, rather than duplicating the table/bar/disk-row
markup in JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
This commit is contained in:
Barely Removable 2026-09-05 10:01:51 -07:00
parent 106bd16de8
commit 2dedd4e915
4 changed files with 137 additions and 87 deletions

12
app.py
View file

@ -1855,6 +1855,18 @@ def system_page():
ombi=r["ombi"], admin_unwatched=r["admin_unwatched"]) ombi=r["ombi"], admin_unwatched=r["admin_unwatched"])
@app.route("/api/array/refresh")
@login_required
def api_array_refresh():
"""Re-renders just the Array section's markup for P105's refresh
button. Unraid's stats aren't cached (get_unraid_stats hits the
GraphQL API live on every call), so this is a plain re-fetch — the
button exists for the visitor's own peace of mind, not to bypass a
cache. Returns HTML, not JSON: swapped directly into #array-section
rather than re-implementing the table/bar/disk-row markup in JS."""
return render_template("_array_section.html", unraid=get_unraid_stats())
@app.route("/activity") @app.route("/activity")
@login_required @login_required
def activity_page(): def activity_page():

View file

@ -307,6 +307,27 @@
if (armed && !armed.contains(e.target)) disarm(); if (armed && !armed.contains(e.target)) disarm();
}); });
/* ── Array refresh (P105) ─────────────────────────────
Unraid's stats are fetched live already, never cached server-side
(see get_unraid_stats), so this button doesn't bypass a cache —
it exists so the section can be re-fetched without a full page
reload, for the visitor's own peace of mind that it's current. */
window.refreshArray = function (btn) {
if (btn.disabled) return;
btn.disabled = true;
btn.textContent = 'REFRESHING';
fetch(BASE + '/api/array/refresh')
.then(function (r) { return r.text(); })
.then(function (html) {
var section = document.getElementById('array-section');
if (section) section.innerHTML = html;
})
.catch(function () {
btn.disabled = false;
btn.textContent = 'FAILED';
});
};
/* ── Live figures ───────────────────────────────────── /* ── Live figures ─────────────────────────────────────
The queue and the active streams are the only two numbers The queue and the active streams are the only two numbers
that must be accurate to the second, so they are the only that must be accurate to the second, so they are the only

View file

@ -0,0 +1,100 @@
{% from "_macros.html" import service_error, bar, disk_state, disk_row %}
{#- ── Array ─────────────────────────────────────────────────────
Its own partial (rather than inline in system.html) so the
refresh button can re-fetch and swap in just this section via
`/api/array/refresh` without a full page reload — Unraid's stats
aren't cached (see get_unraid_stats), so a stale reading here
only ever means "haven't reloaded the page since", and the button
exists purely for the peace-of-mind of confirming that in place. -#}
{% if unraid.status != 'ok' %}
<div class="tt-field crit">
<span>Array</span>
<span class="tt-field-meta">unreachable
<button class="tt-btn-quiet" type="button" id="array-refresh-btn" onclick="refreshArray(this)" aria-label="Refresh array stats">REFRESH</button>
</span>
</div>
{{ service_error('Unraid', unraid.error) }}
{% else %}
{%- set apct = unraid.array_used_pct | default(0) | float -%}
<div class="tt-field {% if apct >= 90 %}crit{% elif apct >= 75 %}warn{% endif %}">
<span>Array</span>
<span class="tt-field-meta">
{{ unraid.array_state | default('') }}
{%- if unraid.parities %} &middot; parity
{%- for p in unraid.parities %} {{ p.temp if p.temp is not none else '—' }}&deg;C{% endfor %}
{%- endif %}
<button class="tt-btn-quiet" type="button" id="array-refresh-btn" onclick="refreshArray(this)" aria-label="Refresh array stats">REFRESH</button>
</span>
</div>
<div class="tt-figs">
<div class="tt-fig">
<div class="tt-dh tt-fig-v {{ 'alert' if apct >= 90 else ('warn' if apct >= 75 else 'ok') }}">{{ apct | round | int }}%</div>
<div class="tt-fig-l">Used</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_free | default('—') }}</div>
<div class="tt-fig-l">Free</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_total | default('—') }}</div>
<div class="tt-fig-l">Total</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.disk_count | default(0) }}</div>
<div class="tt-fig-l">Disks</div>
</div>
</div>
{% if unraid.disks %}
{#- Desktop: a real table — columns line up, a new metric is a new
column. Hidden below 560px, where six columns stop fitting and
a horizontal scroll would hide the bar and the temperature,
the two readings that matter most on this page. -#}
<div class="tt-table-scroll">
<table class="tt-table">
<thead>
<tr>
<th scope="col">Disk</th>
<th scope="col">Device</th>
<th scope="col" class="num">Size</th>
<th scope="col" class="num">Free</th>
<th scope="col">Usage</th>
<th scope="col" class="num">Temp</th>
</tr>
</thead>
<tbody>
{% for dk in unraid.disks %}
<tr>
<th scope="row">{{ dk.name }}</th>
<td>{{ dk.device | default('—') }}</td>
<td class="num">{{ dk.total | default('—') }}</td>
<td class="num">{{ dk.free | default('—') }}</td>
<td>{{ bar(dk.used_pct | default(0)) }} <span class="val">{{ dk.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% endfor %}
{% for ck in unraid.caches | default([]) %}
<tr>
<th scope="row">{{ ck.name }}</th>
<td>{{ ck.device | default('—') }}</td>
<td class="num">{{ ck.total | default('—') }}</td>
<td class="num">{{ ck.free | default('—') }}</td>
<td>{{ bar(ck.used_pct | default(0)) }} <span class="val">{{ ck.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(ck) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{#- Phone (≤560px): the .tt-row-derived reflow — name, the shared
20-cell bar and the temperature on one line, device/size/free
beneath. Hidden above 560px in favour of the table. -#}
<div class="tt-disk-rows">
{% for dk in unraid.disks %}{{ disk_row(dk) }}{% endfor %}
{% for ck in unraid.caches | default([]) %}{{ disk_row(ck) }}{% endfor %}
</div>
{% endif %}
{% endif %}

View file

@ -1,5 +1,5 @@
{% extends "_shell.html" %} {% extends "_shell.html" %}
{% from "_macros.html" import service_error, bar, disk_state, disk_row, trend %} {% from "_macros.html" import service_error, bar, trend %}
{% block title %}P105 System{% endblock %} {% block title %}P105 System{% endblock %}
{% block content %} {% block content %}
@ -23,93 +23,10 @@
{% endfor %} {% endfor %}
</div> </div>
{#- ── Array ───────────────────────────────────────────────────── -#} <div id="array-section">
{% if unraid.status != 'ok' %} {% include "_array_section.html" %}
<div class="tt-field crit"><span>Array</span><span class="tt-field-meta">unreachable</span></div>
{{ service_error('Unraid', unraid.error) }}
{% else %}
{%- set apct = unraid.array_used_pct | default(0) | float -%}
<div class="tt-field {% if apct >= 90 %}crit{% elif apct >= 75 %}warn{% endif %}">
<span>Array</span>
<span class="tt-field-meta">
{{ unraid.array_state | default('') }}
{%- if unraid.parities %} &middot; parity
{%- for p in unraid.parities %} {{ p.temp if p.temp is not none else '—' }}&deg;C{% endfor %}
{%- endif %}
</span>
</div> </div>
<div class="tt-figs">
<div class="tt-fig">
<div class="tt-dh tt-fig-v {{ 'alert' if apct >= 90 else ('warn' if apct >= 75 else 'ok') }}">{{ apct | round | int }}%</div>
<div class="tt-fig-l">Used</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_free | default('—') }}</div>
<div class="tt-fig-l">Free</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_total | default('—') }}</div>
<div class="tt-fig-l">Total</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.disk_count | default(0) }}</div>
<div class="tt-fig-l">Disks</div>
</div>
</div>
{% if unraid.disks %}
{#- Desktop: a real table — columns line up, a new metric is a new
column. Hidden below 560px, where six columns stop fitting and
a horizontal scroll would hide the bar and the temperature,
the two readings that matter most on this page. -#}
<div class="tt-table-scroll">
<table class="tt-table">
<thead>
<tr>
<th scope="col">Disk</th>
<th scope="col">Device</th>
<th scope="col" class="num">Size</th>
<th scope="col" class="num">Free</th>
<th scope="col">Usage</th>
<th scope="col" class="num">Temp</th>
</tr>
</thead>
<tbody>
{% for dk in unraid.disks %}
<tr>
<th scope="row">{{ dk.name }}</th>
<td>{{ dk.device | default('—') }}</td>
<td class="num">{{ dk.total | default('—') }}</td>
<td class="num">{{ dk.free | default('—') }}</td>
<td>{{ bar(dk.used_pct | default(0)) }} <span class="val">{{ dk.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% endfor %}
{% for ck in unraid.caches | default([]) %}
<tr>
<th scope="row">{{ ck.name }}</th>
<td>{{ ck.device | default('—') }}</td>
<td class="num">{{ ck.total | default('—') }}</td>
<td class="num">{{ ck.free | default('—') }}</td>
<td>{{ bar(ck.used_pct | default(0)) }} <span class="val">{{ ck.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(ck) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{#- Phone (≤560px): the .tt-row-derived reflow — name, the shared
20-cell bar and the temperature on one line, device/size/free
beneath. Hidden above 560px in favour of the table. -#}
<div class="tt-disk-rows">
{% for dk in unraid.disks %}{{ disk_row(dk) }}{% endfor %}
{% for ck in unraid.caches | default([]) %}{{ disk_row(ck) }}{% endfor %}
</div>
{% endif %}
{% endif %}
{#- ── Downloads ───────────────────────────────────────────────── {#- ── Downloads ─────────────────────────────────────────────────
The one place the service holds real history, so the one place a The one place the service holds real history, so the one place a
trend arrow is honest: current speed against the 7-day average. -#} trend arrow is honest: current speed against the 7-day average. -#}