This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:

   - **Completed**: Fixed hardcoded path in `git_pull_execute.sh` with `_BOOT_DIR` variable
   - **Completed**: Partnership adapter pattern — extracted Unraid-specific container logic into `Plugin/unraid/Partnership/containers.sh`
   - **Completed**: Full FolderView3 removal from all partnership scripts, conf, README
   - **Completed**: Extended `conf_populate.sh` with 11 new auto-detected fields (OWNER, arr URLs, SABnzbd/slskd URLs, qBit URL/user/pass, Emby/Jellyfin URLs, TRANSCODE_SSD)
   - **Completed**: First-run wizard rebuilt as uniform multi-step flow in `pages/setup.php`
   - **Completed**: `api/setup.php` extended with `detect`, `ssh_generate`, `populate` actions; `STORAGE_MODE_INTERNAL` auto-set in host.conf creation
   - **Completed**: `api/checklist.php` created — live checklist with 6 items including `master_conf` pull for partner servers
   - **Completed**: Auto Unraid API key creation via `vv_auto_create_api_key()` in `include/config.php`, called in wizard save/pull
   - **Completed**: Auto SSH key generation (`--local-only` flag in `ssh_setup.sh`) called in wizard save/pull
   - **Completed**: HOST2 pull branch removed from wizard — moved to checklist `master_conf` item with inline `vvPullMaster()` button
   - **Completed**: Fullscreen toggle button (`⤢`) added to tab bar — hides `#header`, `#menu`, reclaims `#displaybox` padding; state persists via localStorage
   - **In Progress**: Arrs UI page — user said "both, arrs first" referring to media (arrs) and cert monitor UI pages

2. Key Technical Concepts:

   - **Platform adapter pattern**: `Plugin/$PLATFORM/Partnership/containers.sh` sourced by partnership scripts; functions use caller scope
   - **Boot device detection**: `findmnt -n -o SOURCE /boot` → `lsblk -no pkname` → `lsblk -dno TRAN` → USB = flash mode
   - **Storage mode**: `HOSTN_STORAGE_MODE_INTERNAL=true/false` auto-set during host.conf creation
   - **conf_populate**: Reads running services (docker inspect, config files, APIs) to fill host.conf; idempotent; `--no-push` for wizard use
   - **Checklist pattern**: `api/checklist.php` returns live state per item with optional `action` field; `complete` boolean for banner display
   - **Setup state DB**: `varaverk_setup.db` (key=value flat file) read/written via `vv_setup_state_read()` / `vv_setup_state_write()` — overwrites entire file, must read-merge-write
   - **`vv_auto_create_api_key(hostId, confFile)`**: Shared function in `config.php` calling `unraid-api apikey --create --overwrite --json`
   - **`ssh_setup.sh --local-only`**: Exits after keygen + conf update, before ssh-copy-id and remote verify
   - **Fullscreen mode**: `body.vv-fullscreen` class toggle; CSS hides `#header` and `#menu`, removes `#displaybox` padding-left
   - **Unraid CSS selectors**: `#header` (position:relative top bar), `#menu` (left nav), `#displaybox` (main content wrapper, gets `padding-left:8rem` from `.Theme--sidebar`)
   - **arr_cleanup_stats.db format**: `date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count`
   - **`vv_setup_state_write` overwrites entire file** — always read first: `$state = vv_setup_state_read(); $state['key'] = 'val'; vv_setup_state_write($state);`

3. Files and Code Sections:

   - **`/boot/config/plugins/varaverk/Plugin/unraid/Tools/conf_populate.sh`**
     - Added `_docker_host_port()` and `_docker_volume_host()` helpers
     - Added OWNER detection (strip unRAID- prefix, lowercase)
     - Extended arr loop to also set `${arr_upper}_URL` from port in config.xml
     - Added SABnzbd URL from ini port, slskd URL from yml port
     - New qBittorrent section: URL, username, password (skips `@ByteArray` PBKDF2 hashes)
     - Extended Emby/Jellyfin section to also set URL from docker port binding
     - Added TRANSCODE_SSD from `/transcode` docker volume mount
     - Fixed bug: removed `local api_ver` from top-level for loop (bash only allows `local` inside functions)

   - **`/boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php`** — Complete rewrite
     - Uniform single flow for all hosts (no HOST2 pull branch, no conf-only branch)
     - PHP: only `$detectedHostname = vv_get_hostname()`
     - Detection banner auto-loads via JS fetch to `?action=detect`
     - Role toggle: Primary (HOST1) / Partner (HOST2+) with conditional hostname/slot fields
     - Partner note: "SSH key and master.conf pull are handled automatically after save"
     - Step 2: populate result, guide (3 steps — updated to mention checklist for partner join), API key button, checklist
     - JS functions: `vvSetRole()`, `vvShowStep2(redirect, apiKey)`, `vvRunPopulate()`, `vvLoadChecklist()`, `vvPullMaster()`, `vvRunPopulateBtn()`, `vvCreateKey()`, `vvDoSave()`
     - `pull_master` action in checklist renders inline button calling `vvPullMaster(btn)`
     - `vvPullMaster()` POSTs `{action: 'pull'}` with no params; shows error inline

   - **`/boot/config/plugins/varaverk/Plugin/unraid/api/setup.php`**
     - `$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh'` defined at line 74, after method check, before both pull and save
     - `ssh_generate` action (GET/POST): runs `ssh_setup.sh --local-only`, reads .pub file, returns `{ok, pubkey}`
     - `pull` action refactored: auto-reads HOST1 from master.conf when `host1_hostname` not POSTed; auto-detects `my_slot` via `vv_detect_host()`, `my_hostname` via `vv_get_hostname()`; writes `master_conf_pulled=true` to setup state on success; runs `ssh_setup.sh --local-only`; calls `vv_auto_create_api_key()`
     - `save` action: runs `ssh_setup.sh --local-only`, calls `vv_auto_create_api_key()`, returns `api_key` in response
     - `detect` action (GET): findmnt+lsblk boot transport detection, returns hostname, unraid_ver, transport, boot_device, mode, scripts_dir
     - `populate` action (POST): runs `conf_populate.sh --no-push`, returns `{ok, lines[]}`

   - **`/boot/config/plugins/varaverk/Plugin/unraid/api/checklist.php`** — New file
     ```php
     // Items: identity, host_conf, api_key, ssh_key, populated, master_conf (non-host1 only), partnership (if HOST2 set)
     // master_conf item:
     if ($hostId !== 'host1' && $hostId !== 'unknown') {
         $state   = vv_setup_state_read();
         $pulled  = !empty($state['master_conf_pulled']);
         $items[] = [
             'id'     => 'master_conf',
             'label'  => 'master.conf',
             'ok'     => $pulled,
             'detail' => $pulled ? 'Synced from HOST1' : ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf'),
             'action' => (!$pulled && $host1) ? 'pull_master' : null,
         ];
     }
     // Response: {ok, complete, host_id, items[]}
     ```

   - **`/boot/config/plugins/varaverk/Plugin/unraid/include/config.php`**
     - Added `vv_auto_create_api_key(string $hostId, string $confFile): array`
     - Runs `unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json`
     - Writes key to conf file, returns `['ok'=>true,'key_preview'=>'...']` or `['ok'=>false,'error'=>'...']`

   - **`/boot/config/plugins/varaverk/Plugin/unraid/api/create_api_key.php`** — Refactored to 3-line wrapper
     ```php
     $host = vv_detect_host();
     if (!preg_match('/^host\d+$/', $host)) { echo json_encode(['ok'=>false,'error'=>'Cannot detect local host']); exit; }
     echo json_encode(vv_auto_create_api_key($host, $host . '.conf'));
     ```

   - **`/boot/config/plugins/varaverk/Partnership/ssh_setup.sh`**
     - Added `LOCAL_ONLY=false` in arg parsing, `--local-only` sets it true
     - In setup mode: if `$LOCAL_ONLY == true`, prints summary with pubkey path and manual ssh-copy-id command, exits 0 before remote copy/verify sections

   - **`/boot/config/plugins/varaverk/Plugin/unraid/js/varaverk.js`**
     ```javascript
     function vvToggleExpand() {
       const on  = document.body.classList.toggle('vv-fullscreen');
       const btn = document.getElementById('vv-expand-btn');
       if (btn) { btn.classList.toggle('active', on); btn.title = on ? 'Collapse' : 'Expand'; }
       localStorage.setItem('vv-fullscreen', on ? '1' : '');
     }
     // IIFE restores state on every page load
     ```

   - **`/boot/config/plugins/varaverk/Plugin/unraid/css/varaverk.css`**
     - Added `align-items: flex-end` to `#vv-tabs`
     - Added `#vv-expand-btn` styles (ghost button)
     - Added fullscreen rules:
       ```css
       body.vv-fullscreen #header  { display: none !important; }
       body.vv-fullscreen #menu    { display: none !important; }
       body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5rem !important; }
       ```

   - **`/boot/config/plugins/varaverk/Plugin/unraid/Varaverk.page`**
     - GitHub link wrapped in flex group with expand button:
       ```html
       <div style="margin-left:auto;display:flex;align-items:center;gap:2px;">
         <a href="https://github.com/FailedProxy/Varaverk" ...>⎋ GitHub</a>
         <button id="vv-expand-btn" onclick="vvToggleExpand()" title="Expand">⤢</button>
       </div>
       ```

   - **`/boot/config/plugins/varaverk/data/arr_cleanup_stats.db`** (read only)
     - Format: `date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count`
     - Example: `2026-06-05|radarr|0|0|0|0|0|2721`
     - Written daily by `radarr_cleanup.sh`, `sonarr_cleanup.sh`, `lidarr_cleanup.sh`

   - **`/boot/config/plugins/varaverk/data/arr_recovery_stats.db`** (read only)
     - Format: `date|time|count|bytes` (approximately)

   - **`/boot/config/plugins/varaverk/data/transcode_daily.db`** (read only)
     - Format: `date|value|...|...`

   - **`/boot/config/plugins/varaverk/Plugin/unraid/include/media.php`** (read only, 212 lines)
     - `vv_discover_media_servers()` — reads Emby/Jellyfin/Plex from host conf
     - `vv_media_sessions()` — live session data

   - **`/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php`** (exists, not yet read)
     - Target for the upcoming arrs UI work

