From 43b5443b300b0b07095f35a7839e782dc20e3c7a Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 2 Aug 2026 00:38:22 -0400 Subject: [PATCH] Add structured headers to the PHP include layer, fix monitor state paths All 16 include/ files now carry PURPOSE / DESIGN PRINCIPLES / OPERATIONAL SAFEGUARDS / EXPORTS / CONFIGURATION, keeping the first three section names identical to the bash headers so retrieval can route across both languages. monitor.php read six watchdog state files from /tmp while the watchdogs write to STATE_DIR, so every strike set came back empty and the summary reported healthy unconditionally. docs.php gained path containment before it is wired to a page. --- Plugin/unraid/include/arrs.php | 54 +++++++++++++++++- Plugin/unraid/include/auth.php | 73 ++++++++++++++++++++++++ Plugin/unraid/include/common.php | 64 ++++++++++++++++++++- Plugin/unraid/include/confform.php | 41 +++++++++++++ Plugin/unraid/include/config.php | 60 ++++++++++++++++++- Plugin/unraid/include/docker.php | 46 +++++++++++++++ Plugin/unraid/include/docker_folders.php | 25 ++++++++ Plugin/unraid/include/docs.php | 64 +++++++++++++++++++-- Plugin/unraid/include/fallback.php | 49 +++++++++++++++- Plugin/unraid/include/media.php | 49 +++++++++++++++- Plugin/unraid/include/monitor.php | 72 +++++++++++++++++++---- Plugin/unraid/include/partnership.php | 50 ++++++++++++++++ Plugin/unraid/include/scheduler.php | 60 +++++++++++++++++++ Plugin/unraid/include/unraid_api.php | 44 ++++++++++++++ Plugin/unraid/include/vms.php | 29 ++++++++++ Plugin/unraid/include/watchdog.php | 52 +++++++++++++++++ 16 files changed, 811 insertions(+), 21 deletions(-) diff --git a/Plugin/unraid/include/arrs.php b/Plugin/unraid/include/arrs.php index 52653d1..9ed8b33 100644 --- a/Plugin/unraid/include/arrs.php +++ b/Plugin/unraid/include/arrs.php @@ -1,5 +1,57 @@ true with an empty arrs list, so the +// page can say "not yet collected" rather than implying the partner has no libraries. +// +// Cached remote nodes carry their own age. +// cache_age is attached to every cached node so the UI can show staleness instead of +// presenting 2-hour-old counts as current. +// +// Read-only. Statistics are parsed from the databases the scripts write; nothing here +// triggers a scan, cleanup, or import. +// +// EXPORTS +// Discovery vv_discover_arrs(), vv_arr_known_hosts(), vv_arr_node_names() +// Fetch vv_arr_http(), vv_fetch_arr_live() +// Statistics vv_arr_cleanup_stats(), vv_arr_discovery_stats(), vv_arr_sync_stats(), +// vv_arr_recovery_stats() +// Assembly vv_arrs_local_node() ← called over SSH by remote_arr_cache_writer.sh +// vv_arrs_all() ← local live + remote cached +// +// CONFIGURATION +// HOST*_SONARR_URL / _RADARR_URL / _LIDARR_URL per-instance endpoints +// HOST*_SONARR_API_KEY / _RADARR_API_KEY / _LIDARR_API_KEY per-instance keys +// HOST*_SONARR_TV_ROOT / _RADARR_MOVIES_ROOT / _LIDARR_MUSIC_ROOT +// ARR_SYNC_SONARR_PORT / _RADARR_PORT / _LIDARR_PORT used to reach partner instances +// DATA_DIR arr_cleanup_stats.db, arr_recovery_stats.db, _discovery_history.db +// VV_CACHE_DIR arrs_remote_.json — written by remote_arr_cache_writer.sh +// ═══════════════════════════════════════════════════════════════════════════════════════════════ require_once __DIR__ . '/config.php'; diff --git a/Plugin/unraid/include/auth.php b/Plugin/unraid/include/auth.php index 8342851..e896dde 100644 --- a/Plugin/unraid/include/auth.php +++ b/Plugin/unraid/include/auth.php @@ -1,4 +1,77 @@ bool] or an _err key — so no caller has to distinguish an exception from a +// legitimately empty list. +// +// EXPORTS +// Config vv_auth_conf() +// NPM vv_npm_list_proxies(), vv_npm_list_certs(), vv_npm_create_proxy(), +// vv_npm_update_proxy(), vv_npm_delete_proxy(), vv_npm_toggle_proxy() +// LLDAP vv_lldap_list_users(), vv_lldap_list_groups(), vv_lldap_create_user(), +// vv_lldap_update_user(), vv_lldap_delete_user(), vv_lldap_set_password(), +// vv_lldap_create_group(), vv_lldap_delete_group(), +// vv_lldap_add_to_group(), vv_lldap_remove_from_group() +// Authelia vv_authelia_read_rules(), vv_authelia_write_rules() +// +// CONFIGURATION +// HOST*_NPM_URL admin API — port 7818. Port 81 is the partnership WebUI port +// (HOST*_PARTNERSHIP_AUTH_WEBUIS), not the API. Easy to confuse. +// HOST*_NPM_USER / _NPM_PASS +// HOST*_LLDAP_URL / _LLDAP_USER / _LLDAP_PASS +// HOST*_AUTHELIA_CONFIG path to configuration.yml. Lives in the Critical-Data share so +// it is covered by the 30-minute auth sync — not under +// /mnt/user/appdata, which is not synced. +// HOST*_AUTHELIA_CONTAINER restarted after a successful rules write +// ═══════════════════════════════════════════════════════════════════════════════════════════════ + require_once __DIR__ . '/config.php'; // ── Config ──────────────────────────────────────────────────────────────────── diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index deaed51..39c91f8 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -1,9 +1,69 @@ false] when transcode_state.db does +// not exist, so the caller can distinguish "not running" from "zero sessions". +// +// External IP lookups are cached and time-boxed. +// curl runs with --max-time and the result is cached 300s, so a slow or unreachable +// endpoint cannot stall a page render. +// +// Read-only throughout. Nothing here starts, stops, or reconfigures anything. +// +// EXPORTS +// System vv_system_info(), vv_system_resources(), vv_cpu_per_core(), vv_memory_breakdown() +// Storage vv_df(), vv_storage_pools(), vv_array_disks(), vv_disk_io_rates(), +// vv_disk_thresholds(), vv_disk_entry(), vv_parity_status() +// Hardware vv_gpu_stats(), vv_gpu_stats_all(), vv_gpu_processes(), vv_ups_stats() +// Containers vv_docker_containers(), vv_docker_stopped() +// Network vv_network_stats() +// Remote vv_remote_hosts_stats() +// Misc vv_transcode_sessions(), vv_log_tail(), vv_parse_bash_array() +// +// CONFIGURATION +// STATE_DIR transcode_state.db lives here +// HOST*_UNRAID_API_KEY per-host, read from every host*.conf for remote metrics +// VV_CACHE_DIR ext_ip (300s) and monitor_remote_ (written externally) +// ═══════════════════════════════════════════════════════════════════════════════════════════════ + require_once __DIR__ . '/config.php'; require_once __DIR__ . '/unraid_api.php'; -// Common helpers shared across all Varaverk pages. - function vv_system_info(): array { // ── Shared local reads (always needed regardless of API) ────────────────── $ident = @parse_ini_file('/boot/config/ident.cfg') ?: []; diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php index a5b9590..f6150cf 100644 --- a/Plugin/unraid/include/confform.php +++ b/Plugin/unraid/include/confform.php @@ -1,4 +1,45 @@ / HOST_NAME matching, same 15-char NetBIOS truncation fallback. The +// two implementations must agree — a page that disagrees with the scripts about which +// host it is on is worse than one that cannot tell. +// +// Conf parsing resolves ${VAR} in two passes. +// Bash expands at runtime; PHP reads the file literally. Pass 1 substitutes +// ${SCRIPTS_DIR} from the PHP-side constant, pass 2 resolves remaining ${VAR} against +// the already-parsed set. Without this, every derived path arrives as a literal string. +// +// Read-only with respect to behaviour. +// This file parses conf and reports; it does not decide policy. Callers own that. +// +// OPERATIONAL SAFEGUARDS +// Ambiguous truncated hostnames are refused, never guessed. +// The 15-char fallback accepts a match only when exactly one configured host qualifies. +// Two plausible candidates return 'unknown' rather than picking one — a wrong host +// identity silently routes local work to a remote node. +// +// Cache writes are atomic. +// vv_cache_write() writes .tmp then rename()s into place, so a concurrent reader sees +// either the old payload or the new one, never a half-written file. +// +// Cache reads are age-gated and fail to null. +// Past $maxAge, vv_cache_read() returns null rather than stale data. Callers treat null +// as "no cache" and fall back to a live call — a missing cache can never be the reason +// a page fails to render. +// +// Unknown host degrades instead of guessing. +// vv_detect_host() returns 'unknown' and vv_conf_vars() then loads master.conf alone. +// Shared config still resolves; host-specific values are simply absent. +// +// EXPORTS +// Identity vv_detect_host(), vv_get_hostname(), vv_is_owner(), vv_known_hosts() +// Config vv_conf_vars(), vv_read_conf_raw(), vv_write_conf_raw(), vv_get_conf_files() +// Parsing vv_parse_conf_scalar(), vv_parse_kv_db(), vv_format_uptime() +// Remote vv_resolve_tailscale_ip(), vv_remote_state_cmd(), vv_local_ip() +// Setup state vv_setup_state_read/_write(), vv_push_setup_state(), vv_push_master_conf() +// Cache vv_cache_read(), vv_cache_write() +// Unraid API vv_unraid_api_query(), vv_auto_create_api_key() +// +// CONFIGURATION +// varaverk.cfg SCRIPTS_DIR, CUSTOM_SCRIPTS_DIR +// master.conf HOST / HOST_NAME — host identity +// host*.conf HOST*_SSH_KEY — used for setup-state and conf push +// VV_CACHE_DIR /tmp/vv_cache (tmpfs — RAM speed, cleared on reboot) +// ═══════════════════════════════════════════════════════════════════════════════════════════════ define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg'); diff --git a/Plugin/unraid/include/docker.php b/Plugin/unraid/include/docker.php index dd41f1f..0e2b1de 100644 --- a/Plugin/unraid/include/docker.php +++ b/Plugin/unraid/include/docker.php @@ -1,4 +1,50 @@ if not available. +// ═══════════════════════════════════════════════════════════════════════════════════════════════ +// PURPOSE +// Renders the repo's own markdown — READMEs, Manuals, design notes — inside the WebGUI, +// substituting live conf values into `$VAR` markers so documentation shows what this host +// is actually configured to do rather than a generic example. +// +// STATUS +// Not currently wired. Nothing requires this file and there is no docs page or endpoint +// yet. It is kept because the per-folder README/Manual corpus is exactly what it exists to +// surface. Written to be safe on the day it is connected — see OPERATIONAL SAFEGUARDS. +// +// DESIGN PRINCIPLES +// Documentation is discovered, not enumerated. +// vv_docs_tree() walks SCRIPTS_DIR for *.md. A new folder README appears in the UI +// with no registration step, which is what keeps the docs from drifting out of the +// navigation. +// +// Live values, not example values. +// `$VAR_NAME` in a markdown file is replaced with that variable's current value from +// conf. Unresolved names render in a distinct class rather than being left as-is, so a +// stale variable reference in a doc is visible instead of looking like prose. +// +// Degrades to readable text without Parsedown. +// If the bundled renderer is absent the raw markdown is emitted in a
 block.
