diff --git a/Plugin/unraid/api/arrs.php b/Plugin/unraid/api/arrs.php index 08b7221..4379452 100644 --- a/Plugin/unraid/api/arrs.php +++ b/Plugin/unraid/api/arrs.php @@ -2,6 +2,38 @@ 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 = SCRIPTS_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', 90); if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; } unset($_vv_cached); diff --git a/Plugin/unraid/include/arrs.php b/Plugin/unraid/include/arrs.php index 266d4a0..5715167 100644 --- a/Plugin/unraid/include/arrs.php +++ b/Plugin/unraid/include/arrs.php @@ -240,32 +240,61 @@ function vv_arr_recovery_stats(): array { 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(); - $allNodes = vv_discover_arrs(); - $result = []; - foreach ($allNodes as $node) { - $h = $node['host']; - $isLocal = ($h === $currentHost || $currentHost === 'unknown'); - $nodeOut = ['host' => $h, 'name' => $names[$h] ?? strtoupper($h), 'local' => $isLocal, 'arrs' => []]; + // Local node — full live data + $result = [vv_arrs_local_node()]; - foreach ($node['arrs'] as $arr) { - $entry = ['type' => $arr['type'], 'root' => $arr['root']]; - if ($isLocal) { - $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']); - } else { - $entry['online'] = false; - $entry['remote'] = true; - } - $nodeOut['arrs'][] = $entry; + // 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' => [], + ]; } - $result[] = $nodeOut; } $vars = vv_conf_vars(); diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index 4176954..047c483 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -248,16 +248,41 @@ function _arrCard(arr) { } // ── Node section ────────────────────────────────────────────────────────────── -function _nodeSection(node, ownerHost) { +function _nodeSection(node) { const isLocal = node.local; - const tag = isLocal ? 'local' : ''; - const cards = node.arrs.map(_arrCard).join(''); + const isMiss = node.cache_miss; + + let tag, body; + + if (isLocal) { + tag = 'local'; + body = `
${node.arrs.map(_arrCard).join('')}
`; + } else if (isMiss) { + tag = `no data + `; + body = `
+ No remote data yet — first fetch runs within 2 hours, or click ↻ Fetch now. +
`; + } else { + const age = node.cache_age || 0; + const ageStr = age < 3600 ? Math.floor(age/60) + 'm ago' + : age < 86400 ? Math.floor(age/3600) + 'h ago' + : Math.floor(age/86400) + 'd ago'; + tag = `cached ${ageStr} + `; + body = `
${node.arrs.map(_arrCard).join('')}
`; + } + return `
${node.host.toUpperCase()} ${node.name}${tag}
-
${cards}
+ ${body}
`; } @@ -372,7 +397,7 @@ function _render(data) { } let html = ''; - for (const node of nodes) html += _nodeSection(node, data.host); + for (const node of nodes) html += _nodeSection(node); html += _syncSection(data.sync || {}, data.recovery || {}, data.settings); html += _settingsCard(data.settings); @@ -393,6 +418,20 @@ setInterval(vvArrsLoad, 60000); })(); +// ── Remote node refresh ─────────────────────────────────────────────────────── +function vvArrsRefreshRemote(host) { + const btn = document.getElementById('vv-arr-rfsh-' + host); + if (btn) { btn.disabled = true; btn.textContent = '↻…'; } + fetch(`/plugins/varaverk/api/arrs.php?action=refresh_remote&host=${host}&_=` + Date.now()) + .then(r => r.json()) + .then(d => { + if (btn) { btn.disabled = false; btn.textContent = d.ok ? '↻' : '✗'; } + // Main cache was busted — reload to show fresh data + setTimeout(vvArrsLoad, 500); + }) + .catch(() => { if (btn) { btn.disabled = false; btn.textContent = '↻'; } }); +} + // ── Toggle handler ──────────────────────────────────────────────────────────── function vvArrToggle(track, key, file) { const on = !track.classList.contains('on'); diff --git a/Tools/remote_arr_cache_writer.sh b/Tools/remote_arr_cache_writer.sh new file mode 100755 index 0000000..3041a40 --- /dev/null +++ b/Tools/remote_arr_cache_writer.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# ============================================================================================== +# ============================= Remote Arr Cache Writer ======================================== +# ============================================================================================== +# +# PURPOSE +# ───────────────────────────────────────────────────────────────────────────── +# SSHes to each remote partner host, calls vv_arrs_local_node() on their PHP +# stack, and caches the result locally in /tmp/vv_cache/arrs_remote_hostN.json. +# +# The arrs page reads these files for instant initial load without hitting the +# remote arr APIs on every page view. This script runs every 2 hours so remote +# library counts stay reasonably current without hammering the network. +# +# Accepts --host=HOST2 to refresh a single host (used by the UI refresh button). +# +# ============================================================================================== +# USAGE +# ============================================================================================== +# +# remote_arr_cache_writer.sh — refresh all remote hosts +# remote_arr_cache_writer.sh --host=HOST2 — refresh one host only +# remote_arr_cache_writer.sh --dry-run — show what would happen +# remote_arr_cache_writer.sh --log — verbose output +# +# ============================================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../load_config.sh" + +parse_args "$@" +acquire_lock +detect_hosts + +# Parse --host= from raw args +TARGET_HOST="" +for arg in "$@"; do + case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac +done + +mkdir -p /tmp/vv_cache + +FETCH_OK=0 +FETCH_FAIL=0 +FETCH_SKIP=0 + +echo "" +echo "━━━ $ICON_SYNC Remote Arr Cache Writer ━━━" +[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" +[[ -n "$TARGET_HOST" ]] && echo " Target: $TARGET_HOST" +echo "" + +for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do + [[ "$host_var" == "$MY_ID" ]] && continue + + hostname="${!host_var:-}" + [[ -z "$hostname" ]] && continue + + # If targeting a specific host, skip others + if [[ -n "$TARGET_HOST" ]]; then + [[ "${host_var,,}" != "${TARGET_HOST,,}" && "$host_var" != "$TARGET_HOST" ]] && continue + fi + + host_id="${host_var,,}" # host1, host2, … + cache_file="/tmp/vv_cache/arrs_remote_${host_id}.json" + + echo " $host_var ($hostname)…" + + if [[ "$DRY_RUN" == true ]]; then + warn " DRY RUN — would SSH to $hostname and cache arr data" + (( FETCH_SKIP++ )) + continue + fi + + # Resolve Tailscale IP + REMOTE_IP=$(resolve_tailscale_ip "$hostname") + if [[ -z "$REMOTE_IP" ]]; then + warn " $host_var: cannot resolve Tailscale IP for $hostname — skipping" + (( FETCH_FAIL++ )) + continue + fi + log " $host_var: resolved $hostname → $REMOTE_IP" + + # SSH and call vv_arrs_local_node() on the remote + RESULT=$(ssh -i "$SSH_KEY" \ + -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=no \ + -o BatchMode=yes \ + "root@${REMOTE_IP}" \ + "php -r \"require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php'; echo json_encode(vv_arrs_local_node());\"" \ + 2>/dev/null) + + if [[ -z "$RESULT" ]]; then + warn " $host_var: empty response from SSH — skipping" + (( FETCH_FAIL++ )) + continue + fi + + # Validate JSON + if ! echo "$RESULT" | php -r " + \$d = json_decode(file_get_contents('php://stdin'), true); + exit(!is_array(\$d) || !isset(\$d['arrs']) ? 1 : 0); + " 2>/dev/null; then + warn " $host_var: invalid or unexpected JSON — skipping" + log " Response: ${RESULT:0:200}" + (( FETCH_FAIL++ )) + continue + fi + + echo "$RESULT" > "$cache_file" + CACHED_TYPES=$(echo "$RESULT" | php -r " + \$d = json_decode(file_get_contents('php://stdin'), true); + echo implode(', ', array_column(\$d['arrs'] ?? [], 'type')); + " 2>/dev/null) + echo " $host_var: cached ✅ — ${CACHED_TYPES:-no arrs}" + (( FETCH_OK++ )) +done + +echo "" +echo "━━━━━ $ICON_SUMMARY Cache Writer Summary ━━━━━" +echo " $FETCH_OK updated · $FETCH_FAIL failed · $FETCH_SKIP skipped" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"