4. Errors and Fixes:

   - **`local api_ver` outside function in `conf_populate.sh` line 155**: `local` keyword used in top-level for loop body. Fixed by removing `local` — just `case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac`
   - **`$sshScript` scoping bug in `api/setup.php`**: Variable was defined inside the `save` block but referenced in the `pull` block which exits before reaching `save`. Fixed by moving definition to line 74 (after method check, before both action blocks): `$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';`
   - **`vv_setup_state_write` overwrites entire file**: Discovered it completely replaces the file content. Must read first, merge, then write: `$state = vv_setup_state_read(); $state['master_conf_pulled'] = 'true'; vv_setup_state_write($state);`

5. Problem Solving:

   - **HOST2 chicken-and-egg SSH problem**: HOST2 needed SSH key to pull master.conf, but key generation was part of setup. Solved by: (1) `ssh_setup.sh --local-only` generates key without needing remote; (2) wizard `save` action auto-generates key before any network operations; (3) HOST2 wizard became uniform (same as HOST1); (4) master.conf pull moved to checklist where it can be retried independently
   - **Wizard flow branches**: Three PHP branches (standard, HOST2 pull, conf-only) were fragile — HOST2 needed state file pushed by HOST1 which required SSH that wasn't set up yet. Solved by making wizard uniform: all hosts do identity setup, master.conf pull becomes a checklist item with retry capability
   - **Fullscreen layout**: `#header` is `position:relative` so hiding it collapses naturally. `#menu` hidden. `#displaybox` has `padding-left:8rem` from `.Theme--sidebar` class which must be reset to `1rem` when menu is hidden.

6. All User Messages:

   - "partner, what if we removed the add containers and plugins and had it call a script located in/Varaverk/plugin/****/Partnership/ and have the last part of on board and off board there, leaving all the structure in the main partnership, but the add containers and such can then be os specific? just think out loud"
   - "ok lets do it"
   - "ok do we have partner installing any plugins?"
   - "lets remove it. our own page has support for people who already have it, but is a standalone feature also"
   - "ok now look at our first run. look at what we have that needs implemented, tell me what you think.... i have my idea just curious what path you suggest vs what i think i want"
   - "so i had the app path but that was one of the things that got messed up i guess along with the .conf stuff... anyways, goal was on install os and boot device detected. along with a var up top in host.conf that set the state for internal or external.... had a script that would move the dir.... check in plugin... but anyways os and boot device auto determined, same if master.conf is missing trigger system, then run the auto fill script. then a page or 2 with must have vars, and a quick instruction guide and a checklist... just thinking out loud"
   - "yes lets build it, that was the first actually pushed wizard, had a lot of changes but, i dont know what happened we had a rough week"
   - "conf_populate dry run it"
   - "is there anything else we can detect?"
   - "yeah add all of those"
   - "how much of the host.conf does that take care of"
   - "does this change anything with the wizard"
   - "we could auto create unraid api and push automatically during initial"
   - "we have a ssh script"
   - "yeah do it"
   - "ok so whats the check list look like aftr the wizard"
   - "yeah do it"
   - "so the pull in the wizard... isnt that handled on partnership page, would that all be better in the wizard or as a part of the check list"
   - "yeah do it,"
   - "ok is it possible to have a button that maximizes my system make it full window hiding unraids header and menu bar"
   - "omg, thats sick... im getting closer and closer to being a system on top of a system"
   - "media and cert monitors what did you have in mind"
   - "both, arrs first"

7. Pending Tasks:

   - **Arrs UI page** (`pages/arrs.php`) — primary pending task. Build UI surfacing:
     - Library size trend from `arr_cleanup_stats.db` (tracked_count over last N days per arr)
     - Last cleanup run stats (orphans removed, junk removed, bytes freed)
     - Live library counts / missing / wanted from Radarr/Sonarr/Lidarr APIs
     - Discovery history from `radarr/sonarr/lidarr_discovery_history.db`
     - Possibly transcode load from `transcode_daily.db`
   - **Cert monitor UI page** — after arrs. Needs:
     - Small cache file written by `cert_monitor.sh` (currently outputs notifications only)
     - UI showing domain, expiry date, days remaining, color-coded
     - "Run now" button
   - **Checklist banner on monitor page** — show compact checklist when setup not complete

8. Current Work:

   User said "both, arrs first" in response to the question "Which one interests you more, or both?" about media (arrs) and cert monitor UI pages. The conversation explored the existing data files:
   - `arr_cleanup_stats.db`: `date|arr|orphans|orphan_bytes|junk|junk_bytes|recent|tracked_count` — daily entries, 2721 movies as of 2026-06-05
   - `arr_recovery_stats.db`: recovery run history
   - `radarr/sonarr/lidarr_discovery_history.db`: per-arr discovery history
   - `transcode_daily.db`: transcode load data
   - Existing `Plugin/unraid/api/media.php` (thin, just calls `vv_media_sessions()`)
   - Existing `Plugin/unraid/include/media.php` (212 lines, discovers Emby/Jellyfin/Plex servers from conf, has `vv_media_sessions()`)
   - Existing `Plugin/unraid/pages/arrs.php` (exists but not read)

9. Optional Next Step:

   Build the arrs UI page. User's exact words: "both, arrs first" — meaning build the arrs page first, then cert monitor.

   The arrs page should surface:
   - Library size trend from `arr_cleanup_stats.db` (the `tracked_count` field, last column, per arr per day)
   - Last cleanup stats per arr (orphans, junk, bytes)
   - Live data from Radarr/Sonarr/Lidarr APIs (missing count, wanted, queue)
   - Run cleanup buttons per arr

   Need to first read `pages/arrs.php` and `include/arrs.php` to understand current state before building.

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /root/.claude/projects/-root/d6d51683-5f05-41b4-ad5d-afbe06985ad9.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


Menu="Tasks:95"
Title="Varaverk"
Icon="varaverk.png"
---
<?php
$plugin  = 'varaverk';
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$pluginDir = "$docroot/plugins/$plugin";

require_once "$pluginDir/include/config.php";

// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing
$_master = vv_read_conf_raw('master.conf');
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
$_host1_blank  = empty(trim($_h1m[1] ?? ''));
$_my_hostid    = vv_detect_host();
$_conf_missing = $_my_hostid !== 'unknown'
    && !file_exists(CONF_DIR . '/' . $_my_hostid . '.conf');
if ($_host1_blank || $_conf_missing) {
    include "$pluginDir/pages/setup.php";
    return;
}
unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);

// Determine active tab
$tab = $_GET['tab'] ?? 'monitor';
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings'];
if (!in_array($tab, $validTabs)) $tab = 'monitor';
$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'auth' => 'Auth Stack', 'settings' => 'Settings'];
?>

<link rel="stylesheet" href="/plugins/<?=$plugin?>/css/varaverk.css">

    <?php foreach ($validTabs as $t): ?>
      <a href="?tab=<?=$t?>" class="vv-tab<?= $t === $tab ? ' active' : '' ?>">
        <?= $tabLabels[$t] ?? ucfirst($t) ?>

    <?php endforeach; ?>

        ⎋ GitHub

      <button id="vv-expand-btn" onclick="vvToggleExpand()" title="Expand">⤢</button>

    <?php
      $page = "$pluginDir/pages/$tab.php";
      if (file_exists($page)) include $page;
      else echo "
Page not found: $tab
";
    ?>


// Varaverk — shared JS utilities
// Page-specific JS lives inline in each page partial.

// Flash a status element briefly then fade
function vvFlashStatus(el, msg, ok) {
  el.textContent = msg;
  el.style.color = ok ? '#4caf50' : '#f44336';
  setTimeout(() => { el.textContent = ''; }, 3000);
}

// ── Fullscreen toggle — hides Unraid header + menu ────────────────────────────
function vvToggleExpand() {
  const on  = document.body.classList.toggle('vv-fullscreen');
  const btn = document.getElementById('vv-expand-btn');
  if (btn) { btn.classList.toggle('active', on); btn.title = on ? 'Collapse' : 'Expand'; }
  localStorage.setItem('vv-fullscreen', on ? '1' : '');
}

// Restore state on every page load
(function() {
  if (localStorage.getItem('vv-fullscreen') !== '1') return;
  document.body.classList.add('vv-fullscreen');
  // Button may not exist yet if script runs before DOM — wait for it
  const apply = () => {
    const btn = document.getElementById('vv-expand-btn');
    if (btn) { btn.classList.add('active'); btn.title = 'Collapse'; }
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply);
  else apply();
})();


<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$hostId   = vv_detect_host();
$hostIdUp = strtoupper($hostId);
$master   = vv_read_conf_raw('master.conf');
$confRaw  = ($hostId !== 'unknown') ? vv_read_conf_raw($hostId . '.conf') : '';

$items = [];

// ── Identity ──────────────────────────────────────────────────────────────────
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $master, $m1);
$host1 = trim($m1[1] ?? '');
$items[] = [
    'id'     => 'identity',
    'label'  => 'Server identity',
    'ok'     => !empty($host1),
    'detail' => $host1 ? "HOST1: $host1" : 'HOST1 blank in master.conf',
];

// ── Host conf ─────────────────────────────────────────────────────────────────
$confExists = $hostId !== 'unknown' && file_exists(CONF_DIR . '/' . $hostId . '.conf');
$items[] = [
    'id'     => 'host_conf',
    'label'  => 'Host configuration',
    'ok'     => $confExists,
    'detail' => $confExists
        ? "$hostId.conf present"
        : ($hostId === 'unknown' ? 'Server not yet identified' : "$hostId.conf missing"),
];

