|
|
|
@@ -3,13 +3,12 @@
|
|
|
|
|
<div id="vv-monitor">
|
|
|
|
|
|
|
|
|
|
<div class="vv-row">
|
|
|
|
|
<div class="vv-card" id="vv-fallback" style="flex:1;min-width:250px;">
|
|
|
|
|
<h3>Fallback State</h3>
|
|
|
|
|
<div id="vv-fallback-body">Loading...</div>
|
|
|
|
|
<div class="vv-card" id="vv-system" style="flex:1;min-width:250px;">
|
|
|
|
|
<div id="vv-system-body">Loading...</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="vv-card" id="vv-partner" style="flex:1;min-width:250px;">
|
|
|
|
|
<h3>Partner</h3>
|
|
|
|
|
<h3>Partner & Fallback</h3>
|
|
|
|
|
<div id="vv-partner-body">Loading...</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -290,13 +289,43 @@ function vvPollMonitor() {
|
|
|
|
|
.then(r => r.json())
|
|
|
|
|
.then(d => {
|
|
|
|
|
|
|
|
|
|
// ── Fallback state ──────────────────────────────────────────────────────
|
|
|
|
|
// ── Fallback state ──────────────────────────────────────────────────────
|
|
|
|
|
// ── System ──────────────────────────────────────────────────────────────
|
|
|
|
|
const sys = d.system ?? {};
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
|
|
|
const dateStr = now.toLocaleDateString([], {weekday:'short', day:'numeric', month:'long', year:'numeric'});
|
|
|
|
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone.split('/').pop().replace('_',' ');
|
|
|
|
|
const arrayColor = (sys.array_state === 'STARTED') ? '#4caf50' : '#f44336';
|
|
|
|
|
const ver = (sys.version || '').replace('version=','').replace(/"/g,'');
|
|
|
|
|
|
|
|
|
|
document.getElementById('vv-system-body').innerHTML =
|
|
|
|
|
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px;">
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-size:14px;font-weight:700;color:#ddd;">${sys.name}</div>
|
|
|
|
|
<div style="font-size:11px;color:#666;margin-top:2px;">${sys.comment}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;gap:6px;flex-shrink:0;margin-left:8px;">
|
|
|
|
|
<button onclick="vvArrayAction('stop')" class="vv-sys-btn" title="Stop Array">■</button>
|
|
|
|
|
<button onclick="vvArrayAction('shutdown')" class="vv-sys-btn" title="Shutdown">⏻</button>
|
|
|
|
|
<button onclick="vvArrayAction('restart')" class="vv-sys-btn" title="Restart">↺</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size:22px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
|
|
|
|
<div style="font-size:11px;color:#666;margin-bottom:12px;">${dateStr}, ${tz}</div>
|
|
|
|
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11px;">
|
|
|
|
|
<span style="color:#555;">Model</span> <span style="color:#aaa;">${sys.cpu_model}</span>
|
|
|
|
|
<span style="color:#555;">Registration</span><span style="color:#aaa;">${sys.reg_type}</span>
|
|
|
|
|
<span style="color:#555;">Uptime</span> <span style="color:#aaa;">${sys.uptime}</span>
|
|
|
|
|
<span style="color:#555;">Array</span> <span style="color:${arrayColor};">${sys.array_state}</span>
|
|
|
|
|
<span style="color:#555;">Version</span> <span style="color:#555;">${ver}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
// ── Partner & Fallback ───────────────────────────────────────────────────
|
|
|
|
|
const pt = d.partner ?? {};
|
|
|
|
|
const ptHosts = pt.hosts ?? [];
|
|
|
|
|
const fb = d.fallback ?? {};
|
|
|
|
|
const fbHosts = (d.partner ?? {}).hosts ?? [];
|
|
|
|
|
const fbActive = d.fallback_active ?? [];
|
|
|
|
|
const state = (fb.state ?? 'UNKNOWN').toUpperCase();
|
|
|
|
|
|
|
|
|
|
const stateMap = {
|
|
|
|
|
NORMAL: ['#4caf50', '✓ Nominal'],
|
|
|
|
|
FAILOVER: ['#f44336', '⚠ Failover active'],
|
|
|
|
@@ -305,41 +334,44 @@ function vvPollMonitor() {
|
|
|
|
|
UNKNOWN: ['#444', '— No state file'],
|
|
|
|
|
};
|
|
|
|
|
const [sColor, sLabel] = stateMap[state] ?? ['#444', state];
|
|
|
|
|
const ptStatus = pt.enabled
|
|
|
|
|
? `<span style="color:#4caf50;">enabled</span> · sync every ${pt.sync_min}min`
|
|
|
|
|
: `<span style="color:#555;">disabled</span>`;
|
|
|
|
|
|
|
|
|
|
// Host rows
|
|
|
|
|
let fbHtml = '';
|
|
|
|
|
fbHosts.forEach(h => {
|
|
|
|
|
let ptHtml = `<div style="font-size:10px;color:#666;margin-bottom:8px;">${ptStatus}</div>`;
|
|
|
|
|
ptHosts.forEach(h => {
|
|
|
|
|
const dot = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';
|
|
|
|
|
const label = h.online === null ? 'unknown' : h.online ? 'online' : 'offline';
|
|
|
|
|
const meTag = h.is_me ? `<span style="background:#1a3a1a;color:#4caf50;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">US</span>` : '';
|
|
|
|
|
fbHtml += `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
|
|
|
|
const tags = [
|
|
|
|
|
h.is_me ? `<span style="background:#1a3a1a;color:#4caf50;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">US</span>` : '',
|
|
|
|
|
h.is_owner ? `<span style="background:#1a2a3a;color:#4a9eff;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">OWNER</span>` : '',
|
|
|
|
|
].join('');
|
|
|
|
|
ptHtml += `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
|
|
|
|
<div>
|
|
|
|
|
<span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
|
|
|
|
|
<span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>${meTag}
|
|
|
|
|
<span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>${tags}
|
|
|
|
|
<div style="font-size:10px;color:#555;margin-top:1px;">${h.hostname}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// State line
|
|
|
|
|
fbHtml += `<div style="font-size:11px;color:${sColor};margin:8px 0;padding-top:8px;border-top:1px solid #2a2a2a;">${sLabel}`;
|
|
|
|
|
ptHtml += `<div style="font-size:11px;color:${sColor};margin:6px 0;padding-top:6px;border-top:1px solid #2a2a2a;">${sLabel}`;
|
|
|
|
|
if (state === 'FAILOVER') {
|
|
|
|
|
const start = parseInt(fb.failover_start ?? 0);
|
|
|
|
|
if (start > 0) {
|
|
|
|
|
const sec = Math.floor(Date.now() / 1000) - start;
|
|
|
|
|
fbHtml += ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
|
|
|
|
|
ptHtml += ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fbHtml += `</div>`;
|
|
|
|
|
ptHtml += `</div>`;
|
|
|
|
|
|
|
|
|
|
// Active fallback containers
|
|
|
|
|
if (fbActive.length) {
|
|
|
|
|
fbActive.forEach(group => {
|
|
|
|
|
fbHtml += `<div style="font-size:10px;color:#888;margin-top:6px;margin-bottom:4px;">Running for ${group.hostname}</div>`;
|
|
|
|
|
ptHtml += `<div style="font-size:10px;color:#888;margin-top:6px;margin-bottom:4px;">Running for ${group.hostname}</div>`;
|
|
|
|
|
group.containers.forEach(c => {
|
|
|
|
|
const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;
|
|
|
|
|
fbHtml += `<div style="display:flex;justify-content:space-between;align-items:center;
|
|
|
|
|
ptHtml += `<div style="display:flex;justify-content:space-between;align-items:center;
|
|
|
|
|
background:#1a1a1a;border-radius:4px;padding:4px 8px;margin-bottom:4px;">
|
|
|
|
|
<span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
|
|
|
|
|
<span style="color:#555;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
|
|
|
|
@@ -348,34 +380,6 @@ function vvPollMonitor() {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('vv-fallback-body').innerHTML = fbHtml;
|
|
|
|
|
|
|
|
|
|
// ── Partner ─────────────────────────────────────────────────────────────
|
|
|
|
|
const pt = d.partner ?? {};
|
|
|
|
|
const ptHosts = pt.hosts ?? [];
|
|
|
|
|
const ptStatus = pt.enabled
|
|
|
|
|
? `<span style="color:#4caf50;">enabled</span> · sync every ${pt.sync_min}min`
|
|
|
|
|
: `<span style="color:#555;">disabled</span>`;
|
|
|
|
|
let ptHtml = `<div style="font-size:10px;color:#666;margin-bottom:10px;">${ptStatus}</div>`;
|
|
|
|
|
ptHosts.forEach(h => {
|
|
|
|
|
const dot = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';
|
|
|
|
|
const label = h.online === null ? 'unknown' : h.online ? 'online' : 'offline';
|
|
|
|
|
const tags = [
|
|
|
|
|
h.is_me ? `<span style="background:#1a3a1a;color:#4caf50;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">US</span>` : '',
|
|
|
|
|
h.is_owner ? `<span style="background:#1a2a3a;color:#4a9eff;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">OWNER</span>` : '',
|
|
|
|
|
].join('');
|
|
|
|
|
ptHtml += `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
|
|
|
<div>
|
|
|
|
|
<span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
|
|
|
|
|
<span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>
|
|
|
|
|
${tags}
|
|
|
|
|
<div style="font-size:10px;color:#666;margin-top:1px;">${h.hostname}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="text-align:right;">
|
|
|
|
|
<span style="color:${dot};font-size:10px;">● ${label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('vv-partner-body').innerHTML = ptHtml;
|
|
|
|
|
|
|
|
|
|
// ── CPU ─────────────────────────────────────────────────────────────────
|
|
|
|
@@ -592,4 +596,28 @@ function vvPollStreams() {
|
|
|
|
|
|
|
|
|
|
vvPollStreams();
|
|
|
|
|
setInterval(vvPollStreams, 12000);
|
|
|
|
|
|
|
|
|
|
// ── System clock tick (updates time every 30s without a full poll) ────────────
|
|
|
|
|
function vvTickClock() {
|
|
|
|
|
const el = document.getElementById('vv-system-body');
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
|
|
|
const t = el.querySelector('.vv-clock');
|
|
|
|
|
if (t) t.textContent = timeStr;
|
|
|
|
|
}
|
|
|
|
|
setInterval(vvTickClock, 30000);
|
|
|
|
|
|
|
|
|
|
// ── Array actions ─────────────────────────────────────────────────────────────
|
|
|
|
|
function vvArrayAction(action) {
|
|
|
|
|
const labels = {stop: 'Stop Array', shutdown: 'Shutdown', restart: 'Restart'};
|
|
|
|
|
if (!confirm(`${labels[action] ?? action} — are you sure?`)) return;
|
|
|
|
|
fetch('/plugins/varaverk/api/system.php', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
body: JSON.stringify({action}),
|
|
|
|
|
}).then(r => r.json()).then(d => {
|
|
|
|
|
if (!d.ok) alert('Error: ' + (d.error ?? 'unknown'));
|
|
|
|
|
}).catch(() => alert('Request failed'));
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|