Add Rsync page, upgrade monitor rsync card, partnership overhaul, API key periodic check, docker watchdog manual-stop detection

This commit is contained in:
Gmer4Lfe
2026-06-03 15:58:50 -04:00
parent 8be54ab5ee
commit de6fcc3997
13 changed files with 2546 additions and 343 deletions
+120 -45
View File
@@ -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');
}
})();