// ── Unraid API key ─────────────────────────────────────────────────────────────
$apiKey = trim(vv_parse_conf_scalar($confRaw, $hostIdUp . '_UNRAID_API_KEY'));
$items[] = [
    'id'     => 'api_key',
    'label'  => 'Unraid API key',
    'ok'     => !empty($apiKey),
    'detail' => $apiKey ? 'Key present' : 'Not set',
    'action' => $apiKey ? null : 'create_key',
];

// ── SSH key ────────────────────────────────────────────────────────────────────
$sshPath = trim(vv_parse_conf_scalar($confRaw, $hostIdUp . '_SSH_KEY'));
$sshOk   = $sshPath && file_exists($sshPath);
$items[] = [
    'id'     => 'ssh_key',
    'label'  => 'SSH key',
    'ok'     => $sshOk,
    'detail' => $sshOk
        ? basename($sshPath)
        : ($sshPath ? "Path set but file missing: $sshPath" : 'No key path in host.conf'),
    'action' => $sshOk ? null : 'ssh_setup',
];

// ── Auto-populate (any service key or container detected) ──────────────────────
$populated = false;
foreach (['_RADARR_API_KEY','_SONARR_API_KEY','_LIDARR_API_KEY','_EMBY_CONTAINER','_JELLYFIN_CONTAINER'] as $f) {
    if (trim(vv_parse_conf_scalar($confRaw, $hostIdUp . $f)) !== '') {
        $populated = true;
        break;
    }
}
$items[] = [
    'id'     => 'populated',
    'label'  => 'Auto-populate',
    'ok'     => $populated,
    'detail' => $populated ? 'Services detected in host.conf' : 'No services detected yet',
    'action' => $populated ? null : 'run_populate',
];

// ── master.conf pull (partner servers only) ───────────────────────────────────────────────────
if ($hostId !== 'host1' && $hostId !== 'unknown') {
    $state   = vv_setup_state_read();
    $pulled  = !empty($state['master_conf_pulled']);
    $items[] = [
        'id'     => 'master_conf',
        'label'  => 'master.conf',
        'ok'     => $pulled,
        'detail' => $pulled
            ? 'Synced from HOST1'
            : ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf'),
        'action' => (!$pulled && $host1) ? 'pull_master' : null,
    ];
}

// ── Partnership (only if a partner is configured) ──────────────────────────────
preg_match('/^\s*HOST2\s*=\s*"([^"]*)"/m', $master, $m2);
$host2 = trim($m2[1] ?? '');
if (!empty($host2)) {
    $state  = vv_setup_state_read();
    $p1done = !empty($state['HOST2_PHASE1_DONE']) || !empty($state['host2_phase1_done']);
    $p2done = !empty($state['HOST2_PHASE2_DONE']) || !empty($state['host2_phase2_done']);
    $items[] = [
        'id'     => 'partnership',
        'label'  => 'Partnership',
        'ok'     => $p1done && $p2done,
        'detail' => ($p1done && $p2done)
            ? "Active with $host2"
            : ($p1done ? "Phase 1 done — waiting for HOST2 to complete" : "Not started — run partnership_onboard.sh"),
        'action' => (!$p1done) ? 'onboard' : null,
    ];
}

$allOk = !in_array(false, array_column($items, 'ok'), true);

echo json_encode(['ok' => true, 'complete' => $allOk, 'host_id' => $hostId, 'items' => $items]);


<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
    ? trim($_GET['action'] ?? '')
    : trim($_POST['action'] ?? 'save');

// ── GET: detect environment ────────────────────────────────────────────────────────────────────
if ($action === 'detect') {
    $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
    $bootDisk  = $bootPart
        ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
        : '';
    $transport = $bootDisk
        ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
        : 'unknown';

    $isUsb = ($transport === 'usb');

    preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);

    echo json_encode([
        'ok'           => true,
        'hostname'     => vv_get_hostname(),
        'unraid_ver'   => $vm[1] ?? 'unknown',
        'transport'    => $transport,
        'boot_device'  => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'mode'         => $isUsb ? 'flash' : 'internal',
        'scripts_dir'  => SCRIPTS_DIR,
    ]);
    exit;
}

// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
if ($action === 'ssh_generate') {
    $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
        exit;
    }
    exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
    // Derive pubkey path from hostname
    $hostname  = vv_get_hostname();
    $shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
    $pubPath   = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
    $pubKey    = trim(@file_get_contents($pubPath) ?: '');
    echo json_encode([
        'ok'     => $rc === 0 && !empty($pubKey),
        'pubkey' => $pubKey,
        'error'  => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
    ]);
    exit;
}

// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
if ($action === 'populate') {
    $script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
        exit;
    }
    exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
    $lines = array_values(array_filter(array_map('trim', $out)));
    echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
    exit;
}

$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';

// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
if ($action === 'pull') {
    $mySlot        = trim($_POST['my_slot']        ?? '') ?: strtolower(vv_detect_host());
    $myHostname    = trim($_POST['my_hostname']     ?? '') ?: vv_get_hostname();
    $host1Hostname = trim($_POST['host1_hostname']  ?? '');
    if (!$host1Hostname) {
        $masterRaw = vv_read_conf_raw('master.conf');
        preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
        $host1Hostname = trim($_mh[1] ?? '');
    }
    if (!$host1Hostname) {
        echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
        exit;
    }
    if (!preg_match('/^host\d+$/', $mySlot)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
        exit;
    }

    $hostId    = strtoupper($mySlot);
    $hostIdLow = strtolower($mySlot);

    // Derive SSH key path from this server's hostname
    $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
    $sshKey   = '/root/.ssh/' . $sshOwner . '_rsync_automation';

    if (!file_exists($sshKey)) {
        echo json_encode(['ok' => false, 'error' =>
            "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
        exit;
    }

    // Resolve HOST1 Tailscale IP
    $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
    if (!$ip) {
        echo json_encode(['ok' => false, 'error' =>
            "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
        exit;
    }

    // Get HOST1's SCRIPTS_DIR from their varaverk.cfg
    $sshBase   = 'ssh -i ' . escapeshellarg($sshKey)
               . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
    $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
    preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
    $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations';

    // SCP master.conf from HOST1
    $localMaster = CONF_DIR . '/master.conf';
    $src  = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
    $cmd  = 'scp -i ' . escapeshellarg($sshKey)
          . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
          . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
    exec($cmd, $out, $rc);
    if ($rc !== 0) {
        echo json_encode(['ok' => false, 'error' =>
            'SCP failed: ' . implode('; ', $out) .
            ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
        exit;
    }

    // Create host conf from template if it doesn't exist
    $confFile = $hostIdLow . '.conf';
    if (!file_exists(CONF_DIR . '/' . $confFile)) {
        $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
        if ($template) {
            $bootPart2  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
            $bootDisk2  = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
            $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
            $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
            $conf = str_replace('HOSTN', $hostId,    $template);
            $conf = str_replace('hostn', $hostIdLow, $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
                '${1}"' . $sshKey . '"', $conf);
            $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
                '${1}' . $storageInternal2, $conf);
            vv_write_conf_raw($confFile, $conf);
        }
    }

    if (file_exists($sshScript)) {
        exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
    }
    $apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

    $state = vv_setup_state_read();
    $state['master_conf_pulled'] = 'true';
    vv_setup_state_write($state);

    echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
                      'api_key'  => $apiKeyResult,
                      'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
    exit;
}

// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
$host1      = trim($_POST['host1']      ?? '');
$host2      = trim($_POST['host2']      ?? '');
$mySlot     = trim($_POST['my_slot']    ?? 'host1');
$myHostname = trim($_POST['my_hostname'] ?? '');

if (empty($host1)) {
    echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
    exit;
}
if (!preg_match('/^host\d+$/', $mySlot)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
    exit;
}

// Write HOST1 / HOST2 into master.conf
$master = vv_read_conf_raw('master.conf');
if ($master === '') {
    echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
    exit;
}

$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);

$slotNum = (int) preg_replace('/\D/', '', $mySlot);
if ($slotNum > 2 && !empty($myHostname)) {
    $hostKey = 'HOST' . $slotNum;
    if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
        $master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
            '$1' . "\n    {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
    } else {
        $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
            '${1}"' . addslashes($myHostname) . '"', $master);
    }
}

if (!vv_write_conf_raw('master.conf', $master)) {
    echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
    exit;
}

// Create host*.conf from template
$hostId    = strtoupper($mySlot);
$hostIdLow = strtolower($mySlot);
$confFile  = $hostIdLow . '.conf';

if (!file_exists(CONF_DIR . '/' . $confFile)) {
    $template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
    if ($template) {
        $sshOwner   = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
        $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';

        // Auto-detect storage mode from boot device transport
        $bootPart  = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
        $bootDisk  = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
        $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
        $storageInternal = ($transport !== 'usb') ? 'true' : 'false';

        $conf = str_replace('HOSTN', $hostId,    $template);
        $conf = str_replace('hostn', $hostIdLow, $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
            '${1}"' . $sshKeyPath . '"', $conf);
        $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
            '${1}' . $storageInternal, $conf);
        if (!vv_write_conf_raw($confFile, $conf)) {
            echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
            exit;
        }
    }
}

// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);

// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
if (file_exists($sshScript)) {
    exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
}

// Auto-create Unraid API key and write into the fresh conf
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);