+//       Missing a formatter reduces presentation; it never hides the content.
+//
+// OPERATIONAL SAFEGUARDS
+//   Paths are contained to SCRIPTS_DIR.
+//       $rel is resolved with realpath() and required to remain under SCRIPTS_DIR, be a
+//       regular file, and carry a .md extension. This is deliberate defence for a parameter
+//       that will arrive from a request the moment this is wired up — without it, a
+//       traversal sequence reaches any file the web user can read.
+//
+//   Markdown is rendered in safe mode.
+//       Parsedown runs with setSafeMode(true), and the 
 fallback escapes everything.
+//       These files are trusted today, but they are also synced between hosts.
+//
+//   Substituted conf values are escaped.
+//       htmlspecialchars() is applied to both the value and the variable name, so a conf
+//       value containing markup cannot inject into the rendered page.
+//
+//   Read-only. Discovers and renders; never writes a doc.
+//
+// EXPORTS
+//   vv_docs_tree()     every *.md under SCRIPTS_DIR, relative paths, sorted
+//   vv_docs_render()   one file to HTML with conf substitution applied
+//
+// CONFIGURATION
+//   SCRIPTS_DIR       the containment root and the discovery root
+//   PARSEDOWN_PATH    /usr/local/emhttp/plugins/varaverk/lib/Parsedown.php — optional
+// ═══════════════════════════════════════════════════════════════════════════════════════════════
 
 require_once __DIR__ . '/config.php';
 
