varaverk: System card (identity/time/uptime/buttons); merge Fallback+Partner into one card
This commit is contained in:
@@ -3,6 +3,7 @@ header('Content-Type: application/json');
|
|||||||
require_once dirname(__DIR__) . '/include/monitor.php';
|
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
'system' => vv_system_info(),
|
||||||
'fallback' => vv_fallback_state(),
|
'fallback' => vv_fallback_state(),
|
||||||
'fallback_active' => vv_fallback_active(),
|
'fallback_active' => vv_fallback_active(),
|
||||||
'partner' => vv_partner_state(),
|
'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;
|
.vv-card h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase;
|
||||||
color: #888; letter-spacing: 0.05em; }
|
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 */
|
/* Fallback state badge */
|
||||||
.vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; margin-bottom: 2px; }
|
.vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; margin-bottom: 2px; }
|
||||||
.vv-state-normal { color: #4caf50; }
|
.vv-state-normal { color: #4caf50; }
|
||||||
|
|||||||
@@ -3,6 +3,47 @@ require_once __DIR__ . '/config.php';
|
|||||||
|
|
||||||
// Monitor helpers — docker, GPU, resources, transcode sessions, fallback state.
|
// 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 {
|
function vv_docker_containers(): array {
|
||||||
$out = shell_exec('docker ps --format \'{"name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}\' 2>/dev/null');
|
$out = shell_exec('docker ps --format \'{"name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}\' 2>/dev/null');
|
||||||
$containers = [];
|
$containers = [];
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
<div id="vv-monitor">
|
<div id="vv-monitor">
|
||||||
|
|
||||||
<div class="vv-row">
|
<div class="vv-row">
|
||||||
<div class="vv-card" id="vv-fallback" style="flex:1;min-width:250px;">
|
<div class="vv-card" id="vv-system" style="flex:1;min-width:250px;">
|
||||||
<h3>Fallback State</h3>
|
<div id="vv-system-body">Loading...</div>
|
||||||
<div id="vv-fallback-body">Loading...</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vv-card" id="vv-partner" style="flex:1;min-width:250px;">
|
<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 id="vv-partner-body">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -290,13 +289,43 @@ function vvPollMonitor() {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => {
|
||||||
|
|
||||||
// ── Fallback state ──────────────────────────────────────────────────────
|
// ── System ──────────────────────────────────────────────────────────────
|
||||||
// ── Fallback state ──────────────────────────────────────────────────────
|
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 fb = d.fallback ?? {};
|
||||||
const fbHosts = (d.partner ?? {}).hosts ?? [];
|
|
||||||
const fbActive = d.fallback_active ?? [];
|
const fbActive = d.fallback_active ?? [];
|
||||||
const state = (fb.state ?? 'UNKNOWN').toUpperCase();
|
const state = (fb.state ?? 'UNKNOWN').toUpperCase();
|
||||||
|
|
||||||
const stateMap = {
|
const stateMap = {
|
||||||
NORMAL: ['#4caf50', '✓ Nominal'],
|
NORMAL: ['#4caf50', '✓ Nominal'],
|
||||||
FAILOVER: ['#f44336', '⚠ Failover active'],
|
FAILOVER: ['#f44336', '⚠ Failover active'],
|
||||||
@@ -305,41 +334,44 @@ function vvPollMonitor() {
|
|||||||
UNKNOWN: ['#444', '— No state file'],
|
UNKNOWN: ['#444', '— No state file'],
|
||||||
};
|
};
|
||||||
const [sColor, sLabel] = stateMap[state] ?? ['#444', state];
|
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 ptHtml = `<div style="font-size:10px;color:#666;margin-bottom:8px;">${ptStatus}</div>`;
|
||||||
let fbHtml = '';
|
ptHosts.forEach(h => {
|
||||||
fbHosts.forEach(h => {
|
|
||||||
const dot = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';
|
const dot = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';
|
||||||
const label = h.online === null ? 'unknown' : h.online ? 'online' : 'offline';
|
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>` : '';
|
const tags = [
|
||||||
fbHtml += `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
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>
|
<div>
|
||||||
<span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
|
<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 style="font-size:10px;color:#555;margin-top:1px;">${h.hostname}</div>
|
||||||
</div>
|
</div>
|
||||||
<span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
|
<span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// State line
|
ptHtml += `<div style="font-size:11px;color:${sColor};margin:6px 0;padding-top:6px;border-top:1px solid #2a2a2a;">${sLabel}`;
|
||||||
fbHtml += `<div style="font-size:11px;color:${sColor};margin:8px 0;padding-top:8px;border-top:1px solid #2a2a2a;">${sLabel}`;
|
|
||||||
if (state === 'FAILOVER') {
|
if (state === 'FAILOVER') {
|
||||||
const start = parseInt(fb.failover_start ?? 0);
|
const start = parseInt(fb.failover_start ?? 0);
|
||||||
if (start > 0) {
|
if (start > 0) {
|
||||||
const sec = Math.floor(Date.now() / 1000) - start;
|
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) {
|
if (fbActive.length) {
|
||||||
fbActive.forEach(group => {
|
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 => {
|
group.containers.forEach(c => {
|
||||||
const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;
|
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;">
|
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:#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>
|
<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;
|
document.getElementById('vv-partner-body').innerHTML = ptHtml;
|
||||||
|
|
||||||
// ── CPU ─────────────────────────────────────────────────────────────────
|
// ── CPU ─────────────────────────────────────────────────────────────────
|
||||||
@@ -592,4 +596,28 @@ function vvPollStreams() {
|
|||||||
|
|
||||||
vvPollStreams();
|
vvPollStreams();
|
||||||
setInterval(vvPollStreams, 12000);
|
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user