echo json_encode([
    'ok'       => true,
    'host_id'  => $hostId,
    'api_key'  => $apiKeyResult,
    'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);


1
86
87

88	  Arrs
89
90

91
92

93
Loading…

94

95
96
471

  Arrs

Loading…


Bash

ls /boot/config/plugins/varaverk/Plugin/unraid/include/
echo "---"
ls /boot/config/plugins/varaverk/Plugin/unraid/api/

List include and api directories


arrs.php
auth.php
common.php
confform.php
config.php
docker.php
docker_folders.php
docs.php
fallback.php
media.php
monitor.php
partnership.php
scheduler.php
unraid_api.php
vms.php
watchdog.php
---
api_test.php
arrs.php
auth.php
board.php
checklist.php
clearlock.php
conf_toggle.php
confform.php
config.php
create_api_key.php
docker.php
docker_action.php
dryrun.php
fallback.php
flag_toggle.php
log.php
manual_sync.php
media.php
monitor.php
monitor_fast.php
monitor_remote.php
movescript.php
partnership.php
partnership_ping.php
partnership_settings.php
rawconf.php
readscript.php
recent.php
reorderarray.php
rsync.php
rsync_profiles.php
rsync_standalone.php
rsync_win_arrays.php
run.php
savefolders.php
scheduler.php
script.php
scriptinfo.php
settings.php
setup.php
snapshot.php
status.php
stop.php
storage.php
system.php
watchdog.php


The arrs page is already fully built with a very solid UI. Let me check the existing API:


1	<?php
2	header('Content-Type: application/json');
3	require_once dirname(__DIR__) . '/include/config.php';
4
5	// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
6	$_action = trim($_GET['action'] ?? '');
7	if ($_action === 'refresh_remote') {
8	    $host = strtolower(trim($_GET['host'] ?? ''));
9	    if (!preg_match('/^host\d+$/', $host)) {
10	        echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
11	    }
12	    $script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';
13	    if (!file_exists($script)) {
14	        echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
15	    }
16	    set_time_limit(30);
17	    $out = []; $exit = 0;
18	    exec('bash ' . escapeshellarg($script) . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);
19
20	    $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
21	    $node = null;
22	    if (file_exists($cacheFile)) {
23	        $node = json_decode(file_get_contents($cacheFile), true) ?: null;
24	        if ($node) {
25	            $node['cached']    = true;
26	            $node['cache_age'] = time() - (int)filemtime($cacheFile);
27	        }
28	    }
29	    // Bust the main arrs cache so next poll gets fresh data
30	    @unlink(VV_CACHE_DIR . '/arrs.json');
31	    echo json_encode(['ok' => $exit === 0, 'node' => $node, 'output' => implode("\n", $out)]);
32	    exit;
33	}
34	unset($_action);
35
36	// ── Normal data load ──────────────────────────────────────────────────────────
37	$_vv_cached = vv_cache_read('arrs', 300);
38	if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
39	unset($_vv_cached);
40
41	require_once dirname(__DIR__) . '/include/arrs.php';
42	echo json_encode(vv_arrs_all());
43

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
$_action = trim($_GET['action'] ?? '');
if ($_action === 'refresh_remote') {
    $host = strtolower(trim($_GET['host'] ?? ''));
    if (!preg_match('/^host\d+$/', $host)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
    }
    $script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
    }
    set_time_limit(30);
    $out = []; $exit = 0;
    exec('bash ' . escapeshellarg($script) . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);

    $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
    $node = null;
    if (file_exists($cacheFile)) {
        $node = json_decode(file_get_contents($cacheFile), true) ?: null;
        if ($node) {
            $node['cached']    = true;
            $node['cache_age'] = time() - (int)filemtime($cacheFile);
        }
    }
    // Bust the main arrs cache so next poll gets fresh data
    @unlink(VV_CACHE_DIR . '/arrs.json');
    echo json_encode(['ok' => $exit === 0, 'node' => $node, 'output' => implode("\n", $out)]);
    exit;
}
unset($_action);

// ── Normal data load ──────────────────────────────────────────────────────────
$_vv_cached = vv_cache_read('arrs', 300);
if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
unset($_vv_cached);

require_once dirname(__DIR__) . '/include/arrs.php';
echo json_encode(vv_arrs_all());


1	<?php
2	// Arr (Sonarr / Radarr / Lidarr) data helpers
3
4	require_once __DIR__ . '/config.php';
5
6	// ── Conf helpers ──────────────────────────────────────────────────────────────
7
8	function vv_arr_scalar(string $raw, string $key): string {
9	    return vv_parse_conf_scalar($raw, $key);
10	}
11
12	function vv_arr_known_hosts(): array {
13	    return vv_known_hosts();
14	}
15
16	function vv_arr_node_names(): array {
17	    return array_map(fn($name) => $name, vv_arr_known_hosts());
18	}
19
20	// ── Discovery ─────────────────────────────────────────────────────────────────
21
22	function vv_discover_arrs(): array {
23	    $nodes = [];
24	    $defs  = [
25	        'sonarr' => ['SONARR_URL', 'SONARR_API_KEY', 'SONARR_TV_ROOT',     'v3'],
26	        'radarr' => ['RADARR_URL', 'RADARR_API_KEY', 'RADARR_MOVIES_ROOT', 'v3'],
27	        'lidarr' => ['LIDARR_URL', 'LIDARR_API_KEY', 'LIDARR_MUSIC_ROOT',  'v1'],
28	    ];
29	    foreach (array_keys(vv_arr_known_hosts()) as $h) {
30	        $raw = vv_read_conf_raw($h . '.conf');
31	        if (!$raw) continue;
32	        $pfx  = strtoupper($h) . '_';
33	        $get  = fn($k) => vv_arr_scalar($raw, $pfx . $k);
34	        $arrs = [];
35	        foreach ($defs as $type => [$uk, $ak, $rk, $api]) {
36	            $url = $get($uk);
37	            $key = $get($ak);
38	            if ($url && $key && !str_contains($key, 'your-')) {
39	                $arrs[] = ['type' => $type, 'url' => $url, 'key' => $key,
40	                           'root' => $get($rk), 'api' => $api];
41	            }
42	        }
43	        if ($arrs) $nodes[] = ['host' => $h, 'arrs' => $arrs];
44	    }
45	    return $nodes;
46	}
47
48	// ── HTTP ──────────────────────────────────────────────────────────────────────
49
50	function vv_arr_http(string $url, string $apiKey, string $path, int $timeout = 4): ?array {
51	    $ctx = stream_context_create(['http' => [
52	        'timeout'       => $timeout,
53	        'header'        => "X-Api-Key: $apiKey\r\nAccept: application/json\r\n",
54	        'ignore_errors' => true,
55	    ]]);
56	    $raw = @file_get_contents(rtrim($url, '/') . $path, false, $ctx);
57	    return $raw ? (json_decode($raw, true) ?: null) : null;
58	}
59
60	// ── Live arr data ─────────────────────────────────────────────────────────────
61
62	function vv_fetch_arr_live(array $arr): array {
63	    $url  = $arr['url'];
64	    $key  = $arr['key'];
65	    $base = '/api/' . $arr['api'];
66	    $type = $arr['type'];
67
68	    $out = ['online' => false, 'version' => null, 'health' => [],
69	            'queue'  => ['dl' => 0, 'warn' => 0, 'err' => 0], 'disk' => []];
70
71	    $sys = vv_arr_http($url, $key, "$base/system/status");
72	    if (!$sys) return $out;
73	    $out['online']  = true;
74	    $out['version'] = $sys['version'] ?? null;
75
76	    if ($type === 'sonarr') {
77	        $data = vv_arr_http($url, $key, "$base/series");
78	        if (is_array($data)) {
79	            $out['total']     = count($data);
80	            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
81	            $out['episodes']  = array_sum(array_column($data, 'episodeFileCount'));
82	        }
83	    } elseif ($type === 'radarr') {
84	        $data = vv_arr_http($url, $key, "$base/movie");
85	        if (is_array($data)) {
86	            $out['total']     = count($data);
87	            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
88	            $out['files']     = count(array_filter($data, fn($x) => !empty($x['hasFile'])));
89	        }
90	    } elseif ($type === 'lidarr') {
91	        $data = vv_arr_http($url, $key, "$base/artist");
92	        if (is_array($data)) {
93	            $out['total']     = count($data);
94	            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
95	            $out['albums']    = array_sum(array_map(
96	                fn($a) => $a['statistics']['albumCount'] ?? $a['albumCount'] ?? 0, $data));
97	        }
98	    }
99
100	    $q = vv_arr_http($url, $key, "$base/queue?page=1&pageSize=500");
101	    if (is_array($q)) {
102	        foreach (($q['records'] ?? $q) as $r) {
103	            if (!is_array($r)) continue;
104	            $s   = $r['status'] ?? '';
105	            $tds = strtolower($r['trackedDownloadStatus'] ?? '');
106	            $tst = strtolower($r['trackedDownloadState']  ?? '');
107	            if ($s === 'downloading')                           $out['queue']['dl']++;
108	            if ($tds === 'warning' || $tst === 'downloadingstalled') $out['queue']['warn']++;
109	            if ($tds === 'error')                               $out['queue']['err']++;
110	        }
111	    }
112
113	    $h = vv_arr_http($url, $key, "$base/health");
114	    if (is_array($h)) $out['health'] = $h;
115
116	    $d = vv_arr_http($url, $key, "$base/diskspace");
117	    if (is_array($d)) $out['disk'] = $d;
118
119	    return $out;
120	}
121
122	// ── Log stats ─────────────────────────────────────────────────────────────────
123
124	function vv_arr_cleanup_stats(string $type): array {
125	    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
126	              'radarr' => 'Media/radarr_cleanup',
127	              'lidarr' => 'Media/lidarr_cleanup'];
128	    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
129	    $out  = ['last_run' => null, 'end' => null, 'status' => null,
130	             'tracked'  => null, 'total' => null,
131	             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];
132
133	    $jf = $base . '.json';
134	    if (!file_exists($jf)) return $out;
135	    $meta = json_decode(file_get_contents($jf), true) ?: [];
136	    $out['last_run'] = $meta['start']  ?? null;
137	    $out['end']      = $meta['end']    ?? null;
138	    $out['status']   = $meta['status'] ?? null;
139
140	    $lf = $base . '.log';
141	    if (!file_exists($lf)) return $out;
142	    $log   = file_get_contents($lf);
143	    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
144	    $blk   = count($parts) > 1 ? end($parts) : $log;
145
146	    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
147	        $out['tracked'] = (int)str_replace(',', '', $m[1]);
148	        $out['total']   = (int)str_replace(',', '', $m[2]);
149	    }
150	    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
151	        $out['orphans']    = (int)str_replace(',', '', $m[1]);
152	        $out['orphans_sz'] = trim($m[2]);
153	    }
154	    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
155	        $out['junk'] = (int)str_replace(',', '', $m[1]);
156	    }
157	    return $out;
158	}
159
160	function vv_arr_discovery_stats(string $type): array {
161	    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
162	              'radarr' => 'Media/playback_aware_radarr_discovery',
163	              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
164	    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
165	    $out  = ['last_run' => null, 'status' => null, 'added' => null];
166
167	    $jf = $base . '.json';
168	    if (!file_exists($jf)) return $out;
169	    $meta = json_decode(file_get_contents($jf), true) ?: [];
170	    $out['last_run'] = $meta['start']  ?? null;
171	    $out['status']   = $meta['status'] ?? null;
172
173	    $lf = $base . '.log';
174	    if (file_exists($lf)) {
175	        $log = file_get_contents($lf);
176	        if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
177	        elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
178	    }
179	    return $out;
180	}
181
182	function vv_arr_sync_stats(): array {
183	    $base = LOG_DIR . '/Media/arr_sync';
184	    $out  = ['last_run' => null, 'status' => null, 'added' => null,
185	             'nodes'    => null, 'blocklist_count' => null];
186
187	    $jf = $base . '.json';
188	    if (file_exists($jf)) {
189	        $meta = json_decode(file_get_contents($jf), true) ?: [];
190	        $out['last_run'] = $meta['start']  ?? null;
191	        $out['status']   = $meta['status'] ?? null;
192	    }
193
194	    $master = vv_read_conf_raw('master.conf');
195	    if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
196	        $blPath = trim($m[1]);
197	        if (file_exists($blPath)) {
198	            $out['blocklist_count'] = count(array_filter(
199	                file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
200	        }
201	    }
202
203	    $lf = $base . '.log';
204	    if (file_exists($lf)) {
205	        $log = file_get_contents($lf);
206	        if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
207	        if (preg_match('/Nodes?[:\s]+(\d+)/i',      $log, $m)) $out['nodes'] = (int)$m[1];
208	    }
209	    return $out;
210	}
211
212	function vv_arr_recovery_stats(): array {
213	    $base = LOG_DIR . '/Media/arrs_failed_stalled_recovery';
214	    $out  = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];
215
216	    $jf = $base . '.json';
217	    if (!file_exists($jf)) return $out;
218	    $meta = json_decode(file_get_contents($jf), true) ?: [];
219	    $out['last_run'] = $meta['start']  ?? null;
220	    $out['status']   = $meta['status'] ?? null;
221
222	    $lf = $base . '.log';
223	    if (file_exists($lf)) {
224	        $log = file_get_contents($lf);
225	        if (preg_match('/Removed[:\s]+(\d+)/i',   $log, $m)) $out['fixed']    = (int)$m[1];
226	        if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
227	    }
228	    return $out;
229	}
230
231	// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────
232
233	function vv_arrs_local_node(): array {
234	    $host  = vv_detect_host();
235	    $names = vv_arr_node_names();
236	    $arrs  = [];
237
238	    foreach (vv_discover_arrs() as $node) {
239	        if ($node['host'] !== $host) continue;
240	        foreach ($node['arrs'] as $arr) {
241	            $entry = ['type' => $arr['type'], 'root' => $arr['root']];
242	            $entry = array_merge($entry, vv_fetch_arr_live($arr));
243	            $entry['cleanup']   = vv_arr_cleanup_stats($arr['type']);
244	            $entry['discovery'] = vv_arr_discovery_stats($arr['type']);
245	            $arrs[] = $entry;
246	        }
247	        break;
248	    }
249
250	    return [
251	        'host'  => $host,
252	        'name'  => $names[$host] ?? strtoupper($host),
253	        'local' => true,
254	        'arrs'  => $arrs,
255	    ];
256	}
257
258	// ── Entry point ───────────────────────────────────────────────────────────────
259
260	function vv_arrs_all(): array {
261	    $currentHost = vv_detect_host();
262	    $names       = vv_arr_node_names();
263
264	    // Local node — full live data
265	    $result = [vv_arrs_local_node()];
266
267	    // Remote nodes — read from file cache written by remote_arr_cache_writer.sh
268	    foreach (array_keys($names) as $h) {
269	        if ($h === $currentHost) continue;
270	        $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $h . '.json';
271	        if (file_exists($cacheFile)) {
272	            $node = json_decode(file_get_contents($cacheFile), true) ?: [];
273	            $node['cached']    = true;
274	            $node['cache_age'] = time() - (int)filemtime($cacheFile);
275	            $result[] = $node;
276	        } else {
277	            $result[] = [
278	                'host'       => $h,
279	                'name'       => $names[$h],
280	                'local'      => false,
281	                'cached'     => false,
282	                'cache_miss' => true,
283	                'arrs'       => [],
284	            ];
285	        }
286	    }
287
288	    $vars = vv_conf_vars();
289	    $myId = strtoupper($currentHost);
290
291	    return [
292	        'nodes'    => $result,
293	        'sync'     => vv_arr_sync_stats(),
294	        'recovery' => vv_arr_recovery_stats(),
295	        'host'     => $currentHost,
296	        'settings' => [
297	            'arr_sync_enabled'   => ($vars['ARR_SYNC_ENABLED']               ?? 'true') !== 'false',
298	            'sonarr_recovery'    => ($vars[$myId . '_SONARR_RECOVERY']       ?? 'true') !== 'false',
299	            'radarr_recovery'    => ($vars[$myId . '_RADARR_RECOVERY']       ?? 'true') !== 'false',
300	            'lidarr_recovery'    => ($vars[$myId . '_LIDARR_RECOVERY']       ?? 'true') !== 'false',
301	            'recovery_age_hours' => (int)($vars['ARR_IMPORT_RECOVERY_AGE']   ?? 6),
302	            'sonarr_port'        => (int)($vars['ARR_SYNC_SONARR_PORT']      ?? 8989),
303	            'radarr_port'        => (int)($vars['ARR_SYNC_RADARR_PORT']      ?? 7878),
304	            'lidarr_port'        => (int)($vars['ARR_SYNC_LIDARR_PORT']      ?? 8686),
305	            'my_host'            => $currentHost,
306	            'my_id'              => $myId,
307	        ],
308	        'ts'       => time(),
309	    ];
310	}
311

