Rsync sync log: stream live output from tee'd log file, preserve last run
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -735,47 +735,67 @@ function _esc(s) {
|
||||
return (s||'').replace(/&/g,'&').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 `<div class="vv-ry-slog-row" style="background:#0a1a0a;border-radius:3px;margin-bottom:4px;padding:4px 6px;border:none;">
|
||||
<span class="vv-ry-pill run">LIVE</span>
|
||||
<span class="vv-ry-slog-name" style="color:#ccc;">${_esc(a.profile)}</span>
|
||||
<span class="vv-ry-elapsed" data-started="${startedTs}"
|
||||
style="font-size:11px;color:#4a9eff;font-family:monospace;grid-column:3/span 3;text-align:right;">${_dur(a.elapsed || 0)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const histRows = recent.map(r => {
|
||||
const cls = r.status === 'success' ? 'on' : 'err';
|
||||
const bytes = r.bytes > 0 ? _fmtBytes(r.bytes) : '';
|
||||
return `<div class="vv-ry-slog-row">
|
||||
<span class="vv-ry-pill ${cls}" style="font-size:9px;">${r.status === 'success' ? 'ok' : 'fail'}</span>
|
||||
<span class="vv-ry-slog-name">${_esc(r.profile)}</span>
|
||||
<span class="vv-ry-slog-dt">${r.date} ${r.time.slice(0,5)}</span>
|
||||
<span class="vv-ry-slog-dur">${_dur(r.duration)}</span>
|
||||
<span class="vv-ry-slog-byt">${bytes}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
let _logPollTimer = null;
|
||||
let _logElapsed = 0;
|
||||
let _logTicker = null;
|
||||
|
||||
function _syncLogCard() {
|
||||
return `<div class="vv-ry-card" style="grid-column:span 5;display:flex;flex-direction:column;min-height:0;">
|
||||
<div class="vv-ry-sec">Sync Activity</div>
|
||||
${activeRows}
|
||||
${activeRows && histRows ? '<hr class="vv-ry-sep">' : ''}
|
||||
<div class="vv-ry-slog-scrl">
|
||||
${histRows || '<div style="color:#333;font-size:11px;padding:6px 0;">No sync history</div>'}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
||||
<div class="vv-ry-sec" style="margin-bottom:0;">Sync Activity</div>
|
||||
<span id="vv-ry-log-hdr" style="font-size:10px;color:#3a3a3a;"></span>
|
||||
</div>
|
||||
<div id="vv-ry-log-live"></div>
|
||||
<div class="vv-ry-slog-scrl" id="vv-ry-log-lines"
|
||||
style="font-family:monospace;font-size:10px;color:#888;white-space:pre-wrap;word-break:break-all;">
|
||||
<span style="color:#333;">Loading…</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 =
|
||||
`<div style="display:flex;align-items:center;gap:8px;background:#0a1a0a;border-radius:3px;padding:4px 8px;margin-bottom:4px;">
|
||||
<span class="vv-ry-pill run">LIVE</span>
|
||||
<span style="font-size:11px;color:#ccc;">${_esc(d.profile)}</span>
|
||||
<span id="vv-ry-log-elapsed" style="margin-left:auto;font-size:11px;color:#4a9eff;font-family:monospace;">${_dur(d.elapsed || 0)}</span>
|
||||
</div>`;
|
||||
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 => {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user