diff --git a/Plugin/unraid/Tools/api_cache_writer.php b/Plugin/unraid/Tools/api_cache_writer.php index 69f35ee..08ec8cd 100644 --- a/Plugin/unraid/Tools/api_cache_writer.php +++ b/Plugin/unraid/Tools/api_cache_writer.php @@ -112,6 +112,7 @@ if (vv_ai_ui_on()) { $monitor = [ 'system' => vv_system_info(), + 'varaverk' => vv_varaverk_state(), 'fallback' => vv_fallback_state(), 'fallback_active' => vv_fallback_active(), 'partner' => vv_partner_state(), diff --git a/Plugin/unraid/api/monitor.php b/Plugin/unraid/api/monitor.php index 09b9460..76fdb6c 100644 --- a/Plugin/unraid/api/monitor.php +++ b/Plugin/unraid/api/monitor.php @@ -112,6 +112,7 @@ if (vv_ai_ui_on()) { echo json_encode([ 'system' => vv_system_info(), + 'varaverk' => vv_varaverk_state(), 'fallback' => vv_fallback_state(), 'fallback_active' => vv_fallback_active(), 'partner' => vv_partner_state(), diff --git a/Plugin/unraid/include/monitor.php b/Plugin/unraid/include/monitor.php index 5577dcb..a907431 100644 --- a/Plugin/unraid/include/monitor.php +++ b/Plugin/unraid/include/monitor.php @@ -329,6 +329,76 @@ function vv_watchdog_summary(): array { ]; } +// What Varaverk itself is occupying and doing, as opposed to what the machine is. +// +// The System card described the host — model, uptime, load — and said nothing about the thing +// whose dashboard it is. These are the figures that are Varaverk's own and that nothing else on +// the page reports. +// +// The cache size is the one worth watching. VV_CACHE_ROOT is /tmp/varaverk, and on Unraid /tmp is +// on rootfs, which is RAM — so this directory is memory, not disk, and the arr payload cache is +// most of it. master.conf says never to move these onto flash, which makes the size the thing to +// keep an eye on instead. Reported next to the rootfs percentage it consumes, because 187 MB +// means nothing without the ceiling it counts against. +// +// du rather than a recursive PHP walk: both roots are small and page-cached, measured at 3ms +// each, and this is assembled once a minute by the cache writer rather than per page load. +function vv_varaverk_state(): array { + $du = function (string $path): ?int { + if (!is_dir($path)) return null; + $out = shell_exec('du -sb ' . escapeshellarg($path) . ' 2>/dev/null'); + return preg_match('/^(\d+)/', (string)$out, $m) ? (int)$m[1] : null; + }; + + // Locks whose process is still alive. A lock file alone does not mean a job is running — + // acquire_lock() clears one whose pid is gone, and three were sitting in LOCK_DIR from jobs + // that finished days ago. Counting files would have reported four jobs running and one + // actually was. + $running = []; + $stale = 0; + foreach ((array)@glob('/tmp/unraid_locks/*.lock') as $lock) { + $content = trim((string)@file_get_contents($lock)); + if ($content === '') { $stale++; continue; } + [$pid, $name] = array_pad(explode(':', $content, 2), 2, ''); + $pid = (int)$pid; + if ($pid > 1 && @posix_kill($pid, 0)) { + $running[] = $name !== '' ? $name : basename($lock, '.lock'); + } else { + $stale++; + } + } + sort($running); + + // Newest partner conf in the RAM cache. conf_sync fills it; an age climbing past its schedule + // means the mesh has stopped talking, which nothing else on this page would show. + $confAge = null; + foreach ((array)@glob(VV_CONF_RAM_CACHE_DIR . '/host*.conf') as $c) { + $m = @filemtime($c); + if ($m && ($confAge === null || (time() - $m) < $confAge)) $confAge = time() - $m; + } + + // Storage mode as a fact about where this install actually is, not as the conf toggle's word + // for it — the toggle is what someone intended and the path is what happened. + // + // "internal", not "flash". /boot is the internal mode in Varaverk's own vocabulary and on this + // hardware it is a mirrored NVMe pool, not a USB stick; calling it flash on the dashboard + // would invite a write-wear worry that does not apply here. + $internal = str_starts_with(SCRIPTS_DIR, '/boot'); + + return [ + 'cache_root' => VV_CACHE_ROOT, + 'cache_bytes' => $du(VV_CACHE_ROOT), + 'data_bytes' => $du(DATA_DIR), + 'scripts_dir' => SCRIPTS_DIR, + 'storage' => $internal ? 'internal' : 'appdata', + 'commit' => trim((string)shell_exec( + 'git -C ' . escapeshellarg(SCRIPTS_DIR) . ' rev-parse --short HEAD 2>/dev/null')), + 'jobs_running' => $running, + 'locks_stale' => $stale, + 'conf_age_sec' => $confAge, + ]; +} + function vv_scripts_status(): array { $logDir = LOG_DIR; $statFiles = array_merge( diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 2216d7c..7c2979d 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -949,6 +949,55 @@ function vvPollMonitor(live) { : '—'; const _coreMeta = _threadInfo ? ` (${_threadInfo})` : ''; + // ── Varaverk's own footprint ──────────────────────────────────────────── + // The rows above describe the machine. These describe the thing whose dashboard this is, + // and nothing else on the page reports them. + const vv = d.varaverk ?? {}; + const _rootPct = d.watchdog?.stability?.rootfs_pct ?? null; + + // The cache is RAM, not disk: VV_CACHE_ROOT is /tmp/varaverk and /tmp lives on rootfs, which + // on Unraid is a memory filesystem. Shown against the rootfs percentage because a size with + // no ceiling beside it is a number nobody can act on. + // Local formatters. vvFmt() is a GB formatter — feeding it megabytes prints "186 GB" — and + // vvRelTime() takes a timestamp where these carry an age in seconds. Both would have been + // silently wrong rather than broken. + const _vvSize = b => b == null ? '—' + : b >= 1073741824 ? (b / 1073741824).toFixed(1) + ' GB' : Math.round(b / 1048576) + ' MB'; + const _vvAge = s => s == null ? '—' + : s < 90 ? s + 's' : s < 5400 ? Math.round(s / 60) + 'm' + : s < 172800 ? Math.round(s / 3600) + 'h' : Math.round(s / 86400) + 'd'; + + const _cacheColor = _rootPct == null ? '#888' + : _rootPct >= 90 ? '#f44336' : _rootPct >= 75 ? '#ff9800' : '#888'; + // The suffix belongs to a real figure. "— in RAM" reads as a measurement that came back + // empty rather than one that was never taken. + const _cacheStr = vv.cache_bytes == null + ? `not measured` + : `${_vvSize(vv.cache_bytes)} in RAM` + + (_rootPct != null ? ` · rootfs ${_rootPct}%` : '') + ``; + + const _dataStr = vv.data_bytes == null + ? `not measured` + : `${_vvSize(vv.data_bytes)} on ${vvEscHtml(vv.storage || '?')}`; + + // Jobs actually running — locks whose process is alive. A stale lock is not a running job + // and acquire_lock() clears it on the next run, so it is reported as an aside rather than + // as a fault. + const _jobs = vv.jobs_running ?? []; + const _jobStr = _jobs.length === 0 + ? `idle` + : `${_jobs.length} running` + + ` · ${vvEscHtml(_jobs.slice(0, 2).join(', '))}` + + (_jobs.length > 2 ? ` +${_jobs.length - 2}` : '') + ``; + const _staleStr = (vv.locks_stale ?? 0) > 0 + ? ` · ${vv.locks_stale} stale lock${vv.locks_stale !== 1 ? 's' : ''}` : ''; + + // Conf sync freshness. An age climbing past its schedule means the mesh has stopped talking, + // which no other row on this page would show. + const _confAge = vv.conf_age_sec; + const _confStr = _confAge == null ? `none cached` + : `${_vvAge(_confAge)} ago`; + document.getElementById('vv-system-body').innerHTML = `
@@ -988,6 +1037,18 @@ function vvPollMonitor(live) { Load ${_loadStr} Running ${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''} Version ${ver} +
+ +
+
Varaverk
+
+ Cache ${_cacheStr} + Data ${_dataStr} + Jobs ${_jobStr}${_staleStr} + Conf ${_confStr} + Build ${vvEscHtml(vv.commit || '—')} +
`; // ── Partner ──────────────────────────────────────────────────────────────