<?php
// Arr (Sonarr / Radarr / Lidarr) data helpers

require_once __DIR__ . '/config.php';

// ── Conf helpers ──────────────────────────────────────────────────────────────

function vv_arr_scalar(string $raw, string $key): string {
    return vv_parse_conf_scalar($raw, $key);
}

function vv_arr_known_hosts(): array {
    return vv_known_hosts();
}

function vv_arr_node_names(): array {
    return array_map(fn($name) => $name, vv_arr_known_hosts());
}

// ── Discovery ─────────────────────────────────────────────────────────────────

function vv_discover_arrs(): array {
    $nodes = [];
    $defs  = [
        'sonarr' => ['SONARR_URL', 'SONARR_API_KEY', 'SONARR_TV_ROOT',     'v3'],
        'radarr' => ['RADARR_URL', 'RADARR_API_KEY', 'RADARR_MOVIES_ROOT', 'v3'],
        'lidarr' => ['LIDARR_URL', 'LIDARR_API_KEY', 'LIDARR_MUSIC_ROOT',  'v1'],
    ];
    foreach (array_keys(vv_arr_known_hosts()) as $h) {
        $raw = vv_read_conf_raw($h . '.conf');
        if (!$raw) continue;
        $pfx  = strtoupper($h) . '_';
        $get  = fn($k) => vv_arr_scalar($raw, $pfx . $k);
        $arrs = [];
        foreach ($defs as $type => [$uk, $ak, $rk, $api]) {
            $url = $get($uk);
            $key = $get($ak);
            if ($url && $key && !str_contains($key, 'your-')) {
                $arrs[] = ['type' => $type, 'url' => $url, 'key' => $key,
                           'root' => $get($rk), 'api' => $api];
            }
        }
        if ($arrs) $nodes[] = ['host' => $h, 'arrs' => $arrs];
    }
    return $nodes;
}

// ── HTTP ──────────────────────────────────────────────────────────────────────

function vv_arr_http(string $url, string $apiKey, string $path, int $timeout = 4): ?array {
    $ctx = stream_context_create(['http' => [
        'timeout'       => $timeout,
        'header'        => "X-Api-Key: $apiKey\r\nAccept: application/json\r\n",
        'ignore_errors' => true,
    ]]);
    $raw = @file_get_contents(rtrim($url, '/') . $path, false, $ctx);
    return $raw ? (json_decode($raw, true) ?: null) : null;
}

// ── Live arr data ─────────────────────────────────────────────────────────────

