From 57773bcaf54446c205df094203b9007747785eda Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 6 Jun 2026 16:06:42 -0400 Subject: [PATCH] Rsync sync log: stream live output from tee'd log file, preserve last run --- Plugin/unraid/api/rsync.php | 42 +++++++++++++ Plugin/unraid/pages/rsync.php | 112 +++++++++++++++++----------------- Rsync/rsync.sh | 11 ++++ 3 files changed, 110 insertions(+), 55 deletions(-) diff --git a/Plugin/unraid/api/rsync.php b/Plugin/unraid/api/rsync.php index 2ddf2d6..ab21918 100644 --- a/Plugin/unraid/api/rsync.php +++ b/Plugin/unraid/api/rsync.php @@ -3,6 +3,48 @@ 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(); diff --git a/Plugin/unraid/pages/rsync.php b/Plugin/unraid/pages/rsync.php index 978a050..a9b2565 100644 --- a/Plugin/unraid/pages/rsync.php +++ b/Plugin/unraid/pages/rsync.php @@ -735,47 +735,67 @@ function _esc(s) { return (s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } -let _liveTicker = null; -let _fastPollTimer = null; - -function _syncLogCard(data) { - const active = data.active || []; - const history = data.bw_history || []; - const recent = [...history].reverse().slice(0, 14); - const now = Math.floor(Date.now() / 1000); - - const activeRows = active.map(a => { - const startedTs = now - (a.elapsed || 0); - return `
- LIVE - ${_esc(a.profile)} - ${_dur(a.elapsed || 0)} -
`; - }).join(''); - - const histRows = recent.map(r => { - const cls = r.status === 'success' ? 'on' : 'err'; - const bytes = r.bytes > 0 ? _fmtBytes(r.bytes) : ''; - return `
- ${r.status === 'success' ? 'ok' : 'fail'} - ${_esc(r.profile)} - ${r.date} ${r.time.slice(0,5)} - ${_dur(r.duration)} - ${bytes} -
`; - }).join(''); +let _logPollTimer = null; +let _logElapsed = 0; +let _logTicker = null; +function _syncLogCard() { return `
-
Sync Activity
- ${activeRows} - ${activeRows && histRows ? '
' : ''} -
- ${histRows || '
No sync history
'} +
+
Sync Activity
+ +
+
+
+ Loading…
`; } +function _fetchLog() { + fetch('/plugins/varaverk/api/rsync.php?action=rsync_log') + .then(r => r.json()) + .then(d => { + const linesEl = document.getElementById('vv-ry-log-lines'); + const liveEl = document.getElementById('vv-ry-log-live'); + const hdrEl = document.getElementById('vv-ry-log-hdr'); + if (!linesEl) return; + + if (d.live && d.profile) { + _logElapsed = d.elapsed || 0; + if (!_logTicker) { + _logTicker = setInterval(() => { + _logElapsed++; + const el = document.getElementById('vv-ry-log-elapsed'); + if (el) el.textContent = _dur(_logElapsed); + }, 1000); + } + if (liveEl) liveEl.innerHTML = + `
+ LIVE + ${_esc(d.profile)} + ${_dur(d.elapsed || 0)} +
`; + if (hdrEl) hdrEl.textContent = ''; + } else { + if (_logTicker) { clearInterval(_logTicker); _logTicker = null; } + if (liveEl) liveEl.innerHTML = ''; + if (hdrEl) hdrEl.textContent = d.profile ? 'last: ' + d.profile : ''; + } + + const atBottom = linesEl.scrollHeight - linesEl.scrollTop <= linesEl.clientHeight + 10; + linesEl.textContent = (d.lines || []).join('\n') || (d.profile ? '(no output captured)' : 'No sync history'); + if (atBottom) linesEl.scrollTop = linesEl.scrollHeight; + }) + .catch(() => {}); +} + +function _startLogPoll() { + _fetchLog(); + if (!_logPollTimer) _logPollTimer = setInterval(_fetchLog, 2000); +} + // ── Settings section ────────────────────────────────────────────────────────── function _toggle(name, currentVal, label) { const id = 'vv-ry-tog-' + name; @@ -856,29 +876,11 @@ function _render(data) { html += _lastSyncCard(data); html += _windowsRow(data); html += _bwSection(data); - html += _syncLogCard(data); + html += _syncLogCard(); html += _settingsSection(data); document.getElementById('vv-ry-grid').innerHTML = html; - // Live elapsed ticker — updates every second without re-fetching - if (_liveTicker) { clearInterval(_liveTicker); _liveTicker = null; } - if ((data.active || []).length) { - _liveTicker = setInterval(() => { - document.querySelectorAll('.vv-ry-elapsed').forEach(el => { - const s = parseInt(el.dataset.started, 10); - if (s) el.textContent = _dur(Math.floor(Date.now() / 1000) - s); - }); - }, 1000); - } - - // Fast-poll (5s) while syncs are active; return to 30s when idle - const hasActive = (data.active || []).length > 0; - if (hasActive && !_fastPollTimer) { - _fastPollTimer = setInterval(vvRyLoad, 5000); - } else if (!hasActive && _fastPollTimer) { - clearInterval(_fastPollTimer); - _fastPollTimer = null; - } + _startLogPoll(); // Restore any window panels that were open before the re-render _vvRyWinOpen.forEach(key => { diff --git a/Rsync/rsync.sh b/Rsync/rsync.sh index 8f5d98b..cfe2411 100755 --- a/Rsync/rsync.sh +++ b/Rsync/rsync.sh @@ -205,6 +205,12 @@ fi # Acquire per-profile lock and check global concurrent limit acquire_rsync_lock "$PROFILE_NAME" +# Tee all output to a live log file for the Varaverk UI +VV_LIVE_LOG="/tmp/unraid_locks/rsync_${PROFILE_NAME}.log" +VV_LAST_LOG="/tmp/unraid_locks/rsync_${PROFILE_NAME}.last.log" +: > "$VV_LIVE_LOG" +exec 1> >(tee -a "$VV_LIVE_LOG") 2>&1 + # ── Load profile settings ───────────────────────────────────────────────────────────────────── BW_LIMIT=${PROFILE_BW_LIMIT[$PROFILE_NAME]:-$BW_LIMIT} RETRY_COUNT=${PROFILE_RETRY_COUNT[$PROFILE_NAME]:-$RETRY_COUNT} @@ -446,5 +452,10 @@ else fi echo "━━━━━━━━━━━━━━━━━━━━━━━" +# Flush tee and preserve log for UI +exec 1>&-; wait +cp "$VV_LIVE_LOG" "$VV_LAST_LOG" 2>/dev/null +rm -f "$VV_LIVE_LOG" + [[ "$RSYNC_SUCCESS" == false ]] && [[ "$DRY_RUN" == false ]] && exit 1 exit 0 \ No newline at end of file