@@ -24,8 +73,15 @@ function vv_docs_tree(): array {
 }
 
 function vv_docs_render(string $rel, array $vars): string {
-    $path = SCRIPTS_DIR . '/' . $rel;
-    if (!file_exists($path)) return '

File not found.

'; + // Containment check — $rel is expected to come from a request parameter once this is + // wired to a page. Resolve it and require the result to stay inside SCRIPTS_DIR and to + // still be a .md file, so a traversal sequence cannot reach arbitrary files. + $base = realpath(SCRIPTS_DIR); + $path = realpath(SCRIPTS_DIR . '/' . $rel); + if ($base === false || $path === false) return '

File not found.

'; + if (!str_starts_with($path, $base . '/')) return '

File not found.

'; + if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'md') return '

File not found.

'; + if (!is_file($path)) return '

File not found.

'; $md = file_get_contents($path); diff --git a/Plugin/unraid/include/fallback.php b/Plugin/unraid/include/fallback.php index f1e99b3..c45671c 100644 --- a/Plugin/unraid/include/fallback.php +++ b/Plugin/unraid/include/fallback.php @@ -1,5 +1,52 @@ _TIER. +// Named for the host being covered, not the host doing the covering — see the fallback +// section of the top-level README for why that reads backwards at first. +// +// OPERATIONAL SAFEGUARDS +// A missing state file parses as empty, not as NORMAL. +// vv_fb_local_state() hands an empty string to the parser when the file is absent, so +// the page shows unknown rather than asserting everything is fine. Reporting a healthy +// state for a fallback process that is not running would be the worst possible lie on +// this page. +// +// An unreachable partner degrades to what is locally known. +// Remote SSH failures return empty rather than propagating an error, so one dark node +// cannot blank the whole page — which is precisely the situation this page exists for. +// +// Read-only over SSH. +// The only remote commands issued are a state-file read and `docker ps`. Nothing here +// starts or stops a container on either side. +// +// EXPORTS +// State vv_fb_local_state(), vv_fb_remote_state(), vv_fb_parse_state() +// Containers vv_fb_local_running(), vv_fb_remote_running(), vv_fb_covers() +// Assembly vv_fb_known_hosts(), vv_fb_all() +// Parsing vv_fb_bash_array(), vv_fb_scalar() +// +// CONFIGURATION +// STATE_DIR fallback_state.db — written by Fallback/fallback.sh +// FALLBACK__TIER1..4 containers covered per tier, keyed by the covered host +// HOST*_SSH_KEY used to read partner state +// ═══════════════════════════════════════════════════════════════════════════════════════════════ + require_once __DIR__ . '/config.php'; require_once __DIR__ . '/partnership.php'; // vv_pt_ssh(), vv_pt_ts_peers() diff --git a/Plugin/unraid/include/media.php b/Plugin/unraid/include/media.php index b86c289..3fa4fd4 100644 --- a/Plugin/unraid/include/media.php +++ b/Plugin/unraid/include/media.php @@ -1,5 +1,52 @@ ), both host confs are tried so the page still shows +// something useful instead of nothing. +// +// Session shape is normalised across server types. +// Emby, Jellyfin and Plex return quite different payloads; callers get one consistent +// structure and do not branch on server type. +// +// OPERATIONAL SAFEGUARDS +// Every request is time-boxed at 3 seconds. +// Session lookups run inside a page render, so a hung media server must not hold the +// request open. The stream context timeout is the only thing standing between a +// wedged Emby and a page that never returns. +// +// Any failure yields an empty list, never an exception. +// Unreachable server, non-JSON body, or an unexpected shape all return [] — the panel +// renders empty and the rest of the page is unaffected. +// +// Read-only. Sessions are observed; nothing is stopped, transcoded, or messaged. +// +// EXPORTS +// vv_discover_media_servers() configured Emby / Jellyfin / Plex instances for this host +// vv_media_sessions() normalised active sessions across all discovered servers +// vv_fetch_jf_sessions() Jellyfin-specific fetch +// vv_fetch_plex_sessions() Plex-specific fetch +// vv_media_conf_scalar() conf scalar reader used by the above +// +// CONFIGURATION +// HOST*_EMBY_URL / _EMBY_API_KEY / _EMBY_CONTAINER +// HOST*_JELLYFIN_URL / _JELLYFIN_API_KEY / _JELLYFIN_CONTAINER +// HOST*_PLEX_URL / _PLEX_TOKEN +// ═══════════════════════════════════════════════════════════════════════════════════════════════ require_once __DIR__ . '/config.php'; diff --git a/Plugin/unraid/include/monitor.php b/Plugin/unraid/include/monitor.php index 588db93..3a05bc8 100644 --- a/Plugin/unraid/include/monitor.php +++ b/Plugin/unraid/include/monitor.php @@ -1,7 +1,59 @@ $v) { @@ -169,10 +221,10 @@ function vv_watchdog_summary(): array { $rwLevel = (int)($rw['rm_action_level'] ?? 0); $daemonStrikes = (int)($dock['daemon_strikes'] ?? 0); - $oomCount = (int)trim(@file_get_contents('/tmp/system_watchdog_oom.db') ?: '0'); + $oomCount = (int)trim(@file_get_contents(STATE_DIR . '/system_watchdog_oom.db') ?: '0'); - // ── Stability watchdog strikes (/tmp/system_watchdog_state.db) ─────────── - $stabRaw = @file_get_contents('/tmp/system_watchdog_state.db') ?: ''; + // ── Stability watchdog strikes (STATE_DIR) ─────────── + $stabRaw = @file_get_contents(STATE_DIR . '/system_watchdog_state.db') ?: ''; $stabStrikes = []; foreach (explode("\n", $stabRaw) as $line) { $line = trim($line); @@ -182,8 +234,8 @@ function vv_watchdog_summary(): array { if ($count > 0) $stabStrikes[trim($k)] = $count; } - // ── Storage watchdog strikes (/tmp/storage_watchdog_state.db) ──────────── - $storRaw = @file_get_contents('/tmp/storage_watchdog_state.db') ?: ''; + // ── Storage watchdog strikes (STATE_DIR) ──────────── + $storRaw = @file_get_contents(STATE_DIR . '/storage_watchdog_state.db') ?: ''; $growthStrikes = []; $logStrikes = []; foreach (explode("\n", $storRaw) as $line) { $line = trim($line); @@ -198,8 +250,8 @@ function vv_watchdog_summary(): array { $logStrikes[substr($key, strlen('appdata_log_'))] = $count; } - // ── Network watchdog NPM strikes (/tmp/network_watchdog_state.db) ──────── - $netRaw = @file_get_contents('/tmp/network_watchdog_state.db') ?: ''; + // ── Network watchdog NPM strikes (STATE_DIR) ──────── + $netRaw = @file_get_contents(STATE_DIR . '/network_watchdog_state.db') ?: ''; $npmStrikes = 0; foreach (explode("\n", $netRaw) as $line) { $line = trim($line); diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php index edf3b76..c6776bf 100644 --- a/Plugin/unraid/include/partnership.php +++ b/Plugin/unraid/include/partnership.php @@ -1,4 +1,54 @@