function vv_fetch_arr_live(array $arr): array {
    $url  = $arr['url'];
    $key  = $arr['key'];
    $base = '/api/' . $arr['api'];
    $type = $arr['type'];

    $out = ['online' => false, 'version' => null, 'health' => [],
            'queue'  => ['dl' => 0, 'warn' => 0, 'err' => 0], 'disk' => []];

    $sys = vv_arr_http($url, $key, "$base/system/status");
    if (!$sys) return $out;
    $out['online']  = true;
    $out['version'] = $sys['version'] ?? null;

    if ($type === 'sonarr') {
        $data = vv_arr_http($url, $key, "$base/series");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['episodes']  = array_sum(array_column($data, 'episodeFileCount'));
        }
    } elseif ($type === 'radarr') {
        $data = vv_arr_http($url, $key, "$base/movie");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['files']     = count(array_filter($data, fn($x) => !empty($x['hasFile'])));
        }
    } elseif ($type === 'lidarr') {
        $data = vv_arr_http($url, $key, "$base/artist");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['albums']    = array_sum(array_map(
                fn($a) => $a['statistics']['albumCount'] ?? $a['albumCount'] ?? 0, $data));
        }
    }

    $q = vv_arr_http($url, $key, "$base/queue?page=1&pageSize=500");
    if (is_array($q)) {
        foreach (($q['records'] ?? $q) as $r) {
            if (!is_array($r)) continue;
            $s   = $r['status'] ?? '';
            $tds = strtolower($r['trackedDownloadStatus'] ?? '');
            $tst = strtolower($r['trackedDownloadState']  ?? '');
            if ($s === 'downloading')                           $out['queue']['dl']++;
            if ($tds === 'warning' || $tst === 'downloadingstalled') $out['queue']['warn']++;
            if ($tds === 'error')                               $out['queue']['err']++;
        }
    }

    $h = vv_arr_http($url, $key, "$base/health");
    if (is_array($h)) $out['health'] = $h;

    $d = vv_arr_http($url, $key, "$base/diskspace");
    if (is_array($d)) $out['disk'] = $d;

    return $out;
}

// ── Log stats ─────────────────────────────────────────────────────────────────

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['end']      = $meta['end']    ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (!file_exists($lf)) return $out;
    $log   = file_get_contents($lf);
    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
    $blk   = count($parts) > 1 ? end($parts) : $log;

    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
        $out['tracked'] = (int)str_replace(',', '', $m[1]);
        $out['total']   = (int)str_replace(',', '', $m[2]);
    }
    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
        $out['orphans']    = (int)str_replace(',', '', $m[1]);
        $out['orphans_sz'] = trim($m[2]);
    }
    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
        $out['junk'] = (int)str_replace(',', '', $m[1]);
    }
    return $out;
}

