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:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
Reference in New Issue
Block a user