varaverk: System card (identity/time/uptime/buttons); merge Fallback+Partner into one card

This commit is contained in:
Gmer4Lfe
2026-05-24 12:00:09 -04:00
parent 725c371f21
commit 359b051e64
5 changed files with 147 additions and 49 deletions
@@ -3,6 +3,7 @@ header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/monitor.php';
echo json_encode([
'system' => vv_system_info(),
'fallback' => vv_fallback_state(),
'fallback_active' => vv_fallback_active(),
'partner' => vv_partner_state(),
@@ -0,0 +1,20 @@
<?php
header('Content-Type: application/json');
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $body['action'] ?? '';
$allowed = ['stop', 'shutdown', 'restart'];
if (!in_array($action, $allowed, true)) {
echo json_encode(['ok' => false, 'error' => 'invalid action']);
exit;
}
$cmd = match($action) {
'stop' => '/usr/local/sbin/mdcmd stop',
'shutdown' => '/sbin/shutdown -h now',
'restart' => '/sbin/shutdown -r now',
};
exec($cmd . ' > /dev/null 2>&1 &');
echo json_encode(['ok' => true]);
@@ -16,6 +16,14 @@
.vv-card h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase;
color: #888; letter-spacing: 0.05em; }
/* System card — no h3, no top padding waste */
#vv-system { padding-top: 14px; }
/* System action buttons */
.vv-sys-btn { background: #2a2a2a; border: 1px solid #444; color: #888; border-radius: 4px;
padding: 3px 7px; font-size: 13px; cursor: pointer; line-height: 1; }
.vv-sys-btn:hover { background: #3a3a3a; color: #ccc; border-color: #666; }
/* Fallback state badge */
.vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; margin-bottom: 2px; }
.vv-state-normal { color: #4caf50; }
@@ -3,6 +3,47 @@ require_once __DIR__ . '/config.php';
// Monitor helpers — docker, GPU, resources, transcode sessions, fallback state.
function vv_system_info(): array {
// Identity from ident.cfg
$ident = @parse_ini_file('/boot/config/ident.cfg') ?: [];
// Registration from var.ini
$var = [];
foreach (@file('/var/local/emhttp/var.ini') ?: [] as $line) {
if (preg_match('/^(\w+)="([^"]*)"/', $line, $m)) $var[$m[1]] = $m[2];
}
// CPU model
$cpu = '';
foreach (@file('/proc/cpuinfo') ?: [] as $line) {
if (preg_match('/^model name\s*:\s*(.+)/', $line, $m)) { $cpu = trim($m[1]); break; }
}
// Uptime
$uptimeSec = (int)explode(' ', @file_get_contents('/proc/uptime') ?: '0')[0];
$days = intdiv($uptimeSec, 86400);
$hours = intdiv($uptimeSec % 86400, 3600);
$mins = intdiv($uptimeSec % 3600, 60);
$uptime = ($days > 0 ? "{$days}d " : '')
. ($hours > 0 ? "{$hours}h " : '')
. "{$mins}m";
// Array state
$arrayState = $var['mdState'] ?? 'UNKNOWN';
return [
'name' => $ident['NAME'] ?? gethostname(),
'comment' => $ident['COMMENT'] ?? '',
'timezone' => $ident['timeZone'] ?? 'UTC',
'cpu_model' => $ident['SYS_MODEL'] ?? $cpu,
'reg_type' => 'Unraid OS ' . ($var['regTy'] ?? ''),
'reg_to' => $var['regTo'] ?? '',
'uptime' => $uptime,
'array_state' => $arrayState,
'version' => trim(@file_get_contents('/etc/unraid-version') ?: ''),
];
}
function vv_docker_containers(): array {
$out = shell_exec('docker ps --format \'{"name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}\' 2>/dev/null');
$containers = [];
@@ -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 &amp; 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>