function vv_arr_discovery_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
              'radarr' => 'Media/playback_aware_radarr_discovery',
              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'status' => null, 'added' => null];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_sync_stats(): array {
    $base = LOG_DIR . '/Media/arr_sync';
    $out  = ['last_run' => null, 'status' => null, 'added' => null,
             'nodes'    => null, 'blocklist_count' => null];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;
    }

    $master = vv_read_conf_raw('master.conf');
    if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
        $blPath = trim($m[1]);
        if (file_exists($blPath)) {
            $out['blocklist_count'] = count(array_filter(
                file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
        }
    }

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        if (preg_match('/Nodes?[:\s]+(\d+)/i',      $log, $m)) $out['nodes'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_recovery_stats(): array {
    $base = LOG_DIR . '/Media/arrs_failed_stalled_recovery';
    $out  = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Removed[:\s]+(\d+)/i',   $log, $m)) $out['fixed']    = (int)$m[1];
        if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
    }
    return $out;
}

// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────

function vv_arrs_local_node(): array {
    $host  = vv_detect_host();
    $names = vv_arr_node_names();
    $arrs  = [];

    foreach (vv_discover_arrs() as $node) {
        if ($node['host'] !== $host) continue;
        foreach ($node['arrs'] as $arr) {
            $entry = ['type' => $arr['type'], 'root' => $arr['root']];
            $entry = array_merge($entry, vv_fetch_arr_live($arr));
            $entry['cleanup']   = vv_arr_cleanup_stats($arr['type']);
            $entry['discovery'] = vv_arr_discovery_stats($arr['type']);
            $arrs[] = $entry;
        }
        break;
    }

    return [
        'host'  => $host,
        'name'  => $names[$host] ?? strtoupper($host),
        'local' => true,
        'arrs'  => $arrs,
    ];
}

// ── Entry point ───────────────────────────────────────────────────────────────

function vv_arrs_all(): array {
    $currentHost = vv_detect_host();
    $names       = vv_arr_node_names();

    // Local node — full live data
    $result = [vv_arrs_local_node()];

    // Remote nodes — read from file cache written by remote_arr_cache_writer.sh
    foreach (array_keys($names) as $h) {
        if ($h === $currentHost) continue;
        $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $h . '.json';
        if (file_exists($cacheFile)) {
            $node = json_decode(file_get_contents($cacheFile), true) ?: [];
            $node['cached']    = true;
            $node['cache_age'] = time() - (int)filemtime($cacheFile);
            $result[] = $node;
        } else {
            $result[] = [
                'host'       => $h,
                'name'       => $names[$h],
                'local'      => false,
                'cached'     => false,
                'cache_miss' => true,
                'arrs'       => [],
            ];
        }
    }

    $vars = vv_conf_vars();
    $myId = strtoupper($currentHost);

    return [
        'nodes'    => $result,
        'sync'     => vv_arr_sync_stats(),
        'recovery' => vv_arr_recovery_stats(),
        'host'     => $currentHost,
        'settings' => [
            'arr_sync_enabled'   => ($vars['ARR_SYNC_ENABLED']               ?? 'true') !== 'false',
            'sonarr_recovery'    => ($vars[$myId . '_SONARR_RECOVERY']       ?? 'true') !== 'false',
            'radarr_recovery'    => ($vars[$myId . '_RADARR_RECOVERY']       ?? 'true') !== 'false',
            'lidarr_recovery'    => ($vars[$myId . '_LIDARR_RECOVERY']       ?? 'true') !== 'false',
            'recovery_age_hours' => (int)($vars['ARR_IMPORT_RECOVERY_AGE']   ?? 6),
            'sonarr_port'        => (int)($vars['ARR_SYNC_SONARR_PORT']      ?? 8989),
            'radarr_port'        => (int)($vars['ARR_SYNC_RADARR_PORT']      ?? 7878),
            'lidarr_port'        => (int)($vars['ARR_SYNC_LIDARR_PORT']      ?? 8686),
            'my_host'            => $currentHost,
            'my_id'              => $myId,
        ],
        'ts'       => time(),
    ];
}


The arrs page is already implemented. Let me check if the supporting files exist and the page actually works:


grep -r 'LOG_DIR\|VV_CACHE_DIR\|CONF_DIR\|SCRIPTS_DIR' /boot/config/plugins/varaverk/Plugin/unraid/include/config.php | head -20
echo "---"
ls /tmp/vv_cache/ 2>/dev/null || echo "empty"
echo "---"
ls /boot/config/plugins/varaverk/data/

Check defined constants and cache directory contents


/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('CONF_DIR',    SCRIPTS_DIR . '/Configurations');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('DATA_DIR',    SCRIPTS_DIR . '/data');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('STATE_DIR',   SCRIPTS_DIR . '/State_Files');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('LOG_DIR',     '/var/log/varaverk');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:define('VV_CACHE_DIR',        '/tmp/vv_cache');
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    $localPath = CONF_DIR . '/master.conf';
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:        // Single SSH call: get remote SCRIPTS_DIR and verify plugin is installed,
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:            $sshBase . ' "cfg=$(grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null)'
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:            . ' && sd=$(echo \"$cfg\" | grep -oP \'(?<=SCRIPTS_DIR=\")[^\"]+\')'
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    $path = CONF_DIR . '/' . $filename;
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    $path = CONF_DIR . '/' . $filename;
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:        foreach (glob(CONF_DIR . '/*.conf') as $f) {
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:        @file_put_contents(VV_CACHE_DIR . '/vv_api_debug.json', json_encode([
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:        @file_put_contents(VV_CACHE_DIR . '/vv_api_debug.json', json_encode([
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    $f = VV_CACHE_DIR . '/' . $key . '.json';
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    if (!is_dir(VV_CACHE_DIR)) @mkdir(VV_CACHE_DIR, 0755, true);
/boot/config/plugins/varaverk/Plugin/unraid/include/config.php:    $f   = VV_CACHE_DIR . '/' . $key . '.json';
---
arrs.json
board_partner.json
ext_ip.json
monitor.json
snap_media.json
vv_api_debug.json
vv_cpu_stat.json
vv_diskio_snap.json
vv_net_stat.json
vv_remote_HOST2.json
---
arr_cleanup_stats.db
arr_recovery_stats.db
bandwidth_history.db
container_restart_history.db
lidarr_discovery_history.db
lidarr_tracked.count
radarr_discovery_history.db
sonarr_discovery_history.db
transcode_daily.db


cat /tmp/vv_cache/arrs.json | python3 -c "import json,sys; d=json.load(sys.stdin); [print(k,':', type(v).__name__) for k,v in d.items()]; print('nodes:', len(d.get('nodes',[])));[print(' node:', n.get('host'), 'arrs:', len(n.get('arrs',[])), 'online:', [a.get('online') for a in n.get('arrs',[])]) for n in d.get('nodes',[])]" 2>/dev/null || echo "no cache or parse error"
echo "---"
ls /var/log/varaverk/Media/ 2>/dev/null | head -20

Check current arrs cache and log directory


no cache or parse error
---


ls /var/log/varaverk/ 2>/dev/null | head -10 || echo "no log dir"
echo "---"
cat /tmp/vv_cache/arrs.json 2>/dev/null | head -5 || echo "no cache"
echo "---"
# check if there's an actual arrs.json cache from today
ls -la /tmp/vv_cache/

Check log dir and cache files


Orchestrators/
Plugin/
Tools/
git_pull_execute.json
git_pull_execute.log
git_pull_execute.manual_ts
rebuild_cron.log
---
{"nodes":[{"host":"host1","name":"unRAID-Gmer4Lfe","local":true,"arrs":[{"type":"sonarr","root":"\/mnt\/user\/Tv_Shows","online":true,"version":"4.0.17.2952","health":[{"source":"IndexerLongTermStatusCheck","type":"warning","message":"Indexers unavailable due to failures for more than 6 hours: IPTorrents (Prowlarr), Nzb.su (Prowlarr), FearNoPeer (Prowlarr), Miatrix (Prowlarr), NzbPlanet (Prowlarr), Square Eyed (Prowlarr), scenenzbs (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/sonarr\/system#indexers-are-unavailable-due-to-failures"},{"source":"DownloadClientRemovesCompletedDownloadsCheck","type":"warning","message":"Download client Qbittorrent-Gmer4Lfe is set to remove completed downloads. This can result in downloads being removed from your client before Sonarr can import them.","wikiUrl":"https:\/\/wiki.servarr.com\/sonarr\/system#download-client-removes-completed-downloads"},{"source":"IndexerStatusCheck","type":"warning","message":"Indexers unavailable due to failures: NzbNoob (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/sonarr\/system#indexers-are-unavailable-due-to-failures"}],"queue":{"dl":13,"warn":22,"err":0},"disk":[{"path":"\/","label":"","freeSpace":711016316928,"totalSpace":933634113536},{"path":"\/kids tv","label":"","freeSpace":2268016803840,"totalSpace":11998185066496},{"path":"\/tv","label":"","freeSpace":12889807478784,"totalSpace":83554518999040},{"path":"\/config","label":"","freeSpace":711016316928,"totalSpace":749980352512},{"path":"\/ext-anime-shows","label":"","freeSpace":2252017762304,"totalSpace":8812011192320},{"path":"\/ext-standup-comedy","label":"","freeSpace":4657060249600,"totalSpace":11747339141120},{"path":"\/downloads\/Completed\/Tv Shows","label":"","freeSpace":241520345088,"totalSpace":969083715584}],"total":1164,"monitored":445,"episodes":0,"cleanup":{"last_run":null,"end":null,"status":null,"tracked":null,"total":null,"orphans":0,"orphans_sz":"0B","junk":0},"discovery":{"last_run":null,"status":null,"added":null}},{"type":"radarr","root":"\/mnt\/user\/Movies","online":true,"version":"6.1.1.10360","health":[{"source":"IndexerStatusCheck","type":"warning","message":"Indexers unavailable due to failures: NzbNoob (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/radarr\/system#indexers-are-unavailable-due-to-failures"},{"source":"DownloadClientRemovesCompletedDownloadsCheck","type":"warning","message":"Download client Qbittorrent-Gmer4Lfe is set to remove completed downloads. This can result in downloads being removed from your client before Radarr can import them.","wikiUrl":"https:\/\/wiki.servarr.com\/radarr\/system#download-client-removes-completed-downloads"},{"source":"IndexerLongTermStatusCheck","type":"warning","message":"Indexers unavailable due to failures for more than 6 hours: IPTorrents (Prowlarr), Nzb.su (Prowlarr), FearNoPeer (Prowlarr), Miatrix (Prowlarr), NzbPlanet (Prowlarr), Square Eyed (Prowlarr), scenenzbs (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/radarr\/system#indexers-are-unavailable-due-to-failures"}],"queue":{"dl":0,"warn":0,"err":0},"disk":[{"path":"\/","label":"","freeSpace":711016448000,"totalSpace":933634244608},{"path":"\/config","label":"","freeSpace":711016448000,"totalSpace":749980483584},{"path":"\/downloads\/Completed\/Movies","label":"","freeSpace":241520345088,"totalSpace":969083715584},{"path":"\/ext-anime-movies","label":"","freeSpace":2252017762304,"totalSpace":8812011192320},{"path":"\/ext-stand-up-comedy","label":"","freeSpace":4657060249600,"totalSpace":11747339141120},{"path":"\/kids movies","label":"","freeSpace":2268016803840,"totalSpace":11998185066496},{"path":"\/movies","label":"","freeSpace":7465508143104,"totalSpace":23708219473920}],"total":2860,"monitored":2855,"files":2724,"cleanup":{"last_run":null,"end":null,"status":null,"tracked":null,"total":null,"orphans":0,"orphans_sz":"0B","junk":0},"discovery":{"last_run":null,"status":null,"added":null}},{"type":"lidarr","root":"\/mnt\/user\/Music-New","online":true,"version":"3.1.0.4875","health":[{"source":"IndexerStatusCheck","type":"warning","message":"Indexers unavailable due to failures: NzbNoob (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/lidarr\/system#indexers-are-unavailable-due-to-failures"},{"source":"IndexerLongTermStatusCheck","type":"warning","message":"Indexers unavailable due to failures for more than 6 hours: digitalcarnage (Prowlarr), scenenzbs (Prowlarr), scenenzbs2 (Prowlarr)","wikiUrl":"https:\/\/wiki.servarr.com\/lidarr\/system#indexers-are-unavailable-due-to-failures"}],"queue":{"dl":0,"warn":0,"err":0},"disk":[{"path":"\/config","label":"","freeSpace":711016448000,"totalSpace":933634244608},{"path":"\/downloads\/Completed\/Music","label":"","freeSpace":241410506752,"totalSpace":969079390208},{"path":"\/downloads\/Slskd\/completed","label":"","freeSpace":241410506752,"totalSpace":990526832640},{"path":"\/ext-music","label":"","freeSpace":4657060249600,"totalSpace":11747339141120},{"path":"\/ext-music\/","label":"","freeSpace":4657060249600,"totalSpace":11747339141120}],"total":1345,"monitored":1345,"albums":9429,"cleanup":{"last_run":null,"end":null,"status":null,"tracked":null,"total":null,"orphans":0,"orphans_sz":"0B","junk":0},"discovery":{"last_run":null,"status":null,"added":null}}]},{"host":"host2","name":"unRAID-Jayred365","local":false,"cached":false,"cache_miss":true,"arrs":[]}],"sync":{"last_run":null,"status":null,"added":null,"nodes":null,"blocklist_count":null},"recovery":{"last_run":null,"status":null,"fixed":0,"searched":0},"host":"host1","settings":{"arr_sync_enabled":true,"sonarr_recovery":true,"radarr_recovery":true,"lidarr_recovery":true,"recovery_age_hours":6,"sonarr_port":8989,"radarr_port":7878,"lidarr_port":8686,"my_host":"host1","my_id":"HOST1"},"ts":1780714633}---
total 64
drwxr-xr-x  2 root root   240 Jun  5 22:57 ./
drwxrwxrwt 18 root root   960 Jun  5 22:57 ../
-rw-r--r--  1 root root  5776 Jun  5 22:57 arrs.json
-rw-r--r--  1 root root    57 Jun  5 22:56 board_partner.json
-rw-r--r--  1 root root    22 Jun  5 22:53 ext_ip.json
-rw-r--r--  1 root root 23989 Jun  5 22:57 monitor.json
-rw-r--r--  1 root root    38 Jun  5 22:57 snap_media.json
-rw-r--r--  1 root root  3374 Jun  5 22:57 vv_api_debug.json
-rw-r--r--  1 root root  1680 Jun  5 22:57 vv_cpu_stat.json
-rw-r--r--  1 root root   547 Jun  5 22:57 vv_diskio_snap.json
-rw-r--r--  1 root root    58 Jun  5 22:57 vv_net_stat.json
-rw-r--r--  1 root root   369 Jun  5 22:57 vv_remote_HOST2.json


The arrs page is already fully built. The issue is cleanup/discovery/recovery stats show `null` because the log `.json` files don't exist yet — the data is in the `.db` files in `data/`. I'll add fallbacks to read from those:


122	// ── Log stats ─────────────────────────────────────────────────────────────────
123
124	function vv_arr_cleanup_stats(string $type): array {
125	    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
126	              'radarr' => 'Media/radarr_cleanup',
127	              'lidarr' => 'Media/lidarr_cleanup'];
128	    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
129	    $out  = ['last_run' => null, 'end' => null, 'status' => null,
130	             'tracked'  => null, 'total' => null,
131	             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];
132
133	    $jf = $base . '.json';
134	    if (!file_exists($jf)) return $out;
135	    $meta = json_decode(file_get_contents($jf), true) ?: [];
136	    $out['last_run'] = $meta['start']  ?? null;
137	    $out['end']      = $meta['end']    ?? null;
138	    $out['status']   = $meta['status'] ?? null;
139
140	    $lf = $base . '.log';
141	    if (!file_exists($lf)) return $out;
142	    $log   = file_get_contents($lf);
143	    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
144	    $blk   = count($parts) > 1 ? end($parts) : $log;
145
146	    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
147	        $out['tracked'] = (int)str_replace(',', '', $m[1]);
148	        $out['total']   = (int)str_replace(',', '', $m[2]);
149	    }
150	    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
151	        $out['orphans']    = (int)str_replace(',', '', $m[1]);
152	        $out['orphans_sz'] = trim($m[2]);
153	    }
154	    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
155	        $out['junk'] = (int)str_replace(',', '', $m[1]);
156	    }
157	    return $out;
158	}
159
160	function vv_arr_discovery_stats(string $type): array {
161	    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
162	              'radarr' => 'Media/playback_aware_radarr_discovery',
163	              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
164	    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
165	    $out  = ['last_run' => null, 'status' => null, 'added' => null];
166
167	    $jf = $base . '.json';
168	    if (!file_exists($jf)) return $out;
169	    $meta = json_decode(file_get_contents($jf), true) ?: [];
170	    $out['last_run'] = $meta['start']  ?? null;
171	    $out['status']   = $meta['status'] ?? null;
172
173	    $lf = $base . '.log';
174	    if (file_exists($lf)) {
175	        $log = file_get_contents($lf);
176	        if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
177	        elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
178	    }
179	    return $out;
180	}
181
182	function vv_arr_sync_stats(): array {
183	    $base = LOG_DIR . '/Media/arr_sync';
184	    $out  = ['last_run' => null, 'status' => null, 'added' => null,
185	             'nodes'    => null, 'blocklist_count' => null];
186
187	    $jf = $base . '.json';
188	    if (file_exists($jf)) {
189	        $meta = json_decode(file_get_contents($jf), true) ?: [];
190	        $out['last_run'] = $meta['start']  ?? null;
191	        $out['status']   = $meta['status'] ?? null;
192	    }
193
194	    $master = vv_read_conf_raw('master.conf');
195	    if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
196	        $blPath = trim($m[1]);
197	        if (file_exists($blPath)) {
198	            $out['blocklist_count'] = count(array_filter(
199	                file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
200	        }
201	    }
202
203	    $lf = $base . '.log';
204	    if (file_exists($lf)) {
205	        $log = file_get_contents($lf);
206	        if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
207	        if (preg_match('/Nodes?[:\s]+(\d+)/i',      $log, $m)) $out['nodes'] = (int)$m[1];
208	    }
209	    return $out;
210	}
211
212	function vv_arr_recovery_stats(): array {
213	    $base = LOG_DIR . '/Media/arrs_failed_stalled_recovery';
214	    $out  = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];
215
216	    $jf = $base . '.json';
217	    if (!file_exists($jf)) return $out;
218	    $meta = json_decode(file_get_contents($jf), true) ?: [];
219	    $out['last_run'] = $meta['start']  ?? null;
220	    $out['status']   = $meta['status'] ?? null;
221
222	    $lf = $base . '.log';
223	    if (file_exists($lf)) {
224	        $log = file_get_contents($lf);
225	        if (preg_match('/Removed[:\s]+(\d+)/i',   $log, $m)) $out['fixed']    = (int)$m[1];
226	        if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
227	    }
228	    return $out;
229	}
230
231	// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────

// ── Log stats ─────────────────────────────────────────────────────────────────

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['end']      = $meta['end']    ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (!file_exists($lf)) return $out;
    $log   = file_get_contents($lf);
    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
    $blk   = count($parts) > 1 ? end($parts) : $log;

    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
        $out['tracked'] = (int)str_replace(',', '', $m[1]);
        $out['total']   = (int)str_replace(',', '', $m[2]);
    }
    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
        $out['orphans']    = (int)str_replace(',', '', $m[1]);
        $out['orphans_sz'] = trim($m[2]);
    }
    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
        $out['junk'] = (int)str_replace(',', '', $m[1]);
    }
    return $out;
}

