Add Rsync page, upgrade monitor rsync card, partnership overhaul, API key periodic check, docker watchdog manual-stop detection
This commit is contained in:
+120
-45
@@ -1,4 +1,10 @@
|
||||
<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>
|
||||
<style>
|
||||
@keyframes vvRsPulse {
|
||||
0%,100% { opacity:.5; transform:scaleX(.9); }
|
||||
50% { opacity:1; transform:scaleX(1.05); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="vv-api-banner" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;"></div>
|
||||
|
||||
@@ -1318,74 +1324,143 @@ function vvPollMonitor() {
|
||||
|
||||
// ── 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 rs = d.rsync ?? {};
|
||||
const enabled = rs.enabled ?? true;
|
||||
const windows = rs.windows ?? {};
|
||||
const active = rs.active ?? [];
|
||||
const lastSync = rs.last_sync ?? {};
|
||||
const bwSummary = rs.bw_summary ?? {};
|
||||
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>`;
|
||||
function _dur(s) {
|
||||
if (!s) return '—';
|
||||
if (s < 60) return s + 's';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm' + (s%60 ? String(s%60).padStart(2,'0')+'s' : '');
|
||||
return Math.floor(s/3600) + 'h' + Math.floor((s%3600)/60) + 'm';
|
||||
}
|
||||
function _ago(diff) {
|
||||
if (diff < 60) return diff + 's';
|
||||
if (diff < 3600) return Math.floor(diff/60) + 'm';
|
||||
if (diff < 86400) return Math.floor(diff/3600) + 'h';
|
||||
return Math.floor(diff/86400) + 'd';
|
||||
}
|
||||
function _fmtBytes(b) {
|
||||
if (!b) return '';
|
||||
if (b >= 1073741824) return (b/1073741824).toFixed(1) + 'G';
|
||||
if (b >= 1048576) return (b/1048576).toFixed(0) + 'M';
|
||||
return (b/1024).toFixed(0) + 'K';
|
||||
}
|
||||
|
||||
// Window badges: C D I W
|
||||
html += '<div style="display:flex;gap:4px;margin-bottom:8px;">';
|
||||
const gCol = enabled ? '#4caf50' : '#555';
|
||||
|
||||
// ── Header: gate dot + window badges ──────────────────────────────────
|
||||
let html = `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||||
<span style="font-size:10px;font-weight:600;color:${gCol};">● ${enabled ? 'ENABLED' : 'DISABLED'}</span>
|
||||
<div style="display:flex;gap:3px;">`;
|
||||
[['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>`;
|
||||
const on = windows[k] ?? false;
|
||||
const run = active.some(a => a.profile?.includes(k));
|
||||
const bg = run ? '#1a2a0a' : on ? '#0f1a0f' : '#111';
|
||||
const brd = run ? '#3a6a1a' : on ? '#1a3a1a' : '#222';
|
||||
const col = run ? '#8bc34a' : on ? '#4caf50' : '#333';
|
||||
html += `<span title="${k}" style="font-size:9px;padding:2px 5px;border-radius:2px;
|
||||
background:${bg};border:1px solid ${brd};color:${col};font-weight:600;">${s}</span>`;
|
||||
});
|
||||
html += '</div>';
|
||||
html += `</div></div>`;
|
||||
|
||||
// Active sessions
|
||||
// ── Profile activity bars (7 days) ────────────────────────────────────
|
||||
const profEntries = Object.entries(bwSummary);
|
||||
if (profEntries.length) {
|
||||
const maxRuns = Math.max(...profEntries.map(([,v]) => v.runs), 1);
|
||||
const colors = ['#4caf50','#4a9eff','#ffb74d','#ce93d8','#ef5350','#4dd0e1'];
|
||||
html += `<div style="margin-bottom:6px;">
|
||||
<div style="font-size:9px;color:#2a2a2a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px;">7-day profile activity</div>`;
|
||||
profEntries.forEach(([name, v], i) => {
|
||||
const pct = Math.round(v.runs / maxRuns * 100);
|
||||
const col = colors[i % colors.length];
|
||||
const bytes = v.bytes > 0 ? _fmtBytes(v.bytes) : '';
|
||||
const short = name.length > 18 ? name.slice(0,16) + '…' : name;
|
||||
html += `<div style="margin-bottom:4px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:1px;">
|
||||
<span style="font-size:9px;color:#555;overflow:hidden;text-overflow:ellipsis;
|
||||
white-space:nowrap;max-width:110px;" title="${name}">${short}</span>
|
||||
<span style="font-size:9px;color:#2a2a2a;flex-shrink:0;margin-left:4px;">
|
||||
${v.runs}×${bytes ? ' · ' + bytes : ''}</span>
|
||||
</div>
|
||||
<div style="height:3px;background:#111;border-radius:2px;overflow:hidden;">
|
||||
<div style="height:100%;width:${pct}%;background:${col};border-radius:2px;opacity:.7;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
// ── Active syncs ───────────────────────────────────────────────────────
|
||||
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>
|
||||
html += `<div style="background:#0d1a0a;border:1px solid #1a3a0a;border-radius:3px;
|
||||
padding:4px 8px;margin-bottom:5px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
|
||||
<span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${a.profile}</span>
|
||||
<span style="font-size:10px;color:#6a8a4a;">${_dur(sec)}</span>
|
||||
</div>
|
||||
<div style="height:3px;background:#0a0a0a;border-radius:2px;overflow:hidden;">
|
||||
<div style="height:100%;background:#3a6a1a;border-radius:2px;
|
||||
animation:vvRsPulse 1.4s ease-in-out infinite;width:60%;"></div>
|
||||
</div>
|
||||
</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>
|
||||
// ── Tier last-run rows with recency bars ───────────────────────────────
|
||||
const tierMeta = {
|
||||
critical: {label:'Critical', cadenceSec: 30*60},
|
||||
daily: {label:'Daily', cadenceSec: 24*3600},
|
||||
intermediate: {label:'Interm', cadenceSec: 4*3600},
|
||||
weekly: {label:'Weekly', cadenceSec: 7*86400},
|
||||
};
|
||||
html += `<div style="border-top:1px solid #1a1a1a;padding-top:6px;">`;
|
||||
Object.entries(tierMeta).forEach(([key, meta]) => {
|
||||
const s = lastSync[key];
|
||||
const ts = s?.ts ?? 0;
|
||||
const ok = s?.status === 'ok' || s?.status === 'success';
|
||||
const warn = s?.status === 'warn';
|
||||
const run = s?.status === 'running';
|
||||
const err = s && !ok && !warn && !run && ts > 0;
|
||||
const col = run ? '#4a9eff' : ok ? '#4caf50' : warn ? '#ff9800' : err ? '#f44336' : '#2a2a2a';
|
||||
const icon = run ? '⟳' : ok ? '✓' : warn ? '!' : err ? '✗' : '—';
|
||||
const diff = ts ? now - ts : null;
|
||||
const pct = ts ? Math.min(100, Math.round((now - ts) / meta.cadenceSec * 100)) : 0;
|
||||
const barC = pct >= 100 ? '#3a1a1a' : pct >= 75 ? '#2a1a0a' : '#0a1a0a';
|
||||
const fillC= pct >= 100 ? '#f44336' : pct >= 75 ? '#ff9800' : '#2a6a2a';
|
||||
|
||||
html += `<div style="margin-bottom:4px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px;">
|
||||
<span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${meta.label}</span>
|
||||
<span style="font-size:9px;color:#333;flex:1;text-align:right;margin-right:6px;">
|
||||
${s?.duration ? _dur(s.duration) : ''}</span>
|
||||
<span style="font-size:9px;color:#2a2a2a;width:28px;text-align:right;margin-right:5px;">
|
||||
${diff !== null ? _ago(diff) : '—'}</span>
|
||||
<span style="font-size:10px;color:${col};width:10px;text-align:right;">${icon}</span>
|
||||
</div>
|
||||
<div style="height:2px;background:${barC};border-radius:1px;overflow:hidden;">
|
||||
<div style="height:100%;width:${pct}%;background:${fillC};border-radius:1px;"></div>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
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');
|
||||
if (!enabled) card.classList.add('vv-accent-err');
|
||||
else if (active.length) card.classList.add('vv-accent-ok');
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user