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.
This commit is contained in:
Gmer4Lfe
2026-08-02 00:38:22 -04:00
parent 76c4ca5ccf
commit 43b5443b30
16 changed files with 811 additions and 21 deletions
+53 -1
View File
@@ -1,5 +1,57 @@
<?php
// Arr (Sonarr / Radarr / Lidarr) data helpers
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Arr data layer for the arrs page. Discovers every Sonarr / Radarr / Lidarr instance
// declared in the host confs, fetches live library counts and queue state from each, and
// attaches the cleanup / discovery / sync / recovery statistics the scripts have recorded.
//
// DESIGN PRINCIPLES
// Each host builds its own payload; nobody queries a partner's arrs.
// vv_arrs_local_node() is what remote_arr_cache_writer.sh invokes over SSH on the
// partner, so the partner assembles its own node using its own local URLs and keys.
// This host therefore never holds credentials for a remote's arrs, and no path mapping
// between hosts is involved.
//
// Live for local, cache for remote.
// vv_arrs_all() calls the local node live and reads remote nodes from the JSON the
// cache writer left behind. Cross-host work happens on a 2h timer, never in a page load.
//
// Instances come from conf, not from probing.
// vv_discover_arrs() enumerates what the host confs declare. An arr that exists but is
// not configured is intentionally invisible — conf is the source of truth.
//
// OPERATIONAL SAFEGUARDS
// Every arr HTTP call is time-boxed.
// vv_arr_http() defaults to a 4s timeout and returns null on any failure. One
// unreachable instance costs four seconds, not the page.
//
// A missing remote cache is reported, not faked.
// No cache file yields an explicit cache_miss => 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, <type>_discovery_history.db
// VV_CACHE_DIR arrs_remote_<host>.json — written by remote_arr_cache_writer.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/config.php';
+73
View File
@@ -1,4 +1,77 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The auth-stack control layer. Drives the three services behind every protected hostname:
// Nginx Proxy Manager (proxy hosts and certificates), LLDAP (users and groups), and
// Authelia (access-control rules). Read and write.
//
// OPERATIONAL MODEL
// The only include/ file that routinely mutates external state. Everything else here
// reports; this one creates users, rewrites proxy hosts, edits Authelia's YAML, and
// restarts the Authelia container. Treat every function below as load-bearing.
//
// DESIGN PRINCIPLES
// Credentials come from conf, never from the page.
// NPM and LLDAP credentials are read from host*.conf. The browser never sees them and
// never supplies them.
//
// Tokens are cached per session, not per request.
// NPM and LLDAP tokens are held in $_SESSION with a 23-hour expiry, so a page that
// makes twelve calls authenticates once. Expiry is checked before reuse.
//
// Authelia is edited as text, not parsed and re-emitted.
// Only the access_control block is rewritten, in place. Round-tripping the whole YAML
// through a parser would silently reformat and drop comments from a file that is
// hand-maintained and synced between hosts.
//
// The owner host is the source of truth for auth config.
// Changes are made here and reach the partner through Critical-Data sync, not by
// writing to two hosts from the browser.
//
// OPERATIONAL SAFEGUARDS
// The Authelia config write is atomic and reversible up to the last step.
// Existence check → read → regex replace → write .vv.tmp → rename() into place. A
// failure at any stage returns an error and leaves the original untouched; a failed
// rename unlinks the temp file rather than leaving it beside the real config.
//
// A missing config file is refused, never created.
// Both the read and write paths return 'Config not found' rather than writing a fresh
// file. Creating one would hand Authelia a config with no rules and a default policy —
// an accidental open door. See HOST*_AUTHELIA_CONFIG below.
//
// The container restart is shell-escaped.
// The container name comes from conf and is passed through escapeshellarg(), so a
// malformed conf value cannot become a command.
//
// Auth failure is reported, not retried into a lockout.
// A failed token fetch returns an _err string immediately. Nothing loops on bad
// credentials against a service that may rate-limit or lock the account.
//
// Every remote call has a timeout, and every function returns a structured result —
// ['ok' => 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 ────────────────────────────────────────────────────────────────────
+62 -2
View File
@@ -1,9 +1,69 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The system-metrics library. Everything the monitor page shows about this machine —
// CPU per core, memory breakdown, GPUs, disks and pools, network, UPS, parity, VMs,
// containers, transcodes — plus a roll-up of the same for remote nodes.
//
// DESIGN PRINCIPLES
// Prefer the Unraid API, fall back to reading the system directly.
// vv_api_data() is tried first; when it is unavailable each metric has a local path
// (/proc, /sys, emhttp ini files, shell tools). The API going away degrades detail,
// never the page.
//
// Remote stats read every host*.conf, not just this host's.
// A partner's API key lives in the partner's own conf. vv_remote_hosts_stats() globs
// CONF_DIR for all host*.conf and merges what it finds, because sparse checkout means
// the partner's file arrives through the conf cache rather than from git.
//
// Background cache first, live call second.
// Remote payloads written by remote_arr_cache_writer.sh (every 2h) are used when
// present; otherwise a live call runs behind a 30s inline cache. The expensive path is
// the exception, not the default.
//
// Reports raw numbers, applies no policy.
// Thresholds, alerting and remediation belong to the watchdogs. This file answers
// "what is the value" and nothing else.
//
// OPERATIONAL SAFEGUARDS
// Every read degrades to empty, never fatal.
// Filesystem reads use @ with a ?: fallback and every shell_exec redirects stderr.
// A missing GPU, absent UPS, or unreadable sysfs node yields [] and the corresponding
// card simply does not render. One missing subsystem cannot blank the whole page.
//
// Absent tooling is a normal outcome.
// No nvidia-smi means no GPU section — not an error. The page is built to be correct
// on hardware that lacks any given subsystem.
//
// Missing state files return an explicit unavailable flag.
// vv_transcode_sessions() returns ['available' => 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_<host> (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') ?: [];
+41
View File
@@ -1,4 +1,45 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Maps each script to the conf subsections that configure it, parses those fields into
// form definitions, and writes edits back to the conf file. This is what lets the
// scheduler page edit a script's settings without the user opening master.conf.
//
// DESIGN PRINCIPLES
// Section headers in the conf are the schema.
// The map keys off the literal `# ━━━ Name ━━━` and `# ── Name ──` headers already in
// the conf files. Documentation structure and form structure are the same thing, so a
// new setting placed under an existing header appears in the UI with no code change.
//
// Edits are surgical, never a rewrite.
// Only the changed lines are replaced. Comments, ordering, spacing and every unrelated
// value survive untouched — these files are hand-maintained and heavily commented, and
// a regenerating writer would destroy that.
//
// Structure only; values are not validated.
// Consistent with conf_upgrade.sh, this reconciles shape and leaves correctness to the
// consuming script.
//
// OPERATIONAL SAFEGUARDS
// An unmatched section yields no fields rather than a wrong write.
// If the named subsection is not found the field list comes back empty and nothing is
// written. Guessing at a target line in a conf file is how an unrelated setting gets
// overwritten.
//
// Writes are confined to the parsed line range.
// Each field carries the exact line it came from, so a write cannot land outside the
// subsection it was read from.
//
// EXPORTS
// vv_conf_has_sections() does this script have an editable conf section
// vv_conf_parse_subsection() fields within one named subsection
// vv_conf_all_groups() every mapped group
// vv_conf_fields_for_script() form definition for one script
// vv_conf_write_changes() apply edits back to the conf file
//
// CONFIGURATION
// CONF_DIR master.conf and host*.conf are the read and write targets
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/config.php';
// confform.php — script→conf-section mapping, field parsing, and write-back.
+58 -2
View File
@@ -1,6 +1,62 @@
<?php
// Config file parser and writer.
// Reads master.conf and the appropriate host*.conf based on running host.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Root of the PHP layer. Locates the Varaverk installation, parses master.conf plus this
// host's own host*.conf into a flat array, and provides host identity, remote resolution,
// and the tmpfs payload cache. Every other include/ file requires this one.
//
// DESIGN PRINCIPLES
// varaverk.cfg is the single source of truth for location.
// SCRIPTS_DIR is read from it; CONF_DIR, DATA_DIR, STATE_DIR and DEPLOY_DIR are all
// derived. Storage-mode migration rewrites that one value and every path follows.
//
// Host identity mirrors bash detect_hosts() exactly.
// Same HOST<n> / HOST<n>_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<n> / HOST<n>_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');
+46
View File
@@ -1,4 +1,50 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Docker page backend. Owns Varaverk's container-folder grouping, builds the container
// inventory the page renders, and keeps the folder map in sync with host*.conf so the
// onboarding scripts see the same grouping.
//
// DESIGN PRINCIPLES
// Varaverk owns its own folder store.
// docker_folders.json is the primary record and has no external dependency. The
// folder.view3 plugin is synced to only when it is actually installed, so Varaverk's
// grouping survives that plugin being absent, removed, or reset.
//
// The conf map is the interface to the scripts.
// HOST*_DOCKER_FOLDER_MAP is written so onboarding and partnership scripts can act on
// the same grouping the UI shows, without parsing a UI-owned JSON file.
//
// Inventory is built from docker inspect in one pass.
// vv_dk_inspect_all() collects everything once rather than per-container, because this
// runs on every page load.
//
// OPERATIONAL SAFEGUARDS
// JSON writes are atomic.
// Written to .vv.tmp then rename()d into place, so a reader or a concurrent write never
// observes a truncated folder store — losing it would scatter every container back to
// ungrouped.
//
// The optional folder.view3 sync is best-effort and never fatal.
// Its write is suppressed and its failure ignored. A second plugin's file must not be
// able to fail a Varaverk operation.
//
// A missing store reads as empty, not as an error.
// First run and a deleted file behave identically — no folders yet, page renders.
//
// EXPORTS
// Store vv_dk_read_json(), vv_dk_write_json(), vv_dk_gen_id()
// Conf map vv_dk_read_conf_map(), vv_dk_write_conf_map(),
// vv_dk_sync_conf_to_json(), vv_dk_sync_json_to_conf()
// Folders vv_dk_create_folder(), vv_dk_rename_folder(), vv_dk_delete_folder(),
// vv_dk_move_container()
// Inventory vv_dk_all(), vv_dk_inspect_all(), vv_dk_webui(), vv_dk_icon()
//
// CONFIGURATION
// VV_DOCKER_JSON /boot/config/plugins/varaverk/docker_folders.json — primary
// VV_FV3_JSON folder.view3's docker.json — synced only if present
// HOST*_DOCKER_FOLDER_MAP conf mirror consumed by the onboarding scripts
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Docker tab — folder management and container inventory
// Primary store: /boot/config/plugins/varaverk/docker_folders.json (Varaverk-owned, no external deps)
// Optional sync: /boot/config/plugins/folder.view3/docker.json (only if folder.view3 is installed)
+25
View File
@@ -1,4 +1,29 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Small compatibility layer over docker.php — resolves a container's WebUI URL and returns
// the folder grouping in the shape older callers expect.
//
// DESIGN PRINCIPLES
// Thin by intent. The folder store and inventory live in docker.php; this file only adapts
// their output. New work belongs there, not here.
//
// WebUI resolution reads the dockerMan template, which is where Unraid records the port and
// path a container's UI actually lives on — not guessed from the published ports.
//
// OPERATIONAL SAFEGUARDS
// A container with no template, or no WebUI declared, returns empty and simply renders
// without a link. Absence is normal, not an error.
//
// Read-only. Resolves and reshapes; the store is written only through docker.php.
//
// EXPORTS
// vv_container_webui() WebUI URL for one container, or empty
// vv_get_docker_folders() folder grouping in the legacy shape
//
// CONFIGURATION
// Inherits everything from docker.php — see that file's CONFIGURATION block.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/docker.php';
+60 -4
View File
@@ -1,6 +1,55 @@
<?php
// Docs — markdown file discovery, $VAR substitution, and rendering.
// Requires parsedown or similar. Falls back to <pre> 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 <pre> 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 <pre> 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 '<p>File not found.</p>';
// 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 '<p>File not found.</p>';
if (!str_starts_with($path, $base . '/')) return '<p>File not found.</p>';
if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'md') return '<p>File not found.</p>';
if (!is_file($path)) return '<p>File not found.</p>';
$md = file_get_contents($path);
+48 -1
View File
@@ -1,5 +1,52 @@
<?php
// Fallback tab data helpers
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Fallback page data layer. Reports what state each node is in (NORMAL / FALLBACK /
// NO_INTERNET / DARK), which tiers have activated, and which covered containers are
// actually running — for this host and for every partner.
//
// DESIGN PRINCIPLES
// Observes fallback.sh; never participates in it.
// State is read from the state file the running fallback process maintains. This file
// has no opinion about whether a failover should happen and cannot trigger, advance,
// or hand back one. The page is a window, not a lever.
//
// Remote state is read the same way local state is.
// vv_fb_remote_state() SSHes over and reads the partner's own fallback_state.db rather
// than inferring the partner's state from what this host can see. A node's state is
// whatever that node believes, not what its neighbour guesses.
//
// Coverage lists come from conf, tier membership from FALLBACK_<HOST>_TIER<n>.
// 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_<HOST>_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()
+48 -1
View File
@@ -1,5 +1,52 @@
<?php
// Media server session helpers — reads HOST*_EMBY_* / JELLYFIN_* / PLEX_* from host conf.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Media server session helpers. Discovers the Emby / Jellyfin / Plex instances declared in
// the host confs and returns who is currently watching what, for the monitor page's
// now-playing panel.
//
// DESIGN PRINCIPLES
// Conf declares the servers; nothing is probed or auto-detected.
// A server appears only because a URL and key were configured for it. Three server
// types are supported side by side — this is not an either/or.
//
// Placeholder credentials count as absent.
// A key still containing "your-" is a template default that was never filled in, so the
// server is skipped rather than queried. Half-configured is treated as unconfigured.
//
// Unknown host reads both host slots.
// When vv_detect_host() cannot identify the machine (development, or a hostname that
// does not match any HOST<n>), 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';
+62 -10
View File
@@ -1,7 +1,59 @@
<?php
require_once __DIR__ . '/common.php';
// Monitor-page-specific helpers — partner state, fallback state, watchdog summary, scripts status.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Monitor-page roll-ups that are not raw system metrics: partner reachability, fallback
// state, the single-glance watchdog health summary, script run status, and rsync progress.
// Raw hardware numbers come from common.php; this file answers "is anything wrong".
//
// DESIGN PRINCIPLES
// One boolean has to be trustworthy.
// vv_watchdog_summary() reduces every watchdog's state to 'healthy'. It is the only
// thing most people look at, so it is conjunctive — healthy requires every strike set
// empty, every level zero, the NIC up and sshd alive. Any doubt resolves to not-healthy.
//
// State paths derive from STATE_DIR, never hardcoded.
// The watchdogs write under STATE_DIR, which follows SCRIPTS_DIR through a storage-mode
// migration. Hardcoding an absolute path here silently decouples the page from the
// scripts — see OPERATIONAL SAFEGUARDS.
//
// Summarises; does not re-derive.
// Strike counts come from the files the watchdogs wrote. This file never recomputes
// whether a container is unhealthy — that decision belongs to the watchdog that owns it.
//
// OPERATIONAL SAFEGUARDS
// Missing state must not read as healthy — and once did.
// Six state files were read from /tmp while the watchdogs write to STATE_DIR. Every
// read returned empty, every strike set came back clear, and 'healthy' was therefore
// always true: a permanent false all-clear on the page whose whole job is raising the
// alarm. Fixed 2026-08-02. If a strike set ever looks suspiciously empty, verify the
// path against where the watchdog actually writes before trusting it.
//
// Live stability probes are cheap and time-boxed.
// df, sensors, pgrep and ps run per render, so each is a single command with stderr
// discarded and a scalar result. Nothing here iterates over containers or disks.
//
// Shell arguments are escaped.
// Paths passed to df go through escapeshellarg(); the NIC name is read from sysfs
// rather than interpolated from user input.
//
// Read-only. Reports on watchdogs, fallback and scripts; never starts, stops, or clears any
// of them.
//
// EXPORTS
// vv_partner_state() partner reachability and identity
// vv_fallback_state() current fallback state for this host
// vv_fallback_active() whether this host is currently covering, and what
// vv_watchdog_summary() the health roll-up described above
// vv_scripts_status() last-run status per scheduled script
// vv_rsync_status() current/last rsync progress
//
// CONFIGURATION
// STATE_DIR fallback_state.db, container/resource/system/storage/network watchdog state,
// system_watchdog_oom.db, system_watchdog_reboots.db
// DATA_DIR container_restart_history.db
// ═══════════════════════════════════════════════════════════════════════════════════════════════
function vv_partner_state(): array {
$vars = vv_conf_vars();
@@ -138,8 +190,8 @@ function vv_watchdog_summary(): array {
return $out;
};
$dock = $parseKv(@file_get_contents('/tmp/container_watchdog_state.db') ?: '');
$rw = $parseKv(@file_get_contents('/tmp/resource_watchdog_state.db') ?: '');
$dock = $parseKv(@file_get_contents(STATE_DIR . '/container_watchdog_state.db') ?: '');
$rw = $parseKv(@file_get_contents(STATE_DIR . '/resource_watchdog_state.db') ?: '');
$ctrStrikes = [];
foreach ($dock as $k => $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);
+50
View File
@@ -1,4 +1,54 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Partnership page data layer. Enumerates the nodes in the mesh, reaches each over
// Tailscale, and reports identity, system summary, sync history and reachability — the
// view of "who is in this partnership and are they alive".
//
// DESIGN PRINCIPLES
// Tailscale is the transport; hostnames resolve through it.
// Peers come from `tailscale status`, never from hardcoded IPs. A node that moves
// networks stays reachable because nothing here records where it used to be.
//
// The API is tried first, SSH is the fallback.
// vv_pt_remote_system() prefers the partner's unraid-api and drops to a single
// combined SSH call for version/uptime/load/containers when the API is unavailable.
// One round trip either way.
//
// Every remote read is one command.
// Partner data is gathered in a single SSH invocation rather than several, because
// each one pays full connection setup over a WAN link.
//
// OPERATIONAL SAFEGUARDS
// SSH is time-boxed, non-interactive, and escaped.
// ConnectTimeout, BatchMode=yes so it can never sit waiting for a password, and every
// interpolated value passed through escapeshellarg(). A partner that is powered off
// costs the configured timeout, not a hung page.
//
// A missing or unreadable key is treated as unreachable.
// vv_pt_ssh() returns empty immediately when the key path does not exist, rather than
// invoking ssh and letting it fail slowly.
//
// Unreachable partners degrade per node.
// Each node is collected independently; one dark host leaves its own card empty and
// affects nothing else.
//
// Read-only over SSH. Commands issued are state reads and inventory — this file never
// deploys, starts, or stops anything on a partner.
//
// EXPORTS
// Config vv_pt_config(), vv_pt_nodes(), vv_pt_ts_peers()
// Transport vv_pt_ssh(), vv_pt_ping()
// System vv_pt_local_system(), vv_pt_remote_system()
// Sync vv_pt_sync(), vv_pt_sync_summary(), vv_pt_read_db()
// Assembly vv_partnership_all()
//
// CONFIGURATION
// HOST*_SSH_KEY per-host key used for every partner call
// HOST*_UNRAID_API_KEY preferred path before SSH fallback
// PARTNERSHIP_ENABLED whether the partnership layer is active
// STATE_DIR / DATA_DIR sync history and offline counters
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Partnership page data helpers
require_once __DIR__ . '/config.php';
+60
View File
@@ -1,4 +1,64 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The scheduler's engine. Owns schedule.json, regenerates the Varaverk cron file, builds
// the script library shown on the scheduler page, and reads each script's own header to
// describe it in the UI.
//
// OPERATIONAL MODEL
// Writes state that changes what the machine does on a timer. A bad write here does not
// break a page — it changes which jobs run, or stops them running at all.
//
// DESIGN PRINCIPLES
// The bash header is the description; the UI does not keep its own copy.
// vv_script_description() parses the PURPOSE (or DESCRIPTION) block out of the script
// itself. A script's documentation and its listing cannot drift apart, because they are
// the same text. This is the live consumer of the repo-wide header convention.
//
// schedule.json is per-host and never synced.
// What a node runs is a property of that node. Syncing it would hand a partner this
// host's job list, which is exactly wrong under a mutual-redundancy model.
//
// The cron file is regenerated, never edited in place.
// vv_cron_rebuild() emits the whole file from schedule.json. Incremental edits are how
// a cron file accumulates entries nobody can account for.
//
// Scripts are discovered from conf arrays and the filesystem.
// Orchestrator job lists come from master.conf; user scripts are any *.sh dropped in
// CUSTOM_SCRIPTS_DIR. Neither requires registration in a second place.
//
// OPERATIONAL SAFEGUARDS
// The cron file must stay in /boot.
// update_cron merges plugin *.cron files from there into /etc/cron.d/root. Relocating
// it silently stops every scheduled job — nothing errors, the jobs simply never fire.
//
// The legacy direct cron file is removed on rebuild.
// A file left over from before the update_cron migration would fire every job a second
// time. Rebuild deletes it rather than assuming it is gone.
//
// Custom scripts live outside the git repo.
// CUSTOM_SCRIPTS_DIR points at the User Scripts plugin's own storage, so a user's
// scripts are never touched by a pull and never committed by accident.
//
// Shell arguments are escaped where the schedule feeds a command line.
//
// EXPORTS
// Schedule vv_schedule_load(), vv_schedule_save(), vv_schedule_update(),
// vv_schedule_update_batch(), vv_cron_rebuild(), vv_script_suggested_cron()
// Library vv_script_library(), vv_tools_scripts(), vv_custom_scripts(),
// vv_rsync_standalone(), vv_orch_conf_arrays(), vv_job_tree(), vv_script_children()
// Headers vv_script_header(), vv_script_header_clean(), vv_script_description(),
// vv_readme_section(), vv_parse_user_script_template()
// Conf vv_conf_script_map(), vv_conf_flag_value(), vv_conf_flag_set(),
// vv_conf_toggle_script(), vv_parse_conf_array(), vv_parse_conf_array_full()
// Paths vv_job_flags(), vv_job_log_path(), vv_job_stat_path(), vv_folders_load()
//
// CONFIGURATION
// SCHEDULE_FILE SCRIPTS_DIR/schedule.json — per-host, never synced
// CRON_FILE /boot/config/plugins/varaverk/varaverk.cron — must stay in /boot
// CUSTOM_SCRIPTS_DIR user-authored scripts, outside the repo
// master.conf orchestrator job arrays drive the script tree
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Scheduler — manages schedule.json and the Unraid plugin cron file.
// schedule.json is per-host, never synced.
+44
View File
@@ -1,4 +1,48 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Unraid GraphQL API client. Issues one combined query per request for OS, CPU, memory,
// disks and array state, and records which callers had to fall back when the API is
// unavailable.
//
// DESIGN PRINCIPLES
// One round trip per request, not one per metric.
// vv_api_data() runs a single combined query and caches it for the lifetime of the
// request. A page reading eight metrics still makes one API call.
//
// Fallback is expected, and it is tracked.
// The unraid-api registry is ephemeral — a key that worked yesterday can be gone.
// Every function that had to use a local path records itself via
// vv_api_record_fallback(), so vv_api_get_status() can report precisely which data is
// degraded rather than a single unhelpful "API down".
//
// The API is an optimisation, never a dependency.
// Every value it provides has a local path in common.php. Losing the API costs detail
// and precision, not availability.
//
// OPERATIONAL SAFEGUARDS
// Absent or invalid key degrades silently to local reads.
// No exception, no error banner — the caller gets its value from /proc or sysfs and the
// fallback is recorded for the status endpoint.
//
// Request-lifetime cache only.
// Nothing is persisted to disk here, so a stale API response cannot outlive the page
// that fetched it.
//
// Read-only. Queries state; issues no mutations against unraid-api.
//
// EXPORTS
// vv_api_data() the combined query result, cached per request
// vv_api_get_status() availability plus the list of functions that fell back
// vv_api_record_fallback() called by consumers when they use a local path instead
// vv_api_node_metrics() per-node metric summary
// vv_api_disk_entry() normalised disk record
// vv_local_host_stats() local summary in the same shape as a remote node
//
// CONFIGURATION
// HOST*_UNRAID_API_KEY written every array start and every 15 min by
// Plugin/unraid/System_Essentials/unraid_api_key_renew.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Unraid GraphQL API — single-request master fetch + per-function fallback tracking.
// All API-first functions call vv_api_data() then fall back to local reads on null.
//
+29
View File
@@ -1,4 +1,33 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// VM inventory for the monitor page — name, state, and assigned resources for each libvirt
// domain on this host.
//
// DESIGN PRINCIPLES
// Prefer the Unraid API, fall back to virsh.
// The API path returns the full set in one call; virsh is queried per domain only when
// the API is unavailable.
//
// Reports VM state; never changes it. Nothing here starts, stops, or reconfigures a domain.
//
// OPERATIONAL SAFEGUARDS
// Domain names are shell-escaped.
// Every virsh invocation passes the name through escapeshellarg(), so a domain named
// with shell metacharacters cannot become a command.
//
// A host with no VMs, or no libvirt at all, returns an empty list.
// The VM card simply does not render. This is the expected state on a host that does
// not run VMs, not a failure.
//
// Unknown state is reported as 'unknown' rather than assumed stopped.
//
// EXPORTS
// vv_get_vms() every libvirt domain with state and assigned resources
//
// CONFIGURATION
// None. Reads libvirt through the API or virsh; no Varaverk conf variables involved.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/unraid_api.php';
function vv_get_vms(): array {
+52
View File
@@ -1,4 +1,56 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Watchdog page data layer. Collects what every watchdog has recorded — resource strikes,
// container restart history and the failed-container skip list, system strikes and OOM
// count, storage growth and log strikes, network/NPM strikes, and the reboot log — for
// this host and each partner.
//
// DESIGN PRINCIPLES
// Reads the strike counters; never resets them.
// Strike state belongs to the watchdog that owns it. A page that cleared strikes would
// silently undo an escalation the watchdog was deliberately building toward.
//
// One state file per watchdog, parsed independently.
// resource / container / system / storage / network each keep their own db. A watchdog
// that has never run leaves its file absent, which is a distinct and meaningful state.
//
// The skip list is surfaced, not hidden.
// docker_watchdog_failed.db records containers the docker watchdog has given up on.
// Those are exactly the ones an operator needs to see, so they are shown rather than
// filtered out of the healthy-looking list.
//
// OPERATIONAL SAFEGUARDS
// Every state read is suppressed and defaulted.
// @file_get_contents with a ?: fallback throughout — an absent or unreadable db yields
// empty/zero and that watchdog's card renders as "no data", never as a fatal.
//
// Absent counters read as zero, not as unknown-therefore-alarming.
// A watchdog that has not yet written state is reported quiet rather than as a
// problem, so a fresh boot does not light up the page with false strikes.
//
// Remote collection failures are per-node.
// One unreachable partner drops that node's card; the local host and every other
// partner still render.
//
// Read-only. Nothing here restarts a container, clears a strike, or triggers a reboot.
//
// EXPORTS
// Local vv_wd_local_system(), vv_wd_local_states()
// Remote vv_wd_remote_data(), vv_wd_node_config()
// Assembly vv_wd_all()
// Parsing vv_wd_bash_array(), vv_wd_bash_assoc(), vv_wd_scalar(), vv_wd_parse_kv(),
// vv_wd_parse_restart_log(), vv_wd_parse_skiplist(), vv_wd_parse_reboot_log(),
// vv_wd_parse_storage_state(), vv_wd_parse_network_state()
//
// CONFIGURATION
// STATE_DIR resource_watchdog_state.db, container_watchdog_state.db,
// docker_watchdog_failed.db, system_watchdog_state.db,
// system_watchdog_oom.db, storage/network watchdog state
// RW_CRITICAL_CONTAINERS containers the resource watchdog treats as critical
// HOST*_SSH_KEY used to collect partner watchdog state
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/common.php';
require_once __DIR__ . '/partnership.php';