Arrs: remote host cards via SSH cache, vv_arrs_local_node(), 2h refresh schedule

This commit is contained in:
Gmer4Lfe
2026-06-03 18:16:56 -04:00
parent 6c0164713b
commit 2223cf80f7
4 changed files with 245 additions and 23 deletions
+32
View File
@@ -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);
+46 -17
View File
@@ -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']);
// 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 {
$entry['online'] = false;
$entry['remote'] = true;
$result[] = [
'host' => $h,
'name' => $names[$h],
'local' => false,
'cached' => false,
'cache_miss' => true,
'arrs' => [],
];
}
$nodeOut['arrs'][] = $entry;
}
$result[] = $nodeOut;
}
$vars = vv_conf_vars();
+44 -5
View File
@@ -248,16 +248,41 @@ function _arrCard(arr) {
}
// ── Node section ──────────────────────────────────────────────────────────────
function _nodeSection(node, ownerHost) {
function _nodeSection(node) {
const isLocal = node.local;
const tag = isLocal ? '<span class="vv-arr-node-tag">local</span>' : '';
const cards = node.arrs.map(_arrCard).join('');
const isMiss = node.cache_miss;
let tag, body;
if (isLocal) {
tag = '<span class="vv-arr-node-tag">local</span>';
body = `<div class="vv-arr-cards">${node.arrs.map(_arrCard).join('')}</div>`;
} else if (isMiss) {
tag = `<span class="vv-arr-node-tag" style="background:#111;border-color:#222;color:#333;">no data</span>
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
style="font-size:9px;color:#4a9eff;background:#0a1a2a;border:1px solid #1a3a5a;
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻ Fetch now</button>`;
body = `<div style="color:#333;font-size:11px;padding:10px 0;">
No remote data yet — first fetch runs within 2 hours, or click ↻ Fetch now.
</div>`;
} 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 = `<span class="vv-arr-node-tag" style="background:#1a1000;border-color:#3a2800;color:#b87;">cached ${ageStr}</span>
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
style="font-size:9px;color:#666;background:none;border:1px solid #2a2a2a;
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻</button>`;
body = `<div class="vv-arr-cards" style="opacity:.85;">${node.arrs.map(_arrCard).join('')}</div>`;
}
return `<div class="vv-arr-node">
<div class="vv-arr-node-hdr">
<span class="vv-arr-node-id">${node.host.toUpperCase()}</span>
<span class="vv-arr-node-host">${node.name}</span>${tag}
</div>
<div class="vv-arr-cards">${cards}</div>
${body}
</div>`;
}
@@ -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');
+122
View File
@@ -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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"