A fourth candidate source beside the arrs, the system log and container logs: skip-listed containers, strikes past their limit, repeated restarts, unattended reboots and sustained pressure become findings. The kind is deliberately not conf-bound, so it cannot autofix by construction rather than by a switch — and WATCHDOG_SCAN_IGNORE suppresses it, so a knowingly broken container stays quiet. First consumer AI_ASSIST_WATCHDOG has ever had.
1951 lines
125 KiB
Bash
1951 lines
125 KiB
Bash
#!/bin/bash
|
||
# ==============================================================================================
|
||
# ================================= MASTER CONFIGURATION =======================================
|
||
# ==============================================================================================
|
||
# Shared configuration for the unRAID script ecosystem.
|
||
# Contains all settings that apply to every server — thresholds, toggles, profiles, job lists.
|
||
#
|
||
# ── HOW THE THREE-FILE SYSTEM WORKS ──────────────────────────────────────────────────────────
|
||
# Scripts source all three files at startup:
|
||
# source master.conf ← shared config (this file)
|
||
# source host1.conf ← HOST1 credentials, shares, container lists
|
||
# source host2.conf ← HOST2 credentials, shares, container lists
|
||
#
|
||
# Sparse checkout (git) ensures each server only pulls its own host*.conf.
|
||
# HOST2 never sees HOST1 credentials. HOST1 never sees HOST2 credentials.
|
||
#
|
||
# What belongs here: thresholds, toggles, intervals, profiles, job lists
|
||
# What belongs in HOST*: hostnames, SSH keys, API keys, passwords, share paths,
|
||
# container names, failover lists, watchdog containers
|
||
#
|
||
# ── HOW THIS FILE WORKS ───────────────────────────────────────────────────────────────────────
|
||
# Every script sources all three conf files and common.sh at startup.
|
||
# Change a value here and it affects all scripts that use it — no hunting through files.
|
||
# To disable something: comment it out with # rather than deleting it.
|
||
# To add a new rsync profile: add a key to each PROFILE_* array in the RSYNC section.
|
||
# To add or remove orchestrator jobs: edit the arrays in the ORCHESTRATORS section.
|
||
#
|
||
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
|
||
#
|
||
# Section Description
|
||
# ───────────────────────────────────────────────────────────────────────────────────────────
|
||
# SHARED HOST CONFIGURATION DATA_DIR and shared connection settings
|
||
# PARTNERSHIP Mirror relationship lifecycle — onboard/offboard/transfer
|
||
# LOGGING Enable or disable verbose logging
|
||
# NOTIFICATIONS unRAID native and Discord webhook settings
|
||
# GIT / REPO Gitea repository and SSH settings
|
||
#
|
||
# ── ORCHESTRATORS ──────────────────────────────────────────────────────────────────────────
|
||
# ARRAY START Scripts launched at array start (array_started.sh)
|
||
# ARRAY STOP Scripts run at planned shutdown (array_stopping.sh)
|
||
# WATCHDOG ORCHESTRATOR Per-minute watchdog runner (watchdog_orchestrator.sh)
|
||
# SYSTEM WATCHDOG Sub-scripts called by watchdog_orchestrator.sh
|
||
# CRITICAL SYNC MAINTENANCE 30-minute jobs + sync shares + partnership check (critical_sync_maintenance.sh)
|
||
# INTERMEDIATE SYNC MAINTENANCE Arr sync + optional mid-day rsync (intermediate_sync_maintenance.sh)
|
||
# DAILY SYNC MAINTENANCE Job list + media shares (daily_sync_maintenance.sh)
|
||
# WEEKLY SYNC MAINTENANCE Job list + sync shares + update toggles (weekly_sync_maintenance.sh)
|
||
# MONTHLY MAINTENANCE Uptime-triggered heavy tasks — ZFS scrub, SMART tests (monthly_maintenance.sh)
|
||
#
|
||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||
# RSYNC ENABLE/DISABLE Two-tier toggle system — global gate + per-orchestrator
|
||
# RSYNC DEFAULTS Global fallback rsync options and limits
|
||
# REMOTE HEALTH CHECKS Rootfs threshold for pre-flight abort
|
||
# RSYNC PROFILE SYSTEM Per-profile overrides for appdata syncs
|
||
#
|
||
# ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
|
||
# FALLBACK Mutual container failover shared settings
|
||
# FALLBACK TEST Simulated outage settings for fallback_test.sh
|
||
#
|
||
# ── DOCKER ESSENTIALS ──────────────────────────────────────────────────────────────────────
|
||
# DOWNLOADERS RESET Retention and thresholds for slskd, SABnzbd, qBittorrent
|
||
# DOCKER DAILY RESTART Containers restarted daily
|
||
# DOCKER WEEKLY RESTART Containers restarted weekly
|
||
# DOCKER WATCHDOG Thresholds and toggles for container monitoring
|
||
# DOCKER NETWORK CONNECT Networks to ensure + containers to connect
|
||
#
|
||
# ── UNRAID ESSENTIALS ──────────────────────────────────────────────────────────────────────
|
||
# INOTIFY TUNING inotify limits — raised at array start by inotify_tuning.sh
|
||
# SYSTEM TUNING MONITOR Tracks inotify + php-fpm usage over time
|
||
# REBOOT User warning delay before scheduled reboot
|
||
# MOVER Mover stop timeout
|
||
# SYSLOG FILTER Docker veth noise filter file path
|
||
# PHP-FPM PHP-FPM max children config
|
||
# CLEAR LOGS System log file paths
|
||
# WEBGUI WATCHDOG WebGUI nginx + emhttp monitoring and restart
|
||
#
|
||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||
# MEDIA PERMISSIONS Share list, mode and owner for permissions script
|
||
# MEDIA CLEANER Anime and media folder lists and file patterns
|
||
# ARR CLEANUP Lidarr, Sonarr, Radarr orphan file cleanup settings
|
||
# ARR FAILED/STALLED RECOVERY Auto blocklist + re-search settings
|
||
#
|
||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||
# TRANSCODE MANAGER Ramdisk and SSD fallback transcode management
|
||
# TRANSCODE SERVER ARRAY Multi-server session monitoring (Emby, Jellyfin, Plex)
|
||
#
|
||
# ── MONITORS ───────────────────────────────────────────────────────────────────────────────
|
||
# CERTIFICATE MONITOR SSL certificate expiry monitoring
|
||
# BACKUP VERIFY Random sample checksum verification against remote
|
||
# SMART HEALTH Drive SMART attribute monitoring
|
||
# ZFS MEMORY SNAPSHOT Weekly ZFS health and memory diagnostic report
|
||
# BANDWIDTH MONITOR Daily rsync transfer logging and weekly summary
|
||
# HEALTH DIGEST Aggregated system health digest — always/smart/weekly
|
||
# EMBY SESSION REPORT Weekly Emby usage statistics via API
|
||
#
|
||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||
# SYSTEM WATCHDOG Continuous system health monitoring — last line of defense
|
||
#
|
||
# ==============================================================================================
|
||
|
||
# ==============================================================================================
|
||
# ── HOST IDENTITIES ───────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Hostnames for every server in the ecosystem — not credentials, safe for all machines.
|
||
# Must match the exact unRAID hostname AND Tailscale device name (case sensitive).
|
||
# detect_hosts() in common.sh matches the local hostname against these to set MY_ID / REMOTE_ID.
|
||
# Tailscale IP resolution uses these names — no hardcoded IPs needed.
|
||
# To add a new server: add HOST3="unRAID-NewServer" here + create host3.conf.
|
||
HOST1="" # this server's Unraid hostname — must match exactly (case-sensitive)
|
||
HOST2="" # partner server hostname — leave blank if running standalone
|
||
|
||
# ==============================================================================================
|
||
# ── SHARED HOST CONFIGURATION ─────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Requirement: internal NVMe/SSD boot drive. Everything lives in the plugin folder on flash —
|
||
# scripts, state, and data are all available before the array mounts.
|
||
# Both directories are created automatically if they don't exist.
|
||
#
|
||
# DATA_DIR is the one on-disk root. Everything Varaverk persists lives under it, in a
|
||
# subdirectory named for what the files are. Move DATA_DIR and the whole tree follows.
|
||
#
|
||
# It used to be two roots plus two strays: DATA_DIR beside State_Files/ as siblings, with the
|
||
# conf-cache backup off in SCRIPTS_DIR/.cache/vv/d and the arr cache backups loose in DATA_DIR's
|
||
# root. Nothing was wrong with any one of those decisions; together they meant no single place
|
||
# answered "what does Varaverk keep on disk". State is data — it is the data that happens to
|
||
# describe right now — so it belongs under the same root as the rest.
|
||
#
|
||
# db/ — statistics, histories, counters, blocklists. Things that accumulate.
|
||
# state/ — runtime state for every script: watchdogs, fallback, transcode, setup.
|
||
# Requirement: ALL state files MUST use $STATE_DIR. No /tmp, no repo root.
|
||
# ai/ — the retrieval index, operator memory, token ledger, filed bugs, saved chats.
|
||
# cache/ — persistent backups of the tmpfs caches, and ONLY those. A file belongs here when
|
||
# losing it costs a re-fetch and nothing else; anything that is a source of truth
|
||
# belongs in db/ or state/.
|
||
# logs/ — retained log output. Live logging still goes to LOG_DIR (/var/log/varaverk).
|
||
#
|
||
# STATE_DIR keeps its name and changes only its value, which is why this restructure did not
|
||
# touch the 15 conf entries, 18 shell paths and 23 PHP paths that build on it.
|
||
#
|
||
# The tmpfs caches are NOT here and must not be moved here — see VV_CACHE_ROOT below. These are
|
||
# on flash; those are read every second by the WebGUI and rewritten by the hundred megabytes.
|
||
DATA_DIR="/boot/config/plugins/varaverk/data"
|
||
DB_DIR="${DATA_DIR}/db"
|
||
STATE_DIR="${DATA_DIR}/state"
|
||
AI_DATA_DIR="${DATA_DIR}/ai"
|
||
CACHE_BACKUP_DIR="${DATA_DIR}/cache"
|
||
LOG_ARCHIVE_DIR="${DATA_DIR}/logs"
|
||
BACKUP_DIR="${DATA_DIR}/Backups"
|
||
CONF_BACKUP_DIR="${BACKUP_DIR}/Confs" # 0700 — holds credentials
|
||
PERSISTENT_CONF_CACHE="${CACHE_BACKUP_DIR}/conf"
|
||
ARR_CACHE_BACKUP_DIR="${CACHE_BACKUP_DIR}/arr"
|
||
|
||
# ── Cache Roots ──
|
||
# Everything Varaverk keeps in RAM, under one root, defined once.
|
||
#
|
||
# These used to be four literals in load_config.sh and three more in the PHP layer, spread over
|
||
# /tmp/vv_cache, /tmp/arr_cache, /tmp/.cache/vv/d, /tmp/.cache/vv/ai and /tmp/varaverk_ai_jobs —
|
||
# five naming schemes, and no single place that could tell you what Varaverk had in tmpfs. PHP
|
||
# could not read load_config.sh, so it restated the paths it needed and the two layers were kept
|
||
# in agreement by hand. They live here because master.conf is the one file both layers actually
|
||
# read: bash sources it, and the PHP conf parser resolves ${VAR} against it the same way.
|
||
#
|
||
# ONE ROOT, NOT ONE DIRECTORY. The subdirectories are deliberately separate and must stay that
|
||
# way — they have genuinely different rules:
|
||
#
|
||
# conf/ holds partner credentials, is chmod 700, and IS snapshotted to PERSISTENT_CONF_CACHE
|
||
# so it survives a reboot.
|
||
# ai/ holds non-secret token counters that are worthless when stale and must NOT be
|
||
# preserved across a reboot — losing them means "not collected here" until the next
|
||
# sync, which is the honest answer.
|
||
# jobs/ holds in-flight work handed off to a detached worker — an AI answer being generated,
|
||
# a container action being applied. chmod 700: each is readable by anyone who can guess
|
||
# its token, which is why the tokens are random_bytes and not sequential.
|
||
# arr/ is the large one (100s of MB) and is restored from a DATA_DIR backup on demand.
|
||
# api/ is the WebGUI payload cache, and is the only one safe to delete at any moment.
|
||
#
|
||
# Collapsing those into a single directory would give the credentials the token cache's
|
||
# persistence rules, or the reverse. The win here is one definition, not one folder.
|
||
#
|
||
# Point VV_CACHE_ROOT somewhere else and everything follows. It must be on a filesystem that is
|
||
# cleared or safe to clear on boot — every consumer treats a missing cache as a cold start, and
|
||
# nothing here is a source of truth for anything.
|
||
VV_CACHE_ROOT="/tmp/varaverk"
|
||
VV_CACHE_DIR="${VV_CACHE_ROOT}/api" # WebGUI payload cache — monitor, arrs, ai
|
||
CONF_RAM_CACHE_DIR="${VV_CACHE_ROOT}/conf" # partner host*.conf — chmod 700, snapshotted
|
||
ARR_CACHE_DIR="${VV_CACHE_ROOT}/arr" # arr payloads — restored from DATA_DIR on demand
|
||
AI_TOKEN_CACHE_DIR="${VV_CACHE_ROOT}/ai" # partner token ledgers — never preserved
|
||
AI_JOB_DIR="${VV_CACHE_ROOT}/jobs/ai" # in-flight AI answers — chmod 700
|
||
DOCKER_JOB_DIR="${VV_CACHE_ROOT}/jobs/docker" # in-flight container actions — chmod 700
|
||
|
||
# ── Version Parity ──
|
||
# Controls behaviour when local and remote unRAID versions differ.
|
||
# Major version mismatch always aborts regardless of this setting.
|
||
# "warn" — log warning and continue (safe for minor/patch differences)
|
||
# "abort" — refuse to continue (strict — ensures both sides always match)
|
||
UNRAID_VERSION_MISMATCH_ACTION="warn"
|
||
|
||
# ── Conf Sync ──
|
||
# conf_sync.sh pulls partner host confs into /tmp/.vv/ RAM cache on array start and after
|
||
# any conf save — makes partner vars (HOST2_*, etc.) available without touching git.
|
||
CONF_SYNC_ENABLED=true
|
||
|
||
# ── Arr Sync ──
|
||
# arr_sync.sh syncs Lidarr/Sonarr/Radarr libraries across all nodes bidirectionally.
|
||
# Runs before rsync — all nodes agree on tracked library before files are transferred.
|
||
# Remote API keys are read live from each node's config.xml via SSH — never stored here.
|
||
ARR_SYNC_ENABLED=true
|
||
ARR_SYNC_BLOCKLIST="${DB_DIR}/arr_sync_blocklist.tsv"
|
||
ARR_SYNC_CONNECT_TIMEOUT=10 # seconds — SSH connect timeout per node
|
||
ARR_SYNC_API_TIMEOUT=60 # seconds — curl timeout for library fetches
|
||
DOCKER_APPDATA_BASE="/mnt/user/appdata"
|
||
ARR_SYNC_LIDARR_PORT=8686
|
||
ARR_SYNC_SONARR_PORT=8989
|
||
ARR_SYNC_RADARR_PORT=7878
|
||
|
||
# ── Remote Docker Daemon ──
|
||
# Strike system for remote Docker daemon health checks.
|
||
# Scripts that issue remote container commands check the remote daemon first.
|
||
# Below limit → skip operation this run | At limit → notify critical + exit
|
||
REMOTE_DOCKER_STRIKE_LIMIT=3 # consecutive failures before critical notify
|
||
|
||
# ==============================================================================================
|
||
# ── PARTNERSHIP ───────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Manages the relationship lifecycle between two unRAID servers.
|
||
# HOST1 is always the owner (source of truth) — HOST2 is always the mirror.
|
||
# PARTNERSHIP_OWNER_HOST flips to "HOST2" after a --transfer operation.
|
||
# All identity vars (hostnames, SSH keys) live in host*.conf.
|
||
# Hostnames already match Tailscale device names — IP resolution is automatic.
|
||
#
|
||
# State files on /boot/config — survives reboots, available before array starts:
|
||
# /boot/config/partnership_HOST1.db ← HOST1 writes only
|
||
# /boot/config/partnership_HOST2.db ← HOST2 writes only
|
||
# Propagated via SSH — no rsync needed
|
||
#
|
||
# critical_sync_maintenance.sh runs --check every 30min:
|
||
# Reads both state files via SSH
|
||
# Detects offboard requests → finalises from owner side
|
||
# Increments offline counter → auto-offboards after threshold
|
||
# Silent when healthy ✅
|
||
#
|
||
# See README-Partnership.md for full lifecycle documentation.
|
||
|
||
PARTNERSHIP_ENABLED=false
|
||
PARTNERSHIP_OWNER_HOST="HOST1" # "HOST1" or "HOST2" — flips on --transfer
|
||
|
||
# Auth containers reconfigured on onboard/offboard — defined per host in host*.conf.
|
||
# Format: "ContainerName|WebUIPort"
|
||
# HOST1_PARTNERSHIP_AUTH_WEBUIS / HOST2_PARTNERSHIP_AUTH_WEBUIS
|
||
# On onboard → WebUI pointed at owner's Tailscale IP
|
||
# On offboard → WebUI pointed back at localhost
|
||
|
||
# Paths to collect during the grace window after offboard — defined per host in host*.conf.
|
||
# HOST1_PARTNERSHIP_MIRROR_BACKUPS / HOST2_PARTNERSHIP_MIRROR_BACKUPS
|
||
# Notified on offboard — no auto-deletion, manual collection.
|
||
|
||
# Timing — single var controls both Tailscale removal and backup access expiry.
|
||
# Both expire at the same time — keeping backups accessible beyond Tailscale removal is pointless.
|
||
PARTNERSHIP_GRACE_HOURS=6 # hours after offboard before Tailscale removal
|
||
# backup access expires at the same time
|
||
PARTNERSHIP_OFFLINE_THRESHOLD=30 # days either server unreachable before auto-offboard
|
||
# works both directions independently
|
||
|
||
# Partnership and setup state files — all in STATE_DIR per the project requirement.
|
||
# Scripts use these variables; do not hardcode /boot/config paths in scripts.
|
||
PARTNERSHIP_BLOCKLIST_FILE="$STATE_DIR/partnership_blocklist.db"
|
||
VARAVERK_SETUP_FILE="$STATE_DIR/varaverk_setup.db"
|
||
|
||
# Tailscale removal on offboard.
|
||
PARTNERSHIP_REMOVE_TAILSCALE=true # remove mirror from Tailscale tailnet on offboard
|
||
# false = skip removal (manual or testing)
|
||
|
||
# Tailscale API — required when PARTNERSHIP_REMOVE_TAILSCALE=true.
|
||
# Stays in shared conf — only owner uses it, and owner is always running this script.
|
||
# API key: https://login.tailscale.com/admin/settings/keys → Devices write scope
|
||
TAILSCALE_API_KEY="" # tskey-api-...
|
||
TAILSCALE_TAILNET="" # your tailnet name (e.g. yourname.github)
|
||
|
||
# Transfer safety.
|
||
PARTNERSHIP_TRANSFER_CONFIRM="i-understand-this-transfers-ownership"
|
||
PARTNERSHIP_TRANSFER_STRIKES=3 # consecutive health checks required
|
||
PARTNERSHIP_TRANSFER_MAX_ATTEMPTS=20 # max health check attempts before giving up
|
||
|
||
# Onboard settings.
|
||
PARTNERSHIP_ONBOARD_VERIFY=true # verify WebUI reachable after reconfiguration
|
||
PARTNERSHIP_ONBOARD_NOTIFY=true # notify both servers on completion
|
||
PARTNERSHIP_SYNC_INTERVAL=15 # minutes — informational, actual schedule in cron
|
||
|
||
# SSH key lifecycle — managed by Partnership/ssh_setup.sh.
|
||
# Key named after this server: hostname lowercased, unraid- prefix stripped.
|
||
# unRAID-Gmer4Lfe → /root/.ssh/gmer4lfe_rsync_automation
|
||
# Run Partnership/partnership_onboard.sh to generate, copy, and update conf automatically.
|
||
SSH_MAX_STRIKES=5 # consecutive SSH auth failures before critical notify
|
||
SSH_STRIKE_RESET_HRS=24 # hours since last failure before strike counter resets
|
||
|
||
# FolderView3 plugin integration — chodeus/VladoPortos/scolcipitato.
|
||
# Creates a "{Partner}-Failover" folder on onboard, removes + cleans containers on offboard.
|
||
# Folder name derived from remote hostname (strip unraid- prefix case-insensitively).
|
||
# Plugin config: /boot/config/plugins/folder.view3/docker.json
|
||
PARTNERSHIP_FOLDERVIEW3=true # create/remove FolderView3 folder on onboard/offboard
|
||
PARTNERSHIP_FOLDERVIEW3_URL="" # CA plugin URL — leave empty to skip auto-install
|
||
|
||
# ==============================================================================================
|
||
# ── LOGGING ───────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# Controls verbose [LOG] output across all scripts.
|
||
# true = show detailed [LOG] lines — useful for debugging or first-time setup
|
||
# false = show only user-facing output — cleaner for scheduled runs
|
||
ENABLE_LOGGING=false
|
||
|
||
# ==============================================================================================
|
||
# ── NOTIFICATIONS ─────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# unRAID native notification system — integrates with the bell icon in the WebGUI.
|
||
# normal = job completed successfully / warning = something failed or needs attention
|
||
NOTIFY_UNRAID=true
|
||
|
||
# Discord webhook URL — defined per host in host*.conf.
|
||
# HOST1_DISCORD_WEBHOOK / HOST2_DISCORD_WEBHOOK
|
||
# Allows different webhooks per server, or only one server notifying.
|
||
|
||
# Upgrade webhook — standalone PHP listener that receives OnUpgrade events from
|
||
# Sonarr/Radarr/Lidarr and immediately pushes the upgraded file to all mesh nodes.
|
||
# Bypasses Unraid nginx auth — the secret in the URL is the only gate.
|
||
# Run Tools/webhook_setup.sh once to register the connection in each arr.
|
||
# WEBHOOK_PORT 0 disables the listener.
|
||
WEBHOOK_PORT=7821
|
||
WEBHOOK_SECRET="" # auto-generated on first start if empty
|
||
|
||
# ==============================================================================================
|
||
# ── GIT / REPO ────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# Gitea self-hosted repository — used by git_pull_execute.sh.
|
||
# Detects Gitea container location at runtime — works through failover automatically.
|
||
# Falls back to GITEA_DOMAIN if local and Tailscale both fail.
|
||
GITEA_CONTAINER="Gitea"
|
||
GITEA_REPO_PATH="" # e.g. YourUser/Varaverk.git
|
||
GITEA_DOMAIN="" # e.g. git.yourdomain.com — requires NPM + DNS
|
||
TARGET_DIR="/mnt/user/appdata/Varaverk"
|
||
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
|
||
SSH_PORT=221 # Gitea SSH port (default 22, Gitea often uses 221/222)
|
||
GITEA_HTTP_PORT=3000 # Gitea web/API port — used by gitea_ssh_setup.sh
|
||
|
||
# ==============================================================================================
|
||
# ── ORCHESTRATORS ─────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# All orchestrator job lists live here — edit arrays to add/remove scripts.
|
||
# No changes to orchestrator scripts needed when adding or removing jobs.
|
||
# Sections ordered by run frequency: array events first, then shortest interval to longest.
|
||
|
||
# ━━━ Array Start ━━━
|
||
# Scripts launched by array_started.sh when the array comes online.
|
||
# Launched in order — each as a background process.
|
||
# One-shot scripts (ramdisk, syslog, fpm, inotify, network) run and exit naturally.
|
||
# Continuous scripts (failover) run until array stops.
|
||
# Watchdogs (resource_watchdog, docker_watchdog, system_watchdog) are cronned via
|
||
# watchdog_orchestrator.sh — NOT launched here.
|
||
ARRAY_START_SCRIPTS=(
|
||
"Plugin/unraid/System_Essentials/pcie_aer_quiet.sh" # drop AER-spamming dead hardware — runs first so later logs stay readable
|
||
"Plugin/unraid/System_Essentials/unraid_api_key_renew.sh" # re-register Varaverk API key — registry is ephemeral
|
||
"System_Essentials/conf_sync.sh" # pull partner confs + push own conf into /tmp/.vv/ RAM cache
|
||
"System_Essentials/conf_cache_restore.sh" # load partner confs from persistent backup if conf_sync couldn't reach partner
|
||
"Transcodes/ramdisk_setup.sh" # creates ramdisk + symlink before Emby starts
|
||
"System_Essentials/docker_syslog_filter.sh" # suppress veth noise before logs fill
|
||
"Plugin/unraid/System_Essentials/php_fpm_max_children.sh" # WebGUI performance tuning
|
||
"System_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
||
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
||
"Arrs_Stack/arr_cache_prefill.sh" # warm Lidarr/Sonarr/Radarr tracked-data caches before anything reads them cold
|
||
"Arrs_Stack/start_webhook_listener.sh" # arr upgrade webhook listener — continuous
|
||
"Fallback/fallback.sh" # mutual failover — continuous
|
||
)
|
||
|
||
# ━━━ Array Stop ━━━
|
||
# Scripts run by array_stopping.sh for a planned shutdown — stops everything cleanly in order.
|
||
# Run sequentially (foreground) — each must complete before the next starts.
|
||
# Order matters: user scripts first (prevents new ops), then data movement, then containers.
|
||
ARRAY_STOP_SCRIPTS=(
|
||
"System_Essentials/conf_cache_save.sh" # snapshot partner conf RAM cache → /boot before anything stops
|
||
"Plugin/unraid/System_Essentials/user_scripts_stop.sh" # stop background scripts before they start new ops
|
||
"Fallback/fallback.sh --stop" # gracefully stop fallback (not caught by user_scripts_stop)
|
||
"System_Essentials/rsync_stop.sh --rsync-only" # kill rsync; skip container recovery (handled below)
|
||
"Plugin/unraid/System_Essentials/mover_stop.sh" # stop mover after rsync (they conflict on same files)
|
||
"Docker_Essentials/docker_container_stop.sh" # stop all containers last
|
||
)
|
||
|
||
# ━━━ Transcode Management ━━━
|
||
# transcode_management.sh runs TRANSCODE_MANAGEMENT_SCRIPTS in order each cron cycle.
|
||
# Schedule: */7 * * * * (every 7 minutes)
|
||
# Order matters — cleanup first so the manager measures real current ramdisk usage,
|
||
# not usage inflated by stale segment files from ended sessions.
|
||
TRANSCODE_MANAGEMENT_SCRIPTS=(
|
||
"Transcodes/transcode_cleanup.sh" # remove aged segment files before usage is measured
|
||
"Transcodes/transcode_manager.sh" # flip ramdisk/SSD symlink, write daily log entry
|
||
)
|
||
|
||
# ━━━ Watchdog Orchestrator ━━━
|
||
# watchdog_orchestrator.sh runs WATCHDOG_ORCHESTRATOR_SCRIPTS in order each cron cycle.
|
||
# Schedule: */15 * * * * (every 15 minutes)
|
||
# NOT in ARRAY_START_SCRIPTS — has its own cron entry.
|
||
# Order matters — resource first (frees pressure), docker second (heals with freed resources),
|
||
# system third (storage + webgui component health), stability last (last line of defense).
|
||
WATCHDOG_ORCHESTRATOR_SCRIPTS=(
|
||
"Watchdogs/resource_watchdog.sh" # reduce system pressure before healing attempts
|
||
"Watchdogs/docker_watchdog.sh" # heal containers with freed resources
|
||
"Watchdogs/system_watchdog.sh" # system component health — storage + webgui
|
||
"Plugin/unraid/System_Essentials/unraid_api_key_renew.sh" # re-register API key if registry lost it
|
||
"Plugin/unraid/Tools/ai_repair_sweep.sh" # read what the last cycle logged; off unless AI_REPAIR_ENABLED
|
||
"Watchdogs/stability_watchdog.sh" # reboot if all else fails — last line of defense
|
||
)
|
||
# The repair sweep sits ahead of stability deliberately, which is the one exception to "stability
|
||
# last". It reads the previous cycle's logs and may correct the very misconfiguration stability
|
||
# would otherwise reboot for — a wrong port is not fixed by restarting the machine. It is bounded
|
||
# by AI_PROBE_TIMEOUT, exits 0 in every case, and does nothing at all unless AI_REPAIR_ENABLED.
|
||
|
||
# ━━━ System Watchdog ━━━
|
||
# system_watchdog.sh runs SYSTEM_WATCHDOG_SCRIPTS sequentially each cycle.
|
||
# Called by watchdog_orchestrator.sh — not scheduled directly.
|
||
SYSTEM_WATCHDOG_SCRIPTS=(
|
||
"Watchdogs/System/storage_watchdog.sh" # pool growth + runaway log detection
|
||
"Plugin/unraid/Watchdogs/System/webgui_watchdog.sh" # WebGUI availability — nginx → php-fpm → emhttp
|
||
"Watchdogs/System/network_watchdog.sh" # internet, DDNS, Tailscale, NPM proxy
|
||
"Watchdogs/System/conf_cache_watchdog.sh" # maintain persistent conf backup while partner is offline
|
||
)
|
||
|
||
# ━━━ Critical Sync Maintenance ━━━
|
||
# critical_sync_maintenance.sh runs every 30 minutes.
|
||
# Order: CRITICAL_MAINTENANCE_SCRIPTS (jobs) → CRITICAL_SYNC_SHARES (rsync) → partnership --check
|
||
# partnership --check always runs last regardless of rsync gate.
|
||
# Comment out entries to disable without removing.
|
||
CRITICAL_MAINTENANCE_SCRIPTS=(
|
||
"Docker_Essentials/downloaders_reset.sh" # clear stuck download states every 30min
|
||
"Media/play_state_sync.sh" # sync watched/resume state across Emby + Jellyfin
|
||
)
|
||
|
||
# Shares synced every 30 minutes — defined per host in host*.conf.
|
||
# HOST1_CRITICAL_SYNC_SHARES / HOST2_CRITICAL_SYNC_SHARES
|
||
# Format: "/path/to/share" or "/path/to/share|profile-name"
|
||
# Order matters — Critical-Data first (auth stack), then Emby dirty sync.
|
||
|
||
# ━━━ Intermediate Sync Maintenance ━━━
|
||
# intermediate_sync_maintenance.sh runs every 4 hours — arr library sync, artwork fetch,
|
||
# and optional mid-day rsync for any shares that need sub-daily propagation.
|
||
# Schedule: 0 */4 * * *
|
||
# INTERMEDIATE_SYNC_SHARES is host-specific — configure HOST*_INTERMEDIATE_SYNC_SHARES in host*.conf.
|
||
|
||
INTERMEDIATE_MAINTENANCE_SCRIPTS=(
|
||
# Moved here from CRITICAL (30min) 2026-07-17 — nothing in the critical tier actually
|
||
# reads this cache, and every real consumer (arr_sync/arrs_failed_stalled_recovery
|
||
# here, lidarr/sonarr/radarr_cleanup + the classification scans in DAILY) already
|
||
# tolerates staleness well past 4h (arr_get_tracked_data()'s own fallback tolerates up
|
||
# to 1 day). 30min was true "always fresh" but had no consumer that needed it that often.
|
||
"Arrs_Stack/arr_cache_prefill.sh ARR_PREFILL_WAIT_MINUTES=1" # keep the shared arr tracked-data cache fresh
|
||
"Arrs_Stack/arrs_failed_stalled_recovery.sh" # blocklist + re-search failed/stalled arr queue items
|
||
"AI/ai_token_sync.sh" # pull partner AI token ledgers into the tmpfs cache
|
||
)
|
||
# arr_sync.sh runs as a fixed first step in intermediate_sync_maintenance.sh — not listed here.
|
||
# It is controlled by ARR_SYNC_ENABLED (see Arr Sync section above).
|
||
|
||
# ━━━ Daily Sync Maintenance ━━━
|
||
# daily_sync_maintenance.sh runs media share sync first, then iterates
|
||
# DAILY_MAINTENANCE_SCRIPTS for all jobs.
|
||
# Schedule: 0 1 * * * (1am daily)
|
||
DAILY_MAINTENANCE_SCRIPTS=(
|
||
"git_pull_execute.sh" # pull latest scripts — always runs first
|
||
"Media/media_shares_permissions.sh" # apply permissions — runs before cleaners
|
||
"Media/media_cleaner.sh anime" # remove junk from anime shares
|
||
"Media/media_cleaner.sh media" # remove junk from media shares
|
||
"Arrs_Stack/lidarr_release_fixer.sh" # fix wrong release editions before cleanup runs
|
||
"Arrs_Stack/lidarr_duplicate_artist_cleanup.sh" # resolve duplicate MusicBrainz artist entries before cleanup runs
|
||
"Arrs_Stack/lidarr_cleanup.sh" # remove orphaned music files
|
||
"Arrs_Stack/sonarr_cleanup.sh" # remove orphaned TV files
|
||
"Arrs_Stack/radarr_cleanup.sh" # remove orphaned movie files
|
||
# Daily, not weekly — DOWNLOAD_ORPHAN_AGE gates each folder on its own mtime, so
|
||
# frequency costs nothing in safety, but a weekly run clears a whole week of
|
||
# newly-eligible orphans at once and can exceed DOWNLOAD_ORPHAN_MAX_DELETE_GB, which
|
||
# aborts the pass entirely and rolls the backlog into an even larger next run.
|
||
"Arrs_Stack/arr_download_orphan_cleaner.sh" # sweep orphaned completed downloads out of the SAB Completed folders — deletes junk + already-imported leftovers, triggers import scans for genuinely-missing content
|
||
# Runs after cleanup, not before — frees disk space from orphans/junk first, so the
|
||
# searches triggered below (for relocated hasFile=false/episodeFileCount=0 entries)
|
||
# have headroom for the new grabs they cause, rather than contending with clutter
|
||
# cleanup hasn't swept yet.
|
||
"Arrs_Stack/radarr_classification_scan.sh --remove-junk --move" # fix anime/kids misclassification + remove bad-metadata entries
|
||
"Arrs_Stack/sonarr_classification_scan.sh --move" # fix anime/kids misclassification
|
||
"Arrs_Stack/lidarr_missing_art.sh" # fetch missing album/artist artwork (HOST1 only — self-guards)
|
||
"Arrs_Stack/radarr_tmdb_removed.sh" # remove movies dropped from TMDb
|
||
"Arrs_Stack/sonarr_tvdb_removed.sh" # remove series dropped from TVDB
|
||
"Docker_Essentials/docker_update.sh" # pull container image updates before restart
|
||
"Docker_Essentials/docker_daily_restart.sh" # daily container restarts — runs last
|
||
)
|
||
|
||
# Pull latest images for DAILY_RESTART_CONTAINERS before the daily restart.
|
||
# Containers keep running during pull — no extra downtime.
|
||
# Set false to skip updates while still running the daily restart.
|
||
DAILY_CONTAINER_UPDATES=true
|
||
|
||
# Media shares synced daily by daily_sync_maintenance.sh.
|
||
# Defined per-host in host*.conf — HOST1_DAILY_SYNC_SHARES and HOST2_DAILY_SYNC_SHARES.
|
||
# Mesh model: every node pushes every media share. rsync has no --delete so pushes are additive.
|
||
# arr_sync (union) ensures all arr libraries converge first. the arr cleanups remove true orphans.
|
||
# Any node can download content to any share — it propagates to all nodes on the next cycle.
|
||
# Adding HOST3: list every media share in HOST3_DAILY_SYNC_SHARES. No ownership to track.
|
||
# These shares use DEFAULT_RSYNC_OPTS — no profile entry needed.
|
||
# For shares needing custom options or container stops — create a profile in RSYNC section.
|
||
|
||
# Personal encrypted shares defined per-host in host*.conf.
|
||
# ZFS encrypted at dataset level — remote receives encrypted blocks, cannot read content.
|
||
# See README-Rsync_Setup.md for ZFS encryption setup before uncommenting.
|
||
|
||
# ━━━ Weekly Sync Maintenance ━━━
|
||
# weekly_sync_maintenance.sh stops containers both sides → pulls updates →
|
||
# syncs WEEKLY_SYNC_SHARES → restarts → then iterates WEEKLY_MAINTENANCE_SCRIPTS.
|
||
# Schedule: 30 2 * * 0 (Sunday 2:30am)
|
||
WEEKLY_MAINTENANCE_SCRIPTS=(
|
||
"Docker_Essentials/docker_update.sh --weekly" # pull latest images for WEEKLY_RESTART_CONTAINERS before restart
|
||
"Docker_Essentials/docker_weekly_restart.sh" # weekly container restarts after sync
|
||
"System_Essentials/clear_logs.sh" # purge aged logs — Sunday only, low priority
|
||
"Arrs_Stack/arr_full_rescan.sh" # full disk↔DB reconciliation for Lidarr/Sonarr/Radarr — keeps tracked stats honest, runs before discovery so it works off fresh data
|
||
"Arrs_Stack/arr_corruption_scan.sh --remediate" # ffprobe-based corruption sweep of Sonarr's tracked files — deletes+re-searches only after CORRUPTION_SCAN_STRIKE_LIMIT consecutive hits on the same file
|
||
"Arrs_Stack/playback_aware_lidarr_discovery.sh" # behavior-driven music discovery using weekly Emby playback history
|
||
"Arrs_Stack/playback_aware_radarr_discovery.sh" # behavior-driven movie discovery using TMDB recommendations
|
||
"Arrs_Stack/playback_aware_sonarr_discovery.sh" # behavior-driven TV discovery using TMDB recommendations
|
||
)
|
||
|
||
# Pull updates for WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh runs.
|
||
# Set false to skip — docker_weekly_restart.sh still runs regardless.
|
||
WEEKLY_CONTAINER_UPDATES=true
|
||
|
||
# Pull updates for all running containers NOT in daily/weekly managed lists.
|
||
# Ensures every deployed container receives at least one image pull per month.
|
||
# Set false to skip.
|
||
MONTHLY_REMAINING_UPDATES=true
|
||
|
||
# docker_update.sh rebuilds (stop+recreate onto new image) any container whose image changed,
|
||
# in every mode. For daily/weekly, that rebuild is immediately followed by the restart script's
|
||
# own unconditional pass — a container that just got rebuilt would be stopped and started again
|
||
# right after for no reason. docker_update.sh records which containers it rebuilt to these files;
|
||
# docker_daily_restart.sh / docker_weekly_restart.sh read them and skip those containers rather
|
||
# than restarting them a second time. A file older than DOCKER_UPDATE_REBUILT_STALE_HOURS is
|
||
# treated as untrustworthy (docker_update.sh likely didn't run, or didn't run recently) — deleted,
|
||
# and every container in that tier restarts normally, same as if the file never existed.
|
||
DOCKER_UPDATE_REBUILT_DAILY_FILE="${DB_DIR}/docker_update_rebuilt_daily.list"
|
||
DOCKER_UPDATE_REBUILT_WEEKLY_FILE="${DB_DIR}/docker_update_rebuilt_weekly.list"
|
||
DOCKER_UPDATE_REBUILT_STALE_HOURS=12
|
||
|
||
# Shares synced during the weekly maintenance window — defined per host in host*.conf.
|
||
# HOST1_WEEKLY_SYNC_SHARES / HOST2_WEEKLY_SYNC_SHARES
|
||
# Containers stopped both sides before sync — full clean state guaranteed.
|
||
# Profiles drive container stops, excludes, and options — configure in RSYNC section.
|
||
# Order matters — Emby first (larger), then Critical-Data (auth stack).
|
||
|
||
# Container update toggles for the weekly sync window.
|
||
# Containers already stopped for sync — updates pull at no extra downtime.
|
||
# Both false → sync only, no updates.
|
||
WEEKLY_SYNC_UPDATES=true # pull container updates locally during weekly window
|
||
WEEKLY_SYNC_UPDATES_REMOTE=true # pull container updates on remote via SSH
|
||
|
||
# ━━━ Monthly Maintenance ━━━
|
||
# monthly_maintenance.sh fires only when BOTH gates pass:
|
||
# 1. Server uptime >= MONTHLY_UPTIME_THRESHOLD_DAYS days
|
||
# 2. Last run was >= MONTHLY_RUN_INTERVAL_DAYS days ago (or never run)
|
||
# Cron: 0 0 15 * * (15th of the month, midnight — script self-gates, so a spare run is safe)
|
||
# NOT in WATCHDOG_ORCHESTRATOR_SCRIPTS — has its own cron entry.
|
||
# Add scripts that require a long-stable settled system — scrubs, extended drive tests.
|
||
# State file on /boot/config — survives reboots (interval gate independent of uptime gate).
|
||
MONTHLY_MAINTENANCE_SCRIPTS=(
|
||
#"Tools/zfs_pool_scrub.sh" # ZFS pool integrity scrub — not yet built
|
||
#"Tools/smart_long_test.sh" # SMART extended drive health test — not yet built
|
||
)
|
||
|
||
MONTHLY_UPTIME_THRESHOLD_DAYS=30 # minimum uptime in days before maintenance fires
|
||
MONTHLY_RUN_INTERVAL_DAYS=30 # minimum days since last run before running again
|
||
MONTHLY_LAST_RUN_FILE="$STATE_DIR/monthly_maintenance_last_run.db"
|
||
|
||
# ━━━ Sunday Morning Coffee Report ━━━
|
||
# Orchestrator that runs all Sunday monitor scripts in sequence.
|
||
# Schedule: 0 7 * * 0 (Sunday 7am — after weekly_sync_maintenance.sh finishes at ~3am)
|
||
# Each script runs independently and notifies on its own findings.
|
||
COFFEE_REPORT_SCRIPTS=(
|
||
"Monitors/zfs_memory_snapshot.sh" # ZFS pool health + ARC + Docker memory snapshot
|
||
"Monitors/smart_health.sh" # drive SMART attributes — reallocated, pending, temp
|
||
"Monitors/cert_monitor.sh" # SSL certificate expiry for all configured domains
|
||
"Monitors/backup_verify.sh" # rsync mirror integrity via independent MD5 checksums
|
||
"Monitors/bandwidth_monitor.sh" # weekly rsync transfer totals and per-share breakdown
|
||
"Monitors/emby_session_report.sh" # Emby usage — streams, users, library, transcode ratio
|
||
"Monitors/weekly_health_digest.sh" # aggregated digest — watchdogs, fallback, skip list
|
||
)
|
||
|
||
# ==============================================================================================
|
||
# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# ━━━ Rsync Enable/Disable ━━━
|
||
# Two-tier toggle system — Tier 1 overrides Tier 2.
|
||
#
|
||
# Tier 1 — Global gate:
|
||
# RSYNC_ENABLED=false → ALL rsync stops everywhere, no exceptions
|
||
# Use when: remote completely offline, major maintenance, disaster recovery
|
||
#
|
||
# Tier 2 — Per-orchestrator (only applies when Tier 1 is true):
|
||
# Fine grained control — disable specific orchestrators while keeping others
|
||
# Use when: rebuilding secondary, testing, per-window bandwidth management
|
||
#
|
||
# Example — HOST2 data rebuild:
|
||
# RSYNC_ENABLED=true ← rsync works, individual scripts run fine
|
||
# CRITICAL_RSYNC_ENABLED=true ← 30min auth stack sync still runs
|
||
# INTERMEDIATE_RSYNC_ENABLED=false ← skip 4h arr/mid-day rsync during rebuild
|
||
# DAILY_RSYNC_ENABLED=false ← skip daily HDD syncs during rebuild
|
||
# WEEKLY_RSYNC_ENABLED=true ← Emby + Critical-Data still sync (NVMe)
|
||
# MONTHLY_RSYNC_ENABLED=true ← monthly_maintenance.sh rsync section
|
||
# FALLBACK_RSYNC_ENABLED=true ← handback writeback still works
|
||
# → Run individual: bash Rsync/rsync.sh /mnt/user/Movies
|
||
# → When ready: INTERMEDIATE_RSYNC_ENABLED=true DAILY_RSYNC_ENABLED=true
|
||
RSYNC_ENABLED=true # Tier 1 — global gate, overrides everything below
|
||
CRITICAL_RSYNC_ENABLED=true # Tier 2 — critical_sync_maintenance.sh rsync section
|
||
INTERMEDIATE_RSYNC_ENABLED=true # Tier 2 — intermediate_sync_maintenance.sh rsync section
|
||
DAILY_RSYNC_ENABLED=true # Tier 2 — daily_sync_maintenance.sh rsync section
|
||
WEEKLY_RSYNC_ENABLED=true # Tier 2 — weekly_sync_maintenance.sh rsync section
|
||
MONTHLY_RSYNC_ENABLED=true # Tier 2 — monthly_maintenance.sh rsync section
|
||
FALLBACK_RSYNC_ENABLED=true # Tier 2 — fallback.sh writeback jobs on handback
|
||
|
||
# ━━━ Download Webhook ━━━
|
||
# Immediate push to remote nodes on every Sonarr/Radarr/Lidarr Download event.
|
||
# Prevents remotes from searching for content that is already on disk locally.
|
||
DOWNLOAD_WEBHOOK_ENABLED=true
|
||
|
||
# ━━━ Rsync Merge Auto-Promote ━━━
|
||
# When enabled, rsync.sh pre-scans the remote before syncing and promotes to merge
|
||
# mode (pull-then-push) if ≥75% of remote top-level entries exist locally.
|
||
# Named profiles are always excluded from auto-promote regardless of this toggle.
|
||
RSYNC_MERGE_ENABLED=true
|
||
|
||
# ━━━ Rsync Defaults ━━━
|
||
# Global fallback values used when no profile match is found.
|
||
# Media shares in HOST*_DAILY_SYNC_SHARES always use these globals — no profile needed.
|
||
# Appdata shares match profiles by directory basename (lowercased).
|
||
BW_LIMIT=12500 # KB/s — 12500 ≈ 100Mbit
|
||
RETRY_COUNT=3 # retry attempts before giving up
|
||
SLEEP=300 # seconds between retry attempts
|
||
RSYNC_MAX_RUNTIME_HOURS=19 # cap per transfer attempt — pauses and resumes next scheduled run
|
||
# lowered from 23 on 2026-07-16 — daily maintenance jobs alone now
|
||
# take ~4h; 19h cap leaves room for them to still run same-day
|
||
# before the next 1am fire even if a share hits the cap
|
||
CRITICAL_CONTAINER_NAMES=() # containers stopped on REMOTE before rsync — profiles override
|
||
DELAYED_CONTAINERS=() # containers needing delay before starting — profiles override
|
||
CONTAINER_DELAY=5 # seconds before starting delayed containers
|
||
RESTART_VERIFY_WAIT=3 # seconds to wait after restart before checking container is running
|
||
EXCLUDE_DIRS=() # directories excluded from transfer — profiles override
|
||
|
||
# --inplace writes directly to destination — delta against existing file, better for large media
|
||
# --partial keep partial file on interrupted transfer so next run resumes, not re-transfers
|
||
# --timeout kill stalled transfers instead of hanging indefinitely
|
||
# --numeric-ids use UIDs/GIDs numerically — prevents ownership mismatches between servers
|
||
# --delete intentionally omitted — the per-arr cleanups (lidarr/sonarr/radarr_cleanup.sh)
|
||
# enforce media truth post-rsync.
|
||
# Media shares use this default; rsync spreads files only, never removes them.
|
||
# Note: --no-whole-file removed — redundant over SSH (delta transfer is already the default).
|
||
# Note: the arr cleanups only catch true orphans under the union model — intentional removals
|
||
# require arr_sync.sh --blocklist-add first, then manual file deletion.
|
||
DEFAULT_RSYNC_OPTS=(-av --info=progress2 --human-readable --bwlimit="$BW_LIMIT" --inplace --partial --timeout=60 --numeric-ids)
|
||
|
||
# ━━━ Remote Health Checks ━━━
|
||
# Pre-flight — aborts if remote rootfs (/) usage is at or above this percentage.
|
||
# When remote array is down, rsync writes land on rootfs and fill it rapidly.
|
||
ROOTFS_WARN=75
|
||
|
||
# ━━━ Rsync Profile System ━━━
|
||
# Profiles allow per-share rsync behaviour without touching script logic.
|
||
# Profile key matched by basename of directory passed to rsync.sh (lowercased).
|
||
# Override with --profile=name flag.
|
||
#
|
||
# IMPORTANT: PROFILE_RSYNC_OPTS does NOT inherit DEFAULT_RSYNC_OPTS.
|
||
# List ALL desired options explicitly when defining a profile.
|
||
#
|
||
# Current profiles:
|
||
# arrs_stack — arr databases — lower bandwidth, containers stopped for consistency
|
||
# critical-data — auth stack — full stop both sides, Authelia delayed start
|
||
# called by weekly_sync_maintenance.sh — full clean sync weekly
|
||
# critical-fallback — dirty sync — auth stays running both sides, WAL excluded
|
||
# called by critical_sync_maintenance.sh every 30min
|
||
# host1-appdata — HOST1 server-specific appdata — defined in host1.conf
|
||
# host2-appdata — HOST2 server-specific appdata — defined in host2.conf
|
||
# important-data — NextCloud + Postgres — NextCloud delayed start after Postgres
|
||
# emby — weekly clean sync — both Emby stopped, full mirror
|
||
# called by weekly_sync_maintenance.sh only — do NOT schedule separately
|
||
#
|
||
# Every profile lookup falls back to the global default when a key is absent
|
||
# (${PROFILE_X[name]:-$DEFAULT}), so the value/delay/retry/sleep arrays below only list
|
||
# profiles that differ from the default — anything not listed inherits it.
|
||
|
||
declare -A PROFILE_RSYNC_OPTS=(
|
||
[arrs_stack]="-av --info=progress2 --human-readable --bwlimit=$BW_LIMIT --delete --inplace"
|
||
[critical-data]="-av --human-readable --bwlimit=$BW_LIMIT --delete"
|
||
[critical-fallback]="-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file"
|
||
[important-data]="-av --human-readable --bwlimit=$BW_LIMIT"
|
||
[emby]="-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file"
|
||
)
|
||
|
||
# Per-profile bandwidth limits in KB/s
|
||
declare -A PROFILE_BW_LIMIT=(
|
||
[arrs_stack]=5000 # lower — runs alongside other syncs
|
||
[critical-data]=9500 # high — small dataset, sync fast
|
||
[critical-fallback]=9500 # high — small dataset, sync fast
|
||
[important-data]=9500 # high — database sync
|
||
[emby]=8000 # medium — large full mirror
|
||
)
|
||
|
||
# Seconds between retry attempts — only non-default profiles listed (default SLEEP applies otherwise)
|
||
declare -A PROFILE_SLEEP=(
|
||
[critical-fallback]=120 # shorter — frequent dirty sync, retry faster
|
||
)
|
||
|
||
# Containers stopped on BOTH LOCAL and REMOTE before rsync.
|
||
# Local stops first — flushes databases cleanly. Remote stops next — prevents writes.
|
||
# Only running containers get restarted — stopped containers stay stopped.
|
||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||
declare -A PROFILE_CRITICAL_CONTAINER_NAMES=(
|
||
[arrs_stack]="Sonarr Lidarr Readarr Radarr Prowlarr Bazarr Pinchflat"
|
||
[critical-data]="" # e.g. "Mariadb Authelia Redis NginxProxyManager"
|
||
[critical-fallback]="" # dirty sync — auth stays running both sides
|
||
[important-data]="" # e.g. "Postgres NextCloud"
|
||
[emby]="Emby"
|
||
)
|
||
|
||
# Containers needing a delay after rsync before starting.
|
||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||
declare -A PROFILE_DELAYED_CONTAINERS=(
|
||
[arrs_stack]=""
|
||
[critical-data]="" # e.g. "Authelia" — containers that need a delay after db starts
|
||
[critical-fallback]=""
|
||
[important-data]="NextCloud" # wait for Postgres
|
||
[emby]=""
|
||
)
|
||
|
||
# Seconds before starting delayed containers — only non-default profiles listed (default CONTAINER_DELAY applies otherwise)
|
||
declare -A PROFILE_CONTAINER_DELAY=(
|
||
[critical-data]=15 # Mariadb + Redis need time to accept connections
|
||
[important-data]=10 # Postgres needs time before NextCloud
|
||
)
|
||
|
||
# Directories excluded from rsync per profile.
|
||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||
declare -A PROFILE_EXCLUDE_DIRS=(
|
||
[arrs_stack]="logs *.tmp"
|
||
[critical-data]="logs *.tmp *.log nginx/temp nginx/cache __pycache__ notification.txt"
|
||
[critical-fallback]="logs *.tmp *.log nginx/temp nginx/cache __pycache__ notification.txt *.db-wal *.db-shm"
|
||
[important-data]="logs *.tmp"
|
||
[emby]="logs transcodes cache crash*"
|
||
)
|
||
|
||
# Remote restart after dirty sync — restart these on remote IF they were running before sync.
|
||
# Same logic as stop/start — was stopped = stays stopped, was running = gets restarted.
|
||
# Used by dirty sync profiles (critical-fallback) so remote picks up changes.
|
||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||
declare -A PROFILE_REMOTE_RESTART_CONTAINERS=(
|
||
[critical-fallback]="" # containers to restart on remote after dirty sync
|
||
)
|
||
|
||
# Note: disk check is auto-detected from disks.ini — no PROFILE_SKIP_DISK_CHECK needed.
|
||
# check_remote_disks() reads fsType per disk and handles XFS, ZFS, and cache pools automatically.
|
||
|
||
# ==============================================================================================
|
||
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Mutual container failover between two unRAID servers.
|
||
# Each server runs Fallback/fallback.sh independently via array_started.sh.
|
||
# All decisions based on two pings: remote reachable + internet reachable.
|
||
#
|
||
# States: NORMAL | FALLBACK | NO_INTERNET | DARK
|
||
#
|
||
# DDNS rules — absolute:
|
||
# Internet loss → stop own DDNS immediately
|
||
# Failover → start remote DDNS first (Tier 1)
|
||
# Handback → stop remote DDNS → rsync → start containers → start local DDNS last
|
||
#
|
||
# Per-host container lists and tier delays live in host*.conf.
|
||
# Shared settings (intervals, state file, thresholds) live here.
|
||
|
||
EXTERNAL_IP="8.8.8.8"
|
||
FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks
|
||
FALLBACK_HANDBACK_STRIKES=3 # consecutive healthy checks before initiating handback (3×30s = 90s)
|
||
FALLBACK_STATE_FILE="$STATE_DIR/fallback_state.db"
|
||
FALLBACK_ENABLED=false # set true once both servers are configured and paired
|
||
# false = suppresses "not running" warnings in status scripts
|
||
FALLBACK_PARTNERSHIP_SUSPEND_AFTER=120 # minutes without active partnership before suspending
|
||
# 0 = suspend immediately when partnership goes inactive
|
||
|
||
# ━━━ Play State Sync ━━━
|
||
PLAY_SYNC_ENABLED=true
|
||
PLAY_SYNC_REMOTE=true # sync across hosts via Tailscale
|
||
PLAY_SYNC_TYPES="Movie,Episode" # Audio excluded — music library too large; favorites handled separately
|
||
PLAY_SYNC_FAV_TYPES="MusicArtist,MusicAlbum,Movie,Series" # union sync — never unmarks; Audio tracks future
|
||
PLAY_SYNC_PROBE=true # hash fetched state, skip per-item comparison when nothing changed
|
||
PLAY_SYNC_PROBE_MAX_AGE_HOURS=24 # force a full comparison when the fingerprint is older than this
|
||
|
||
# ━━━ Play State Sync — Handback ━━━
|
||
# play_state_sync runs before DNS cutover so users land on current watch state.
|
||
# Retried until success — DNS is held until sync completes or retries are exhausted.
|
||
PLAY_SYNC_HANDBACK_RETRIES=5 # attempts before giving up and proceeding to DNS cutover
|
||
PLAY_SYNC_HANDBACK_RETRY_DELAY=60 # seconds between retry attempts
|
||
|
||
# ━━━ Failover Test ━━━
|
||
# Controlled simulation of a failover event — run manually via fallback_test.sh.
|
||
FALLBACK_TEST_BLOCK_WAIT=150 # seconds to wait after blocking connectivity
|
||
FALLBACK_TEST_HANDBACK_WAIT=360 # seconds to wait before initiating handback
|
||
|
||
# ==============================================================================================
|
||
# ── DOCKER ESSENTIALS ─────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# ━━━ Downloaders Reset ━━━
|
||
# Runs every 30 minutes via CRITICAL_MAINTENANCE_SCRIPTS.
|
||
# Clears stuck states, purges old history, prepares each download client for a clean cycle.
|
||
# Per-host URLs and API keys live in host*.conf.
|
||
DOWNLOADER_RETENTION_DAYS=7 # days — purge history older than this
|
||
|
||
# qBittorrent failsafe — removes torrents older than threshold regardless of ratio
|
||
# deleteFiles=false — removes from qBit but leaves files on disk (arr manages files)
|
||
QBIT_FAILSAFE_MIN_DAYS=180 # days — minimum age before failsafe deletion
|
||
QBIT_FAILSAFE_MIN_RATIO=0 # 0 = age only, no ratio requirement
|
||
|
||
# ━━━ Docker Daily Restart ━━━
|
||
# Containers restarted every day via DAILY_MAINTENANCE_SCRIPTS.
|
||
# Per-host lists live in host*.conf:
|
||
# HOST1_DAILY_RESTART_CONTAINERS
|
||
# HOST2_DAILY_RESTART_CONTAINERS
|
||
# detect_hosts() sets DAILY_RESTART_CONTAINERS to the correct host array at runtime.
|
||
|
||
# ━━━ Docker Weekly Restart ━━━
|
||
# Less critical services restarted weekly via WEEKLY_MAINTENANCE_SCRIPTS (Sunday 2:30am).
|
||
# Containers already stopped for weekly sync — restart adds zero extra downtime.
|
||
# Per-host lists live in host*.conf:
|
||
# HOST1_WEEKLY_RESTART_CONTAINERS
|
||
# HOST2_WEEKLY_RESTART_CONTAINERS
|
||
|
||
# ━━━ Docker Watchdog ━━━
|
||
# Continuous two-tier self-healing container monitoring.
|
||
# Started by array_started.sh — runs until array stops.
|
||
# Re-sources all three conf files each cycle — add/remove containers without restarting watchdog.
|
||
#
|
||
# Tier 1 — strict monitoring of explicitly configured containers:
|
||
# Memory hard limits — immediate restart if exceeded
|
||
# CPU thresholds — strike system, restart after CPU_FAIL_LIMIT sustained strikes
|
||
# HTTP responsiveness — strike system, restart after RESP_FAIL_LIMIT failed checks
|
||
# Required containers — must always be running, strike + skip list with auto-clear
|
||
#
|
||
# Tier 2 — global health scan of ALL running containers:
|
||
# Unhealthy status — Docker HEALTHCHECK unhealthy → restart
|
||
# OOM killed — kernel killed → restart + notify
|
||
# Crash loop detection — RestartCount climbing → notify, critical above limit
|
||
# Dead containers — remove and restart
|
||
# Unexpected exits — non-zero exit code → restart
|
||
#
|
||
# All per-host container lists live in host*.conf:
|
||
# HOST*_WATCHDOG_CONTAINERS — memory hard limits per container
|
||
# HOST*_WATCHDOG_CONTAINER_URLS — HTTP health check URLs
|
||
# HOST*_WATCHDOG_REQUIRED_CONTAINERS — must always be running
|
||
# HOST*_WATCHDOG_SCAN_IGNORE — skip in Tier 2 scan
|
||
# HOST*_WATCHDOG_DEPENDENCIES — dependency ordering for restart decisions
|
||
|
||
# Strike state file — /tmp resets on reboot (correct — no stale strikes after reboot)
|
||
WATCHDOG_STATE_FILE="$STATE_DIR/container_watchdog_state.db"
|
||
|
||
# CPU thresholds — normalised against total core count at runtime
|
||
SOFT_CPU_THRESHOLD=80 # warn at this % of total system CPU
|
||
HARD_CPU_THRESHOLD=85 # strike at this % of total system CPU
|
||
CPU_FAIL_LIMIT=2 # consecutive hard CPU strikes before container restart
|
||
|
||
# Memory soft threshold — warn when container reaches this % of its hard limit
|
||
SOFT_MEM_THRESHOLD=80
|
||
|
||
# HTTP responsiveness
|
||
RESP_FAIL_LIMIT=2 # consecutive failed checks before restart
|
||
CURL_TIMEOUT=5 # seconds per check before timeout
|
||
|
||
# Tier 2 master toggle
|
||
WATCHDOG_SCAN_ALL=true # false = only WATCHDOG_CONTAINERS + required containers
|
||
|
||
# Individual Tier 2 check toggles
|
||
WATCHDOG_RESTART_UNHEALTHY=true
|
||
WATCHDOG_RESTART_DEAD=true
|
||
WATCHDOG_RESTART_CRASHED=true
|
||
WATCHDOG_NOTIFY_OOM=true
|
||
WATCHDOG_NOTIFY_CRASHLOOP=true
|
||
|
||
# Crash loop threshold
|
||
WATCHDOG_CRASH_LIMIT=5 # RestartCount above this = critical crash loop
|
||
|
||
# Required-container strikes — consecutive down-checks before docker_watchdog.sh attempts a restart
|
||
WATCHDOG_REQUIRED_STRIKE_LIMIT=2
|
||
|
||
# Startup grace period — skip restarts while system is still booting
|
||
WATCHDOG_STARTUP_GRACE=600 # seconds after boot before watchdog acts on failures
|
||
|
||
# Restart loop protection — prevents watchdog from endlessly restarting a broken container
|
||
WATCHDOG_CONTAINER_RESTART_LIMIT=3
|
||
WATCHDOG_CONTAINER_RESTART_WINDOW=1 # rolling window in hours
|
||
WATCHDOG_CONTAINER_RESTART_LOG="${DB_DIR}/container_restart_history.db"
|
||
|
||
# Notification batching — one summary per cycle instead of one ping per event
|
||
WATCHDOG_BATCH_NOTIFY=true
|
||
|
||
# Docker daemon health check — hung daemon detection at the start of each cycle
|
||
WATCHDOG_DAEMON_TIMEOUT=20 # seconds — timeout for all docker commands
|
||
WATCHDOG_DAEMON_STRIKE_LIMIT=3 # consecutive failed checks before restart attempt
|
||
WATCHDOG_DAEMON_RESTART_WAIT=900 # seconds to wait after restart before verifying
|
||
|
||
# Appdata size monitoring — two-part catch-all for runaway growth and oversized log files.
|
||
#
|
||
# Part 1 — Growth rate (zero-config):
|
||
# Reads per-container dir totals each cycle via du, compares to previous cycle.
|
||
# Any container growing more than WATCHDOG_APPDATA_GROWTH_GB triggers a focused *.log scan
|
||
# inside that container. No per-container config required — new containers are covered
|
||
# automatically. Baseline built on first run after boot; growth detection starts cycle 2.
|
||
#
|
||
# Part 2 — Absolute log size:
|
||
# Finds *.log / *.log.* files over WATCHDOG_APPDATA_LOG_MAX_GB across all appdata paths.
|
||
# Catches logs that have already stabilised at a large size and are no longer actively growing.
|
||
#
|
||
# Strike system (reuses existing watchdog infrastructure):
|
||
# Strike 1 — warn + notify: condition first detected
|
||
# Strike 2 — warn + escalated notify: still present next cycle
|
||
# Strike 3 (WATCHDOG_APPDATA_STRIKE_LIMIT) — action cycle:
|
||
# If WATCHDOG_APPDATA_TRUNCATE_LOGS=true: truncate *.log files in-place, clear strikes
|
||
# If false: critical notify only, strikes held until condition resolves
|
||
# Condition resolves (growth stops / log drops below threshold) → strikes auto-clear
|
||
#
|
||
# HOST*_WATCHDOG_APPDATA_SIZES (in host*.conf) suppresses growth warnings for a
|
||
# container until its dir exceeds the configured ceiling. Only needed when a container
|
||
# legitimately has large stable data and you want to guarantee it never triggers a false alarm.
|
||
WATCHDOG_CHECK_APPDATA=true
|
||
WATCHDOG_APPDATA_PATHS=("/mnt/docker-unraid/appdata")
|
||
WATCHDOG_APPDATA_GROWTH_GB=2 # flag containers growing more than this per cycle
|
||
WATCHDOG_APPDATA_LOG_MAX_GB=2 # flag *.log files exceeding this size (absolute)
|
||
WATCHDOG_APPDATA_TRUNCATE_LOGS=false # set true to auto-truncate oversized *.log files on action cycle
|
||
WATCHDOG_APPDATA_STRIKE_LIMIT=3 # cycles before action fires (matches existing watchdog pattern)
|
||
WATCHDOG_APPDATA_GROWTH_FILE="$STATE_DIR/watchdog_appdata_growth.db"
|
||
STORAGE_WATCHDOG_STATE_FILE="$STATE_DIR/storage_watchdog_state.db"
|
||
|
||
# ━━━ Docker Network Connect ━━━
|
||
# Ensures custom networks exist and connects containers at array start.
|
||
# Runs once via ARRAY_START_SCRIPTS — idempotent, safe to re-run.
|
||
# Container and network lists are host-specific — defined in host*.conf:
|
||
# HOST1_NETWORK_CONNECT_CONTAINERS / HOST2_NETWORK_CONNECT_CONTAINERS
|
||
# HOST1_NETWORK_CONNECT_NETWORKS / HOST2_NETWORK_CONNECT_NETWORKS
|
||
|
||
# ==============================================================================================
|
||
# ── UNRAID ESSENTIALS ─────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# ━━━ inotify Tuning ━━━
|
||
# Linux inotify limits — applied at every array start by inotify_tuning.sh.
|
||
# Default unRAID values are very low — with many Docker containers watching files
|
||
# you can silently exhaust the limit causing containers to miss file events.
|
||
# Symptoms: containers miss file events, downloads not detected, library not updated.
|
||
# Live TV stutter is a known symptom of inotify exhaustion.
|
||
# These settings are lost on reboot — reapplied automatically at array start.
|
||
INOTIFY_MAX_INSTANCES=1024 # default: 128 — max inotify instances per user
|
||
# 1024 handles ~20-30 containers watching files simultaneously
|
||
INOTIFY_MAX_WATCHES=1048576 # default: 8192 — SHARED budget across ALL users/containers
|
||
# 524288 (512K) was previous value — raised to 1M (1048576)
|
||
# VSCode/Code-Server alone needs ~50K-200K for large workspaces
|
||
# with node_modules. All arr containers + Emby + VSCode share
|
||
# this budget. 1M is safe on 128GB RAM (~128MB kernel memory)
|
||
# If VSCode shows "unable to watch for file changes" → too low
|
||
INOTIFY_MAX_QUEUED_EVENTS=32768 # default: 16384 — max events queued before dropping
|
||
|
||
# ━━━ System Tuning Monitor ━━━
|
||
# Tracks inotify and php-fpm usage over time.
|
||
# Snapshot written every 6 hours by system_tuning_monitor.sh.
|
||
# Read by sunday_morning_coffee_report.sh for weekly peak/avg/warning summary.
|
||
INOTIFY_WARN_PCT=80 # warn if inotify instances exceed this % of limit
|
||
PHP_FPM_WARN_PCT=80 # warn if php-fpm workers exceed this % of max_children
|
||
TUNING_MONITOR_LOG="${DB_DIR}/system_tuning_history.db"
|
||
TUNING_LOG_RETENTION=30 # days before old entries are purged
|
||
|
||
# ━━━ Reboot ━━━
|
||
# Seconds of warning broadcast to logged-in users before server_reboot.sh reboots.
|
||
# Gives users time to save work — 300s = 5 minutes.
|
||
REBOOT_SLEEP=300
|
||
# Seconds to wait for graceful VM shutdown (ACPI signal via virsh) before libvirt
|
||
# stops it anyway — reboot takes priority.
|
||
REBOOT_VM_WAIT=30
|
||
|
||
# ━━━ Mover ━━━
|
||
# Seconds to wait before mover_stop.sh sends SIGTERM to the mover process.
|
||
# Gives mover time to finish current file transfer before being interrupted.
|
||
MOVER_STOP_TIMEOUT=300
|
||
|
||
# ━━━ Syslog Filter ━━━
|
||
# Path for the rsyslog filter file that suppresses Docker veth interface noise.
|
||
# Docker creates a new veth interface for each container — generates hundreds of
|
||
# log lines per hour that have no diagnostic value. Filter removes them at source.
|
||
FILTER_FILE="/etc/rsyslog.d/ignore-docker-veth.conf"
|
||
|
||
# ━━━ PCIe AER Quiet ━━━
|
||
# Master gate for pcie_aer_quiet.sh — removes dead PCIe hardware from the bus at
|
||
# array start so it stops flooding syslog with correctable AER errors.
|
||
#
|
||
# Correctable means the link recovered, so the errors are harmless — but the kernel
|
||
# logs every one. Removing the device ends it at the source. Unlike pci=noaer this
|
||
# keeps uncorrectable AER reporting alive on every other device, which the AI repair
|
||
# triage relies on to tell a real fault from this noise.
|
||
#
|
||
# Devices are listed per host in host*.conf as HOST*_PCIE_QUIET_DEVICES.
|
||
# Off by default — turn on only after filling in that list for this host.
|
||
PCIE_QUIET_ENABLED=false
|
||
|
||
# ━━━ PHP-FPM ━━━
|
||
# Higher max_children allows more concurrent PHP requests to the unRAID WebGUI.
|
||
# Default is very low — increasing it prevents WebGUI slowdowns under load.
|
||
# 250 is safe for servers with 32GB+ RAM.
|
||
PHP_CONF="/etc/php-fpm.d/www.conf"
|
||
PHP_MAX_CHILDREN=250
|
||
|
||
# ━━━ Clear Logs ━━━
|
||
# System log files cleared weekly to prevent rootfs fill over time.
|
||
# These grow continuously — without clearing they eventually consume all rootfs space.
|
||
# clear_logs.sh runs in WEEKLY_MAINTENANCE_SCRIPTS — Sunday 2:30am.
|
||
#
|
||
# Size threshold approach — only clear if log exceeds threshold.
|
||
# Avoids destroying useful recent diagnostic context when logs are small.
|
||
# LOG_MIN_SIZE_MB: skip clearing if log is under this size (not worth clearing)
|
||
# LOG_DOCKER_MAX_MB: clear a container log only if it exceeds this size
|
||
# Docker logs grow fastest on active containers (Emby, SABnzbd, Sonarr)
|
||
# 100MB per container × 30 containers = 3GB before clearing kicks in
|
||
LOG_FILES=(/var/log/syslog /var/log/messages /var/log/dmesg)
|
||
LOG_MIN_SIZE_MB=10 # skip system log if under this size (already small)
|
||
LOG_DOCKER_MAX_MB=100 # clear Docker container log only if over this size (MB)
|
||
|
||
# ━━━ Network Watchdog ━━━
|
||
# Services-layer connectivity — internet reachability, DDNS sync, Tailscale, NPM proxy.
|
||
# Host-specific values (domain, container, NPM URL) live in host*.conf.
|
||
NETWORK_WATCHDOG_ENABLED=true
|
||
NETWORK_WATCHDOG_INTERNET_URL="https://1.1.1.1"
|
||
NETWORK_WATCHDOG_INTERNET_TIMEOUT=5
|
||
NETWORK_WATCHDOG_CHECK_TAILSCALE=true
|
||
NETWORK_WATCHDOG_NPM_TIMEOUT=10
|
||
NETWORK_WATCHDOG_NPM_STRIKE_LIMIT=2
|
||
NETWORK_WATCHDOG_NPM_STATE_FILE="$STATE_DIR/network_watchdog_state.db"
|
||
|
||
# ━━━ WebGUI Watchdog ━━━
|
||
# Monitors unRAID WebGUI responsiveness — escalates through nginx restart → emhttp restart.
|
||
# Separate from docker_watchdog — this monitors the unRAID UI itself, not containers.
|
||
WEBGUI_URL="http://localhost"
|
||
WEBGUI_TIMEOUT=5 # seconds before curl gives up on WebGUI check
|
||
WEBGUI_NGINX_WAIT=15 # seconds after nginx restart before rechecking
|
||
WEBGUI_PHP_WAIT=10 # seconds after php-fpm restart before rechecking
|
||
WEBGUI_EMHTTP_WAIT=30 # seconds after emhttp restart before rechecking
|
||
|
||
# ==============================================================================================
|
||
# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# ━━━ Media Permissions ━━━
|
||
# Applied recursively by media_shares_permissions.sh.
|
||
# Runs first in DAILY_MAINTENANCE_SCRIPTS — arr cleanup depends on correct ownership.
|
||
#
|
||
# Why split directory vs file permissions:
|
||
# Directories need execute bit to enter — 755 allows owner+group+others to traverse
|
||
# Files should NOT be executable — 664 allows owner+group read/write, others read
|
||
# This is POSIX best practice for media servers, not a blanket 777 band-aid
|
||
#
|
||
# Why nobody:users ownership:
|
||
# All ecosystem containers run as PUID=99 (nobody) PGID=100 (users) on unRAID
|
||
# Files owned by nobody:users are accessible to all containers without world-write
|
||
# If this script fixes many files each run → a container has wrong PUID/PGID set
|
||
# Fix: add PUID=99 PGID=100 to the container's environment variables
|
||
#
|
||
# This script runs daily as a failsafe — even with correct container config:
|
||
# rsync may bring files with source ownership if not run with --chown
|
||
# Manual admin copies create root:root files
|
||
# New containers may not have PUID/PGID set yet
|
||
PERMISSIONS_DIR_MODE="755" # directories — traverse + list, no world-write
|
||
PERMISSIONS_FILE_MODE="664" # files — group read/write, no execute
|
||
PERMISSIONS_OWNER="nobody:users"
|
||
|
||
# Share list defined per host in host*.conf:
|
||
# HOST1_MEDIA_PERMISSION_SHARES / HOST2_MEDIA_PERMISSION_SHARES
|
||
|
||
# ━━━ Media Cleaner ━━━
|
||
# Removes junk files from media shares — two profiles: anime and media.
|
||
# Called via DAILY_MAINTENANCE_SCRIPTS. Run manually: Media/media_cleaner.sh anime|media
|
||
# Folder lists defined per host in host*.conf:
|
||
# HOST1_ANIME_CLEAN_FOLDERS / HOST2_ANIME_CLEAN_FOLDERS
|
||
# HOST1_MEDIA_CLEAN_FOLDERS / HOST2_MEDIA_CLEAN_FOLDERS
|
||
|
||
ANIME_FILE_PATTERNS=(
|
||
# ── Checksums ────────────────────────────────────────────────────────────────────────
|
||
'*.sfv' '*.md5' '*.sha1' # verification files — useless post-download
|
||
|
||
# ── Scene / download metadata ─────────────────────────────────────────────────────────
|
||
'*.url' '*.lnk' # scene links
|
||
'*.info' '*.diz' # scene description files
|
||
'*.nzb' # usenet download files
|
||
'*.torrent' # torrent files left by download clients
|
||
|
||
# ── Archives and segments ─────────────────────────────────────────────────────────────
|
||
'*.rar' '*.zip' '*.7z' '*.ace' # archives — source files not needed after extract
|
||
'*.r00' '*.r01' '*.r02' '*.r03' # multi-part rar segments
|
||
'*.r04' '*.r05' '*.r06' '*.r07'
|
||
'*.r08' '*.r09' '*.srr' # scene repair files
|
||
'*.001' '*.002' '*.003' # split archive parts
|
||
'*.gz' '*.tar' '*.bz2' # linux archives
|
||
|
||
# ── Scene samples and proofs ──────────────────────────────────────────────────────────
|
||
'*.sample*' '*.proof*' # scene samples — never needed in library
|
||
|
||
# ── Executables and scripts ───────────────────────────────────────────────────────────
|
||
'*.exe' '*.scr' '*.com' # Windows executables
|
||
'*.bat' '*.cmd' '*.vbs' '*.ps1' # Windows scripts
|
||
'*.msi' '*.dll' '*.sys' # Windows installers and system files
|
||
'*.sh' # shell scripts in media folders = suspicious
|
||
|
||
# ── Incomplete downloads ──────────────────────────────────────────────────────────────
|
||
'*.!ut' '*.!qB' # uTorrent / qBittorrent incomplete markers
|
||
'*.crdownload' '*.opdownload' # Chrome / Opera incomplete downloads
|
||
'*.part' # partial download files
|
||
|
||
# ── Sync conflicts ────────────────────────────────────────────────────────────────────
|
||
'*sync-conflict*' # Syncthing conflict copies
|
||
)
|
||
|
||
# Media = the anime set plus two media-specific extras. Defined by composition so the
|
||
# shared patterns can never drift between the two lists.
|
||
MEDIA_FILE_PATTERNS=(
|
||
"${ANIME_FILE_PATTERNS[@]}"
|
||
|
||
# ── Media-specific extras ─────────────────────────────────────────────────────────────
|
||
'*.iso' # disc images — extracted content is in library
|
||
'*.lrc' # lyrics files — handled by music apps not Emby
|
||
)
|
||
|
||
# ━━━ Arr Cleanup ━━━
|
||
# Orphan file cleanup via Lidarr, Sonarr, and Radarr APIs.
|
||
# Per-host URLs, API keys, and path maps live in host*.conf.
|
||
# detect_hosts() selects correct host vars at runtime.
|
||
#
|
||
# API versions — update MAJOR version here when script is updated for a new arr version:
|
||
# Sonarr v4 → /api/v3/series → /api/v3/episodefile?seriesId=X
|
||
# Radarr v6 → /api/v3/movie → /api/v3/moviefile?movieId=X
|
||
# Lidarr v3 → /api/v1/artist → /api/v1/trackFile?artistId=X
|
||
SONARR_VERSION_MAJOR=4
|
||
RADARR_VERSION_MAJOR=6
|
||
LIDARR_VERSION_MAJOR=3
|
||
|
||
# arr_profile_enforcer.sh — quality profile names by root folder type
|
||
# Root folder paths containing "kids" or "anime" → ARR_KIDS_PROFILE_NAME
|
||
# All other root folders → ARR_*_DEFAULT_PROFILE
|
||
ARR_KIDS_PROFILE_NAME="Kids shows"
|
||
ARR_SONARR_DEFAULT_PROFILE="Any"
|
||
ARR_RADARR_DEFAULT_PROFILE="Any (mine)"
|
||
|
||
# Lidarr shared settings
|
||
LIDARR_RELEASE_FIXER_ENABLED=true # set false to disable without removing from job list
|
||
LIDARR_LOCK_WARN_AGE=3600 # 1hr — large libraries take time, not stuck
|
||
LIDARR_ORPHAN_AGE=7 # days — files must be older than this before eligible for deletion
|
||
LIDARR_MAX_DELETE_GB=5 # require --i-know-what-im-doing if deletion exceeds this
|
||
LIDARR_MIN_TRACKED_PCT=80 # abort if tracked count drops below this % of last run
|
||
# protects against API returning partial data on a bad day
|
||
LIDARR_TRACKED_COUNT_FILE="${DB_DIR}/lidarr_tracked.count"
|
||
LIDARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan
|
||
|
||
# Lidarr tracked-data cache — shared by lidarr_cleanup.sh, lidarr_duplicate_artist_cleanup.sh,
|
||
# lidarr_missing_art.sh, lidarr_release_fixer.sh, and arr_cache_prefill.sh. See
|
||
# lidarr_get_tracked_data() in common.sh for the fresh/stale/rescan-active branching logic.
|
||
LIDARR_CACHE_FILE="${ARR_CACHE_BACKUP_DIR}/lidarr_tracked_cache.json"
|
||
LIDARR_RESCAN_DURATION_DB="${DB_DIR}/lidarr_rescan_duration.db"
|
||
LIDARR_CACHE_MAX_AGE_DAYS=1 # force a live refresh (or rescan-aware wait) past this age
|
||
ARR_PREFILL_WAIT_MINUTES=10 # array-start prefill: how long to retry reaching each arr
|
||
LIDARR_EXTENSIONS=("flac" "mp3" "m4a" "wav" "aac" "ogg" "opus" "wma")
|
||
LIDARR_PROTECTED_PATTERNS=(
|
||
# Metadata
|
||
"*.nfo" "*.tbn"
|
||
# Images — album art, artist images, Emby artwork
|
||
"*.jpg" "*.jpeg" "*.png" "*.webp" "*.svg"
|
||
"poster.*" "fanart.*" "backdrop.*" "clearlogo.*"
|
||
"banner.*" "thumb.*" "landscape.*"
|
||
"folder.*" "cover.*" "album.*" "artist.*" "disc.*"
|
||
# Lyrics
|
||
"*.lrc"
|
||
)
|
||
|
||
# Lidarr artwork fetcher settings
|
||
LIDARR_ART_MIN_SIZE=10000 # bytes — reject downloads smaller than this
|
||
LIDARR_ART_MAX_PARALLEL=4 # concurrent background download jobs
|
||
LIDARR_ART_RETRIES=2 # download retry attempts per image
|
||
LIDARR_ART_SLEEP_BETWEEN=0.2 # seconds between fanart.tv API calls
|
||
LIDARR_ART_RECHECK_DAYS=30 # days before re-querying art that upstream didn't have
|
||
LIDARR_ART_MISS_CACHE="${DB_DIR}/lidarr_art_miss_cache.tsv" # negative cache — art upstream has never had
|
||
# HOST*_FANART_API_KEY / HOST*_LASTFM_API_KEY — set in host*.conf
|
||
|
||
# Lidarr discovery settings (playback_aware_lidarr_discovery.sh)
|
||
LIDARR_DISCOVERY_THRESHOLD=70 # score to accept candidate (0-100)
|
||
LIDARR_DISCOVERY_LOOKBACK_DAYS=7 # Emby play history window in days
|
||
LIDARR_DISCOVERY_MIN_PLAYS=3 # min plays in window before evaluating an artist
|
||
LIDARR_DISCOVERY_USER_CAP_PCT=35 # max % any single user can contribute to play score (prevents one listener dominating)
|
||
LIDARR_DISCOVERY_MAX_ADDS=5 # max artists to add per run — quality over bulk
|
||
LIDARR_DISCOVERY_REJECT_COOLDOWN=30 # days before re-evaluating a rejected artist
|
||
LIDARR_DISCOVERY_HISTORY="${DB_DIR}/lidarr_discovery_history.db"
|
||
|
||
# Sonarr discovery settings (playback_aware_sonarr_discovery.sh)
|
||
SONARR_DISCOVERY_THRESHOLD=52 # score to accept candidate (0-100)
|
||
SONARR_DISCOVERY_LOOKBACK_DAYS=14 # Emby episode play history window in days (shorter than movies — TV watched more frequently)
|
||
SONARR_DISCOVERY_MAX_SEEDS=5 # max seed series from Stage 1
|
||
SONARR_DISCOVERY_MAX_ADDS=3 # max shows to add per run — TV is a larger commitment than movies
|
||
SONARR_DISCOVERY_MIN_VOTE_COUNT=50 # min TMDB votes (TV has fewer votes than movies at same popularity)
|
||
SONARR_DISCOVERY_MIN_RATING=65 # min TMDB vote_average × 10 (65 = 6.5/10)
|
||
SONARR_DISCOVERY_REJECT_COOLDOWN=60 # days before re-evaluating a rejected show
|
||
SONARR_DISCOVERY_USER_EPISODE_CAP=8 # max episodes any one user contributes to seed volume score
|
||
SONARR_DISCOVERY_MONITOR_MODE="all" # Sonarr monitor mode on add: all | future | first | latest | none
|
||
SONARR_DISCOVERY_HISTORY="${DB_DIR}/sonarr_discovery_history.db"
|
||
|
||
# Radarr discovery shared settings
|
||
RADARR_DISCOVERY_THRESHOLD=52 # score to accept candidate (0-100) — lower than Lidarr since diverse seeds rarely overlap
|
||
RADARR_DISCOVERY_LOOKBACK_DAYS=30 # Emby watch history window in days (movies rewatched less often)
|
||
RADARR_DISCOVERY_MAX_SEEDS=5 # max seed movies from Stage 1
|
||
RADARR_DISCOVERY_MAX_ADDS=5 # max movies to add per run
|
||
RADARR_DISCOVERY_MIN_VOTE_COUNT=100 # min TMDB votes to be considered a candidate
|
||
RADARR_DISCOVERY_MIN_RATING=60 # min TMDB vote_average × 10 (60 = 6.0/10)
|
||
RADARR_DISCOVERY_REJECT_COOLDOWN=60 # days before re-evaluating a rejected movie
|
||
RADARR_DISCOVERY_SEED_LIBRARIES=("Movies") # Emby libraries to draw seed movies from
|
||
RADARR_DISCOVERY_HISTORY="${DB_DIR}/radarr_discovery_history.db"
|
||
|
||
# Emby → arr sync library allowlists
|
||
# Only these Emby library names will be considered by the sync tools.
|
||
# Names must match exactly as shown in Emby > Dashboard > Libraries.
|
||
SONARR_EMBY_LIBRARIES=("Anime" "Kids Shows" "Stand-Up Comedy" "TV shows")
|
||
RADARR_EMBY_LIBRARIES=("Movies" "Kid's Movies")
|
||
|
||
# Sonarr shared settings
|
||
SONARR_ORPHAN_AGE=7 # days — files must be older than this before eligible for deletion
|
||
SONARR_MAX_DELETE_GB=10 # require --i-know-what-im-doing if deletion exceeds this
|
||
SONARR_MIN_TRACKED_PCT=80 # abort if tracked count drops below this % of last run
|
||
# protects against API returning partial data on a bad day
|
||
SONARR_TRACKED_COUNT_FILE="${DB_DIR}/sonarr_tracked.count"
|
||
SONARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan
|
||
SONARR_MOVE_POLL_TIMEOUT=3600 # seconds to wait for a single async MoveSeries command to
|
||
# reach "completed" — generous because a large series can sit
|
||
# queued behind other moves already in progress, not just its
|
||
# own copy time
|
||
CORRUPTION_SCAN_STATE_FILE="${DB_DIR}/corruption_scan_state.tsv" # clean-file skip-cache
|
||
CORRUPTION_SCAN_STRIKES_FILE="${DB_DIR}/corruption_scan_strikes.tsv" # consecutive corrupt-detection counts, keyed by host path
|
||
CORRUPTION_SCAN_STRIKE_LIMIT=2 # consecutive corrupt detections (across separate scan runs)
|
||
# required before --remediate deletes+re-searches — guards
|
||
# against a one-off ffprobe hiccup (mid-write file, NFS blip)
|
||
# triggering an unnecessary delete. Resets to 0 the moment a
|
||
# file probes clean again.
|
||
SONARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "ts" "wmv" "mov")
|
||
SONARR_PROTECTED_PATTERNS=(
|
||
# Subtitles
|
||
"*.srt" "*.sub" "*.ass" "*.ssa" "*.idx" "*.vtt"
|
||
# Metadata
|
||
"*.nfo" "*.tbn"
|
||
# Images
|
||
"*.jpg" "*.jpeg" "*.png" "*.webp" "*.svg"
|
||
"poster.*" "fanart.*" "backdrop.*" "clearlogo.*"
|
||
"banner.*" "thumb.*" "landscape.*"
|
||
# Kodi/Emby extras — not tracked by Sonarr API
|
||
"*-trailer.*" "*-featurette.*" "*-behindthescenes.*"
|
||
"*-interview.*" "*-scene.*" "*-short.*" "*-deleted.*"
|
||
"*-clip.*" "*-other.*"
|
||
# Theme songs — stored in show folder, not tracked by arr
|
||
"theme.mp3" "theme.flac" "theme.wav" "theme.m4a" "theme.mka"
|
||
"theme.mkv" "theme.mp4"
|
||
"trailer.*" "trailer *.*" "trailer-*.*"
|
||
)
|
||
|
||
# Radarr shared settings
|
||
RADARR_ORPHAN_AGE=7 # days — files must be older than this before eligible for deletion
|
||
RADARR_MAX_DELETE_GB=30 # require --i-know-what-im-doing if deletion exceeds this
|
||
RADARR_MIN_TRACKED_PCT=80 # abort if tracked count drops below this % of last run
|
||
# protects against API returning partial data on a bad day
|
||
RADARR_TRACKED_COUNT_FILE="${DB_DIR}/radarr_tracked.count"
|
||
RADARR_IMPORT_SCAN_TIMEOUT=600 # seconds to wait for pre-flight import scan
|
||
RADARR_MOVE_POLL_TIMEOUT=3600 # seconds to wait for a single async MoveMovie command to
|
||
# reach "completed" — mirrors SONARR_MOVE_POLL_TIMEOUT
|
||
RADARR_EXTENSIONS=("mkv" "mp4" "avi" "m4v" "wmv" "mov")
|
||
# Identical protected set to Sonarr's (sidecars/artwork/extras the arr doesn't track are
|
||
# the same for movies and shows). Reused by reference so the two can never drift.
|
||
RADARR_PROTECTED_PATTERNS=("${SONARR_PROTECTED_PATTERNS[@]}")
|
||
|
||
# Radarr/Sonarr TMDb/TVDB removed entry cleanup
|
||
RADARR_DROPPED_ADD_EXCLUSION=true # add removed movies to import exclusion list
|
||
SONARR_DROPPED_ADD_EXCLUSION=true # add removed series to import exclusion list
|
||
|
||
# ━━━ Download Orphan Cleaner (arr_download_orphan_cleaner.sh) ━━━
|
||
# Weekly sweep of the SABnzbd Completed folders Sonarr/Radarr import from — deletes junk and
|
||
# parse-verified already-in-library leftovers the arr queue no longer references, triggers
|
||
# import scans for anything the library is actually missing. Built 2026-07-26 after 755G of
|
||
# orphaned completed downloads (accumulating since 2022) filled HOST1's cache pool to 89%.
|
||
# Per-host dirs: HOST*_SONARR_DOWNLOAD_DIR / HOST*_RADARR_DOWNLOAD_DIR (+ _CONTAINER_DIR).
|
||
DOWNLOAD_ORPHAN_CLEANER_ENABLED=true
|
||
DOWNLOAD_ORPHAN_AGE=7 # days — entries younger than this may be mid-import, never touched
|
||
DOWNLOAD_ORPHAN_MIN_VIDEO_MB=50 # no video file above this = junk (par2 debris, samples, dead archives)
|
||
DOWNLOAD_ORPHAN_MAX_DELETE_GB=100 # abort delete pass over this — a partial queue fetch would classify
|
||
# live downloads as orphans, and a huge total is that failure's symptom;
|
||
# --i-know-what-im-doing overrides for known backlogs
|
||
|
||
# ━━━ Arr Content Classification (radarr/sonarr_classification_scan.sh) ━━━
|
||
#
|
||
# Curated lists validated against real library data 2026-07-17 — every entry here was
|
||
# checked against the actual signal ratio (kids%/anime% of titles under that studio/network)
|
||
# before being added, specifically to avoid the false-positive traps found this session:
|
||
# general umbrella studios (Walt Disney Pictures, Paramount Pictures), general streaming
|
||
# platforms (Netflix, Disney+, Hulu), and networks that carry mixed content under one name
|
||
# (The WB, UPN) are deliberately excluded even though they produce plenty of kids/anime
|
||
# content — they also produce just as much non-kids/non-anime content under the same name.
|
||
#
|
||
# Anime always takes priority over kids when both signals could apply (explicit user rule).
|
||
#
|
||
RADARR_ANIME_STUDIOS=(
|
||
"Studio Ghibli" "ufotable" "Toei Company" "Toei Animation" "MAPPA" "Madhouse"
|
||
"Production I.G" "Wit Studio" "Bones" "Trigger" "Kyoto Animation" "CloverWorks"
|
||
"A-1 Pictures" "Sunrise" "J.C.Staff" "Shaft" "Studio Pierrot" "P.A. Works"
|
||
"David Production" "OLM Inc."
|
||
)
|
||
RADARR_KIDS_STUDIOS=(
|
||
"DreamWorks Animation" "Pixar" "Disney Television Animation"
|
||
"Walt Disney Animation Studios" "DisneyToon Studios" "Walt Disney Productions"
|
||
"Walt Disney Feature Animation" "Warner Bros. Animation" "Illumination"
|
||
"Sony Pictures Animation" "Blue Sky Studios" "Mattel" "Rankin/Bass Productions"
|
||
"Mainframe Entertainment" "DreamWorks Animation Television" "ZAG Entertainment"
|
||
"Paramount Animation" "Pacific Data Images" "Cinesite Animation"
|
||
"Cartoon Network Studios" "Nickelodeon Animation Studio" "Nelvana"
|
||
"Aardman Animations"
|
||
)
|
||
# Radarr's TMDb-vote/imdbId combo reliably flags bad metadata matches (proven live —
|
||
# caught 5 genuine junk entries: 2 fake X-Men "Wolverine" titles, Silent Hill: No Escape,
|
||
# Silent Hill 4: The Room, The Purge: Behind the Series — all hasFile=false, no imdbId,
|
||
# under this vote count). Do NOT reuse this threshold/approach for Sonarr — TheTVDB's
|
||
# ratings data is far sparser than TMDb's even for completely legitimate shows (confirmed
|
||
# live: "The Pussycat Dolls Present", a real 2007 MTV show, has ratings.votes=0), so the
|
||
# same heuristic there would flag real content for removal.
|
||
RADARR_JUNK_MIN_VOTES=15
|
||
|
||
SONARR_ANIME_NETWORKS=(
|
||
"Tokyo MX" "AT-X" "TV Tokyo" "Fuji TV" "Nippon TV" "TBS (JP)" "MBS" "TV Asahi"
|
||
"WOWOW" "NHK" "Animax" "tvk" "Teletama" "Kansai TV" "ABC (JP)"
|
||
)
|
||
SONARR_KIDS_NETWORKS=(
|
||
"Cartoon Network" "Cartoon Network (CA)" "Cartoon Network (Canada)" "Nickelodeon"
|
||
"Nick Jr." "Nicktoons" "Disney Channel" "Disney Jr." "Disney XD" "Toon Disney"
|
||
"PBS" "PBS KIDS" "CBeebies" "CBBC" "YTV" "TVOkids" "Discovery Kids"
|
||
"Discovery Family" "Boomerang" "ABC Kids" "Teletoon" "Télétoon" "Milkshake!"
|
||
"Tiny Pop" "Sprout" "Universal Kids" "BabyTV"
|
||
)
|
||
|
||
# ━━━ Arr Failed/Stalled Recovery ━━━
|
||
# Auto blocklist + re-search failed imports and stalled downloads.
|
||
# Runs every 4 hours via INTERMEDIATE_MAINTENANCE_SCRIPTS — no cron entry of its own.
|
||
#
|
||
# Targets five problem types:
|
||
# importFailed — downloaded but arr couldn't import
|
||
# importPending — downloaded, stuck waiting to import (won't self-resolve)
|
||
# importBlocked — arr matched the release by grab-history ID, not title, and
|
||
# refuses to auto-import — gets a smart-import check first,
|
||
# see ARR_SMART_IMPORT_ENABLED below
|
||
# error status — serious failure not covered above
|
||
# stalled — download stuck with no connections or progress
|
||
#
|
||
# Items newer than ARR_IMPORT_RECOVERY_AGE are skipped — gives arr time to retry first.
|
||
# Per-host recovery toggles (HOST1_SONARR_RECOVERY etc.) live in host*.conf.
|
||
ARR_IMPORT_RECOVERY_AGE=6 # hours — skip items newer than this
|
||
# matches cron interval — items eligible after one missed cycle
|
||
ARR_RECOVERY_MAX_ATTEMPTS=3 # consecutive failures before an item is flagged chronic
|
||
# and auto re-search stops (still blocklisted/cleaned up)
|
||
ARR_SMART_IMPORT_ENABLED=true # importBlocked items: try importing a clean same-language
|
||
# upgrade/gap-fill directly before falling back to blocklist
|
||
# + re-search. Sonarr/Radarr only — see try_smart_import()
|
||
ARR_SMART_IMPORT_PREFERRED_LANGUAGE="English" # only treated as a match/upgrade if the
|
||
# candidate is this language; an existing file in a
|
||
# different language is always considered upgradeable
|
||
|
||
# ━━━ Arr Full Library Rescan ━━━
|
||
# arr_full_rescan.sh — weekly disk↔DB reconciliation for Lidarr/Sonarr/Radarr. See script
|
||
# header for why this exists (2026-07-16 stale-stats incident).
|
||
ARR_FULL_RESCAN_TIMEOUT=3600 # seconds per arr — a full-library walk, not a single release
|
||
|
||
# ==============================================================================================
|
||
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Session-based storage allocator using filesystem symlink indirection.
|
||
# ffmpeg resolves the symlink ONCE at session start — existing sessions never affected.
|
||
# Symlink flips between ramdisk and SSD are transparent to active streams.
|
||
#
|
||
# ⚠️ Docker mount — must use shared propagation so symlink flips work inside container:
|
||
# --mount type=bind,source=/mnt/ram-transcode,target=/ext-ram-transcode,bind-propagation=shared
|
||
# Standard path mappings use rprivate — symlink changes are NOT visible inside container.
|
||
|
||
# ━━━ Transcode Manager ━━━
|
||
# tmpfs mount point — created at array start by ramdisk_setup.sh.
|
||
# Must exist before Emby starts so the symlink resolves correctly.
|
||
RAMDISK_PATH="/mnt/ramdisk_transcodes"
|
||
|
||
# Ramdisk size and flip thresholds — defined per host in host*.conf.
|
||
# All three are coupled — if size changes, thresholds must change with it.
|
||
# HOST1_RAMDISK_SIZE / HOST2_RAMDISK_SIZE
|
||
# HOST1_RAMDISK_WARN_GB / HOST2_RAMDISK_WARN_GB ← flip to SSD at this usage
|
||
# HOST1_RAMDISK_LOW_GB / HOST2_RAMDISK_LOW_GB ← flip back to ramdisk at this usage
|
||
|
||
# Symlink that Emby points at — this path NEVER changes regardless of ramdisk/SSD state.
|
||
# Emby resolves the symlink once per session at start — symlink flips are transparent.
|
||
# Must match the container path configured in Emby's Extra Parameters.
|
||
TRANSCODE_LINK="/mnt/ram-transcode"
|
||
|
||
# SSD fallback location — defined per host in host*.conf (cache path differs per server):
|
||
# HOST1_TRANSCODE_SSD / HOST2_TRANSCODE_SSD
|
||
|
||
# Minimum free GB on SSD before allowing flip from ramdisk to SSD.
|
||
RAMDISK_SSD_MIN_GB=20
|
||
|
||
TRANSCODE_MAX_AGE=20 # minutes — HLS segment age before cleanup eligibility
|
||
TRANSCODE_ORPHAN_AGE=30 # minutes — files with no matching active session
|
||
TRANSCODE_FLIP_WARN=3 # notify if symlink flips this many times in one hour
|
||
|
||
TRANSCODE_OWNER="nobody:users"
|
||
TRANSCODE_CHMOD="755"
|
||
|
||
# Operating mode — controls symlink direction behaviour.
|
||
# smart — auto-flips between ramdisk and SSD based on thresholds (default)
|
||
# ramdisk — always uses ramdisk, warns if RAMDISK_WARN_GB exceeded but holds
|
||
# ssd — always uses SSD, never flips to ramdisk
|
||
TRANSCODE_MANAGER_MODE="smart"
|
||
|
||
# Daily statistics log — read by weekly_health_digest.sh for transcode summary.
|
||
TRANSCODE_STATE_FILE="$STATE_DIR/transcode_state.db"
|
||
TRANSCODE_DAILY_LOG="${DB_DIR}/transcode_daily.db"
|
||
TRANSCODE_LOG_RETENTION=90 # days before old entries purged
|
||
|
||
TRANSCODE_CHECK_EMBY=true
|
||
|
||
# ━━━ Transcode Server Array ━━━
|
||
# All media servers sharing the ramdisk transcode space.
|
||
# Format: "ContainerName|URL|APIKey|Type" — Type: emby | jellyfin | plex
|
||
# Entries with placeholder API keys are skipped automatically.
|
||
# ⚠️ Tdarr does NOT belong here — keep Tdarr on SSD, not ramdisk.
|
||
# Defined per host in host*.conf — Emby container names and keys differ per server:
|
||
# HOST1_TRANSCODE_SERVERS / HOST2_TRANSCODE_SERVERS
|
||
|
||
# ==============================================================================================
|
||
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
|
||
# ━━━ Certificate Monitor ━━━
|
||
# Checks SSL certificate expiry via direct openssl connection — no NPM dependency.
|
||
# Checks the actual certificate served by each domain, not what NPM thinks it has.
|
||
# Domains defined per host in host*.conf — each server monitors its own domains:
|
||
# HOST1_CERT_MONITOR_DOMAINS / HOST2_CERT_MONITOR_DOMAINS
|
||
CERT_WARN_DAYS=30 # warn when cert expires within this many days
|
||
CERT_CRIT_DAYS=7 # critical alert within this many days
|
||
CERT_TIMEOUT=10 # seconds per domain before giving up
|
||
|
||
# ━━━ Backup Verify ━━━
|
||
# Verifies rsync mirror health by comparing random file checksums between servers.
|
||
# Catches silent corruption or incomplete syncs that rsync itself wouldn't detect.
|
||
# Defined per host in host*.conf — leave empty to use HOST*_DAILY_SYNC_SHARES automatically:
|
||
# HOST1_BACKUP_VERIFY_SHARES / HOST2_BACKUP_VERIFY_SHARES
|
||
BACKUP_VERIFY_SAMPLE=10 # random files to check per share
|
||
BACKUP_VERIFY_MIN_SIZE=1M # minimum file size to include in sample
|
||
|
||
# ━━━ SMART Health ━━━
|
||
# Monitors drive SMART attributes — discovers all drives via /dev/sd* and /dev/nvme*.
|
||
# Thresholds read from /boot/config/plugins/dynamix/dynamix.cfg at runtime
|
||
# (hot/max/hotssd/maxssd) — these vars are fallback only if dynamix.cfg not found.
|
||
SMART_TEMP_WARN=45 # fallback — Celsius warn threshold
|
||
SMART_TEMP_CRIT=55 # fallback — Celsius critical threshold
|
||
# Drives to ignore defined per host in host*.conf — hardware is server-specific:
|
||
# HOST1_SMART_IGNORE_DRIVES / HOST2_SMART_IGNORE_DRIVES
|
||
|
||
# ━━━ ZFS Memory Snapshot ━━━
|
||
# Weekly ZFS pool health and memory diagnostic report — informational only.
|
||
ZFS_REPORT_LOG="${LOG_ARCHIVE_DIR}/zfs-weekly-health.log"
|
||
ZFS_REPORT_ARC_WARN_PCT=90 # warn if ARC using more than this % of its max
|
||
ZFS_REPORT_ARC_FREE_WARN_GB=10 # warn if ARC headroom (max - current) drops below this GB
|
||
ZFS_REPORT_AVAIL_WARN_GB=20 # warn if less than this GB available on ZFS pool
|
||
ZFS_REPORT_DOCKER_TOP=10 # how many top Docker containers to show by memory
|
||
# Pool ignore list defined per host in host*.conf — pool names are server-specific:
|
||
# HOST1_ZFS_REPORT_IGNORE_POOLS / HOST2_ZFS_REPORT_IGNORE_POOLS
|
||
|
||
# ━━━ Bandwidth Monitor ━━━
|
||
# Called automatically by rsync.sh after each sync — one bounded write per run.
|
||
# Tracks transfer size, duration and profile per sync for weekly summary reporting.
|
||
BANDWIDTH_LOG="${DB_DIR}/bandwidth_history.db"
|
||
BANDWIDTH_LOG_RETENTION=90 # days before old entries purged
|
||
BANDWIDTH_WARN_GB=50 # flag syncs larger than this in weekly report
|
||
|
||
# Stats files — written by cleanup and recovery scripts, read by coffee report.
|
||
# All in DATA_DIR — array always running when these are written.
|
||
ARR_CLEANUP_STATS="${DB_DIR}/arr_cleanup_stats.db" # lidarr/sonarr/radarr orphan stats
|
||
ARR_RECOVERY_STATS="${DB_DIR}/arr_recovery_stats.db" # blocklist + re-search stats
|
||
ARR_RECOVERY_FAILURE_COUNTS="${DB_DIR}/arr_recovery_failure_counts.db" # per-item chronic-failure tracking
|
||
|
||
# ━━━ Health Digest ━━━
|
||
# Aggregated system health summary — reads existing state files, no new writes.
|
||
# Three profiles control when the digest is sent:
|
||
# always — sends every run regardless of findings
|
||
# smart — sends only when DIGEST_SMART_ON_* conditions are found
|
||
# weekly — sends once per week on DIGEST_DAY only
|
||
DIGEST_PROFILE="weekly" # always | smart | weekly
|
||
DIGEST_DAY="Sunday"
|
||
DIGEST_SMART_ON_WATCHDOG=true # send if any watchdog strikes are active
|
||
DIGEST_SMART_ON_FALLBACK=true # send if fallback state is not NORMAL
|
||
DIGEST_SMART_ON_CERT_WARN=true # send if any cert is under CERT_WARN_DAYS
|
||
DIGEST_SMART_ON_BANDWIDTH=true # send if any transfer exceeded BANDWIDTH_WARN_GB
|
||
|
||
# ━━━ Emby Session Report ━━━
|
||
# Weekly Emby usage statistics via API — no persistent writes, queries fresh each run.
|
||
# URL and API key pulled from HOST*_EMBY_URL and HOST*_EMBY_API_KEY in host*.conf.
|
||
EMBY_REPORT_DAYS=7 # days to include in the report period
|
||
EMBY_REPORT_TOP_N=10 # number of top content items to show
|
||
|
||
# ==============================================================================================
|
||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Pressure reduction layer — keeps the system comfortable before things break.
|
||
# Called by watchdog_orchestrator.sh. Single-pass, not a continuous loop.
|
||
#
|
||
# ── PRESSURE LEVELS ───────────────────────────────────────────────────────────────────────────
|
||
# Level 1 (soft) — throttle SABnzbd + qBit download speeds
|
||
# Level 2 (medium) — further throttle + docker pause background containers
|
||
# Level 3 (hard) — docker stop optional containers, signal docker_watchdog to defer
|
||
#
|
||
# ── PER-HOST CONTAINER LISTS ──────────────────────────────────────────────────────────────────
|
||
# HOST*_RW_PAUSE_CONTAINERS — docker pause at medium pressure (in host*.conf)
|
||
# HOST*_RW_STOP_CONTAINERS — docker stop at hard pressure (in host*.conf)
|
||
|
||
RW_ENABLED=true
|
||
RW_STATE_FILE="$STATE_DIR/resource_watchdog_state.db"
|
||
|
||
# ━━━ Pressure Thresholds ━━━
|
||
# Graduated RAM response — resource_watchdog acts before system_watchdog reboots.
|
||
# RW_RAM_SOFT_GB > RW_RAM_MEDIUM_GB > RW_RAM_HARD_GB > SYS_WATCHDOG_MEM_GB always
|
||
RW_RAM_SOFT_GB=12 # throttle start — reduce background load
|
||
RW_RAM_MEDIUM_GB=8 # pause background containers
|
||
RW_RAM_HARD_GB=6 # stop optional containers (was SYS_WATCHDOG_MEM_SHUTDOWN_GB)
|
||
RW_RAM_RECOVER_GB=20 # RAM must reach this before restoring hard-stopped containers
|
||
|
||
# Load average thresholds — multiplier × core count
|
||
RW_LOAD_SOFT_MULTIPLIER=2.0 # soft pressure: 2× cores sustained
|
||
RW_LOAD_MEDIUM_MULTIPLIER=3.0 # medium pressure: 3× cores sustained
|
||
|
||
# Consecutive runs at lower pressure before de-escalating
|
||
RW_RECOVER_CYCLES=3
|
||
|
||
# ━━━ SABnzbd Throttle ━━━
|
||
# Speed values: "50M" = 50 MB/s, "0" = unlimited
|
||
RW_SABNZBD_ENABLED=true
|
||
RW_SABNZBD_SPEED_SOFT="50M"
|
||
RW_SABNZBD_SPEED_MEDIUM="10M"
|
||
|
||
# ━━━ qBittorrent Throttle ━━━
|
||
# KB/s — 0 = unlimited
|
||
RW_QBIT_ENABLED=true
|
||
RW_QBIT_DL_SOFT=51200 # 50 MB/s
|
||
RW_QBIT_DL_MEDIUM=10240 # 10 MB/s
|
||
|
||
# ━━━ Critical Containers ━━━
|
||
# Never paused or stopped regardless of pressure level.
|
||
# Keep DNS, auth, media serving, and live TV always running.
|
||
RW_CRITICAL_CONTAINERS=(
|
||
"NginxProxyManager" # reverse proxy — internet access
|
||
"Authelia" # auth — nothing accessible without it
|
||
"Authelia-Secondary"
|
||
"Mariadb-Authelia" # Authelia dependency
|
||
"Mariadb-Authelia-Secondary"
|
||
"Redis-Authelia" # Authelia dependency
|
||
"Redis-Authelia-Secondary"
|
||
"AdGuard-Home" # DNS — all LAN resolution
|
||
"Emby" # media server — Live TV buffering
|
||
"Dispatcharr" # Live TV scheduler — loses state if stopped
|
||
"Dispatcharr-Basic"
|
||
"Dispatcharr-Iptv-Users"
|
||
)
|
||
|
||
# ==============================================================================================
|
||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Single-pass system health check — last line of defense before a crash.
|
||
# Called by watchdog_orchestrator.sh every 15 minutes. NOT started by array_started.sh.
|
||
# Re-sources config at each orchestrator run — config changes take effect immediately.
|
||
#
|
||
# ── THREE-TIER RESPONSE SYSTEM ────────────────────────────────────────────────────────────────
|
||
# CRITICAL — bypass ALL strikes, reboot immediately
|
||
# Docker daemon down, rootfs 100%, kernel oops, FD exhaustion, /boot read-only
|
||
#
|
||
# URGENT — bypass strikes only when OOM confirms active crisis
|
||
# RAM < MEM_GB AND OOM kills >= OOM_LIMIT in this cycle → reboot NOW
|
||
# Without OOM confirmation → normal strike system
|
||
#
|
||
# STANDARD — strike system (N consecutive failures → reboot)
|
||
# RAM tiers, high load, CPU temp, zombies, /var/log, /tmp, containers
|
||
#
|
||
# ── RAM ───────────────────────────────────────────────────────────────────────────────────────
|
||
# SYS_WATCHDOG_MEM_GB — strike system → reboot (or OOM bypass)
|
||
# Warn/shutdown/recover RAM tiers are handled by resource_watchdog.sh
|
||
|
||
# ━━━ State Files ━━━
|
||
SYS_WATCHDOG_STATE_FILE="$STATE_DIR/system_watchdog_state.db"
|
||
DOCKER_WATCHDOG_FAILED_FILE="$STATE_DIR/docker_watchdog_failed.db"
|
||
DOCKER_WATCHDOG_INTENTIONAL_FILE="$STATE_DIR/docker_intentional_stops.db"
|
||
SYS_WATCHDOG_REBOOT_LOG="$STATE_DIR/system_watchdog_reboots.db"
|
||
SYS_WATCHDOG_OOM_FILE="$STATE_DIR/system_watchdog_oom.db"
|
||
|
||
# ━━━ Strike and Reboot Loop Settings ━━━
|
||
# Strike system: a check must fail this many consecutive cycles before action is taken.
|
||
# Single spikes (one bad reading) are ignored — sustained problems trigger reboot.
|
||
SYS_WATCHDOG_STRIKE_LIMIT=2 # consecutive failures before reboot trigger
|
||
|
||
# Reboot loop protection — if system keeps rebooting something is seriously wrong.
|
||
# After REBOOT_LIMIT reboots in REBOOT_WINDOW_HRS → shutdown instead of reboot.
|
||
SYS_WATCHDOG_REBOOT_LIMIT=3
|
||
SYS_WATCHDOG_REBOOT_WINDOW_HRS=12
|
||
|
||
# ━━━ RAM Reboot Threshold ━━━
|
||
# Reboot trigger only — warn/shutdown/recover handled by resource_watchdog.sh
|
||
# RW_RAM_HARD_GB > SYS_WATCHDOG_MEM_GB always (RM acts before watchdog reboots)
|
||
SYS_WATCHDOG_MEM_GB=4 # strike system → reboot
|
||
|
||
# ━━━ OOM Bypass Settings ━━━
|
||
# OOM bypass: if RAM is critically low AND kernel OOM kills exceed this threshold
|
||
# in a single cycle → bypass strike system and reboot immediately.
|
||
# Rate-based: kills per 5-minute cycle, not absolute count.
|
||
# Rationale: 1-2 kills = docker_watchdog handles it ✅
|
||
# 3+ kills while RAM critical = system dying faster than watchdogs can heal ✅
|
||
SYS_WATCHDOG_OOM_LIMIT=3 # OOM kills in one cycle to trigger bypass
|
||
|
||
# ━━━ Thresholds ━━━
|
||
# Set at "about to become unstable" — not "things are a bit high".
|
||
|
||
# rootfs (/) usage — two levels: strike at 95%, critical bypass at 99%
|
||
SYS_WATCHDOG_ROOTFS_PCT=95 # standard strike threshold
|
||
SYS_WATCHDOG_ROOTFS_CRITICAL_PCT=99 # bypass strikes — truly full, writes failing
|
||
|
||
# /var/log usage — log spam indicates something broken
|
||
SYS_WATCHDOG_LOG_PCT=95
|
||
|
||
# /tmp usage — tmpfs fills from downloads, lock files fail at 100%
|
||
SYS_WATCHDOG_TMP_PCT=90 # warn + attempt clear
|
||
SYS_WATCHDOG_TMP_CRITICAL_PCT=98 # bypass strikes if clear failed
|
||
|
||
# ZFS ARC pinned percentage — ARC not releasing after reclaim = memory stuck
|
||
SYS_WATCHDOG_ARC_PINNED_PCT=98
|
||
SYS_WATCHDOG_ARC_RELEASE_PCT=95
|
||
|
||
# Load average multiplier — threshold = MULTIPLIER × CPU core count
|
||
# MULTIPLIER=3 on 16-core = load of 48 before triggering
|
||
SYS_WATCHDOG_LOAD_MULTIPLIER=3
|
||
|
||
# Zombie process count — large numbers = serious process management failure
|
||
SYS_WATCHDOG_ZOMBIE_LIMIT=50
|
||
|
||
# CPU temperature — sustained high temp causes throttling or kernel panic
|
||
SYS_WATCHDOG_CPU_TEMP_MAX=95
|
||
|
||
# File descriptor exhaustion — system-wide FD limit near exhaustion
|
||
# New connections fail silently, Docker can't spawn processes, SSH fails
|
||
SYS_WATCHDOG_FD_CRITICAL_PCT=95 # bypass strikes — critical tier
|
||
|
||
# Runaway process — single non-container process consuming excessive CPU
|
||
# Multiple strikes before action — single spikes are normal
|
||
SYS_WATCHDOG_RUNAWAY_CPU_PCT=90 # % single process must sustain
|
||
SYS_WATCHDOG_RUNAWAY_STRIKES=3 # consecutive cycles before warning
|
||
|
||
# Array disk errors — accumulating mdstat errors = disk failing NOW
|
||
SYS_WATCHDOG_MDSTAT_ERROR_LIMIT=5 # new errors in one cycle before acting
|
||
|
||
# ━━━ Check Toggles ━━━
|
||
# Per-host — moved to host*.conf
|
||
# Different servers may have different hardware, NICs, and check requirements
|
||
# See HOST*_SYS_WATCHDOG_CHECK_* in host*.conf
|
||
|
||
# ━━━ Abort Toggles ━━━
|
||
# Conditions that prevent reboot even when a threshold is hit.
|
||
# CRITICAL tier bypasses these — a truly critical condition reboots regardless.
|
||
# true = abort standard reboot if this condition is active
|
||
# false = reboot anyway
|
||
SYS_WATCHDOG_ABORT_ON_ZFS_UNHEALTHY=true # rebooting with bad pool risks data loss
|
||
SYS_WATCHDOG_ABORT_ON_PARITY=false # aborting parity better than crashing mid-check
|
||
SYS_WATCHDOG_ABORT_ON_MOVER=false # aborting move better than crashing mid-move
|
||
|
||
# ==============================================================================================
|
||
# ── AI / RAG ──────────────────────────────────────────────────────────────────────────────────
|
||
# ━━━ Conf Backups ━━━
|
||
# Every write through the plugin copies the conf aside first, to CONF_BACKUP_DIR with an
|
||
# ISO-8601 stamp. The confs are gitignored, so that directory is the whole recovery path — there
|
||
# is no history to revert to. A backup that cannot be taken cancels the write.
|
||
# Oldest are pruned past this count, per conf file.
|
||
CONF_BACKUP_RETAIN=30
|
||
|
||
# ==============================================================================================
|
||
# Varaverk works exactly as well with AI off as with it on. Nothing below is required for any
|
||
# script to function — every feature that can lean on AI has a complete non-AI path, and the
|
||
# AI path is an enhancement layered on top. If Ollama is unreachable, callers proceed without it.
|
||
#
|
||
# AI_ENABLED is necessary but not sufficient. Every feature stays individually off until it has
|
||
# earned it — narration for months before anything is allowed near a decision.
|
||
|
||
# ━━━ AI Master Switch ━━━
|
||
# Fail-closed: anything other than the literal "true" means off.
|
||
AI_ENABLED=false
|
||
AI_CONNECT_TIMEOUT=5 # seconds — probe when resolving which node has Ollama
|
||
AI_REQUEST_TIMEOUT=240 # seconds — must clear a cold model load
|
||
AI_RESOLVE_CACHE_TTL=300 # seconds — don't re-probe the mesh every invocation
|
||
AI_MAX_RETRIES=1 # AI is enhancement; do not retry hard
|
||
|
||
# ━━━ AI Retrieval Index ━━━
|
||
# The RAG index over this repo's own headers and documentation. Regenerable in minutes and
|
||
# gitignored — it is derived data, never a source of truth.
|
||
#
|
||
# Only git-tracked files are ever indexed. Configurations/, State_Files/ and data/ are
|
||
# gitignored, which is what makes it structurally impossible for a credential to reach the
|
||
# index: the files holding them were never in the repo. Do not "improve" this to a filesystem
|
||
# walk — an embedded secret cannot be rotated out of a vector.
|
||
AI_INDEX_DB="${AI_DATA_DIR}/ai_index.db"
|
||
AI_INDEX_BATCH=32 # chunks per embed request
|
||
# A pull is the only thing that changes tracked files on a server, so it is the only moment the
|
||
# index can go stale — and staleness is invisible in the answers, which keep citing the old
|
||
# text with full confidence. Incremental: unchanged files are skipped, a no-op run is ~66ms.
|
||
# Also gated on AI_ENABLED, and ai_index.sh refuses on its own unless that is true.
|
||
AI_INDEX_ON_PULL=true # re-index after a git pull that changed tracked files
|
||
AI_SEARCH_K=8 # chunks retrieved per query
|
||
AI_SEARCH_PER_FILE=3 # cap per file so one document cannot fill the context
|
||
|
||
# ━━━ AI Memory ━━━
|
||
# A small operator-maintained file the assistant is given at the start of every conversation:
|
||
# who you are, how this install is set up, decisions already made, things it should stop asking.
|
||
#
|
||
# Injected into the prompt, never indexed. It lives under DATA_DIR, which is gitignored — that
|
||
# is deliberate and load-bearing. Indexing it would embed a file that changes constantly, and
|
||
# vector similarity is the wrong way to retrieve "things I was told to remember"; it also keeps
|
||
# personal notes out of a repository that gets pushed.
|
||
#
|
||
# The character cap is a context budget, not a style guide. At 16384 the retrieved passages,
|
||
# the model's reasoning and the conversation history are already competing; memory takes its
|
||
# share off the top of every single turn, so keep it short and factual.
|
||
# Two slots, and which one a fact lands in decides how much authority it carries.
|
||
#
|
||
# assisted written by the operator. The prompt tells the model to prefer it over retrieved
|
||
# passages — a human asserting a fact about their own machine outranks a doc that
|
||
# may be stale. This is the file the AI tab edits.
|
||
# learned proposed by the assistant, accepted by the operator. A hint only: retrieval
|
||
# overrules it, and it is trimmed first when the budget bites. Deliberately the
|
||
# weaker seat, because memory the model writes AND the prompt ranks above the source
|
||
# code would let one wrong conclusion restate itself forever.
|
||
#
|
||
# AI_MEMORY_FILE is the pre-split name and is still honoured: while the assisted file does not
|
||
# exist, the legacy path is read instead, so an upgrade loses nothing.
|
||
AI_MEMORY_ASSISTED_FILE="${AI_DATA_DIR}/mem_assisted.md"
|
||
AI_MEMORY_LEARNED_FILE="${AI_DATA_DIR}/mem_learned.md"
|
||
AI_MEMORY_FILE="${AI_DATA_DIR}/ai_memory.md" # legacy — read only if the assisted file is absent
|
||
|
||
# Which profiles each slot is given. "*" is all of them. Narrowing is how a General Chat question
|
||
# about bash syntax stops carrying this machine's PCIe topology and disk serials.
|
||
AI_MEMORY_ASSISTED_PROFILES="*"
|
||
AI_MEMORY_LEARNED_PROFILES="*"
|
||
|
||
AI_MEMORY_MAX_CHARS=4000 # ~1000 tokens — the budget for both slots together
|
||
# A ceiling on the learned slot alone, well under the total. A store that grows on its own would
|
||
# otherwise end up occupying the whole budget, and the operator's own memory is what would get
|
||
# truncated away — the exact inversion of which one matters.
|
||
AI_MEMORY_LEARNED_MAX_CHARS=1200
|
||
|
||
# ━━━ AI Learned Memory ━━━
|
||
# Lets the assistant propose facts worth keeping. It never writes memory directly: a candidate is
|
||
# filed in data/ai/mem_proposals/ and the operator accepts or dismisses it, exactly as findings
|
||
# work. Accepted text lands in the learned slot, which the prompt ranks BELOW retrieval.
|
||
#
|
||
# The reason for the indirection: memory rides on every future prompt. A model allowed to write
|
||
# its own — and told to prefer it over the passages — would restate a wrong conclusion forever,
|
||
# reading its own claim back as evidence. A bad proposal must cost one dismissal, not that.
|
||
#
|
||
# Costs nothing while off: the instruction that asks for a candidate is only added to the prompt
|
||
# when this is true, so a disabled feature is genuinely absent rather than merely ignored.
|
||
AI_MEMORY_LEARN_ENABLED=false
|
||
|
||
# Writes accepted candidates without asking. Cannot outrank its parent — with proposing off this
|
||
# does nothing. Leave it false until the proposals have proven good for a while.
|
||
AI_MEMORY_LEARN_AUTO_ACCEPT=false
|
||
|
||
# ━━━ AI Stored Conversations ━━━
|
||
# How many past conversations the AI tab and the Monitor tab's AI row keep. One JSON file per
|
||
# conversation under DATA_DIR/ai_chats, saved automatically when a turn completes; the oldest
|
||
# drop off once the count is exceeded.
|
||
#
|
||
# A cap rather than a retention age. These are read by picking one out of a short list, and a
|
||
# list you have to scroll is a list you stop using — the useful window is the handful of things
|
||
# you were recently working on, which is a count, not a date.
|
||
#
|
||
# Not indexed and never retrieved into a prompt on their own. A stored chat only re-enters the
|
||
# model's context when the operator explicitly reopens it, and it is re-validated per message on
|
||
# the way in, exactly as live history is.
|
||
AI_CHAT_HISTORY_MAX=10 # conversations kept — clamped to 1-50
|
||
|
||
# ━━━ AI Token Accounting ━━━
|
||
# One row per completed turn, appended by whichever path ran it — the WebGUI worker and the
|
||
# ai_query.sh CLI both write here, so the totals are not silently the tab's alone. Format is
|
||
# pipe-delimited to match the other data/*.db files:
|
||
#
|
||
# date|time|host|profile|source|prompt_tokens|completion_tokens|tok_s
|
||
#
|
||
# The host column records where the turn ran, not where it is read. Each host keeps its own
|
||
# data/ and nothing syncs it, so a host only ever sees its own rows — the column is there so
|
||
# the file is already shaped right if the partner payload fetch is ever extended to carry it.
|
||
# A column added later cannot be backfilled.
|
||
#
|
||
# Retention is by row count rather than age: pruning is considered only when the file passes a
|
||
# size threshold, so an ordinary turn costs one stat() and an append.
|
||
AI_TOKEN_DB="${AI_DATA_DIR}/ai_token_history.db"
|
||
AI_TOKEN_RETAIN_ROWS=20000 # oldest rows dropped past this — years of ordinary use
|
||
|
||
# AI/ai_token_sync.sh pulls each partner's ledger into the tmpfs cache the tab reads, so the
|
||
# fleet total is a fleet total. Same trick conf_sync.sh uses for partner confs, minus the push:
|
||
# nothing here is needed by anyone else, so the reader fetches its own data and controls its
|
||
# own freshness. An unreachable partner is a quiet skip, not a warning — a partner is expected
|
||
# to be down for long stretches, and a four-hourly warn trains you to ignore the script.
|
||
AI_TOKEN_SYNC_ENABLED=true
|
||
|
||
# ━━━ AI Feature Toggles ━━━
|
||
# Tier 1 is narration — it cannot change a decision. Tier 2 adds context to a decision a script
|
||
# already made. Tier 3 assists a human. Enable in that order, and give each one weeks.
|
||
AI_ASSIST_REPORTS=false # tier 1 — digest / coffee report narration
|
||
AI_ASSIST_WATCHDOG=false # tier 2 — file a finding when a watchdog counter passes its limit (needs AI_REPAIR_ENABLED)
|
||
AI_ASSIST_DISCOVERY=false # tier 2 — discovery / classification judgement calls
|
||
AI_ASSIST_CLEANUP=false # tier 2 — orphan and stuck-import triage
|
||
AI_ASSIST_ONBOARD=false # tier 3 — onboarding / settings assistance
|
||
|
||
# ━━━ AI Repair ━━━
|
||
# Reads the log of a job that has finished, turns known error shapes into findings, and offers
|
||
# or applies a repair. Two switches, because detecting and repairing are separate things to
|
||
# trust.
|
||
#
|
||
# AI_REPAIR_ENABLED on its own reads, files findings and offers fixes, and writes nothing. Run
|
||
# it there first — long enough to read what it finds and disagree with some of it.
|
||
#
|
||
# AI_REPAIR_AUTOFIX_ENABLED is what allows a value to be written unattended, and only ever a
|
||
# value a probe has answered on. Never a toggle: whether something should be switched on is a
|
||
# decision about intent, and a probe cannot prove intent the way it can prove a port answers.
|
||
AI_REPAIR_ENABLED=false
|
||
AI_REPAIR_AUTOFIX_ENABLED=false
|
||
|
||
# ━━━ AI Repair Findings ━━━
|
||
# Misconfigurations found in this installation, as opposed to defects in Varaverk — those go to
|
||
# ai_bugs. A finding is open until the configuration is right, and closes itself when the probe
|
||
# that proved the fault starts passing. Closed ones are kept for a while, because "this happened
|
||
# before and here is what fixed it" is worth more than the disk. Open findings are never pruned:
|
||
# an unresolved problem does not stop mattering because it is old.
|
||
AI_FINDING_RETAIN_DAYS=90
|
||
# Seconds a single probe may take. Nothing is written to conf that has not answered a probe, so
|
||
# this is the budget for proving a candidate — kept short because a sweep may try several, and
|
||
# an address worth switching to answers quickly or is not worth switching to.
|
||
AI_PROBE_TIMEOUT=4
|
||
# Tell the operator when a finding needs them. Only findings the sweep cannot resolve itself are
|
||
# announced, one notification for all of them rather than one each, and each is announced once —
|
||
# it stays quiet through every later pass until the fault changes or gets worse. Delivery is
|
||
# whatever notify() is set up to use: NOTIFY_UNRAID and the host's Discord webhook.
|
||
#
|
||
# On by default, unlike the two switches above. Those gate reading and writing, which are things
|
||
# to be trusted first. This gates telling someone, which is the reason for having looked.
|
||
AI_REPAIR_NOTIFY_ENABLED=true
|
||
|
||
# ━━━ AI Repair: what it reads ━━━
|
||
# Beyond Varaverk's own logs and the arrs' health endpoints.
|
||
#
|
||
# The system log catches what Varaverk cannot see about itself — a disk throwing I/O errors, a
|
||
# filesystem going read-only, a PCIe link retraining every two minutes. Container restarts and
|
||
# OOM kills are deliberately left to docker_watchdog, which already notifies on them.
|
||
#
|
||
# Container logs catch the opposite blind spot: docker_watchdog watches whether a container is
|
||
# up and answering, which a container that has been unable to write to its database all day
|
||
# passes perfectly. Only environment faults are matched — disk full, read-only filesystem,
|
||
# corrupt database, fd limits, expired certificates — because those strings come from libc, the
|
||
# kernel and SQLite and mean the same thing in all fifty containers. Anything app-specific
|
||
# belongs in that app's own health endpoint.
|
||
#
|
||
# Both are bounded by time (since the last pass) and by a line cap, so a flood costs one pass.
|
||
# Tools/ai_log_check.sh replays this host's real logs against the patterns — run it after
|
||
# changing any of them.
|
||
AI_REPAIR_SYSLOG_ENABLED=true
|
||
AI_REPAIR_SYSLOG_MAX_LINES=4000
|
||
AI_REPAIR_CONTAINER_LOGS_ENABLED=true
|
||
AI_REPAIR_CONTAINER_LOG_LINES=400
|
||
|
||
# ━━━ AI Web Search ━━━
|
||
# General Chat only. Every other profile either reads this installation or changes it; chat holds
|
||
# no capability at all, and that is exactly why searching is the one thing it may do — a read that
|
||
# leaves the house is safe on the profile that cannot act on what it finds. The Varaverk assistant
|
||
# deliberately does not get it: its contract is that answers come from this installation's own
|
||
# documents, and a web result there is an answer that looks sourced and is not.
|
||
#
|
||
# Off by default, and not because it is dangerous. Searching sends the operator's question to
|
||
# something outside this house, and that is their decision to make rather than a default to
|
||
# inherit. Nothing turns it on.
|
||
#
|
||
# Asked for per turn as well — there is a checkbox on the AI tab, and a question is only searched
|
||
# when it is ticked. A question about this machine hands off to the assistant before the search
|
||
# would run, so it never reaches the internet even with the box ticked.
|
||
#
|
||
# Provider: degoog | searxng | brave | tavily
|
||
# degoog self-hosted, no key — set HOST*_DEGOOG_URL. Aggregates several engines and returns
|
||
# them merged. The default, and the only one verified against a live service here
|
||
# searxng self-hosted, no key, no third party — set HOST*_SEARXNG_URL, and enable format: [json]
|
||
# in its own settings.yml, which the default image ships with off
|
||
# brave HOST*_WEB_SEARCH_API_KEY, free tier available
|
||
# tavily HOST*_WEB_SEARCH_API_KEY
|
||
AI_WEB_SEARCH_ENABLED=false
|
||
AI_WEB_SEARCH_PROVIDER=degoog
|
||
AI_WEB_SEARCH_RESULTS=4
|
||
AI_WEB_SEARCH_TIMEOUT=6
|
||
|
||
# ━━━ AI Chat Caution ━━━
|
||
# General Chat cannot see this installation, and web search gives it confident-sounding material
|
||
# about the outside world. The danger is the overlap: a question about THIS machine, answered
|
||
# from a general page about Unraid, reads exactly like an answer about this machine.
|
||
#
|
||
# Detection is grammatical rather than topical — possessives, "this box", state questions, "what
|
||
# happened last night" — because the topics are unbounded and the grammar is not. When it fires,
|
||
# chat keeps its caution and defers instead of answering from the web.
|
||
#
|
||
# Pipe-separated extra phrases for what grammar misses: a nickname for the box, a share name,
|
||
# anything that in practice means "mine". Matched as literal text, not as patterns.
|
||
AI_CHAT_MY_SYSTEM_PHRASES=""
|
||
|
||
# ━━━ Bug Reports ━━━
|
||
# A bug the assistant files is written here and goes nowhere until the owner sends it. Nothing is
|
||
# ever transmitted automatically — the report is shown in full, read-only, and sending is a
|
||
# second, separate press.
|
||
#
|
||
# Two destinations, and they are not a fallback chain. Fetching code from several mirrors is
|
||
# harmless because they all serve the same thing; sending a report is not, because the
|
||
# destinations are different people. Local ships OFF so an install that has configured nothing
|
||
# reports upstream rather than silently into a tracker nobody reads.
|
||
#
|
||
# LOCAL ON — reports go to your own Gitea (see HOSTN_BUG_REPORT_* in host*.conf) and stay there.
|
||
# They do NOT reach the Varaverk maintainer. Turn it on if you want your own backlog.
|
||
# LOCAL OFF — reports open a prefilled GitHub issue you submit under your own account.
|
||
BUG_REPORT_LOCAL_ENABLED=false
|
||
BUG_REPORT_GITHUB_REPO="FailedProxy/Varaverk"
|
||
|
||
# ━━━ AI Conf Write Access ━━━
|
||
# Separate switch from AI_ENABLED, off by default, and an explicit key whitelist. Never paths,
|
||
# never credentials, never a container name. An empty whitelist means no writes regardless of
|
||
# the toggle.
|
||
AI_CONF_WRITE_ENABLED=false
|
||
AI_CONF_WRITE_KEYS=()
|
||
|
||
# ==============================================================================================
|
||
# ──────────────────────── End Of User Variables ───────────────────────────────────────────────
|
||
# ==============================================================================================
|