Add Rsync card to monitor page Row 3 (before GPU)

New span-1 card shows global RSYNC_ENABLED gate, per-window badges (C/D/I/W),
active profile names + elapsed time from lock files, and last-sync timestamp
per orchestrator window from script log files.

GPU shrunk from span 2 to span 1 to make room. Row 3 is now:
Rsync(1) | GPU(1) | Transcode(2) | Streams(4)
This commit is contained in:
Gmer4Lfe
2026-06-01 21:10:44 -04:00
parent 08fc551d9f
commit 3e71c314d7
3 changed files with 141 additions and 2 deletions
+1
View File
@@ -28,6 +28,7 @@ echo json_encode([
'disk_io' => vv_disk_io_rates(),
'watchdog' => vv_watchdog_summary(),
'scripts' => vv_scripts_status(),
'rsync' => vv_rsync_status(),
'thresholds' => vv_disk_thresholds(),
'vms' => vv_get_vms(),
'docker_folders' => vv_get_docker_folders(),
+55
View File
@@ -319,3 +319,58 @@ function vv_scripts_status(): array {
'error_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'error')),
];
}
function vv_rsync_status(): array {
$vars = vv_conf_vars();
$enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';
$windows = [
'critical' => ($vars['CRITICAL_RSYNC_ENABLED'] ?? 'false') !== 'false',
'daily' => ($vars['DAILY_RSYNC_ENABLED'] ?? 'false') !== 'false',
'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true') !== 'false',
'weekly' => ($vars['WEEKLY_RSYNC_ENABLED'] ?? 'false') !== 'false',
];
// Active rsync profiles — from lock files
$lockDir = '/tmp/unraid_locks';
$active = [];
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'));
$active[] = [
'profile' => $profile,
'pid' => (int)$pid,
'elapsed' => time() - (int)filemtime($lf),
];
}
// Last completed run per window (orchestrator log files)
$scriptMap = [
'critical' => 'critical_sync_maintenance',
'daily' => 'daily_sync_maintenance',
'intermediate' => 'intermediate_sync_maintenance',
'weekly' => 'weekly_sync_maintenance',
];
$lastSync = [];
foreach ($scriptMap as $key => $scriptName) {
$logFile = LOG_DIR . "/$scriptName.json";
if (!file_exists($logFile)) continue;
$stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
$lastSync[$key] = [
'ts' => (int)($stat['end'] ?? $stat['start'] ?? 0),
'status' => $stat['status'] ?? 'unknown',
'duration' => isset($stat['start'], $stat['end'])
? (int)$stat['end'] - (int)$stat['start'] : null,
];
}
return [
'enabled' => $enabled,
'windows' => $windows,
'active' => $active,
'last_sync' => $lastSync,
];
}
+85 -2
View File
@@ -98,8 +98,18 @@
<div id="vv-docker-folders-body">Loading...</div>
</div>
<!-- Row 3: GPU | Transcode | Streams -->
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 2;">
<!-- Row 3: Rsync | GPU | Transcode | Streams -->
<div class="vv-card" id="vv-rsync-card" style="grid-column:span 1;">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 10,4 8,2"/><polyline points="10,8 2,8 4,10"/></svg></span>
Rsync
</span>
</h3>
<div id="vv-rsync-body">Loading...</div>
</div>
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 1;">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
@@ -1306,6 +1316,79 @@ function vvPollMonitor() {
else if (sys.array_state && sys.array_state !== 'UNKNOWN') c.classList.add('vv-accent-err');
})();
// ── Rsync ────────────────────────────────────────────────────────────────
(function() {
const rs = d.rsync ?? {};
const enabled = rs.enabled ?? true;
const windows = rs.windows ?? {};
const active = rs.active ?? [];
const lastSync = rs.last_sync ?? {};
const now = Math.floor(Date.now() / 1000);
const el = document.getElementById('vv-rsync-body');
if (!el) return;
const gColor = enabled ? '#4caf50' : '#555';
let html = `<div style="font-size:10px;color:${gColor};margin-bottom:7px;">● ${enabled ? 'enabled' : 'disabled'}</div>`;
// Window badges: C D I W
html += '<div style="display:flex;gap:4px;margin-bottom:8px;">';
[['C','critical'],['D','daily'],['I','intermediate'],['W','weekly']].forEach(([s,k]) => {
const on = windows[k] ?? false;
html += `<span title="${k}" style="font-size:9px;padding:1px 6px;border-radius:3px;
background:${on?'#1a2a1a':'#1a1a1a'};border:1px solid ${on?'#2a5a2a':'#252525'};
color:${on?'#4caf50':'#444'};">${s}</span>`;
});
html += '</div>';
// Active sessions
if (active.length) {
active.forEach(a => {
const sec = a.elapsed ?? 0;
const dur = sec < 60 ? sec+'s' : sec < 3600 ? Math.floor(sec/60)+'m'+String(sec%60).padStart(2,'0')+'s' : Math.floor(sec/3600)+'h'+Math.floor((sec%3600)/60)+'m';
html += `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:3px;">
<span style="color:#ff9800;font-weight:500;">⟳ ${a.profile}</span>
<span style="color:#ff9800;">${dur}</span>
</div>`;
});
html += `<div style="height:1px;background:#222;margin:5px 0;"></div>`;
}
// Last sync per window
const syncRows = [['critical','critical'],['daily','daily'],['intermediate','interm'],['weekly','weekly']];
let hasSyncs = false;
let syncHtml = '';
syncRows.forEach(([key, label]) => {
const s = lastSync[key];
if (!s || !s.ts) return;
hasSyncs = true;
const diff = now - s.ts;
const ago = diff < 60 ? diff+'s' : diff < 3600 ? Math.floor(diff/60)+'m' : diff < 86400 ? Math.floor(diff/3600)+'h' : Math.floor(diff/86400)+'d';
const ok = s.status === 'ok' || s.status === 'success';
const warn = s.status === 'warn';
const run = s.status === 'running';
const col = ok ? '#4caf50' : warn ? '#ff9800' : run ? '#4fc3f7' : '#f44336';
const icon = ok ? '✓' : warn ? '!' : run ? '●' : '✗';
syncHtml += `<div style="display:flex;justify-content:space-between;align-items:center;font-size:10px;margin-bottom:3px;">
<span style="color:#555;">${label}</span>
<span style="color:#3a3a3a;">${ago}</span>
<span style="color:${col};">${icon}</span>
</div>`;
});
if (hasSyncs) {
html += `<div style="font-size:9px;color:#333;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;">Last sync</div>`;
html += syncHtml;
}
el.innerHTML = html;
const card = document.getElementById('vv-rsync-card');
if (card) {
card.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
if (active.length) card.classList.add('vv-accent-ok');
}
})();
// ── GPU ─────────────────────────────────────────────────────────────────
const gpu = d.gpu ?? {};
const gpuProcs = d.gpu_procs ?? [];