diff --git a/Plugin/unraid/Tools/ui_map_build.php b/Plugin/unraid/Tools/ui_map_build.php
new file mode 100644
index 0000000..16498d3
--- /dev/null
+++ b/Plugin/unraid/Tools/ui_map_build.php
@@ -0,0 +1,217 @@
+ ['file','subsection','fields']
+foreach (vv_get_conf_files() as $file) {
+ foreach (vv_conf_all_groups($file) as $g) {
+ if (empty($g['fields'])) continue;
+ $sections[$file . "\0" . $g['subsection']] = $g;
+ }
+}
+
+// ── Route 1: the Scheduler, one script at a time ─────────────────────────────────────────────
+// A section may be reached through several scripts — a shared threshold belongs to whichever
+// scripts read it — so routes accumulate rather than overwrite.
+$routes = []; // section key => list of human routes
+foreach (VV_SCRIPT_CONF_SECTIONS as $script => $subs) {
+ $label = vv_pretty_label(basename($script, '.sh'));
+ foreach ((array) $subs as $sub) {
+ foreach ($sections as $k => $g) {
+ if (strcasecmp($g['subsection'], $sub) !== 0) continue;
+ $routes[$k][] = "Scheduler tab → **{$label}** → Config → *{$g['subsection']}*";
+ }
+ }
+}
+
+// ── Route 2: pages that show sections by subject ─────────────────────────────────────────────
+foreach (VV_UI_SECTION_SURFACES as $surface) {
+ $re = '/\b' . preg_quote((string) $surface['match'], '/') . '\b/i';
+ foreach ($sections as $k => $g) {
+ if (!preg_match($re, (string) $g['subsection'])) continue;
+ $routes[$k][] = $surface['route'] . " → *{$g['subsection']}*";
+ }
+}
+
+// ── Route 3: pages with a purpose-built control for one named setting ────────────────────────
+// Read out of the page source rather than declared, so a control that is added or removed moves
+// the map with it. The pattern is the literal key in a change payload or a toggle call — the one
+// shape these pages have in common. Keys assembled at runtime are invisible here and correctly
+// fall through to the conf-only list rather than being guessed at.
+// Every key this host's confs actually define, so a declared route can be checked against
+// reality rather than trusted.
+$known = [];
+foreach ($sections as $g) foreach ($g['fields'] as $f) $known[$f['key']] = true;
+
+// This machine's host slot, for substituting HOSTN in declared keys.
+$slot = 'HOST1';
+foreach (vv_get_conf_files() as $f) {
+ if (preg_match('/^host(\d+)\.conf$/i', $f, $hm)) { $slot = 'HOST' . $hm[1]; break; }
+}
+
+$keyRoutes = []; // KEY => list of routes
+$stale = [];
+foreach (VV_UI_PAGE_ROUTES as $page => $spec) {
+ $route = is_array($spec) ? $spec['route'] : $spec;
+ $src = @file_get_contents(dirname(__DIR__) . '/pages/' . $page);
+ if ($src === false) { fwrite(STDERR, "note: $page not found, skipped\n"); continue; }
+ if (preg_match_all('/(?:key|name)\s*:\s*\'([A-Z][A-Z0-9_]{3,})\'|\(this,\s*\'([A-Z][A-Z0-9_]{3,})\'\)/',
+ $src, $m, PREG_SET_ORDER)) {
+ foreach ($m as $hit) {
+ $key = $hit[1] !== '' ? $hit[1] : ($hit[2] ?? '');
+ if ($key !== '' && isset($known[$key])) $keyRoutes[$key][] = $route;
+ }
+ }
+ foreach ((array) (is_array($spec) ? ($spec['also'] ?? []) : []) as $decl) {
+ $key = str_replace('HOSTN', $slot, $decl);
+ if (isset($known[$key])) { $keyRoutes[$key][] = $route; continue; }
+ $stale[] = "$page declares $decl (→ $key) which no conf defines";
+ }
+}
+
+// ── How each control is described to someone who has to find it ──────────────────────────────
+const UI_CONTROL_WORDS = [
+ 'bool' => 'a switch',
+ 'int' => 'a number box',
+ 'enum' => 'a dropdown',
+ 'secret' => 'a masked box with a **Show** button',
+ 'lines' => 'a list, one entry per line',
+ 'path' => 'a text box',
+ 'text' => 'a text box',
+];
+
+$md = "# Where every setting lives in the web UI\n\n";
+$md .= "Generated by `Tools/ui_map_build.php` — do not edit by hand.\n\n";
+$md .= "Every setting below can be changed in the browser. Nothing here needs a conf file opened\n"
+ . "over SSH, and the routes are what to tell someone who asks where a setting is.\n\n";
+$md .= "Two surfaces show settings, and which one holds a given section depends on what the\n"
+ . "section is about:\n\n";
+$md .= "- **The Scheduler tab** shows the settings belonging to one script. Pick the script, open\n"
+ . " **Config**, and its sections appear there.\n";
+$md .= "- **The AI tab** shows the AI sections together under **Settings → Configuration**.\n\n";
+$md .= "Both write through the same guarded path: the change is validated, the conf is backed up,\n"
+ . "the result is syntax-checked and read back, and a bad write is rolled back.\n\n";
+$md .= "A setting is edited by finding its row and changing the control described below. The Save\n"
+ . "button sends only what was actually changed.\n\n---\n";
+
+// A section with no section-level route may still have per-key routes, if a page carries a
+// purpose-built control for some of its settings. That is a reachable section — just one whose
+// route is stated per row rather than once at the top.
+foreach ($sections as $k => $g) {
+ if (!empty($routes[$k])) continue;
+ foreach ($g['fields'] as $f) {
+ if (!empty($keyRoutes[$f['key']])) { $routes[$k][] = '__perkey__'; break; }
+ }
+}
+
+$reachable = 0; $unreachable = [];
+ksort($sections);
+foreach ($sections as $k => $g) {
+ if (empty($routes[$k])) { $unreachable[] = $g; continue; }
+ $reachable++;
+
+ $md .= "\n## " . $g['subsection'] . "\n\n";
+ $md .= "In `" . $g['file'] . "`.\n\n";
+
+ $seen = array_values(array_diff(array_unique($routes[$k]), ['__perkey__']));
+ if (!$seen) {
+ $md .= "No single page shows this section. Individual settings below carry their own route.\n\n";
+ } else {
+ $md .= count($seen) === 1 ? "Route: " . $seen[0] . "\n\n"
+ : "Reachable from:\n\n" . implode("\n", array_map(fn($r) => "- $r", $seen)) . "\n\n";
+ }
+
+ $md .= "| Setting | Control | Where | What it does |\n|---|---|---|---|\n";
+ foreach ($g['fields'] as $f) {
+ $ctl = UI_CONTROL_WORDS[$f['widget'] ?? 'text'] ?? 'a text box';
+ if (($f['widget'] ?? '') === 'enum' && !empty($f['choices'])) {
+ $ctl .= ' (' . implode(', ', array_map(fn($c) => $c['value'], $f['choices'])) . ')';
+ }
+ if (!empty($f['unit'])) $ctl .= ', in ' . $f['unit'];
+ if (isset($f['min'])) $ctl .= ', ' . $f['min'] . '–' . $f['max'];
+ // The conf's own comment. Newlines and pipes would break the table row.
+ $desc = trim(preg_replace('/\s+/', ' ', (string) ($f['desc'] ?? '')));
+ $desc = str_replace('|', '\\|', $desc);
+ if (mb_strlen($desc) > 400) $desc = mb_substr($desc, 0, 397) . '…';
+ // A per-key route wins for that row: a purpose-built control is a better answer than
+ // "somewhere in this section", and it is often on a different page entirely.
+ $where = !empty($keyRoutes[$f['key']])
+ ? implode(', ', array_unique($keyRoutes[$f['key']]))
+ : ($seen ? 'in this section' : '—');
+ $md .= '| `' . $f['key'] . '` | ' . $ctl . ' | ' . $where . ' | '
+ . ($desc !== '' ? $desc : '—') . " |\n";
+ }
+}
+
+if ($unreachable) {
+ $md .= "\n---\n\n## Settings with no route through the UI\n\n";
+ $md .= "These sections are not rendered by any page, so they can only be changed by editing\n"
+ . "the conf file. If one of these is asked about, say so plainly rather than inventing a\n"
+ . "route — mapping it into the Scheduler is a code change, not a setting.\n\n";
+ foreach ($unreachable as $g) {
+ $keys = implode(', ', array_map(fn($f) => '`' . $f['key'] . '`', $g['fields']));
+ $md .= '- **' . $g['subsection'] . '** (`' . $g['file'] . "`) — $keys\n";
+ }
+}
+
+$existing = is_readable($outAbs) ? file_get_contents($outAbs) : null;
+$same = $existing !== null && $existing === $md;
+
+foreach ($stale as $s) fwrite(STDERR, "STALE ROUTE: $s\n");
+printf("%d sections reachable, %d with no UI route\n", $reachable, count($unreachable));
+printf("%d settings documented\n", array_sum(array_map(
+ fn($k) => empty($routes[$k]) ? 0 : count($sections[$k]['fields']), array_keys($sections))));
+
+if ($check) {
+ echo $same ? "up to date\n" : "OUT OF DATE — re-run without --check\n";
+ exit($same ? 0 : 1);
+}
+if ($same) { echo "no change\n"; exit(0); }
+
+if (@file_put_contents($outAbs, $md) === false) {
+ fwrite(STDERR, "could not write $outAbs\n");
+ exit(1);
+}
+printf("wrote %s (%d bytes)\n", $outRel, strlen($md));
diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php
index 0917498..bdb1990 100644
--- a/Plugin/unraid/include/confform.php
+++ b/Plugin/unraid/include/confform.php
@@ -94,6 +94,50 @@ require_once __DIR__ . '/config.php';
// confform.php — script→conf-section mapping, field parsing, and write-back.
+// Which page shows conf sections by subject rather than by script, and the route a person would
+// be told to follow to reach it. Declared once because two things need it and they must agree:
+// the page itself builds its ?sections= query from this, and Tools/ui_map_build.php turns it into
+// navigation instructions the assistant can give. A route written down in only one of those two
+// places is a route that goes stale the first time a card moves.
+//
+// `match` is the whole-word needle matched against section headers — see api/confform.php.
+const VV_UI_SECTION_SURFACES = [
+ [
+ 'match' => 'ai',
+ 'tab' => 'AI',
+ 'route' => 'AI tab → Settings → Configuration',
+ ],
+ [
+ 'match' => 'partnership',
+ 'tab' => 'Partnership',
+ // Array fields only — the card is one collapsible block per list, because these are
+ // container lists dozens of lines long and a flat form would be unreadable.
+ 'route' => 'Partnership tab → Array Settings',
+ ],
+];
+
+// Pages that edit named keys rather than whole sections — a purpose-built control for one
+// setting, not a form over a conf section. Only the route is declared: the keys themselves are
+// read out of the page source by Tools/ui_map_build.php, so a control added or removed changes
+// the map without anyone remembering to update a list.
+//
+// A page missing from here is not broken; its settings simply appear as conf-only in the map,
+// which is the honest answer until someone gives it a route.
+// `also` lists keys the page assembles at runtime, which no amount of reading the source will
+// reveal — settings.php writes HOST1_DISCORD_WEBHOOK through a PHP variable holding the host
+// slot. Written with HOSTN, substituted per machine, the same convention conf_upgrade uses. The
+// generator checks each one exists and complains if it does not, so a stale entry is loud rather
+// than a route to a control that was removed.
+const VV_UI_PAGE_ROUTES = [
+ 'settings.php' => ['route' => 'Settings tab',
+ 'also' => ['HOSTN_DISCORD_WEBHOOK', 'HOSTN_STORAGE_MODE_INTERNAL']],
+ 'fallback.php' => ['route' => 'Fallback tab'],
+ 'rsync.php' => ['route' => 'Rsync tab'],
+ 'arrs.php' => ['route' => 'Arrs tab'],
+ 'watchdog.php' => ['route' => 'Watchdog tab'],
+ 'docker.php' => ['route' => 'Docker tab'],
+];
+
// Map: script relative id → subsection names (must match # ━━━ Name ━━━ or # ── Name ── headers).
const VV_SCRIPT_CONF_SECTIONS = [
// Orchestrators
diff --git a/Plugin/unraid/pages/ai.php b/Plugin/unraid/pages/ai.php
index 5e677c7..abc4bd9 100644
--- a/Plugin/unraid/pages/ai.php
+++ b/Plugin/unraid/pages/ai.php
@@ -461,6 +461,10 @@ vv_ai_chat_markup('vv-ai', [
// the Scheduler uses, so there is one allowlist and one write path rather than an AI-shaped
// copy of both — api/ai.php deliberately has no conf-writing action at all.
const API_CONF = '/plugins/varaverk/api/confform.php';
+ // Which sections this tab claims, from the one registry that also generates the UI map. Hard
+ // coding "ai" here again would be a second place for the answer to live, and the map would go
+ // on describing a route this page had stopped taking.
+ const CONF_SECTIONS = = json_encode(VV_UI_SECTION_SURFACES[0]['match'] ?? 'ai') ?>;
// The conversation itself — profiles, transcript, composer, source viewer, storage — is
// include/ai_chat.php. What remains on this page is everything that surrounds it and exists
@@ -1098,7 +1102,7 @@ vv_ai_chat_markup('vv-ai', [
function loadConf() {
if (confLoaded) return;
confLoaded = true;
- fetch(API_CONF + '?sections=ai').then(r => r.json())
+ fetch(API_CONF + '?sections=' + encodeURIComponent(CONF_SECTIONS)).then(r => r.json())
.then(d => {
if (!d.ok) { $('vv-ai-conf').innerHTML = `
${esc(d.error || 'could not be read')}
`; return; }
VvConfUI.render('vv-ai-conf', d.groups || []);
diff --git a/Plugin/unraid/pages/readme/ui-map.md b/Plugin/unraid/pages/readme/ui-map.md
new file mode 100644
index 0000000..c87ac16
--- /dev/null
+++ b/Plugin/unraid/pages/readme/ui-map.md
@@ -0,0 +1,1445 @@
+# Where every setting lives in the web UI
+
+Generated by `Tools/ui_map_build.php` — do not edit by hand.
+
+Every setting below can be changed in the browser. Nothing here needs a conf file opened
+over SSH, and the routes are what to tell someone who asks where a setting is.
+
+Two surfaces show settings, and which one holds a given section depends on what the
+section is about:
+
+- **The Scheduler tab** shows the settings belonging to one script. Pick the script, open
+ **Config**, and its sections appear there.
+- **The AI tab** shows the AI sections together under **Settings → Configuration**.
+
+Both write through the same guarded path: the change is validated, the conf is backed up,
+the result is syntax-checked and read back, and a bad write is rolled back.
+
+A setting is edited by finding its row and changing the control described below. The Save
+button sends only what was actually changed.
+
+---
+
+## Arr Recovery Toggles
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Recovery Toggles*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_LIDARR_RECOVERY` | a switch | in this section | HOST1 only — exits cleanly on HOST2 |
+| `HOST1_SONARR_RECOVERY` | a switch | in this section | — |
+| `HOST1_RADARR_RECOVERY` | a switch | in this section | — |
+
+## Backup Verify
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Backup Verify** → Config → *Backup Verify*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_BACKUP_VERIFY_SHARES` | a list, one entry per line | in this section | Leave empty to use HOST1_DAILY_SYNC_SHARES automatically. |
+
+## Certificate Monitor
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Cert Monitor** → Config → *Certificate Monitor*
+- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_CERT_MONITOR_DOMAINS` | a list, one entry per line | in this section | — |
+
+## Corruption Scan
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Arr Corruption Scan** → Config → *Corruption Scan*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_FFPROBE_CONTAINER` | a text box | in this section | — |
+| `HOST1_FFPROBE_BIN` | a text box | in this section | — |
+| `HOST1_FFPROBE_PATH_MAP` | a list, one entry per line | in this section | — |
+
+## Critical Sync Shares
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Critical Sync Maintenance** → Config → *Critical Sync Shares*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_CRITICAL_SYNC_SHARES` | a list, one entry per line | in this section | Appdata shares synced every 30 minutes. Format: "/path/to/share" or "/path/to/share\|profile-name" |
+
+## DDNS
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Fallback** → Config → *DDNS*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_DDNS_CONTAINERS` | a list, one entry per line | in this section | DDNS containers this host manages. |
+
+## Daily Sync Shares
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Daily Sync Maintenance** → Config → *Daily Sync Shares*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_DAILY_SYNC_SHARES` | a list, one entry per line | in this section | Media shares this host pushes to all other nodes every night. Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed. |
+
+## Docker Daily Restart
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Docker Daily Restart** → Config → *Docker Daily Restart*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_DAILY_RESTART_CONTAINERS` | a list, one entry per line | in this section | — |
+
+## Docker Network Connect
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Docker Network Connect** → Config → *Docker Network Connect*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_NETWORK_CONNECT_CONTAINERS` | a list, one entry per line | in this section | — |
+| `HOST1_NETWORK_CONNECT_NETWORKS` | a list, one entry per line | in this section | — |
+
+## Docker Watchdog
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_WATCHDOG_CONTAINERS` | a list, one entry per line | in this section | Memory hard limits in MB — immediate restart if exceeded. 20GB=20480 16GB=16384 8GB=8192 4GB=4096 2GB=2048 1GB=1024 |
+| `HOST1_WATCHDOG_CONTAINER_URLS` | a list, one entry per line | in this section | HTTP health check URLs — checked every cycle. |
+| `HOST1_WATCHDOG_CONTAINER_API_CHECKS` | a list, one entry per line | in this section | API-level health checks — catches HTTP-200-but-internally-frozen containers (DB lock, deadlocked thread, etc.) that a basic HTTP check above would miss. Pick an endpoint that forces a real DB round-trip — a lightweight status endpoint may stay 200 even while the rest of the app is locked up. Format: ["ContainerName"]="url\|APIKey" |
+| `HOST1_WATCHDOG_REQUIRED_CONTAINERS` | a list, one entry per line | in this section | Required containers — must always be running. |
+| `HOST1_WATCHDOG_SCAN_IGNORE` | a list, one entry per line | in this section | Containers to skip in Tier 2 global scan. |
+| `HOST1_WATCHDOG_DEPENDENCIES` | a list, one entry per line | in this section | Dependency ordering — skip restarting a container if its dependency is also down. |
+| `HOST1_WATCHDOG_APPDATA_SIZES` | a list, one entry per line | in this section | Per-container appdata growth suppress ceilings in MB. |
+
+## Docker Weekly Restart
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Docker Weekly Restart** → Config → *Docker Weekly Restart*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_WEEKLY_RESTART_CONTAINERS` | a list, one entry per line | in this section | — |
+
+## Downloaders
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Downloaders Reset** → Config → *Downloaders*
+- Scheduler tab → **Resource Watchdog** → Config → *Downloaders*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_SLSKD_URL` | a text box | in this section | — |
+| `HOST1_SLSKD_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_SLSKD_FAILED_IMPORTS_DIR` | a text box | in this section | — |
+| `HOST1_SABNZBD_URL` | a text box | in this section | — |
+| `HOST1_SABNZBD_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_QBIT_URL` | a text box | in this section | — |
+| `HOST1_QBIT_USERNAME` | a text box | in this section | — |
+| `HOST1_QBIT_PASSWORD` | a masked box with a **Show** button | in this section | — |
+
+## Emby
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Emby Session Report** → Config → *Emby*
+- Scheduler tab → **Server Reboot** → Config → *Emby*
+- Scheduler tab → **Playback Aware Lidarr Discovery** → Config → *Emby*
+- Scheduler tab → **Playback Aware Radarr Discovery** → Config → *Emby*
+- Scheduler tab → **Playback Aware Sonarr Discovery** → Config → *Emby*
+- Scheduler tab → **Emby To Lidarr Sync** → Config → *Emby*
+- Scheduler tab → **Emby To Radarr Sync** → Config → *Emby*
+- Scheduler tab → **Emby To Sonarr Sync** → Config → *Emby*
+- Scheduler tab → **Emby Database Repair** → Config → *Emby*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_EMBY_CONTAINER` | a text box | in this section | — |
+| `HOST1_EMBY_URL` | a text box | in this section | — |
+| `HOST1_EMBY_API_KEY` | a masked box with a **Show** button | in this section | — |
+
+## Intermediate Sync Shares
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Intermediate Sync Maintenance** → Config → *Intermediate Sync Shares*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_INTERMEDIATE_SYNC_SHARES` | a list, one entry per line | in this section | Shares synced every 4 hours. Leave empty to skip mid-day rsync. |
+
+## Internet Loss
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Fallback** → Config → *Internet Loss*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `FALLBACK_HOST1_STOP_ON_NO_NET` | a list, one entry per line | in this section | Containers stopped immediately when internet is lost. |
+
+## Lidarr
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Lidarr Cleanup** → Config → *Lidarr*
+- Scheduler tab → **Lidarr Missing Art** → Config → *Lidarr*
+- Scheduler tab → **Lidarr Release Fixer** → Config → *Lidarr*
+- Scheduler tab → **Lidarr Duplicate Artist Cleanup** → Config → *Lidarr*
+- Scheduler tab → **Playback Aware Lidarr Discovery** → Config → *Lidarr*
+- Scheduler tab → **Emby To Lidarr Sync** → Config → *Lidarr*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_LIDARR_URL` | a text box | in this section | — |
+| `HOST1_LIDARR_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_LIDARR_MUSIC_ROOT` | a text box | in this section | — |
+| `HOST1_FANART_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_LASTFM_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_LIDARR_PATH_MAP` | a list, one entry per line | in this section | — |
+
+## Media Cleaner
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Media Cleaner** → Config → *Media Cleaner*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_ANIME_CLEAN_FOLDERS` | a list, one entry per line | in this section | — |
+| `HOST1_MEDIA_CLEAN_FOLDERS` | a list, one entry per line | in this section | — |
+
+## Media Permissions
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Media Shares Permissions** → Config → *Media Permissions*
+- Scheduler tab → **Bulk Permissions Repair** → Config → *Media Permissions*
+- Scheduler tab → **Trailer Folder Migration** → Config → *Media Permissions*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_MEDIA_PERMISSION_SHARES` | a list, one entry per line | in this section | — |
+
+## Network Watchdog
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_NETWORK_WATCHDOG_DDNS_DOMAIN` | a text box | in this section | — |
+| `HOST1_NETWORK_WATCHDOG_DDNS_CONTAINER` | a text box | in this section | — |
+| `HOST1_NETWORK_WATCHDOG_NPM_URL` | a text box | in this section | — |
+
+## Notifications
+
+In `host1.conf`.
+
+No single page shows this section. Individual settings below carry their own route.
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_DISCORD_WEBHOOK` | a text box | Settings tab | Discord webhook — leave blank to disable. |
+
+## Ollama
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Index** → Config → *Ollama*
+- Scheduler tab → **Ai Query** → Config → *Ollama*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_OLLAMA_URL` | a text box | in this section | e.g. http://localhost:11434 — empty if no local Ollama |
+| `HOST1_OLLAMA_GPU_UUID` | a text box | in this section | pins Ollama to one card on multi-GPU hosts |
+| `HOST1_OLLAMA_MODEL` | a text box | in this section | generation — must fully offload; see README-AI.md |
+
+## PARTNERSHIP
+
+In `host1.conf`.
+
+Route: Partnership tab → Array Settings → *PARTNERSHIP*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_PARTNERSHIP_AUTH_WEBUIS` | a list, one entry per line | in this section | Auth containers reconfigured on onboard/offboard. Format: "ContainerName\|WebUIPort" |
+| `HOST1_PARTNERSHIP_AUTH_STACK` | a list, one entry per line | in this section | XML templates pushed to mirror during onboard — auth stack. Dependencies (databases) must come before apps that depend on them. |
+| `HOST1_PARTNERSHIP_ARR_STACK` | a list, one entry per line | in this section | XML templates pushed to mirror during onboard — arr stack. |
+| `HOST1_PARTNERSHIP_SERVICES_STACK` | a list, one entry per line | in this section | XML templates pushed to mirror during onboard — other services (not auth, not arr). |
+| `HOST1_PARTNERSHIP_MIRROR_BACKUPS` | a list, one entry per line | in this section | Paths the partner should collect during the grace window after offboard. |
+| `HOST1_PARTNERSHIP_OWN_CONTAINERS` | a list, one entry per line | in this section | Containers parked on this server when partnership is active. |
+| `HOST1_PARTNERSHIP_REPLACE_CONTAINERS` | a list, one entry per line | in this section | Containers stopped on THIS server before deploying the mirror's stack on onboard. |
+| `HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS` | a list, one entry per line | in this section | Arr containers stopped on this server when mirror's arr stack is deployed. |
+| `HOST1_PARTNERSHIP_PROVISION_EMBY_ADMIN` | a switch | in this section | owner controls whether Emby is shared |
+| `HOST1_PARTNERSHIP_EMBY_PORT` | a number box | in this section | — |
+| `HOST1_PARTNERSHIP_EMBY_ADMIN_USER` | a text box | in this section | this server's desired Emby username |
+| `HOST1_PARTNERSHIP_EMBY_ADMIN_PASS` | a masked box with a **Show** button | in this section | this server's desired Emby password |
+
+## Radarr
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Radarr Cleanup** → Config → *Radarr*
+- Scheduler tab → **Radarr Classification Scan** → Config → *Radarr*
+- Scheduler tab → **Playback Aware Radarr Discovery** → Config → *Radarr*
+- Scheduler tab → **Radarr Tmdb Removed** → Config → *Radarr*
+- Scheduler tab → **Emby To Radarr Sync** → Config → *Radarr*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_RADARR_URL` | a text box | in this section | — |
+| `HOST1_RADARR_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_TMDB_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_RADARR_MOVIES_ROOT` | a text box | in this section | — |
+| `HOST1_RADARR_GENERAL_ROOT` | a text box | in this section | rootFolderPath literal for the general root — target for reverse-kids-leak moves |
+| `HOST1_RADARR_KIDS_ROOT` | a text box | in this section | rootFolderPath literal, as reported by Radarr API |
+| `HOST1_RADARR_ANIME_ROOT` | a text box | in this section | — |
+| `HOST1_RADARR_DOWNLOAD_DIR` | a text box | in this section | — |
+| `HOST1_RADARR_DOWNLOAD_CONTAINER_DIR` | a text box | in this section | — |
+| `HOST1_RADARR_PATH_MAP` | a list, one entry per line | in this section | — |
+
+## SMART Health
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Smart Health** → Config → *SMART Health*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_SMART_IGNORE_DRIVES` | a list, one entry per line | in this section | — |
+
+## SYSTEM WATCHDOG
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Watchdog Orchestrator** → Config → *SYSTEM WATCHDOG*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_SYS_WATCHDOG_NIC` | a text box | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_DOCKER_DAEMON` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_ROOTFS` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_KERNEL_OOPS` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_FD` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_BOOT` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_OOM` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_RAM` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_LOG` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_ARC` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_CPU_TEMP` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_LOAD` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_ZOMBIES` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_CONTAINERS` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_TMP` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_MDSTAT` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_NETWORK` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_SSHD` | a switch | in this section | — |
+| `HOST1_SYS_WATCHDOG_CHECK_RUNAWAY` | a switch | in this section | — |
+
+## Sonarr
+
+In `host1.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Sonarr Cleanup** → Config → *Sonarr*
+- Scheduler tab → **Sonarr Classification Scan** → Config → *Sonarr*
+- Scheduler tab → **Playback Aware Sonarr Discovery** → Config → *Sonarr*
+- Scheduler tab → **Sonarr Tvdb Removed** → Config → *Sonarr*
+- Scheduler tab → **Emby To Sonarr Sync** → Config → *Sonarr*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_SONARR_URL` | a text box | in this section | — |
+| `HOST1_SONARR_API_KEY` | a masked box with a **Show** button | in this section | — |
+| `HOST1_SONARR_TV_ROOT` | a text box | in this section | — |
+| `HOST1_SONARR_GENERAL_ROOT` | a text box | in this section | rootFolderPath literal for the general root — target for reverse-kids-leak moves |
+| `HOST1_SONARR_KIDS_ROOT` | a text box | in this section | rootFolderPath literal, as reported by Sonarr API |
+| `HOST1_SONARR_ANIME_ROOT` | a text box | in this section | — |
+| `HOST1_SONARR_DOWNLOAD_DIR` | a text box | in this section | — |
+| `HOST1_SONARR_DOWNLOAD_CONTAINER_DIR` | a text box | in this section | — |
+| `HOST1_SONARR_PATH_MAP` | a list, one entry per line | in this section | — |
+
+## Storage mode
+
+In `host1.conf`.
+
+No single page shows this section. Individual settings below carry their own route.
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_STORAGE_MODE_INTERNAL` | a switch | Settings tab | Controls where Varaverk stores scripts, conf, and state files. true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct) false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime) Auto-detected from boot device transport on first setup. To change: Settings → Storage → Migrate. |
+
+## Weekly Sync Shares
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Weekly Sync Maintenance** → Config → *Weekly Sync Shares*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_WEEKLY_SYNC_SHARES` | a list, one entry per line | in this section | Appdata shares synced during the weekly maintenance window. Profiles (emby, critical-data) drive container stops — define in master.conf. |
+
+## ZFS Report
+
+In `host1.conf`.
+
+Route: Scheduler tab → **Zfs Memory Snapshot** → Config → *ZFS Report*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `HOST1_ZFS_REPORT_IGNORE_POOLS` | a list, one entry per line | in this section | — |
+
+## AI Conf Write Access
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Conf Write Access*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_CONF_WRITE_ENABLED` | a switch | in this section | 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_KEYS` | a list, one entry per line | in this section | — |
+
+## AI Feature Toggles
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Token Sync** → Config → *AI Feature Toggles*
+- AI tab → Settings → Configuration → *AI Feature Toggles*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_ASSIST_REPORTS` | a switch | in this section | tier 1 — digest / coffee report narration |
+| `AI_ASSIST_WATCHDOG` | a switch | in this section | tier 2 — context on a flagged condition |
+| `AI_ASSIST_DISCOVERY` | a switch | in this section | tier 2 — discovery / classification judgement calls |
+| `AI_ASSIST_CLEANUP` | a switch | in this section | tier 2 — orphan and stuck-import triage |
+| `AI_ASSIST_ONBOARD` | a switch | in this section | tier 3 — onboarding / settings assistance |
+
+## AI Learned Memory
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Learned Memory*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_MEMORY_LEARN_ENABLED` | a switch | in this section | 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_AUTO_ACCEPT` | a switch | in this section | 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 Master Switch
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Index** → Config → *AI Master Switch*
+- Scheduler tab → **Ai Query** → Config → *AI Master Switch*
+- Scheduler tab → **Ai Repair Sweep** → Config → *AI Master Switch*
+- AI tab → Settings → Configuration → *AI Master Switch*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_ENABLED` | a switch | Settings tab | Fail-closed: anything other than the literal "true" means off. |
+| `AI_CONNECT_TIMEOUT` | a number box, in seconds | in this section | seconds — probe when resolving which node has Ollama |
+| `AI_REQUEST_TIMEOUT` | a number box, in seconds | in this section | seconds — must clear a cold model load |
+| `AI_RESOLVE_CACHE_TTL` | a number box, in seconds | in this section | seconds — don't re-probe the mesh every invocation |
+| `AI_MAX_RETRIES` | a number box | in this section | AI is enhancement; do not retry hard |
+
+## AI Memory
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Memory*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_MEMORY_ASSISTED_FILE` | a text box | in this section | 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_LEARNED_FILE` | a text box | in this section | — |
+| `AI_MEMORY_FILE` | a text box | in this section | — |
+| `AI_MEMORY_ASSISTED_PROFILES` | a text box | in this section | 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_LEARNED_PROFILES` | a text box | in this section | — |
+| `AI_MEMORY_MAX_CHARS` | a number box | in this section | ~1000 tokens — truncated with a notice if exceeded |
+| `AI_MEMORY_LEARNED_MAX_CHARS` | a number box | in this section | 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 Repair
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Repair Sweep** → Config → *AI Repair*
+- AI tab → Settings → Configuration → *AI Repair*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_REPAIR_ENABLED` | a switch | in this section | 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_AUTOFIX_ENABLED` | a switch | in this section | — |
+
+## AI Repair Findings
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Repair Sweep** → Config → *AI Repair Findings*
+- AI tab → Settings → Configuration → *AI Repair Findings*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_FINDING_RETAIN_DAYS` | a number box | in this section | 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 probl… |
+| `AI_PROBE_TIMEOUT` | a number box, in seconds | in this section | 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_REPAIR_NOTIFY_ENABLED` | a switch | in this section | 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: what it reads
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Repair: what it reads*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_REPAIR_SYSLOG_ENABLED` | a switch | in this section | 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_MAX_LINES` | a number box | in this section | — |
+| `AI_REPAIR_CONTAINER_LOGS_ENABLED` | a switch | in this section | — |
+| `AI_REPAIR_CONTAINER_LOG_LINES` | a number box | in this section | — |
+
+## AI Retrieval Index
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Ai Index** → Config → *AI Retrieval Index*
+- Scheduler tab → **Ai Query** → Config → *AI Retrieval Index*
+- AI tab → Settings → Configuration → *AI Retrieval Index*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_INDEX_DB` | a text box | in this section | 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_BATCH` | a number box | in this section | chunks per embed request |
+| `AI_INDEX_ON_PULL` | a switch | in this section | re-index after a git pull that changed tracked files |
+| `AI_SEARCH_K` | a number box | in this section | chunks retrieved per query |
+| `AI_SEARCH_PER_FILE` | a number box | in this section | cap per file so one document cannot fill the context |
+
+## AI Stored Conversations
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Stored Conversations*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_CHAT_HISTORY_MAX` | a number box, in conversations, 1–50 | in this section | conversations kept — clamped to 1-50 |
+
+## AI Token Accounting
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Token Accounting*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_TOKEN_DB` | a text box | in this section | 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_RETAIN_ROWS` | a number box | in this section | oldest rows dropped past this — years of ordinary use |
+| `AI_TOKEN_SYNC_ENABLED` | a switch | in this section | 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 stretche… |
+
+## AI Web Search
+
+In `master.conf`.
+
+Route: AI tab → Settings → Configuration → *AI Web Search*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `AI_WEB_SEARCH_ENABLED` | a switch | in this section | 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_SEARC… |
+| `AI_WEB_SEARCH_PROVIDER` | a text box | in this section | — |
+| `AI_WEB_SEARCH_RESULTS` | a number box | in this section | — |
+| `AI_WEB_SEARCH_TIMEOUT` | a number box | in this section | — |
+
+## Arr Cleanup
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Radarr Cleanup** → Config → *Arr Cleanup*
+- Scheduler tab → **Lidarr Cleanup** → Config → *Arr Cleanup*
+- Scheduler tab → **Sonarr Cleanup** → Config → *Arr Cleanup*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SONARR_VERSION_MAJOR` | a number box | in this section | 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 |
+| `RADARR_VERSION_MAJOR` | a number box | in this section | — |
+| `LIDARR_VERSION_MAJOR` | a number box | in this section | — |
+| `ARR_KIDS_PROFILE_NAME` | a text box | in this section | 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_SONARR_DEFAULT_PROFILE` | a text box | in this section | — |
+| `ARR_RADARR_DEFAULT_PROFILE` | a text box | in this section | — |
+| `LIDARR_RELEASE_FIXER_ENABLED` | a switch | in this section | set false to disable without removing from job list |
+| `LIDARR_LOCK_WARN_AGE` | a number box | in this section | 1hr — large libraries take time, not stuck |
+| `LIDARR_ORPHAN_AGE` | a number box, in days | in this section | days — files must be older than this before eligible for deletion |
+| `LIDARR_MAX_DELETE_GB` | a number box | in this section | require --i-know-what-im-doing if deletion exceeds this |
+| `LIDARR_MIN_TRACKED_PCT` | a number box | in this section | abort if tracked count drops below this % of last run |
+| `LIDARR_TRACKED_COUNT_FILE` | a text box | in this section | — |
+| `LIDARR_IMPORT_SCAN_TIMEOUT` | a number box, in seconds | in this section | seconds to wait for pre-flight import scan |
+| `LIDARR_CACHE_FILE` | a text box | in this section | 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_RESCAN_DURATION_DB` | a text box | in this section | — |
+| `LIDARR_CACHE_MAX_AGE_DAYS` | a number box | in this section | force a live refresh (or rescan-aware wait) past this age |
+| `ARR_PREFILL_WAIT_MINUTES` | a number box | in this section | array-start prefill: how long to retry reaching each arr |
+| `LIDARR_EXTENSIONS` | a list, one entry per line | in this section | — |
+| `LIDARR_PROTECTED_PATTERNS` | a list, one entry per line | in this section | — |
+| `LIDARR_ART_MIN_SIZE` | a number box | in this section | bytes — reject downloads smaller than this |
+| `LIDARR_ART_MAX_PARALLEL` | a number box | in this section | concurrent background download jobs |
+| `LIDARR_ART_RETRIES` | a number box | in this section | download retry attempts per image |
+| `LIDARR_ART_SLEEP_BETWEEN` | a text box | in this section | seconds between fanart.tv API calls |
+| `LIDARR_ART_RECHECK_DAYS` | a number box, in days | in this section | days before re-querying art that upstream didn't have |
+| `LIDARR_ART_MISS_CACHE` | a text box | in this section | negative cache — art upstream has never had |
+| `LIDARR_DISCOVERY_THRESHOLD` | a number box | in this section | score to accept candidate (0-100) |
+| `LIDARR_DISCOVERY_LOOKBACK_DAYS` | a number box | in this section | Emby play history window in days |
+| `LIDARR_DISCOVERY_MIN_PLAYS` | a number box | in this section | min plays in window before evaluating an artist |
+| `LIDARR_DISCOVERY_USER_CAP_PCT` | a number box | in this section | max % any single user can contribute to play score (prevents one listener dominating) |
+| `LIDARR_DISCOVERY_MAX_ADDS` | a number box | in this section | max artists to add per run — quality over bulk |
+| `LIDARR_DISCOVERY_REJECT_COOLDOWN` | a number box, in days | in this section | days before re-evaluating a rejected artist |
+| `LIDARR_DISCOVERY_HISTORY` | a text box | in this section | — |
+| `SONARR_DISCOVERY_THRESHOLD` | a number box | in this section | score to accept candidate (0-100) |
+| `SONARR_DISCOVERY_LOOKBACK_DAYS` | a number box | in this section | Emby episode play history window in days (shorter than movies — TV watched more frequently) |
+| `SONARR_DISCOVERY_MAX_SEEDS` | a number box | in this section | max seed series from Stage 1 |
+| `SONARR_DISCOVERY_MAX_ADDS` | a number box | in this section | max shows to add per run — TV is a larger commitment than movies |
+| `SONARR_DISCOVERY_MIN_VOTE_COUNT` | a number box | in this section | min TMDB votes (TV has fewer votes than movies at same popularity) |
+| `SONARR_DISCOVERY_MIN_RATING` | a number box | in this section | min TMDB vote_average × 10 (65 = 6.5/10) |
+| `SONARR_DISCOVERY_REJECT_COOLDOWN` | a number box, in days | in this section | days before re-evaluating a rejected show |
+| `SONARR_DISCOVERY_USER_EPISODE_CAP` | a number box | in this section | max episodes any one user contributes to seed volume score |
+| `SONARR_DISCOVERY_MONITOR_MODE` | a dropdown (all, future, first, latest, none) | in this section | Sonarr monitor mode on add: all \| future \| first \| latest \| none |
+| `SONARR_DISCOVERY_HISTORY` | a text box | in this section | — |
+| `RADARR_DISCOVERY_THRESHOLD` | a number box | in this section | score to accept candidate (0-100) — lower than Lidarr since diverse seeds rarely overlap |
+| `RADARR_DISCOVERY_LOOKBACK_DAYS` | a number box | in this section | Emby watch history window in days (movies rewatched less often) |
+| `RADARR_DISCOVERY_MAX_SEEDS` | a number box | in this section | max seed movies from Stage 1 |
+| `RADARR_DISCOVERY_MAX_ADDS` | a number box | in this section | max movies to add per run |
+| `RADARR_DISCOVERY_MIN_VOTE_COUNT` | a number box | in this section | min TMDB votes to be considered a candidate |
+| `RADARR_DISCOVERY_MIN_RATING` | a number box | in this section | min TMDB vote_average × 10 (60 = 6.0/10) |
+| `RADARR_DISCOVERY_REJECT_COOLDOWN` | a number box, in days | in this section | days before re-evaluating a rejected movie |
+| `RADARR_DISCOVERY_SEED_LIBRARIES` | a list, one entry per line | in this section | — |
+| `RADARR_DISCOVERY_HISTORY` | a text box | in this section | — |
+| `SONARR_EMBY_LIBRARIES` | a list, one entry per line | in this section | 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. |
+| `RADARR_EMBY_LIBRARIES` | a list, one entry per line | in this section | — |
+| `SONARR_ORPHAN_AGE` | a number box, in days | in this section | days — files must be older than this before eligible for deletion |
+| `SONARR_MAX_DELETE_GB` | a number box | in this section | require --i-know-what-im-doing if deletion exceeds this |
+| `SONARR_MIN_TRACKED_PCT` | a number box | in this section | abort if tracked count drops below this % of last run |
+| `SONARR_TRACKED_COUNT_FILE` | a text box | in this section | — |
+| `SONARR_IMPORT_SCAN_TIMEOUT` | a number box, in seconds | in this section | seconds to wait for pre-flight import scan |
+| `SONARR_MOVE_POLL_TIMEOUT` | a number box, in seconds | in this section | seconds to wait for a single async MoveSeries command to |
+| `CORRUPTION_SCAN_STATE_FILE` | a text box | in this section | clean-file skip-cache |
+| `CORRUPTION_SCAN_STRIKES_FILE` | a text box | in this section | consecutive corrupt-detection counts, keyed by host path |
+| `CORRUPTION_SCAN_STRIKE_LIMIT` | a number box, in consecutive | in this section | consecutive corrupt detections (across separate scan runs) |
+| `SONARR_EXTENSIONS` | a list, one entry per line | in this section | — |
+| `SONARR_PROTECTED_PATTERNS` | a list, one entry per line | in this section | — |
+| `RADARR_ORPHAN_AGE` | a number box, in days | in this section | days — files must be older than this before eligible for deletion |
+| `RADARR_MAX_DELETE_GB` | a number box | in this section | require --i-know-what-im-doing if deletion exceeds this |
+| `RADARR_MIN_TRACKED_PCT` | a number box | in this section | abort if tracked count drops below this % of last run |
+| `RADARR_TRACKED_COUNT_FILE` | a text box | in this section | — |
+| `RADARR_IMPORT_SCAN_TIMEOUT` | a number box, in seconds | in this section | seconds to wait for pre-flight import scan |
+| `RADARR_MOVE_POLL_TIMEOUT` | a number box, in seconds | in this section | seconds to wait for a single async MoveMovie command to |
+| `RADARR_EXTENSIONS` | a list, one entry per line | in this section | — |
+| `RADARR_PROTECTED_PATTERNS` | a list, one entry per line | in this section | — |
+| `RADARR_DROPPED_ADD_EXCLUSION` | a switch | in this section | add removed movies to import exclusion list |
+| `SONARR_DROPPED_ADD_EXCLUSION` | a switch | in this section | add removed series to import exclusion list |
+
+## Arr Content Classification (radarr/sonarr_classification_scan.sh)
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Radarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
+- Scheduler tab → **Sonarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RADARR_ANIME_STUDIOS` | a list, one entry per line | in this section | — |
+| `RADARR_KIDS_STUDIOS` | a list, one entry per line | in this section | — |
+| `RADARR_JUNK_MIN_VOTES` | a number box | in this section | — |
+| `SONARR_ANIME_NETWORKS` | a list, one entry per line | in this section | — |
+| `SONARR_KIDS_NETWORKS` | a list, one entry per line | in this section | — |
+
+## Arr Failed/Stalled Recovery
+
+In `master.conf`.
+
+Route: Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Failed/Stalled Recovery*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ARR_IMPORT_RECOVERY_AGE` | a number box, in hours | Arrs tab | hours — skip items newer than this |
+| `ARR_RECOVERY_MAX_ATTEMPTS` | a number box, in consecutive | in this section | consecutive failures before an item is flagged chronic |
+| `ARR_SMART_IMPORT_ENABLED` | a switch | in this section | importBlocked items: try importing a clean same-language |
+| `ARR_SMART_IMPORT_PREFERRED_LANGUAGE` | a text box | in this section | only treated as a match/upgrade if the |
+
+## Arr Full Library Rescan
+
+In `master.conf`.
+
+Route: Scheduler tab → **Arr Full Rescan** → Config → *Arr Full Library Rescan*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ARR_FULL_RESCAN_TIMEOUT` | a number box, in seconds | in this section | seconds per arr — a full-library walk, not a single release |
+
+## Array Start
+
+In `master.conf`.
+
+Route: Scheduler tab → **Array Started** → Config → *Array Start*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ARRAY_START_SCRIPTS` | a list, one entry per line | in this section | 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 Stop
+
+In `master.conf`.
+
+Route: Scheduler tab → **Array Stopping** → Config → *Array Stop*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ARRAY_STOP_SCRIPTS` | a list, one entry per line | in this section | 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. |
+
+## Backup Verify
+
+In `master.conf`.
+
+Route: Scheduler tab → **Backup Verify** → Config → *Backup Verify*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `BACKUP_VERIFY_SAMPLE` | a number box | in this section | random files to check per share |
+| `BACKUP_VERIFY_MIN_SIZE` | a text box | in this section | minimum file size to include in sample |
+
+## Bandwidth Monitor
+
+In `master.conf`.
+
+Route: Scheduler tab → **Bandwidth Monitor** → Config → *Bandwidth Monitor*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `BANDWIDTH_LOG` | a text box | in this section | 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_RETENTION` | a number box, in days | in this section | days before old entries purged |
+| `BANDWIDTH_WARN_GB` | a number box | Rsync tab | flag syncs larger than this in weekly report |
+| `ARR_CLEANUP_STATS` | a text box | in this section | lidarr/sonarr/radarr orphan stats |
+| `ARR_RECOVERY_STATS` | a text box | in this section | blocklist + re-search stats |
+| `ARR_RECOVERY_FAILURE_COUNTS` | a text box | in this section | per-item chronic-failure tracking |
+
+## Certificate Monitor
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Cert Monitor** → Config → *Certificate Monitor*
+- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `CERT_WARN_DAYS` | a number box | in this section | warn when cert expires within this many days |
+| `CERT_CRIT_DAYS` | a number box | in this section | critical alert within this many days |
+| `CERT_TIMEOUT` | a number box, in seconds | in this section | seconds per domain before giving up |
+
+## Clear Logs
+
+In `master.conf`.
+
+Route: Scheduler tab → **Clear Logs** → Config → *Clear Logs*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `LOG_FILES` | a list, one entry per line | in this section | 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 be… |
+| `LOG_MIN_SIZE_MB` | a number box | in this section | skip system log if under this size (already small) |
+| `LOG_DOCKER_MAX_MB` | a number box | in this section | clear Docker container log only if over this size (MB) |
+
+## Critical Sync Maintenance
+
+In `master.conf`.
+
+Route: Scheduler tab → **Critical Sync Maintenance** → Config → *Critical Sync Maintenance*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `CRITICAL_MAINTENANCE_SCRIPTS` | a list, one entry per line | in this section | 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. |
+
+## Daily Sync Maintenance
+
+In `master.conf`.
+
+Route: Scheduler tab → **Daily Sync Maintenance** → Config → *Daily Sync Maintenance*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `DAILY_MAINTENANCE_SCRIPTS` | a list, one entry per line | in this section | daily_sync_maintenance.sh runs media share sync first, then iterates DAILY_MAINTENANCE_SCRIPTS for all jobs. Schedule: 0 1 * * * (1am daily) |
+| `DAILY_CONTAINER_UPDATES` | a switch | in this section | 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. |
+
+## Docker Watchdog
+
+In `master.conf`.
+
+Route: Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `WATCHDOG_STATE_FILE` | a text box | in this section | Strike state file — /tmp resets on reboot (correct — no stale strikes after reboot) |
+| `SOFT_CPU_THRESHOLD` | a number box | in this section | warn at this % of total system CPU |
+| `HARD_CPU_THRESHOLD` | a number box | in this section | strike at this % of total system CPU |
+| `CPU_FAIL_LIMIT` | a number box, in consecutive | in this section | consecutive hard CPU strikes before container restart |
+| `SOFT_MEM_THRESHOLD` | a number box | in this section | Memory soft threshold — warn when container reaches this % of its hard limit |
+| `RESP_FAIL_LIMIT` | a number box, in consecutive | in this section | consecutive failed checks before restart |
+| `CURL_TIMEOUT` | a number box, in seconds | in this section | seconds per check before timeout |
+| `WATCHDOG_SCAN_ALL` | a switch | in this section | false = only WATCHDOG_CONTAINERS + required containers |
+| `WATCHDOG_RESTART_UNHEALTHY` | a switch | in this section | Individual Tier 2 check toggles |
+| `WATCHDOG_RESTART_DEAD` | a switch | in this section | — |
+| `WATCHDOG_RESTART_CRASHED` | a switch | in this section | — |
+| `WATCHDOG_NOTIFY_OOM` | a switch | in this section | — |
+| `WATCHDOG_NOTIFY_CRASHLOOP` | a switch | in this section | — |
+| `WATCHDOG_CRASH_LIMIT` | a number box | in this section | RestartCount above this = critical crash loop |
+| `WATCHDOG_REQUIRED_STRIKE_LIMIT` | a number box | in this section | Required-container strikes — consecutive down-checks before docker_watchdog.sh attempts a restart |
+| `WATCHDOG_STARTUP_GRACE` | a number box, in seconds | in this section | seconds after boot before watchdog acts on failures |
+| `WATCHDOG_CONTAINER_RESTART_LIMIT` | a number box | in this section | Restart loop protection — prevents watchdog from endlessly restarting a broken container |
+| `WATCHDOG_CONTAINER_RESTART_WINDOW` | a number box | in this section | rolling window in hours |
+| `WATCHDOG_CONTAINER_RESTART_LOG` | a text box | in this section | — |
+| `WATCHDOG_BATCH_NOTIFY` | a switch | in this section | Notification batching — one summary per cycle instead of one ping per event |
+| `WATCHDOG_DAEMON_TIMEOUT` | a number box, in seconds | in this section | seconds — timeout for all docker commands |
+| `WATCHDOG_DAEMON_STRIKE_LIMIT` | a number box, in consecutive | in this section | consecutive failed checks before restart attempt |
+| `WATCHDOG_DAEMON_RESTART_WAIT` | a number box, in seconds | in this section | seconds to wait after restart before verifying |
+| `WATCHDOG_CHECK_APPDATA` | a switch | in this section | 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_APPDATA_PATHS` | a list, one entry per line | in this section | — |
+| `WATCHDOG_APPDATA_GROWTH_GB` | a number box | in this section | flag containers growing more than this per cycle |
+| `WATCHDOG_APPDATA_LOG_MAX_GB` | a number box | in this section | flag *.log files exceeding this size (absolute) |
+| `WATCHDOG_APPDATA_TRUNCATE_LOGS` | a switch | in this section | set true to auto-truncate oversized *.log files on action cycle |
+| `WATCHDOG_APPDATA_STRIKE_LIMIT` | a number box | in this section | cycles before action fires (matches existing watchdog pattern) |
+| `WATCHDOG_APPDATA_GROWTH_FILE` | a text box | in this section | — |
+| `STORAGE_WATCHDOG_STATE_FILE` | a text box | in this section | — |
+
+## Download Orphan Cleaner (arr_download_orphan_cleaner.sh)
+
+In `master.conf`.
+
+Route: Scheduler tab → **Arr Download Orphan Cleaner** → Config → *Download Orphan Cleaner (arr_download_orphan_cleaner.sh)*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `DOWNLOAD_ORPHAN_CLEANER_ENABLED` | a switch | in this section | 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_D… |
+| `DOWNLOAD_ORPHAN_AGE` | a number box, in days | in this section | days — entries younger than this may be mid-import, never touched |
+| `DOWNLOAD_ORPHAN_MIN_VIDEO_MB` | a number box | in this section | no video file above this = junk (par2 debris, samples, dead archives) |
+| `DOWNLOAD_ORPHAN_MAX_DELETE_GB` | a number box | in this section | abort delete pass over this — a partial queue fetch would classify |
+
+## Downloaders Reset
+
+In `master.conf`.
+
+Route: Scheduler tab → **Downloaders Reset** → Config → *Downloaders Reset*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `DOWNLOADER_RETENTION_DAYS` | a number box, in days | in this section | days — purge history older than this |
+| `QBIT_FAILSAFE_MIN_DAYS` | a number box, in days | in this section | days — minimum age before failsafe deletion |
+| `QBIT_FAILSAFE_MIN_RATIO` | a number box | in this section | 0 = age only, no ratio requirement |
+
+## Emby Session Report
+
+In `master.conf`.
+
+Route: Scheduler tab → **Emby Session Report** → Config → *Emby Session Report*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `EMBY_REPORT_DAYS` | a number box, in days | in this section | days to include in the report period |
+| `EMBY_REPORT_TOP_N` | a number box | in this section | number of top content items to show |
+
+## FALLBACK
+
+In `master.conf`.
+
+No single page shows this section. Individual settings below carry their own route.
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `EXTERNAL_IP` | a text box | — | — |
+| `FALLBACK_CHECK_INTERVAL` | a number box, in seconds | Fallback tab | seconds between fallback state checks |
+| `FALLBACK_HANDBACK_STRIKES` | a number box, in consecutive | Fallback tab | consecutive healthy checks before initiating handback (3×30s = 90s) |
+| `FALLBACK_STATE_FILE` | a text box | — | — |
+| `FALLBACK_ENABLED` | a switch | Fallback tab | HOST2 back online |
+| `FALLBACK_PARTNERSHIP_SUSPEND_AFTER` | a number box, in minutes | Fallback tab | minutes without active partnership before suspending |
+
+## Failover Test
+
+In `master.conf`.
+
+Route: Scheduler tab → **Fallback Test** → Config → *Failover Test*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `FALLBACK_TEST_BLOCK_WAIT` | a number box, in seconds | in this section | seconds to wait after blocking connectivity |
+| `FALLBACK_TEST_HANDBACK_WAIT` | a number box, in seconds | in this section | seconds to wait before initiating handback |
+
+## Health Digest
+
+In `master.conf`.
+
+Route: Scheduler tab → **Weekly Health Digest** → Config → *Health Digest*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `DIGEST_PROFILE` | a dropdown (always, smart, weekly) | in this section | always \| smart \| weekly |
+| `DIGEST_DAY` | a text box | in this section | — |
+| `DIGEST_SMART_ON_WATCHDOG` | a switch | in this section | send if any watchdog strikes are active |
+| `DIGEST_SMART_ON_FALLBACK` | a switch | in this section | send if fallback state is not NORMAL |
+| `DIGEST_SMART_ON_CERT_WARN` | a switch | in this section | send if any cert is under CERT_WARN_DAYS |
+| `DIGEST_SMART_ON_BANDWIDTH` | a switch | in this section | send if any transfer exceeded BANDWIDTH_WARN_GB |
+
+## Intermediate Sync Maintenance
+
+In `master.conf`.
+
+Route: Scheduler tab → **Intermediate Sync Maintenance** → Config → *Intermediate Sync Maintenance*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `INTERMEDIATE_MAINTENANCE_SCRIPTS` | a list, one entry per line | in this section | — |
+
+## LOGGING
+
+In `master.conf`.
+
+No single page shows this section. Individual settings below carry their own route.
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ENABLE_LOGGING` | a switch | Settings tab | 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 |
+
+## Media Cleaner
+
+In `master.conf`.
+
+Route: Scheduler tab → **Media Cleaner** → Config → *Media Cleaner*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ANIME_FILE_PATTERNS` | a list, one entry per line | in this section | — |
+| `MEDIA_FILE_PATTERNS` | a list, one entry per line | in this section | — |
+
+## Media Permissions
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Media Shares Permissions** → Config → *Media Permissions*
+- Scheduler tab → **Bulk Permissions Repair** → Config → *Media Permissions*
+- Scheduler tab → **Trailer Folder Migration** → Config → *Media Permissions*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PERMISSIONS_DIR_MODE` | a text box | in this section | directories — traverse + list, no world-write |
+| `PERMISSIONS_FILE_MODE` | a text box | in this section | files — group read/write, no execute |
+| `PERMISSIONS_OWNER` | a text box | in this section | — |
+
+## Monthly Maintenance
+
+In `master.conf`.
+
+Route: Scheduler tab → **Monthly Maintenance** → Config → *Monthly Maintenance*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `MONTHLY_MAINTENANCE_SCRIPTS` | a list, one entry per line | in this section | 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,… |
+| `MONTHLY_UPTIME_THRESHOLD_DAYS` | a number box | in this section | minimum uptime in days before maintenance fires |
+| `MONTHLY_RUN_INTERVAL_DAYS` | a number box | in this section | minimum days since last run before running again |
+| `MONTHLY_LAST_RUN_FILE` | a text box | in this section | — |
+
+## Mover
+
+In `master.conf`.
+
+Route: Scheduler tab → **Mover Stop** → Config → *Mover*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `MOVER_STOP_TIMEOUT` | a number box, in seconds | in this section | Seconds to wait before mover_stop.sh sends SIGTERM to the mover process. Gives mover time to finish current file transfer before being interrupted. |
+
+## NOTIFICATIONS
+
+In `master.conf`.
+
+No single page shows this section. Individual settings below carry their own route.
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `NOTIFY_UNRAID` | a switch | Settings tab | unRAID native notification system — integrates with the bell icon in the WebGUI. normal = job completed successfully / warning = something failed or needs attention |
+| `WEBHOOK_PORT` | a number box | — | 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_SECRET` | a masked box with a **Show** button | — | auto-generated on first start if empty |
+
+## Network Watchdog
+
+In `master.conf`.
+
+Route: Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `NETWORK_WATCHDOG_ENABLED` | a switch | in this section | Services-layer connectivity — internet reachability, DDNS sync, Tailscale, NPM proxy. Host-specific values (domain, container, NPM URL) live in host*.conf. |
+| `NETWORK_WATCHDOG_INTERNET_URL` | a text box | in this section | — |
+| `NETWORK_WATCHDOG_INTERNET_TIMEOUT` | a number box | in this section | — |
+| `NETWORK_WATCHDOG_CHECK_TAILSCALE` | a switch | in this section | — |
+| `NETWORK_WATCHDOG_NPM_TIMEOUT` | a number box | in this section | — |
+| `NETWORK_WATCHDOG_NPM_STRIKE_LIMIT` | a number box | in this section | — |
+| `NETWORK_WATCHDOG_NPM_STATE_FILE` | a text box | in this section | — |
+
+## OOM Bypass Settings
+
+In `master.conf`.
+
+Route: Scheduler tab → **Stability Watchdog** → Config → *OOM Bypass Settings*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SYS_WATCHDOG_OOM_LIMIT` | a number box | in this section | OOM kills in one cycle to trigger bypass |
+
+## PARTNERSHIP
+
+In `master.conf`.
+
+Route: Partnership tab → Array Settings → *PARTNERSHIP*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PARTNERSHIP_ENABLED` | a switch | Fallback tab | — |
+| `PARTNERSHIP_OWNER_HOST` | a text box | in this section | "HOST1" or "HOST2" — flips on --transfer |
+| `PARTNERSHIP_GRACE_HOURS` | a number box, in hours | in this section | hours after offboard before Tailscale removal |
+| `PARTNERSHIP_OFFLINE_THRESHOLD` | a number box, in days | in this section | days either server unreachable before auto-offboard |
+| `PARTNERSHIP_BLOCKLIST_FILE` | a text box | in this section | 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. |
+| `VARAVERK_SETUP_FILE` | a text box | in this section | — |
+| `PARTNERSHIP_REMOVE_TAILSCALE` | a switch | in this section | remove mirror from Tailscale tailnet on offboard |
+| `TAILSCALE_API_KEY` | a masked box with a **Show** button | in this section | tskey-api-... |
+| `TAILSCALE_TAILNET` | a text box | in this section | your tailnet name (e.g. yourname.github) |
+| `PARTNERSHIP_TRANSFER_CONFIRM` | a text box | in this section | Transfer safety. |
+| `PARTNERSHIP_TRANSFER_STRIKES` | a number box, in consecutive | in this section | consecutive health checks required |
+| `PARTNERSHIP_TRANSFER_MAX_ATTEMPTS` | a number box | in this section | max health check attempts before giving up |
+| `PARTNERSHIP_ONBOARD_VERIFY` | a switch | in this section | verify WebUI reachable after reconfiguration |
+| `PARTNERSHIP_ONBOARD_NOTIFY` | a switch | in this section | notify both servers on completion |
+| `PARTNERSHIP_SYNC_INTERVAL` | a number box, in minutes | in this section | minutes — informational, actual schedule in cron |
+| `SSH_MAX_STRIKES` | a number box, in consecutive | in this section | consecutive SSH auth failures before critical notify |
+| `SSH_STRIKE_RESET_HRS` | a number box, in hours | in this section | hours since last failure before strike counter resets |
+| `PARTNERSHIP_FOLDERVIEW3` | a switch | in this section | create/remove FolderView3 folder on onboard/offboard |
+| `PARTNERSHIP_FOLDERVIEW3_URL` | a text box | in this section | CA plugin URL — leave empty to skip auto-install |
+
+## PHP-FPM
+
+In `master.conf`.
+
+Route: Scheduler tab → **Php Fpm Max Children** → Config → *PHP-FPM*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PHP_CONF` | a text box | in this section | 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_MAX_CHILDREN` | a number box | in this section | — |
+
+## Play State Sync
+
+In `master.conf`.
+
+Route: Scheduler tab → **Play State Sync** → Config → *Play State Sync*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PLAY_SYNC_ENABLED` | a switch | in this section | — |
+| `PLAY_SYNC_REMOTE` | a switch | in this section | sync across hosts via Tailscale |
+| `PLAY_SYNC_TYPES` | a text box | in this section | Audio excluded — music library too large; favorites handled separately |
+| `PLAY_SYNC_FAV_TYPES` | a text box | in this section | union sync — never unmarks; Audio tracks future |
+| `PLAY_SYNC_PROBE` | a switch | in this section | hash fetched state, skip per-item comparison when nothing changed |
+| `PLAY_SYNC_PROBE_MAX_AGE_HOURS` | a number box | in this section | force a full comparison when the fingerprint is older than this |
+
+## Play State Sync — Handback
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Play State Sync** → Config → *Play State Sync — Handback*
+- Scheduler tab → **Fallback** → Config → *Play State Sync — Handback*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PLAY_SYNC_HANDBACK_RETRIES` | a number box, in attempts | in this section | attempts before giving up and proceeding to DNS cutover |
+| `PLAY_SYNC_HANDBACK_RETRY_DELAY` | a number box, in seconds | in this section | seconds between retry attempts |
+
+## Pressure Thresholds
+
+In `master.conf`.
+
+Route: Scheduler tab → **Resource Watchdog** → Config → *Pressure Thresholds*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RW_RAM_SOFT_GB` | a number box | in this section | throttle start — reduce background load |
+| `RW_RAM_MEDIUM_GB` | a number box | in this section | pause background containers |
+| `RW_RAM_HARD_GB` | a number box | in this section | stop optional containers (was SYS_WATCHDOG_MEM_SHUTDOWN_GB) |
+| `RW_RAM_RECOVER_GB` | a number box | in this section | RAM must reach this before restoring hard-stopped containers |
+| `RW_LOAD_SOFT_MULTIPLIER` | a text box | in this section | soft pressure: 2× cores sustained |
+| `RW_LOAD_MEDIUM_MULTIPLIER` | a text box | in this section | medium pressure: 3× cores sustained |
+| `RW_RECOVER_CYCLES` | a number box, in consecutive | in this section | Consecutive runs at lower pressure before de-escalating |
+
+## RAM Reboot Threshold
+
+In `master.conf`.
+
+Route: Scheduler tab → **Stability Watchdog** → Config → *RAM Reboot Threshold*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SYS_WATCHDOG_MEM_GB` | a number box | in this section | strike system → reboot |
+
+## Reboot
+
+In `master.conf`.
+
+Route: Scheduler tab → **Server Reboot** → Config → *Reboot*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `REBOOT_SLEEP` | a number box, in seconds | in this section | Seconds of warning broadcast to logged-in users before server_reboot.sh reboots. Gives users time to save work — 300s = 5 minutes. |
+| `REBOOT_VM_WAIT` | a number box, in seconds | in this section | Seconds to wait for graceful VM shutdown (ACPI signal via virsh) before libvirt stops it anyway — reboot takes priority. |
+
+## Rsync Defaults
+
+In `master.conf`.
+
+Route: Scheduler tab → **Rsync** → Config → *Rsync Defaults*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `BW_LIMIT` | a number box, in kb | Rsync tab | KB/s — 12500 ≈ 100Mbit |
+| `RETRY_COUNT` | a number box | Rsync tab | retry attempts before giving up |
+| `SLEEP` | a number box, in seconds | Rsync tab | seconds between retry attempts |
+| `RSYNC_MAX_RUNTIME_HOURS` | a number box | in this section | cap per transfer attempt — pauses and resumes next scheduled run |
+| `CRITICAL_CONTAINER_NAMES` | a list, one entry per line | in this section | — |
+| `DELAYED_CONTAINERS` | a list, one entry per line | in this section | — |
+| `CONTAINER_DELAY` | a number box, in seconds | in this section | seconds before starting delayed containers |
+| `RESTART_VERIFY_WAIT` | a number box, in seconds | in this section | seconds to wait after restart before checking container is running |
+| `EXCLUDE_DIRS` | a list, one entry per line | in this section | — |
+| `DEFAULT_RSYNC_OPTS` | a list, one entry per line | in this section | --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 (lida… |
+
+## Rsync Enable/Disable
+
+In `master.conf`.
+
+Route: Scheduler tab → **Rsync** → Config → *Rsync Enable/Disable*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RSYNC_ENABLED` | a switch | in this section | Tier 1 — global gate, overrides everything below |
+| `CRITICAL_RSYNC_ENABLED` | a switch | in this section | Tier 2 — critical_sync_maintenance.sh rsync section |
+| `INTERMEDIATE_RSYNC_ENABLED` | a switch | in this section | Tier 2 — intermediate_sync_maintenance.sh rsync section |
+| `DAILY_RSYNC_ENABLED` | a switch | in this section | Tier 2 — daily_sync_maintenance.sh rsync section |
+| `WEEKLY_RSYNC_ENABLED` | a switch | in this section | Tier 2 — weekly_sync_maintenance.sh rsync section |
+| `MONTHLY_RSYNC_ENABLED` | a switch | in this section | Tier 2 — monthly_maintenance.sh rsync section |
+| `FALLBACK_RSYNC_ENABLED` | a switch | Fallback tab | Tier 2 — fallback.sh writeback jobs on handback |
+
+## Rsync Merge Auto-Promote
+
+In `master.conf`.
+
+Route: Scheduler tab → **Rsync** → Config → *Rsync Merge Auto-Promote*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RSYNC_MERGE_ENABLED` | a switch | in this section | 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 Profile System
+
+In `master.conf`.
+
+Route: Scheduler tab → **Rsync** → Config → *Rsync Profile System*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `PROFILE_RSYNC_OPTS` | a list, one entry per line | in this section | — |
+| `PROFILE_BW_LIMIT` | a list, one entry per line | in this section | Per-profile bandwidth limits in KB/s |
+| `PROFILE_SLEEP` | a list, one entry per line | in this section | Seconds between retry attempts — only non-default profiles listed (default SLEEP applies otherwise) |
+| `PROFILE_CRITICAL_CONTAINER_NAMES` | a list, one entry per line | in this section | 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 |
+| `PROFILE_DELAYED_CONTAINERS` | a list, one entry per line | in this section | Containers needing a delay after rsync before starting. SPACE-SEPARATED STRINGS — converted to array at runtime |
+| `PROFILE_CONTAINER_DELAY` | a list, one entry per line | in this section | Seconds before starting delayed containers — only non-default profiles listed (default CONTAINER_DELAY applies otherwise) |
+| `PROFILE_EXCLUDE_DIRS` | a list, one entry per line | in this section | Directories excluded from rsync per profile. SPACE-SEPARATED STRINGS — converted to array at runtime |
+| `PROFILE_REMOTE_RESTART_CONTAINERS` | a list, one entry per line | in this section | 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 |
+
+## SABnzbd Throttle
+
+In `master.conf`.
+
+Route: Scheduler tab → **Resource Watchdog** → Config → *SABnzbd Throttle*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RW_SABNZBD_ENABLED` | a switch | in this section | Speed values: "50M" = 50 MB/s, "0" = unlimited |
+| `RW_SABNZBD_SPEED_SOFT` | a text box | in this section | — |
+| `RW_SABNZBD_SPEED_MEDIUM` | a text box | in this section | — |
+
+## SMART Health
+
+In `master.conf`.
+
+Route: Scheduler tab → **Smart Health** → Config → *SMART Health*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SMART_TEMP_WARN` | a number box | in this section | fallback — Celsius warn threshold |
+| `SMART_TEMP_CRIT` | a number box | in this section | fallback — Celsius critical threshold |
+
+## Strike and Reboot Loop Settings
+
+In `master.conf`.
+
+Route: Scheduler tab → **Stability Watchdog** → Config → *Strike and Reboot Loop Settings*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SYS_WATCHDOG_STRIKE_LIMIT` | a number box, in consecutive | in this section | consecutive failures before reboot trigger |
+| `SYS_WATCHDOG_REBOOT_LIMIT` | a number box | in this section | 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_WINDOW_HRS` | a number box | in this section | — |
+
+## Sunday Morning Coffee Report
+
+In `master.conf`.
+
+Route: Scheduler tab → **Sunday Morning Coffee Report** → Config → *Sunday Morning Coffee Report*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `COFFEE_REPORT_SCRIPTS` | a list, one entry per line | in this section | 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. |
+
+## Syslog Filter
+
+In `master.conf`.
+
+Route: Scheduler tab → **Docker Syslog Filter** → Config → *Syslog Filter*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `FILTER_FILE` | a text box | in this section | 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. |
+
+## System Tuning Monitor
+
+In `master.conf`.
+
+Route: Scheduler tab → **System Tuning Monitor** → Config → *System Tuning Monitor*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `INOTIFY_WARN_PCT` | a number box | in this section | warn if inotify instances exceed this % of limit |
+| `PHP_FPM_WARN_PCT` | a number box | in this section | warn if php-fpm workers exceed this % of max_children |
+| `TUNING_MONITOR_LOG` | a text box | in this section | — |
+| `TUNING_LOG_RETENTION` | a number box, in days | in this section | days before old entries are purged |
+
+## System Watchdog
+
+In `master.conf`.
+
+Route: Scheduler tab → **Watchdog Orchestrator** → Config → *System Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `SYSTEM_WATCHDOG_SCRIPTS` | a list, one entry per line | in this section | system_watchdog.sh runs SYSTEM_WATCHDOG_SCRIPTS sequentially each cycle. Called by watchdog_orchestrator.sh — not scheduled directly. |
+
+## Transcode Management
+
+In `master.conf`.
+
+Route: Scheduler tab → **Transcode Management** → Config → *Transcode Management*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `TRANSCODE_MANAGEMENT_SCRIPTS` | a list, one entry per line | in this section | 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 Manager
+
+In `master.conf`.
+
+Reachable from:
+
+- Scheduler tab → **Transcode Management** → Config → *Transcode Manager*
+- Scheduler tab → **Transcode Manager** → Config → *Transcode Manager*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RAMDISK_PATH` | a text box | in this section | tmpfs mount point — created at array start by ramdisk_setup.sh. Must exist before Emby starts so the symlink resolves correctly. |
+| `TRANSCODE_LINK` | a text box | in this section | 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. |
+| `RAMDISK_SSD_MIN_GB` | a number box | in this section | Minimum free GB on SSD before allowing flip from ramdisk to SSD. |
+| `TRANSCODE_MAX_AGE` | a number box, in minutes | in this section | minutes — HLS segment age before cleanup eligibility |
+| `TRANSCODE_ORPHAN_AGE` | a number box, in minutes | in this section | minutes — files with no matching active session |
+| `TRANSCODE_FLIP_WARN` | a number box | in this section | notify if symlink flips this many times in one hour |
+| `TRANSCODE_OWNER` | a text box | in this section | — |
+| `TRANSCODE_CHMOD` | a text box | in this section | — |
+| `TRANSCODE_MANAGER_MODE` | a text box | in this section | 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_STATE_FILE` | a text box | in this section | Daily statistics log — read by weekly_health_digest.sh for transcode summary. |
+| `TRANSCODE_DAILY_LOG` | a text box | in this section | — |
+| `TRANSCODE_LOG_RETENTION` | a number box, in days | in this section | days before old entries purged |
+| `TRANSCODE_CHECK_EMBY` | a switch | in this section | — |
+
+## Watchdog Orchestrator
+
+In `master.conf`.
+
+Route: Scheduler tab → **Watchdog Orchestrator** → Config → *Watchdog Orchestrator*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `WATCHDOG_ORCHESTRATOR_SCRIPTS` | a list, one entry per line | in this section | 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). |
+
+## WebGUI Watchdog
+
+In `master.conf`.
+
+Route: Scheduler tab → **Webgui Watchdog** → Config → *WebGUI Watchdog*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `WEBGUI_URL` | a text box | in this section | Monitors unRAID WebGUI responsiveness — escalates through nginx restart → emhttp restart. Separate from docker_watchdog — this monitors the unRAID UI itself, not containers. |
+| `WEBGUI_TIMEOUT` | a number box, in seconds | in this section | seconds before curl gives up on WebGUI check |
+| `WEBGUI_NGINX_WAIT` | a number box, in seconds | in this section | seconds after nginx restart before rechecking |
+| `WEBGUI_PHP_WAIT` | a number box, in seconds | in this section | seconds after php-fpm restart before rechecking |
+| `WEBGUI_EMHTTP_WAIT` | a number box, in seconds | in this section | seconds after emhttp restart before rechecking |
+
+## Weekly Sync Maintenance
+
+In `master.conf`.
+
+Route: Scheduler tab → **Weekly Sync Maintenance** → Config → *Weekly Sync Maintenance*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `WEEKLY_MAINTENANCE_SCRIPTS` | a list, one entry per line | in this section | 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_CONTAINER_UPDATES` | a switch | in this section | Pull updates for WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh runs. Set false to skip — docker_weekly_restart.sh still runs regardless. |
+| `MONTHLY_REMAINING_UPDATES` | a switch | in this section | 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. |
+| `DOCKER_UPDATE_REBUILT_DAILY_FILE` | a text box | in this section | 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.… |
+| `DOCKER_UPDATE_REBUILT_WEEKLY_FILE` | a text box | in this section | — |
+| `DOCKER_UPDATE_REBUILT_STALE_HOURS` | a number box | in this section | — |
+| `WEEKLY_SYNC_UPDATES` | a switch | in this section | pull container updates locally during weekly window |
+| `WEEKLY_SYNC_UPDATES_REMOTE` | a switch | in this section | pull container updates on remote via SSH |
+
+## ZFS Memory Snapshot
+
+In `master.conf`.
+
+Route: Scheduler tab → **Zfs Memory Snapshot** → Config → *ZFS Memory Snapshot*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `ZFS_REPORT_LOG` | a text box | in this section | Weekly ZFS pool health and memory diagnostic report — informational only. |
+| `ZFS_REPORT_ARC_WARN_PCT` | a number box | in this section | warn if ARC using more than this % of its max |
+| `ZFS_REPORT_ARC_FREE_WARN_GB` | a number box | in this section | warn if ARC headroom (max - current) drops below this GB |
+| `ZFS_REPORT_AVAIL_WARN_GB` | a number box | in this section | warn if less than this GB available on ZFS pool |
+| `ZFS_REPORT_DOCKER_TOP` | a number box | in this section | how many top Docker containers to show by memory |
+
+## inotify Tuning
+
+In `master.conf`.
+
+Route: Scheduler tab → **Inotify Tuning** → Config → *inotify Tuning*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `INOTIFY_MAX_INSTANCES` | a number box | in this section | default: 128 — max inotify instances per user |
+| `INOTIFY_MAX_WATCHES` | a number box | in this section | default: 8192 — SHARED budget across ALL users/containers |
+| `INOTIFY_MAX_QUEUED_EVENTS` | a number box | in this section | default: 16384 — max events queued before dropping |
+
+## qBittorrent Throttle
+
+In `master.conf`.
+
+Route: Scheduler tab → **Resource Watchdog** → Config → *qBittorrent Throttle*
+
+| Setting | Control | Where | What it does |
+|---|---|---|---|
+| `RW_QBIT_ENABLED` | a switch | in this section | KB/s — 0 = unlimited |
+| `RW_QBIT_DL_SOFT` | a number box | in this section | 50 MB/s |
+| `RW_QBIT_DL_MEDIUM` | a number box | in this section | 10 MB/s |
+
+---
+
+## Settings with no route through the UI
+
+These sections are not rendered by any page, so they can only be changed by editing
+the conf file. If one of these is asked about, say so plainly rather than inventing a
+route — mapping it into the Scheduler is a code change, not a setting.
+
+- **Authelia** (`host1.conf`) — `HOST1_AUTHELIA_CONFIG`, `HOST1_AUTHELIA_CONTAINER`
+- **Fallback Tiers — What HOST1 Wants Covered When Down** (`host1.conf`) — `FALLBACK_HOST1_TIER1`, `FALLBACK_HOST1_TIER2`, `FALLBACK_HOST1_TIER3`, `FALLBACK_HOST1_TIER4`
+- **Gitea** (`host1.conf`) — `HOST1_GITEA_API_TOKEN`
+- **Identity** (`host1.conf`) — `HOST1_SSH_KEY`, `HOST1_STORAGE_PATH`, `HOST1_OWNER`, `HOST1_OWNER_EMAIL`
+- **Jellyfin** (`host1.conf`) — `HOST1_JELLYFIN_CONTAINER`, `HOST1_JELLYFIN_URL`, `HOST1_JELLYFIN_API_KEY`
+- **Monthly Sync Shares** (`host1.conf`) — `HOST1_MONTHLY_SYNC_SHARES`
+- **NginxProxyManager** (`host1.conf`) — `HOST1_NPM_URL`, `HOST1_NPM_USER`, `HOST1_NPM_PASS`
+- **PCIe AER Quiet** (`host1.conf`) — `HOST1_PCIE_QUIET_DEVICES`
+- **Personal Shares** (`host1.conf`) — `HOST1_PERSONAL_SHARES`
+- **RESOURCE MANAGER** (`host1.conf`) — `HOST1_RW_PAUSE_CONTAINERS`, `HOST1_RW_STOP_CONTAINERS`
+- **Rsync Writeback** (`host1.conf`) — `HOST1_TIER1_WRITEBACK_DELAY`, `FALLBACK_HOST1_WRITEBACK_TIER1`, `FALLBACK_HOST1_WRITEBACK_TIER2`, `FALLBACK_HOST1_WRITEBACK_TIER3`, `FALLBACK_HOST1_WRITEBACK_TIER4`
+- **TRANSCODES** (`host1.conf`) — `HOST1_RAMDISK_SIZE`, `HOST1_RAMDISK_WARN_GB`, `HOST1_RAMDISK_LOW_GB`, `HOST1_TRANSCODE_SSD`, `HOST1_TRANSCODE_SERVERS`
+- **Tier Delays — HOST1 Outage Timers** (`host1.conf`) — `HOST1_TIER2_DELAY`, `HOST1_TIER3_DELAY`, `HOST1_TIER4_DELAY`
+- **Unraid API** (`host1.conf`) — `HOST1_UNRAID_API_KEY`
+- **Web search** (`host1.conf`) — `HOST1_DEGOOG_URL`, `HOST1_SEARXNG_URL`, `HOST1_WEB_SEARCH_API_KEY`, `HOST1_OLLAMA_EMBED_MODEL`
+- **lldap** (`host1.conf`) — `HOST1_LLDAP_URL`, `HOST1_LLDAP_USER`, `HOST1_LLDAP_PASS`
+- **Abort Toggles** (`master.conf`) — `SYS_WATCHDOG_ABORT_ON_ZFS_UNHEALTHY`, `SYS_WATCHDOG_ABORT_ON_PARITY`, `SYS_WATCHDOG_ABORT_ON_MOVER`
+- **Arr Sync** (`master.conf`) — `ARR_SYNC_ENABLED`, `ARR_SYNC_BLOCKLIST`, `ARR_SYNC_CONNECT_TIMEOUT`, `ARR_SYNC_API_TIMEOUT`, `DOCKER_APPDATA_BASE`, `ARR_SYNC_LIDARR_PORT`, `ARR_SYNC_SONARR_PORT`, `ARR_SYNC_RADARR_PORT`
+- **Cache Roots** (`master.conf`) — `VV_CACHE_ROOT`, `VV_CACHE_DIR`, `CONF_RAM_CACHE_DIR`, `ARR_CACHE_DIR`, `AI_TOKEN_CACHE_DIR`, `AI_JOB_DIR`, `DOCKER_JOB_DIR`
+- **Conf Backups** (`master.conf`) — `CONF_BACKUP_RETAIN`
+- **Conf Sync** (`master.conf`) — `CONF_SYNC_ENABLED`
+- **Critical Containers** (`master.conf`) — `RW_CRITICAL_CONTAINERS`
+- **Download Webhook** (`master.conf`) — `DOWNLOAD_WEBHOOK_ENABLED`
+- **GIT / REPO** (`master.conf`) — `GITEA_CONTAINER`, `GITEA_REPO_PATH`, `GITEA_DOMAIN`, `TARGET_DIR`, `GITEA_SSH_KEY`, `SSH_PORT`, `GITEA_HTTP_PORT`
+- **HOST IDENTITIES** (`master.conf`) — `HOST1`, `HOST2`
+- **PCIe AER Quiet** (`master.conf`) — `PCIE_QUIET_ENABLED`
+- **PER-HOST CONTAINER LISTS** (`master.conf`) — `RW_ENABLED`, `RW_STATE_FILE`
+- **Remote Docker Daemon** (`master.conf`) — `REMOTE_DOCKER_STRIKE_LIMIT`
+- **Remote Health Checks** (`master.conf`) — `ROOTFS_WARN`
+- **SHARED HOST CONFIGURATION** (`master.conf`) — `DATA_DIR`, `DB_DIR`, `STATE_DIR`, `AI_DATA_DIR`, `CACHE_BACKUP_DIR`, `LOG_ARCHIVE_DIR`, `BACKUP_DIR`, `CONF_BACKUP_DIR`, `PERSISTENT_CONF_CACHE`, `ARR_CACHE_BACKUP_DIR`
+- **State Files** (`master.conf`) — `SYS_WATCHDOG_STATE_FILE`, `DOCKER_WATCHDOG_FAILED_FILE`, `DOCKER_WATCHDOG_INTENTIONAL_FILE`, `SYS_WATCHDOG_REBOOT_LOG`, `SYS_WATCHDOG_OOM_FILE`
+- **Thresholds** (`master.conf`) — `SYS_WATCHDOG_ROOTFS_PCT`, `SYS_WATCHDOG_ROOTFS_CRITICAL_PCT`, `SYS_WATCHDOG_LOG_PCT`, `SYS_WATCHDOG_TMP_PCT`, `SYS_WATCHDOG_TMP_CRITICAL_PCT`, `SYS_WATCHDOG_ARC_PINNED_PCT`, `SYS_WATCHDOG_ARC_RELEASE_PCT`, `SYS_WATCHDOG_LOAD_MULTIPLIER`, `SYS_WATCHDOG_ZOMBIE_LIMIT`, `SYS_WATCHDOG_CPU_TEMP_MAX`, `SYS_WATCHDOG_FD_CRITICAL_PCT`, `SYS_WATCHDOG_RUNAWAY_CPU_PCT`, `SYS_WATCHDOG_RUNAWAY_STRIKES`, `SYS_WATCHDOG_MDSTAT_ERROR_LIMIT`
+- **Version Parity** (`master.conf`) — `UNRAID_VERSION_MISMATCH_ACTION`