Shared cache (lidarr_get_tracked_data() in common.sh) so scripts stop
hitting Lidarr's live API for tracked counts every run, and stop
treating a mid-rescan dip as a genuine problem — a whole-library
RescanFolders legitimately makes trackFileCount read far below normal
while it re-verifies every file (confirmed 2026-07-16: 22% of normal
mid-scan). Cache reads fresh-if-recent, waits out an active rescan
(calibrated to that command's own historical duration, tracked per
command name since RescanFolders and DownloadedAlbumsScan take wildly
different amounts of time), then falls back to a stale cache rather
than hard-failing after a few strikes.
lidarr_cleanup.sh: no longer stacks a fresh DownloadedAlbumsScan on
top of one already running, and the tracked-count floor check now
waits out a genuine rescan instead of aborting on every overlap.
lidarr_duplicate_artist_cleanup.sh (new): finds case-insensitive
duplicate artist entries — same display name, different MusicBrainz
ID, added when a search/list-sync matches the wrong same-named artist.
Deletes the empty phantom side and blocks it from Import List
Exclusions, leaves genuinely-different-real-artists alone (checked by
album title overlap, deduped per-artist first so a legitimate reissue
under an artist's own catalog doesn't false-flag as cross-artist
overlap), and only notifies for the rare case where both sides have
real, overlapping content.
lidarr_cache_prefill.sh (new): warms the cache at array start so nothing
reads it cold after boot.
lidarr_missing_art.sh, lidarr_release_fixer.sh: write-through the cache
as a side effect of fetches they already needed for their own purposes.
Most importBlocked downloads are junk/duplicates and still fall straight
through to blocklist+research unchanged. But some are releases arr already
correctly parsed (episode/movie, quality, language all known) that just
trip the title-vs-grab-history safety net — those get imported directly
if the target has no file yet, or the candidate is a same-language
resolution upgrade over what's already there. Sonarr/Radarr only; Lidarr's
manual-import matching doesn't reliably resolve album/track context.
Gated by ARR_SMART_IMPORT_ENABLED (default true) and
ARR_SMART_IMPORT_PREFERRED_LANGUAGE (default English).
Both had a basic HTTP check (Emby) or no coverage at all (Jellyfin), but
neither would have caught today's real incident: Jellyfin's SQLite
database locked up hard (repeated 'database table is locked' errors,
30s+ query timeouts) while its own /System/Info endpoint kept responding
200 the whole time — a basic HTTP check on that endpoint would never have
tripped. /Users forces an actual DB round-trip and was confirmed live to
hang during the exact incident.
Generalized the API check's success condition to also accept array-shaped
responses (/Users returns an array; the existing check only recognized
object fields like .ServerName/.Id/.Version, which would error when
applied to an array) — benefits any future array-returning endpoint, not
just this one. Also corrected the host.conf.template's API_CHECKS format
comment, which described a 3-field format the code never actually used.
Some items (e.g. an album missing 1-2 tracks where no available release
matches the existing edition/track count) can never resolve via blind
retry. Without a limit, the same media ID gets blocklisted and re-searched
every 4 hours forever — confirmed live on ~19 Lidarr albums cycling
identically across five consecutive runs today, each one downloading a
fresh release, failing import for the same structural reason, and
starting over.
Tracks consecutive failures per (arr_type, media_id) in a persisted state
file. After ARR_RECOVERY_MAX_ATTEMPTS (default 3) failures, the item is
still blocklisted and removed from the queue, but auto re-search stops —
notified once when it crosses the threshold, then left for manual review
instead of retried forever.
The memory warning used plain system-wide 'free' RAM (via free -g), a
carryover from before this script was ZFS-specific. ZFS ARC deliberately
consumes most otherwise-unused RAM, so 'free' being low is normal and not
a meaningful signal — it fired a false alarm on 2026-07-05 (free 5.8Gi,
but available a healthy 45Gi).
Replaced ZFS_REPORT_FREE_WARN_GB with ZFS_REPORT_ARC_FREE_WARN_GB: warns
when ARC headroom (ARC_MAX - ARC_CURRENT) drops below threshold, which is
what actually indicates ARC is running out of room to grow. Available RAM
check is unchanged — it's a legitimate system-pressure signal on its own.
acquire_lock (no args) ran before profile inference, so every rsync.sh
invocation — regardless of share — fought over one generic, unparameterized
lock. The per-profile acquire_rsync_lock() further down (with
RSYNC_MAX_CONCURRENT) never got a chance to matter: a single slow transfer
(e.g. Movies during the HOST2 rebuild) monopolized the lock and starved
every other profile, including Critical-Data's 30-minute sync, for days.
Removed the generic acquire_lock call; acquire_rsync_lock "$PROFILE_NAME"
already provides correct per-profile locking on its own.
Also added RSYNC_MAX_RUNTIME_HOURS (default 23): any single transfer
attempt exceeding it is terminated via timeout, logged as paused rather
than failed, and resumes from where it left off next scheduled run
(safe because --partial is already in DEFAULT_RSYNC_OPTS). Bounds the
worst case for one huge/stuck share instead of letting it hold its lock
indefinitely.
- 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
Codebase-wide audit pass: fixed real bugs (SSH hangs missing BatchMode,
local-outside-function no-ops, variable name collisions, a truncated
ratio calc, wrong state-dir path, DARK vs NO_INTERNET drift, and more),
then pulled logic that was duplicated across multiple scripts — arr
cleanup safety gates, docker restart ordering, container maintenance
stop/restart, watchdog state-file helpers, partnership role resolution,
cert expiry checks, remote node discovery, and TMDB discovery scoring —
into common.sh so each now has a single implementation.
TRANSCODE_LOG_RETENTION was documented as trimming the daily transcode log
but never referenced — log grew unbounded. TRANSCODE_ORPHAN_AGE was shown in
--status but never used, so empty session folders were deleted immediately
instead of after the documented grace period, risking a race against ffmpeg
creating a folder just before writing its first segment.
docker_watchdog.sh's daemon-health thresholds were only hardcoded fallbacks
despite comments claiming they were master.conf-configurable, and it
referenced a heartbeat feature that was never implemented (that's owned by
watchdog_orchestrator.sh, its caller) — added the three thresholds to
master.conf for real and removed the stale heartbeat claim.
Also added the missing HOSTN_PARTNERSHIP_SERVICES_STACK block to
host.conf.template (containers.sh already read it via detect_hosts, just
never had a template entry) and corrected play_state_sync.sh's doc comment
for PLAY_SYNC_TYPES' actual default.
Same audit as the orchestrator standardization pass (2a062e5), extended to
every remaining script. Found the same class of bug independently recurring:
ramdisk_stop.sh checked $LOG (nothing assigns it, should be $ENABLE_LOGGING),
partnership_onboard.sh checked $LOG_MODE (same issue), emby_session_report.sh
checked $TRANSCODE_PCT which was never computed so the high-transcode alert
could never fire, and storage_migrate.sh never called detect_hosts() so
$MY_ID was empty, silently breaking the post-migration host*.conf update.
partnership_manager.sh used `local` at top-level script scope (invalid outside
a function) and had two master.conf path references missing "Configurations/".
Along the way: several scripts (share_setup.sh, conf_sync.sh,
downloaders_reset.sh, transcode_cleanup.sh, transcode_manager.sh,
remote_arr_cache_writer.sh, upgrade_webhook_handler.sh) had no explicit
trailing exit code, so they always reported success regardless of real
failures. play_state_sync.sh was missing the partnership gate its own header
documented, so remote play-state sync ran even with PARTNERSHIP_ENABLED=false;
it also always exited 0 on sync errors. arr_profile_enforcer.sh and
webhook_setup.sh hand-rolled their own flag parsing instead of common.sh's
parse_args, so --log silently did nothing on either.
system_watchdog.sh was itself an un-standardized mini-orchestrator — converted
to the shared run_orch_child()/JOB_PASS/JOB_FAIL pattern, added the missing
failure notification, and fixed dry-run to pass --dry-run down to children
instead of skipping them outright. Also fixed a stale webgui_watchdog.sh path
in master.conf.template that would break system_watchdog.sh on any fresh
install.
Closed a sibling-drift gap: radarr_cleanup.sh and sonarr_cleanup.sh were
missing lidarr_cleanup.sh's tracked-count percentage-drop safety gate and its
"not configured on this host, skip cleanly" guard — both now match Lidarr's
7-gate model.
Every orchestrator invoked its children differently — four near-duplicate
run_job() copies, a differently-shaped run_watchdog(), or plain inline bash
calls, each with its own take on path resolution, pass/fail naming, and
dry-run threading. Extracted one shared run_orch_child() into common.sh so
there's a single place to fix or extend this behavior going forward.
Along the way: watchdog_orchestrator.sh and monthly_maintenance.sh were
checking $VERBOSE, a variable nothing in the codebase ever assigns, so --log
silently did nothing beyond basic logging on those two. Fixed to
$ENABLE_LOGGING. watchdog_orchestrator.sh and array_started.sh had no
trailing exit, so their exit codes reflected whatever the last command
happened to return rather than actual success/failure. transcode_management.sh
had no failure notification and no summary at all. Also made
transcode_management.sh's two-script pipeline config-driven
(TRANSCODE_MANAGEMENT_SCRIPTS in master.conf) instead of hardcoded, for room
to extend it later without editing the orchestrator itself.
Exit code 0 on a required container (docker stop, UI stop) now reads as
intentional — no strike, no restart. Non-zero exits still trigger the
existing strike → restart path.
Adds --pause / --resume management commands and a persistent intentional-
stops state file for maintenance windows where even the exit-code heuristic
isn't enough. Containers auto-cleared from the list when seen running again.
Media/ now holds only media-level scripts (cleaner, permissions, play_state_sync).
All arr management scripts (cleanup, discovery, sync, webhooks, release fixer) live in Arrs_Stack/.
Reads MUSICBRAINZ_ALBUMID from FLAC (vorbis block type 4) and MP3 (ID3v2 TXXX)
files, matches against Lidarr's known releases, switches monitored=true to the
correct one, and queues RefreshArtist. Runs before lidarr_cleanup.sh in the
daily job list so the strike system doesn't act on files that just needed a
release correction.
Favorites on any server propagate to all others; never unmarks.
Covers MusicArtist, MusicAlbum, Movie, Series (Audio tracks future).
Provider map extended to include FAV_TYPES alongside SYNC_TYPES,
with Series/Episode TVDB IDs namespaced to avoid collisions.
Retries up to PLAY_SYNC_HANDBACK_RETRIES times (default 5, 60s apart)
before giving up — one successful run catches all state regardless of
outage length, so users land on current watch state after DNS flips.
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.
Writes partner confs from RAM cache to /boot/config/.cache/vv/d/ while
remote is offline, and removes the backup when remote comes back. Called
each minute via SYSTEM_WATCHDOG_SCRIPTS so crashes and power loss are
covered — not just graceful shutdowns.
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.
conf_cache_save.sh runs first on array stop — snapshots partner confs from RAM
cache to /boot/config/varaverk/conf_bak/ before anything else shuts down.
conf_cache_restore.sh runs after conf_sync.sh on array start — if partner was
unreachable and RAM cache is incomplete, loads the backup into RAM then removes
it. Normal reboots: backup written, fresh pull succeeds, backup deleted unused.
Edge case (partner down at boot): backup fills the gap so fallback.sh has the
partner vars it needs to operate correctly.
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.
Adds SSH key (hostname convention), arr path maps (docker volume mounts),
Authelia container + config path, boot device storage mode detection,
and master.conf HOST identity + Gitea container. Also fixes RADARR_MOVIE_ROOT
→ RADARR_MOVIES_ROOT to match the variable name used by all other scripts.
Deployment/conf_templates/ had two stale files: host.conf.template was a copy of host1.conf
(not a HOSTN template), and master.conf had hardcoded hostnames and an old appdata DATA_DIR path.
The correct templates already exist in Configurations/ (master.conf.template, host.conf.template).
Updated varaverk.plg and git_pull_execute.sh to read from Configurations/ directly.
Removed TMPL_DIR variable from git_pull_execute.sh — CONF_DIR covers both.
- 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
Skip list belongs to docker_watchdog, not stability_watchdog (legacy
naming from when docker+system were the only two watchdogs). Rename
variable and physical file system_watchdog_failed.db →
docker_watchdog_failed.db across all files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plugin folder /boot/config/plugins/varaverk is the single home for
scripts, data, and state. Available at boot before array mounts.
USB boot is not a supported configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap varaverk.cfg default and conf template default back to
/mnt/user/appdata/Varaverk — safe for USB boot users. Flash paths
(/boot/config/plugins/varaverk) are set in the user's gitignored
personal configs and don't affect public installs.
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>