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
'}
+
+
+
+ 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