function vv_arr_discovery_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
              'radarr' => 'Media/playback_aware_radarr_discovery',
              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'status' => null, 'added' => null];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_sync_stats(): array {
    $base = LOG_DIR . '/Media/arr_sync';
    $out  = ['last_run' => null, 'status' => null, 'added' => null,
             'nodes'    => null, 'blocklist_count' => null];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;
    }

    $master = vv_read_conf_raw('master.conf');
    if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
        $blPath = trim($m[1]);
        if (file_exists($blPath)) {
            $out['blocklist_count'] = count(array_filter(
                file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
        }
    }

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        if (preg_match('/Nodes?[:\s]+(\d+)/i',      $log, $m)) $out['nodes'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_recovery_stats(): array {
    $base = LOG_DIR . '/Media/arrs_failed_stalled_recovery';
    $out  = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Removed[:\s]+(\d+)/i',   $log, $m)) $out['fixed']    = (int)$m[1];
        if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
    }
    return $out;
}

// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────


Edit

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['end']      = $meta['end']    ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (!file_exists($lf)) return $out;
    $log   = file_get_contents($lf);
    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
    $blk   = count($parts) > 1 ? end($parts) : $log;

    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
        $out['tracked'] = (int)str_replace(',', '', $m[1]);
        $out['total']   = (int)str_replace(',', '', $m[2]);
    }
    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
        $out['orphans']    = (int)str_replace(',', '', $m[1]);
        $out['orphans_sz'] = trim($m[2]);
    }
    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
        $out['junk'] = (int)str_replace(',', '', $m[1]);
    }
    return $out;
}

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['end']      = $meta['end']    ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log   = file_get_contents($lf);
            $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
            $blk   = count($parts) > 1 ? end($parts) : $log;

            if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
                $out['tracked'] = (int)str_replace(',', '', $m[1]);
                $out['total']   = (int)str_replace(',', '', $m[2]);
            }
            if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
                $out['orphans']    = (int)str_replace(',', '', $m[1]);
                $out['orphans_sz'] = trim($m[2]);
            }
            if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
                $out['junk'] = (int)str_replace(',', '', $m[1]);
            }
        }
    }

    // Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/arr_cleanup_stats.db';
        if (file_exists($dbFile)) {
            $last = null;
            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $p = explode('|', $line);
                if (count($p) >= 8 && $p[1] === $type) $last = $p;
            }
            if ($last) {
                $out['last_run'] = strtotime($last[0] . ' 23:59:00') ?: null;
                $out['status']   = 'ok';
                $out['orphans']  = (int)$last[2];
                $out['junk']     = (int)$last[4];
                $out['tracked']  = (int)$last[7];
            }
        }
    }

    return $out;
}


The file /boot/config/plugins/varaverk/Plugin/unraid/include/arrs.php has been updated successfully. (file state is current in your context — no need to Read it back)

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['end']      = $meta['end']    ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (!file_exists($lf)) return $out;
    $log   = file_get_contents($lf);
    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
    $blk   = count($parts) > 1 ? end($parts) : $log;

    if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
        $out['tracked'] = (int)str_replace(',', '', $m[1]);
        $out['total']   = (int)str_replace(',', '', $m[2]);
    }
    if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
        $out['orphans']    = (int)str_replace(',', '', $m[1]);
        $out['orphans_sz'] = trim($m[2]);
    }
    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
        $out['junk'] = (int)str_replace(',', '', $m[1]);
    }
    return $out;
}

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/sonarr_cleanup',
              'radarr' => 'Media/radarr_cleanup',
              'lidarr' => 'Media/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['end']      = $meta['end']    ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log   = file_get_contents($lf);
            $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
            $blk   = count($parts) > 1 ? end($parts) : $log;

            if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
                $out['tracked'] = (int)str_replace(',', '', $m[1]);
                $out['total']   = (int)str_replace(',', '', $m[2]);
            }
            if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
                $out['orphans']    = (int)str_replace(',', '', $m[1]);
                $out['orphans_sz'] = trim($m[2]);
            }
            if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
                $out['junk'] = (int)str_replace(',', '', $m[1]);
            }
        }
    }

    // Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/arr_cleanup_stats.db';
        if (file_exists($dbFile)) {
            $last = null;
            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $p = explode('|', $line);
                if (count($p) >= 8 && $p[1] === $type) $last = $p;
            }
            if ($last) {
                $out['last_run'] = strtotime($last[0] . ' 23:59:00') ?: null;
                $out['status']   = 'ok';
                $out['orphans']  = (int)$last[2];
                $out['junk']     = (int)$last[4];
                $out['tracked']  = (int)$last[7];
            }
        }
    }

    return $out;
}

-    if (!file_exists($jf)) return $out;

-    $meta = json_decode(file_get_contents($jf), true) ?: [];

-    $out['last_run'] = $meta['start']  ?? null;

-    $out['end']      = $meta['end']    ?? null;

-    $out['status']   = $meta['status'] ?? null;

+        $meta = json_decode(file_get_contents($jf), true) ?: [];

+        $out['last_run'] = $meta['start']  ?? null;

+        $out['end']      = $meta['end']    ?? null;

+        $out['status']   = $meta['status'] ?? null;

-    if (!file_exists($lf)) return $out;

-    $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);

-    $blk   = count($parts) > 1 ? end($parts) : $log;

+            $log   = file_get_contents($lf);

+            $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);

+            $blk   = count($parts) > 1 ? end($parts) : $log;

-        $out['tracked'] = (int)str_replace(',', '', $m[1]);

+                $out['tracked'] = (int)str_replace(',', '', $m[1]);

+                $out['orphans']    = (int)str_replace(',', '', $m[1]);

+                $out['orphans_sz'] = trim($m[2]);

+            if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {

-        $out['orphans']    = (int)str_replace(',', '', $m[1]);

-        $out['orphans_sz'] = trim($m[2]);

+    // Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count

+        $dbFile = DATA_DIR . '/arr_cleanup_stats.db';

+            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {

+                $p = explode('|', $line);

+                $out['status']   = 'ok';

+                $out['orphans']  = (int)$last[2];

+                $out['junk']     = (int)$last[4];

+                $out['tracked']  = (int)$last[7];

-    if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {


function vv_arr_discovery_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
              'radarr' => 'Media/playback_aware_radarr_discovery',
              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'status' => null, 'added' => null];

    $jf = $base . '.json';
    if (!file_exists($jf)) return $out;
    $meta = json_decode(file_get_contents($jf), true) ?: [];
    $out['last_run'] = $meta['start']  ?? null;
    $out['status']   = $meta['status'] ?? null;

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_discovery_stats(string $type): array {
    $slugs = ['sonarr' => 'Media/playback_aware_sonarr_discovery',
              'radarr' => 'Media/playback_aware_radarr_discovery',
              'lidarr' => 'Media/playback_aware_lidarr_discovery'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'status' => null, 'added' => null];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log = file_get_contents($lf);
            if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
            elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
        }
    }

    // Fallback: per-title history db — status|id|date[|title]
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/' . $type . '_discovery_history.db';
        if (file_exists($dbFile)) {
            $lastDate = null; $added = 0;
            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $p = explode('|', $line);
                if (count($p) < 3) continue;
                $date = $p[2];
                if ($date !== $lastDate) { $lastDate = $date; $added = 0; }
                if ($p[0] === 'ACCEPT') $added++;
            }
            if ($lastDate) {
                $out['last_run'] = strtotime($lastDate . ' 23:59:00') ?: null;
                $out['status']   = 'ok';
                $out['added']    = $added;
            }
        }
    }

    return $out;
}