The readiness probe wrapped the remote command in raw double quotes with
manually backslash-escaped inner quotes. shell_exec() runs its command
through an extra local `sh -c` layer beyond the ssh invocation itself, and
because the remote command was double-quoted (not single-quoted/opaque),
that extra local layer expanded the $(...)/${...} substitutions using
HOST1's own environment before ssh ever sent anything to the remote host.
Confirmed live: the exact same command run directly (one shell layer)
returned the correct remote SCRIPTS_DIR; run through an extra sh -c layer
(matching shell_exec's real behavior) it silently evaluated everything
against HOST1's local varaverk.cfg instead, producing an empty probe result
every time — so every push silently reported "plugin not installed" even
though HOST2 was fully installed and reachable.
Fix: build the remote command as a plain string and escapeshellarg() it as
a whole, same pattern vv_pt_ssh() already used safely elsewhere. Verified
live — probe now returns HOST2's real SCRIPTS_DIR and the master.conf push
lands with a matching checksum on both hosts.
- unraid_api.php comment still told you to run Deployment/deploy.sh,
deleted a while back
- VaraverkSettings.page linked to a CHANGELOG.md that doesn't exist
anywhere in the repo
- Manual-Plugin.md / README-Plugin.md described manually heredoc-ing the
.plg locally and a .txz-based install that isn't wired up — rewritten
to describe the actual install flow (raw .plg URL via Plugins ->
Install Plugin), with plugin_setup.sh's dev-symlink role called out
separately from that install path
- WEBGUI_PHP_WAIT was referenced by webgui_watchdog.sh but never defined
in master.conf, always silently falling back to a hardcoded default
- arrs.php/confform.php still pointed at Media/ for arr cleanup/discovery
scripts moved to Arrs_Stack/ in b4bc926 — broke the Arrs page's stats
and the per-script settings editor for those scripts
- docker_folders.php read directly from the optional folder.view3 plugin's
file instead of Varaverk's own docker_folders.json (the primary store
since the Docker tab got its own config) — left the Monitor page's
Docker Folders widget empty on any host without folder.view3 installed
- vv_wd_remote_data() read remote watchdog state files from hardcoded
/tmp or /boot/config paths instead of the remote's actual STATE_DIR
(which resolves dynamically and can differ under flash mode) — remote
node's Watchdog panel was always empty; same wrong path also used for
two local reads (system_watchdog_oom.db, watchdog_appdata_growth.db)
- rsync.php referenced a {HOST}_MONTHLY_SYNC_SHARES conf var that never
existed (monthly_maintenance.sh has no rsync section) — nulled out to
match the existing pattern used for the fallback window
- vv_arr_node_names() did a pointless identity array_map
- vv_dk_webui() had its own duplicate local-IP resolution instead of
using vv_local_ip(), despite config.php's comment claiming that exact
duplication was already consolidated
Custom Scripts (the Scheduler page's inline editor) used to save into the
git-tracked Custom/ folder, so anything saved there would end up on GitHub.
They now live in /boot/config/plugins/user.scripts/Varaverk/Scripts, same
folder family as Unraid's own User Scripts plugin. Import Script lets you
browse the whole server and move an existing script in instead of only
creating new ones inline — always a move, never a copy, so no stray
duplicate is left where it came from.
Unraid API returns fsUsed=0 when a disk's filesystem is unmounted (spun
down). disks.ini keeps the last-known value in KB even after spindown —
use it as fallback when isSpinning=false and fsUsed=0. Also remove the
mounted-only guard in the ini fallback path (vv_disk_entry) for the same
reason. Stale comments in user_script_plug-in.sh and partnership_manager.sh
also cleaned up.
All state/data file paths in scripts and PHP now resolve via STATE_DIR / DATA_DIR /
PERSISTENT_CONF_CACHE instead of hardcoded /boot/config/ or /tmp/ paths, so the
ecosystem works in both internal and appdata storage modes.
PHP layer (watchdog.php, partnership.php, fallback.php, monitor.php, snapshot.php,
config.php): all state reads switched to STATE_DIR constant; remote state reads use
the new vv_remote_state_cmd() helper which resolves the remote's SCRIPTS_DIR via
their varaverk.cfg before building the path.
conf_sync.sh: fixed SCRIPTS_ROOT → SCRIPTS_DIR bug on MY_CONF path; added
_remote_scripts_dir() to resolve partner's SCRIPTS_DIR before SCP pull.
fallback.php page: added controls card (PARTNERSHIP_ENABLED, FALLBACK_ENABLED,
FALLBACK_RSYNC_ENABLED toggles), status grid, and settings card.
README and Manual updated for System_Essentials, Watchdogs, Fallback, Rsync,
Media, Monitors, Orchestrators, Partnership: added new scripts (conf_sync,
conf_cache_save/restore, conf_cache_watchdog, play_state_sync, start_webhook_listener,
upgrade_webhook_handler), corrected all stale /boot/config/ state file paths to
$STATE_DIR/$DATA_DIR, noted webgui/php_fpm/mover/user_scripts scripts moved to
Plugin/unraid/System_Essentials, fixed start_webhook_listener.sh header (Node.js,
not PHP -S).
Adds require_partnership() to common.sh — exits cleanly when PARTNERSHIP_ENABLED=false.
Removes FALLBACK_PARTNERSHIP_REQUIRED toggle — partnership is now always required,
not optional. Cross-server scripts (rsync, conf sync, fallback, arr sync, play state,
backup verify) all call require_partnership after detect_hosts.
All FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER* references updated to
FALLBACK_${REMOTE_ID}_TIER* across fallback_test.sh, partnership_manager.sh,
docker_update.sh, mesh_monitor.sh, and monitor.php. mesh_monitor.sh drops
the inner covering-host loop — tier data now lives in the covered host's own
conf so no cross-host scan is needed. monitor.php reads from the covered
host's conf file rather than the local host's.
Previously HOST1 defined what it would run for HOST2 (FALLBACK_HOST1_COVERS_HOST2_TIER*).
Now each host defines what it wants run when IT goes down (FALLBACK_HOST1_TIER*), and the
covering server reads the down host's conf via the RAM/persistent cache.
get_tier_containers() reads FALLBACK_${REMOTE_ID}_TIER* instead of
FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER*. Tier data migrated to the correct host confs.
Writeback paths and delays were already REMOTE_ID-based — no change needed there.
Web files now served via symlink to the git repo so git pull changes survive
reboots without rebuilding the txz. Also includes: docker pull/rebuild/restart
with live log streaming, arr_profile_enforcer for Sonarr/Radarr quality
profiles, monitor page cache fix (background writer now in cron), and
ARR_KIDS/SONARR/RADARR profile name vars in master.conf.
Live TV and Direct Stream sessions never appear in the transcoding
session list but do write segments to the ramdisk. Card now shows
the active segment count so the ramdisk usage is explained.
PHP reads master.conf values like STATE_DIR="${SCRIPTS_DIR}/State_Files"
literally — the ${SCRIPTS_DIR} token is never expanded, so any PHP code
using the returned STATE_DIR value got an invalid path. Two-pass resolution:
SCRIPTS_DIR first (from PHP constant), then remaining ${VAR} tokens using
the now-resolved var set (covers DATA_DIR-based paths like ARR_SYNC_BLOCKLIST).
All bash scripts are now platform-neutral. Unraid-specific paths, commands,
and service checks moved to Plugin/unraid/adapter.sh. Core scripts call
platform_*() functions exclusively — no direct OS paths in runtime logic.
New adapter functions: platform_storage_path, platform_webui_install_path,
platform_scripts_dir_probe_cmd, platform_setup_db_path, platform_storage_healthy,
platform_is_service_enabled, platform_get_temp_thresholds, platform_disk_states_path,
platform_rebuild_container, platform_push_conf, platform_push_setup_state,
platform_get_templates_dir, platform_send_os_notification.
Partnership services stack (Emby/Jellyfin/Seerr/SeerrFin) added as third
onboarding stack alongside auth and arr stacks.
- Replace python3/PyYAML Authelia ACL parser with pure PHP (no deps available on Unraid)
- Cert tab now pulls live from NPM API instead of cert_monitor.sh — auto-discovers all managed certs sorted by urgency
- Watchdog page: add missing GB constant and _fmtBytes/_relTime functions that were causing silent render failure
- Rsync settings card: pin to far-right 3 columns (grid-column:6/-1), toggle grid narrowed to 2 columns
- Add CLAUDE.md project context file on /boot for session persistence across reboots
- claude_startup.sh: symlink CLAUDE.md into /root on array start
- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs);
remove standalone cert page and top-level tab
- cert_monitor.sh: write JSON status cache to State_Files/cert_status.json
after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals
- api/cert.php: new — serves cached cert status; falls back to configured
domains as UNKN when no cache exists; POST action=run triggers live check
- arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now
read from data/*.db files when log JSON files don't yet exist
- config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar
signs read correctly from conf files
- host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS
- Partnership adapter pattern: Unraid-specific container logic extracted to
Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/
- First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved
to checklist; auto SSH keygen and API key creation on save
- api/checklist.php: live setup checklist with pull_master action
- Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
vv_conf_vars() reads conf as text; TRANSCODE_STATE_FILE=$STATE_DIR/... is
returned as a literal string, not expanded. Build path from STATE_DIR directly.
vv_transcode_sessions() was hardcoded to /tmp/transcode_state.db but master.conf
sets TRANSCODE_STATE_FILE=$STATE_DIR/transcode_state.db which resolves to
/boot/config/plugins/varaverk/State_Files/transcode_state.db. Page always showed
"ramdisk_setup.sh not yet run" despite state file being present and populated.
- ARRAY_START_SCRIPTS: add api_cache_writer.sh so monitor/arrs cache is populated
immediately on array start (/tmp is tmpfs — cleared on reboot, so first-boot
load was hitting live API for up to 60 seconds until the cron fired)
- api/monitor.php + arrs.php: raise cache TTL from 90s to 300s — stale-but-instant
beats a 6-second live API wait if the writer is momentarily behind
- include/unraid_api.php: replace last inline uptime formatter with vv_format_uptime()
Consolidations (config.php gains 5 shared utilities):
- vv_format_uptime() replaces 4 inline uptime-formatting blocks
- vv_parse_conf_scalar() replaces vv_arr_scalar/vv_wd_scalar/vv_fb_scalar/vv_media_conf_scalar
- vv_known_hosts() replaces vv_arr_known_hosts/vv_fb_known_hosts + inline parser in watchdog
- vv_parse_kv_db() replaces inline key=value parsing in snapshot and monitor
- vv_local_ip() replaces duplicate in docker_folders.php and inline in docker.php
All module-level function names kept as thin aliases so call sites unchanged.
Critical bug fixes:
- api/system.php: added require_once config.php and POST-only guard (no auth on shutdown)
- api/movescript.php + reorderarray.php: use vv_write_conf_raw (atomic) + vv_push_master_conf
- api/snapshot.php: share /tmp/vv_cpu_stat.json with vv_cpu_per_core() instead of own state file
Correctness:
- vv_cpu_per_core() and vv_network_stats(): atomic tmp+rename for state files (concurrent poll safety)
- ext_ip curl cache moved from /tmp/vv_ext_ip.cache to vv_cache_read/write (canonical cache dir)
- monitor_remote.php + board.php + snapshot.php: all use vv_cache_read/write instead of ad-hoc /tmp files
HTTP method guards added to write-only APIs that were missing them:
- api/scheduler.php, conf_toggle.php, flag_toggle.php
New span-1 card shows global RSYNC_ENABLED gate, per-window badges (C/D/I/W),
active profile names + elapsed time from lock files, and last-sync timestamp
per orchestrator window from script log files.
GPU shrunk from span 2 to span 1 to make room. Row 3 is now:
Rsync(1) | GPU(1) | Transcode(2) | Streams(4)
disk_io now returns {tr, tw} cumulative GB alongside {r, w} rates.
A centered span in each card h3 shows ↓total ↑total (since last boot),
updating each poll. Helpers updated for the new object format.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds vv_disk_io_rates() — snapshots /proc/diskstats each poll and
computes per-device MB/s. Exposed as disk_io in the monitor API.
JS:
- Per-disk rows: ↓read ↑write inline next to disk name (green/amber, hidden when idle)
- Pool rows: aggregate pool I/O next to pool name
- Array header: total array I/O in the subtitle line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses the last 'Ramdisk freed' and 'SSD freed' values from the transcode
management log and displays them as small ↓XMB annotations inside each tag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds height/codec fields to both Emby/Jellyfin and Plex session data,
then surfaces them as chip groups (· 2 4K 3 1080p · 4 H.264 2 HEVC)
in the streams card header alongside the existing device type bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move SCRIPTS_DIR/DATA_DIR/STATE_DIR from appdata to /boot/config/plugins/varaverk
- All state files now in STATE_DIR (no more /tmp or /boot/config root writes)
- Bootstrap: Gitea-first clone with GitHub fallback, no array dependency
- varaverk.cfg seeded with Gitea connection settings
- .gitignore: add State_Files/, varaverk.cfg, varaverk-*.txz
- Partnership/transcode/fallback scripts use STATE_DIR variables
- PHP config.php: DATA_DIR/STATE_DIR constants, VV_SETUP_STATE_FILE dynamic
- deploy.sh PROD_ROOT updated to plugin flash dir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
partnership_onboard.sh (--phase1-only):
- Test if SSH already works first (BatchMode) — skip ssh-copy-id if yes
- If ssh-copy-id fails (no TTY in background): soft-fail, generate key locally,
print pubkey for manual install, write HOST2_KEY_READY=true, run local setup
- Only skip conf push if SSH truly unavailable; otherwise proceeds normally
- Exit 0 with clear manual-step instructions instead of hard failure
include/partnership.php:
- Read HOST2_KEY_READY from setup.db; expose as key_ready on remote nodes
pages/partnership.php:
- Node card: amber "Key generated · install on HOST2 then Push Conf" badge
when key_ready=true and phase=0
- "Push Conf (key installed)" button appears when key_ready=true — runs
--phase1-only --skip-ssh (assumes user manually did ssh-copy-id)
- vvPtPushConf() top-level function
vv_partner_state() now reads setup.db and adds onboard_phase (0/1/2/null)
to each host entry.
Partner card JS renders a badge under the hostname for remote hosts that
haven't completed Phase 2: amber "Awaiting onboard" (phase 1) or grey
"Not provisioned" (phase 0). Phase 2 complete shows nothing extra.
partnership_manager.sh:
- --local-only flag for --onboard: skips remote pre-flight, WebUI reconfig,
state push; runs FolderView3 folder creation + writes HOST1_LOCAL_DONE=true
to setup.db + pushes it; exits without writing ACTIVE (partnership not yet
established until Phase 2)
partnership_onboard.sh:
- Phase 1 exit now runs partnership_manager.sh --onboard --local-only after
SSH + conf push, so HOST1 is fully configured on its own side before Phase 2
include/partnership.php:
- vv_pt_nodes() reads HOST1_LOCAL_DONE from setup.db; exposes as local_done
on the self node (null for remote nodes)
pages/partnership.php:
- Self node card shows green "Local setup complete · waiting for partner"
badge when local_done=true
- Actions: "Complete HOST1 Setup" button appears when HOST1 hasn't run local
setup yet but HOST2 is already at phase 1 — lets user trigger it manually
- vvPtLocalSetup() top-level function calls partnership_manager.sh via run.php
include/monitor.php: vv_fallback_state() now returns:
enabled, check_interval, handback_strikes, handback_strikes_required,
partnership_suspended, partner_lost_at, partnership_required,
partnership_suspend_after — all read from state file + conf vars.
pages/monitor.php: fallback card render rebuilt:
Disabled: clear message with how to enable
Suspended: partnership inactive with elapsed time
FAILOVER: outage duration + T1/T2/T3/T4 tier badges (active=green,
pending=dark) + handback strike progress dots (●●○) when remote
comes back and strikes are counting
NO_INTERNET/DARK: short contextual line
NORMAL: quiet "monitoring · 30s · partnership gated" meta line
Accent border: disabled/suspended → warn, NORMAL → ok, FAILOVER → err
unraid_api.php: track key_missing separately from general fallbacks.
vv_api_data() sets key_missing=true when HOST*_UNRAID_API_KEY is empty.
vv_api_get_status() includes key_missing in the status payload.
monitor.php: two distinct banner states instead of one orange warning.
key_missing → subtle dark/grey note: "API key not configured — add
HOST1_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring"
key present but unreachable → existing orange ⚠ with fallback list.
No banner at all when API is working correctly.
Fix 1 — Monitor partner card (common.php + monitor.php):
vv_remote_hosts_stats() no longer skips hosts with no API key.
Returns no_api_key:true entry instead. Monitor JS renders
"API key not configured — complete Onboard to enable" instead
of a blank space.
Fix 2 — rawconf push not alarming pre-onboard (config.php + scheduler.php):
Probe failures (Tailscale not found, plugin not installed) now carry
ready:false. JS treats ready:false results silently — button shows
"✓ Saved" not "push failed: HOST2" before SSH is set up.
Only genuine post-onboard failures (ready not false) show as errors.
Fix 3 — Partnership Onboard button (partnership.php):
Disabled with tooltip when no partner hostname is in master.conf.
When partner configured but not onboarded, adds explanation note:
"Onboard will generate your SSH key, exchange it with the partner..."
No partner configured: "Edit master.conf and set HOST2."
Varaverk.page: also shows wizard when local host.conf is missing
(handles master.conf pushed by HOST1 before HOST2 installs plugin).
pages/setup.php: three wizard flows
- Standard: blank master.conf, fill hostnames, redirect to scheduler
- Host2/state file: state file detected, pull master.conf from HOST1 via SSH
- Conf-only: master.conf already filled (was pushed), just create local host.conf
api/setup.php:
- save action: writes master.conf + host.conf, creates varaverk_setup.db state file,
redirects to ?tab=scheduler&vv_setup=master.conf
- pull action: resolves HOST1 Tailscale IP, queries HOST1 SCRIPTS_DIR, SCPs
master.conf, creates local host.conf, redirects to ?tab=scheduler&vv_setup=hostN.conf
include/config.php: vv_setup_state_read/write/push helpers.
vv_push_setup_state() pushes varaverk_setup.db to /boot/config/ on all known
remotes — no plugin-readiness probe needed (flash is always accessible).
api/rawconf.php: calls vv_push_setup_state() alongside master.conf push.
pages/scheduler.php: setup mode via ?vv_setup=<conf> URL param.
Auto-opens the specified conf file on page load (DOMContentLoaded).
vvSaveRawConf: in setup mode, skips confirm dialog and forces sequence:
master.conf save → auto-open hostN.conf
hostN.conf save → redirect to Monitor (setup complete)
Single SSH probe per remote checks all three conditions in one call:
- plugin cfg exists (varaverk.cfg present with SCRIPTS_DIR)
- Configurations/ dir exists at that path
- master.conf already present on remote
Any failure skips that host entirely rather than pushing blind.
Uses the discovered SCRIPTS_DIR as the SCP destination, not ours.
Instead of assuming the remote uses the same appdata path as the local
host, SSH-grep their varaverk.cfg first and SCP to their actual
Configurations/ dir. Falls back to /mnt/user/appdata/Varaverk if the
plugin isn't installed or the file can't be read.