Stop the dashboard polling harder than its data can change
Every poll after the first asked for ?live=1, so the page paid a full collection — partner SSH timeouts included — every two seconds while the tmpfs cache it was built around went unused. Polls are now guarded against overlap and stop while the tab is hidden, repeated failures say so instead of leaving the last good reading on screen, and two functions nothing called are gone.
This commit is contained in:
@@ -5,6 +5,13 @@
|
||||
// most destructive operations the plugin can perform, deliberately isolated in one small
|
||||
// file rather than folded into a general-purpose action endpoint.
|
||||
//
|
||||
// STATUS
|
||||
// No UI caller. pages/monitor.php held the only one — a vvArrayAction() that no button on any
|
||||
// tab invoked — and it was removed 2026-08-07 rather than left as an unreachable handler for
|
||||
// these three commands. The endpoint is kept because it is complete and correct, and because
|
||||
// power control is a thing this plugin will plausibly want; wiring it up is adding buttons, not
|
||||
// writing an endpoint. Anything added here must keep the guarantees below intact.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// The command is detached and the response returns immediately. A shutdown kills the web
|
||||
// server that is serving this request, so waiting on the child would mean the browser sees
|
||||
|
||||
@@ -7,14 +7,25 @@
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Two poll rates, deliberately split.
|
||||
// api/monitor_fast.php carries the cheap, fast-moving values (1–2s); api/monitor.php
|
||||
// carries the full payload on a slower cycle. Everything refreshing at the fast rate
|
||||
// would put real load on the WebGUI this page exists to watch.
|
||||
// api/monitor_fast.php carries the cheap, fast-moving values at 1s; api/monitor.php
|
||||
// carries the full payload at 5s. Everything refreshing at the fast rate would put real
|
||||
// load on the WebGUI this page exists to watch.
|
||||
//
|
||||
// Served from the tmpfs cache, not live calls.
|
||||
// api_cache_writer.sh refreshes the payload every minute and the endpoint serves that.
|
||||
// ?live=1 bypasses it. A missing cache always falls back to a live call, so the cache
|
||||
// can never be why the dashboard fails to load.
|
||||
// ?live=1 bypasses it, and is used for exactly one thing: the poll that follows a
|
||||
// container action, where the point is to see the result. A missing cache always falls
|
||||
// back to a live call, so the cache can never be why the dashboard fails to load.
|
||||
//
|
||||
// This principle was written before it was true. The page sent ?live=1 on every poll but
|
||||
// the first, so it paid a full collection — partner SSH timeouts included, per that
|
||||
// endpoint's own warning — every two seconds, and the cache it describes was used once
|
||||
// per page load. Fixed 2026-08-07. If this page ever feels heavy, check here first.
|
||||
//
|
||||
// Polls are guarded, not merely scheduled.
|
||||
// vvPollRunner() drops a tick while the previous request is still open and stops
|
||||
// entirely while the tab is hidden. Both polls ran unconditionally before, so a slow
|
||||
// collection stacked requests behind itself and a background tab polled forever.
|
||||
//
|
||||
// Missing subsystems simply do not render.
|
||||
// No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros
|
||||
@@ -57,8 +68,7 @@
|
||||
// DEPENDS ON
|
||||
// include/monitor.php required directly for initial render
|
||||
// api/monitor.php full payload, slower cycle
|
||||
// api/monitor_fast.php fast-moving values, 1–2s
|
||||
// api/system.php system info
|
||||
// api/monitor_fast.php fast-moving values, 1s
|
||||
// api/media.php now-playing sessions
|
||||
// api/docker_action.php container actions
|
||||
// api/flag_toggle.php toggles
|
||||
@@ -412,14 +422,6 @@ function vvDrawNetChart(canvas, rxData, txData, maxBps) {
|
||||
|
||||
// ── CPU helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
function vvCoreColor(freqMhz, maxMhz, minMhz) {
|
||||
if (!freqMhz || !maxMhz || maxMhz === minMhz) return '#4caf50';
|
||||
const t = Math.max(0, Math.min(1, (freqMhz - minMhz) / (maxMhz - minMhz)));
|
||||
// blue(220°) → green(120°) → red(0°) as t goes 0→1
|
||||
const hue = Math.round((1 - t) * 220);
|
||||
return `hsl(${hue},70%,45%)`;
|
||||
}
|
||||
|
||||
function vvRenderCpu(cpu) {
|
||||
const overall = cpu.overall ?? 0;
|
||||
const cores = cpu.cores ?? [];
|
||||
@@ -697,14 +699,54 @@ function vvDiskCol(disks) {
|
||||
|
||||
// ── Poll ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
let _vvFirstPoll = true;
|
||||
// Runs fn on an interval under three rules, because a dashboard that polls harder than its data
|
||||
// changes is load on the machine it exists to watch:
|
||||
//
|
||||
// in-flight a tick arriving while the previous request is still open is dropped rather than
|
||||
// queued. A slow collection used to stack requests behind itself at 1–2s intervals,
|
||||
// and the slow case is precisely the loaded one.
|
||||
// hidden nothing polls while the tab is not visible. This page ran 1s and 2s timers
|
||||
// forever in a background tab.
|
||||
// resume one immediate tick when the tab comes back, so returning to it does not show a
|
||||
// frozen dashboard for a full interval.
|
||||
//
|
||||
// fn must return the fetch promise, or the in-flight flag can never clear.
|
||||
function vvPollRunner(fn, ms) {
|
||||
let busy = false;
|
||||
const tick = () => {
|
||||
if (busy || document.hidden) return;
|
||||
busy = true;
|
||||
Promise.resolve(fn()).catch(() => {}).finally(() => { busy = false; });
|
||||
};
|
||||
tick();
|
||||
setInterval(tick, ms);
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) tick(); });
|
||||
}
|
||||
|
||||
function vvPollMonitor() {
|
||||
const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';
|
||||
_vvFirstPoll = false;
|
||||
fetch(_url)
|
||||
// Consecutive poll failures. A dashboard whose endpoint has died looks exactly like one where
|
||||
// nothing is happening, which is the worst way for it to fail — every number on screen stays at
|
||||
// its last good value and nothing says so. Three in a row rather than one, so a single blip
|
||||
// during a restart does not throw a banner.
|
||||
let vvPollFails = 0;
|
||||
function vvPollFailed() {
|
||||
if (++vvPollFails < 3) return;
|
||||
const banner = document.getElementById('vv-api-banner');
|
||||
if (!banner) return;
|
||||
Object.assign(banner.style, {display:'', background:'#1a0d0d', border:'1px solid #4a1f1f', color:'#ef5350'});
|
||||
banner.textContent = 'Monitor data is not updating — ' + vvPollFails
|
||||
+ ' consecutive failed polls. Values below are the last good reading.';
|
||||
}
|
||||
|
||||
// live=1 bypasses the endpoint's cache and collects everything fresh. Reserved for the poll that
|
||||
// follows an action, where the cache has just been dropped and the point is to see the result.
|
||||
// Every ordinary poll reads the cache, which is what the endpoint was built for and what its own
|
||||
// design principles describe — this used to send live=1 on every poll after the first, so the
|
||||
// page paid a full collection, partner SSH timeouts included, every two seconds.
|
||||
function vvPollMonitor(live) {
|
||||
return fetch('/plugins/varaverk/api/monitor.php' + (live ? '?live=1' : ''))
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
vvPollFails = 0;
|
||||
|
||||
// ── API status banner ────────────────────────────────────────────────────
|
||||
const apiStatus = d._api_status ?? {};
|
||||
@@ -1776,15 +1818,16 @@ function vvPollMonitor() {
|
||||
vvRenderDockerFolders(dfData);
|
||||
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(vvPollFailed);
|
||||
}
|
||||
|
||||
vvPollMonitor();
|
||||
setInterval(vvPollMonitor, 2000);
|
||||
// 5s against a payload the cache writer refreshes once a minute. Polling faster cannot make the
|
||||
// data newer — it only decides how soon the page notices the writer's update.
|
||||
vvPollRunner(vvPollMonitor, 5000);
|
||||
|
||||
// ── Fast poll: CPU, memory, network — 1-second live updates ──────────────────
|
||||
function vvPollFast() {
|
||||
fetch('/plugins/varaverk/api/monitor_fast.php')
|
||||
return fetch('/plugins/varaverk/api/monitor_fast.php')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
// CPU
|
||||
@@ -1842,8 +1885,9 @@ function vvPollFast() {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
vvPollFast();
|
||||
setInterval(vvPollFast, 1000);
|
||||
// Stays at 1s — this endpoint reads /proc and borrows its slow fields from the full cache, so it
|
||||
// is cheap enough to be the one thing that genuinely updates live.
|
||||
vvPollRunner(vvPollFast, 1000);
|
||||
|
||||
// Pin pools card width to CPU card width across rows
|
||||
function vvSyncCardWidths() {
|
||||
@@ -2064,7 +2108,7 @@ function vvRenderStreams() {
|
||||
}
|
||||
|
||||
function vvPollStreams() {
|
||||
fetch('/plugins/varaverk/api/media.php')
|
||||
return fetch('/plugins/varaverk/api/media.php')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
vvLastSessions = d.sessions ?? [];
|
||||
@@ -2083,8 +2127,11 @@ function vvPollStreams() {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
vvPollStreams();
|
||||
setInterval(vvPollStreams, 12000);
|
||||
// Guarded like the others — this one reaches out to every configured media server, so a wedged
|
||||
// Emby is exactly the case where unguarded ticks would stack.
|
||||
vvPollRunner(vvPollStreams, 12000);
|
||||
// Local only: re-renders the rows already held, advancing each progress bar between polls. No
|
||||
// request, so it stays a plain interval.
|
||||
setInterval(vvRenderStreams, 1000);
|
||||
|
||||
// ── Pools card ────────────────────────────────────────────────────────────────
|
||||
@@ -2272,9 +2319,10 @@ function vvDockerAction(action, name, webui) {
|
||||
// the card it would have changed is redrawn from a payload either way.
|
||||
if (!d || !d.ok) alert('Container ' + action + ' failed: ' + ((d && (d.error || d.output)) || 'unknown error'));
|
||||
vvDfActive = null;
|
||||
// The endpoint drops the monitor cache on success, so this poll collects fresh rather than
|
||||
// re-reading the payload that was written before the action happened.
|
||||
setTimeout(vvPollMonitor, 1500);
|
||||
// The endpoint drops the monitor cache on success, and this poll asks for a live collection
|
||||
// besides — either alone is enough, but between them the card cannot redraw itself from a
|
||||
// payload assembled before the action happened.
|
||||
setTimeout(() => vvPollMonitor(true), 1500);
|
||||
})
|
||||
.catch(() => alert('Container ' + action + ' failed: request error'));
|
||||
}
|
||||
@@ -2429,16 +2477,8 @@ document.addEventListener('click', () => {
|
||||
if (vvDfActive !== null) { vvDfActive = null; vvRenderDockerFolders(vvDfData); }
|
||||
});
|
||||
|
||||
// ── Array actions ─────────────────────────────────────────────────────────────
|
||||
function vvArrayAction(action) {
|
||||
const labels = {stop: 'Stop Array', shutdown: 'Shutdown', restart: 'Restart'};
|
||||
if (!confirm(`${labels[action] ?? action} — are you sure?`)) return;
|
||||
fetch('/plugins/varaverk/api/system.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action}),
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (!d.ok) alert('Error: ' + (d.error ?? 'unknown'));
|
||||
}).catch(() => alert('Request failed'));
|
||||
}
|
||||
// Array power actions (stop / shutdown / restart) were wired here to api/system.php, but nothing
|
||||
// on this page ever called the function — there are no such buttons, on this or any other tab.
|
||||
// Removed rather than left as an unreachable handler for the platform's three most destructive
|
||||
// operations. The endpoint stays; see its header for why it is kept unwired.
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user