:{"scripts":[…],"shares":[…]}},"settings":{…},"ts"} // log {"ok":true,"live":bool,"profile","elapsed","lines":[…]} // // DEPENDS ON // include/monitor.php vv_rsync_status() // include/config.php vv_conf_vars(), vv_read_conf_raw(), vv_detect_host(), // vv_parse_bash_array(), DATA_DIR // /tmp/unraid_locks rsync_*.lock, rsync_*.log, rsync_*.last.log — written by rsync.sh // DATA_DIR bandwidth_history.db // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/config.php'; require_once dirname(__DIR__) . '/include/monitor.php'; // ── Live sync log ───────────────────────────────────────────────────────────── $action = $_GET['action'] ?? ''; if ($action === 'rsync_log') { $lockDir = '/tmp/unraid_locks'; $lines = []; $profile = null; $live = false; $elapsed = 0; // Active sync — read live log foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) { $content = trim(@file_get_contents($lf) ?: ''); [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, ''); if (!$pid || !file_exists("/proc/$pid")) continue; $profile = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock')); $elapsed = time() - (int)filemtime($lf); $liveLog = "$lockDir/rsync_{$profile}.log"; $raw = file_exists($liveLog) ? (file($liveLog, FILE_IGNORE_NEW_LINES) ?: []) : []; $live = true; $lines = $raw; break; } // No active sync — use most recent last.log if (!$live) { $lastLogs = glob("$lockDir/rsync_*.last.log") ?: []; if ($lastLogs) { usort($lastLogs, fn($a, $b) => filemtime($b) <=> filemtime($a)); $lastLog = $lastLogs[0]; $profile = preg_replace('/^rsync_(.+)\.last\.log$/', '$1', basename($lastLog)); $lines = file($lastLog, FILE_IGNORE_NEW_LINES) ?: []; } } // Strip ANSI escape codes, keep last 200 lines $lines = array_map(fn($l) => preg_replace('/\x1b\[[0-9;]*[mGKHF]/', '', $l), $lines); $lines = array_slice($lines, -200); echo json_encode(['ok' => true, 'live' => $live, 'profile' => $profile, 'elapsed' => $elapsed, 'lines' => array_values($lines)]); exit; } $base = vv_rsync_status(); $vars = vv_conf_vars(); $base['windows']['fallback'] = ($vars['FALLBACK_RSYNC_ENABLED'] ?? 'true') !== 'false'; $base['windows']['monthly'] = ($vars['MONTHLY_RSYNC_ENABLED'] ?? 'true') !== 'false'; // Bandwidth history — last 30 days $bwLog = DB_DIR . '/bandwidth_history.db'; $warnGb = (float)($vars['BANDWIDTH_WARN_GB'] ?? 50); $cutoff = date('Y-m-d', strtotime('-30 days')); $history = []; // When rsync last actually moved anything, taken from the whole file rather than the window above. // // This is the only record of a transfer. last_sync is built from the orchestrators' run records, // and those orchestrators do a great deal that is not rsync — git pull, permissions, cleaners, arr // cleanup, docker updates — and run on their schedule whether or not RSYNC_ENABLED is true. So the // page could report a healthy 56-minute "daily sync" for a subsystem that had not run in a month, // which is exactly what it was doing: the last entry here is 2026-07-16, the day the global gate // was closed. // // Deliberately outside the 30-day cutoff. The last transfer is 29 days old as this is written and // would have dropped out of the window within days, taking the page from a wrong answer to no // answer — which on this question is not an improvement. $lastTransfer = null; if (file_exists($bwLog)) { foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $p = explode('|', $line); if (count($p) < 5) continue; // Recorded before the cutoff test, and unconditionally: the file is chronological, so the // last line to pass the shape check is the most recent transfer however old it is. $lastTransfer = [ 'ts' => strtotime($p[0] . ' ' . $p[1]) ?: null, 'profile' => $p[2], 'status' => trim($p[4]), ]; if ($p[0] < $cutoff) continue; $history[] = [ 'date' => $p[0], 'time' => $p[1], 'profile' => $p[2], 'duration' => (int)$p[3], 'status' => trim($p[4]), 'bytes' => isset($p[5]) ? (int)$p[5] : 0, ]; } } // Per-window orch arrays (scripts + sync shares) $masterRaw = vv_read_conf_raw('master.conf'); $myId = strtoupper(vv_detect_host()); $hostRaw = vv_read_conf_raw(vv_detect_host() . '.conf'); $winArrayDefs = [ 'critical' => ['CRITICAL_MAINTENANCE_SCRIPTS', "{$myId}_CRITICAL_SYNC_SHARES"], 'intermediate' => ['INTERMEDIATE_MAINTENANCE_SCRIPTS', "{$myId}_INTERMEDIATE_SYNC_SHARES"], 'daily' => ['DAILY_MAINTENANCE_SCRIPTS', "{$myId}_DAILY_SYNC_SHARES"], 'weekly' => ['WEEKLY_MAINTENANCE_SCRIPTS', "{$myId}_WEEKLY_SYNC_SHARES"], // monthly_maintenance.sh has no rsync section (ZFS scrub/SMART tests only) — no shares var exists. 'monthly' => ['MONTHLY_MAINTENANCE_SCRIPTS', null], 'fallback' => [null, null], ]; $winArrays = []; foreach ($winArrayDefs as $win => [$sv, $shv]) { $winArrays[$win] = [ 'scripts' => $sv ? vv_parse_bash_array($masterRaw, $sv) : [], 'shares' => $shv ? vv_parse_bash_array($hostRaw, $shv) : [], ]; } echo json_encode([ 'enabled' => $base['enabled'], 'windows' => $base['windows'], 'active' => $base['active'], 'last_sync' => $base['last_sync'], 'last_transfer' => $lastTransfer, 'bw_history' => $history, 'bw_warn_gb' => $warnGb, 'win_arrays' => $winArrays, 'settings' => [ 'bw_limit' => (int)($vars['BW_LIMIT'] ?? 0), 'retry_count' => (int)($vars['RETRY_COUNT'] ?? 3), 'sleep' => (int)($vars['SLEEP'] ?? 300), 'bw_warn_gb' => (float)($vars['BANDWIDTH_WARN_GB'] ?? 50), ], 'ts' => time(), ]);