varaverk: redesign Fallback card — host online/offline list, state line, active fallback containers with image

This commit is contained in:
Gmer4Lfe
2026-05-24 11:47:08 -04:00
parent 6bb44cae8b
commit cead4eb46f
3 changed files with 109 additions and 26 deletions
@@ -3,8 +3,9 @@ header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/monitor.php';
echo json_encode([
'fallback' => vv_fallback_state(),
'partner' => vv_partner_state(),
'fallback' => vv_fallback_state(),
'fallback_active' => vv_fallback_active(),
'partner' => vv_partner_state(),
'resources' => vv_system_resources(),
'cpu' => vv_cpu_per_core(),
'mem' => vv_memory_breakdown(),
@@ -308,6 +308,61 @@ function vv_fallback_state(): array {
];
}
function vv_parse_bash_array(string $raw, string $varName): array {
if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(([^)]*)\)/ms', $raw, $m)) return [];
$items = [];
foreach (explode("\n", $m[1]) as $line) {
$line = trim(preg_replace('/#.*$/', '', $line), " \t\"'");
if ($line !== '') $items[] = $line;
}
return $items;
}
function vv_fallback_active(): array {
$vars = vv_conf_vars();
$myName = trim(shell_exec('hostname -s') ?: '');
// Identify which HOST id we are
$myId = null;
foreach (['HOST1', 'HOST2', 'HOST3', 'HOST4'] as $id) {
if (strcasecmp($vars[$id] ?? '', $myName) === 0) { $myId = $id; break; }
}
if (!$myId) return [];
// Running containers: name → image
$running = [];
$psOut = shell_exec("docker ps --format '{\"n\":\"{{.Names}}\",\"i\":\"{{.Image}}\"}' 2>/dev/null") ?: '';
foreach (explode("\n", trim($psOut)) as $line) {
$c = json_decode($line, true);
if ($c) $running[strtolower($c['n'])] = $c['i'];
}
// Parse FALLBACK arrays from this host's conf
$confFile = strtolower($myId) . '.conf';
$rawConf = vv_read_conf_raw($confFile);
$result = [];
foreach (['HOST1', 'HOST2', 'HOST3', 'HOST4'] as $covered) {
if ($covered === $myId) continue;
$coveredHostname = $vars[$covered] ?? '';
if (!$coveredHostname) continue;
$names = [];
for ($tier = 1; $tier <= 4; $tier++)
$names = array_merge($names, vv_parse_bash_array($rawConf, "FALLBACK_{$myId}_COVERS_{$covered}_TIER{$tier}"));
$active = [];
foreach ($names as $name) {
if (isset($running[strtolower($name)]))
$active[] = ['name' => $name, 'image' => $running[strtolower($name)]];
}
if ($active) $result[] = ['host_id' => $covered, 'hostname' => $coveredHostname, 'containers' => $active];
}
return $result;
}
function vv_transcode_sessions(): array {
$stateFile = '/tmp/transcode_state.db';
if (!file_exists($stateFile)) return ['available' => false];
@@ -3,10 +3,9 @@
<div id="vv-monitor">
<div class="vv-row">
<div class="vv-card" id="vv-fallback">
<div class="vv-card" id="vv-fallback" style="min-width:220px;">
<h3>Fallback State</h3>
<div class="vv-state-badge" id="vv-fallback-state">—</div>
<div id="vv-fallback-desc"></div>
<div id="vv-fallback-body">Loading...</div>
</div>
<div class="vv-card" id="vv-partner" style="flex:2;min-width:220px;">
@@ -292,36 +291,64 @@ function vvPollMonitor() {
.then(d => {
// ── Fallback state ──────────────────────────────────────────────────────
const fb = d.fallback ?? {};
const state = (fb.state ?? 'UNKNOWN').toUpperCase();
const badge = document.getElementById('vv-fallback-state');
badge.textContent = state;
badge.className = 'vv-state-badge vv-state-' + state.toLowerCase();
// ── Fallback state ──────────────────────────────────────────────────────
const fb = d.fallback ?? {};
const fbHosts = (d.partner ?? {}).hosts ?? [];
const fbActive = d.fallback_active ?? [];
const state = (fb.state ?? 'UNKNOWN').toUpperCase();
const descMap = {
NORMAL: ['#4caf50', '✓', 'Nominal'],
FAILOVER: ['#f44336', '⚠', 'Running peer containers'],
NO_INTERNET: ['#ff9800', '⚡', 'Internet lost'],
DARK: ['#9e9e9e', '◌', 'Isolation mode'],
UNKNOWN: ['#555', '?', 'State unavailable'],
const stateMap = {
NORMAL: ['#4caf50', '✓ Nominal'],
FAILOVER: ['#f44336', '⚠ Failover active'],
NO_INTERNET: ['#ff9800', '⚡ Internet lost'],
DARK: ['#9e9e9e', '◌ Dark mode'],
UNKNOWN: ['#444', '— No state file'],
};
const [dColor, dIcon, dText] = descMap[state] ?? ['#555', '?', ''];
let descHtml = `<div style="font-size:12px;color:${dColor};margin-top:6px;">${dIcon} ${dText}</div>`;
const [sColor, sLabel] = stateMap[state] ?? ['#444', state];
// Host rows
let fbHtml = '';
fbHosts.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;">
<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}
<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}`;
if (state === 'FAILOVER') {
const start = parseInt(fb.failover_start ?? 0);
if (start > 0) {
const sec = Math.floor(Date.now() / 1000) - start;
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60);
descHtml += `<div style="font-size:11px;color:#888;margin-top:3px;">Duration: ${h}h ${m}m</div>`;
fbHtml += ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
}
const tiers = ['tier2_started','tier3_started','tier4_started']
.filter(k => fb[k] === 'true')
.map(k => k.replace('_started','').replace('tier','T'));
if (tiers.length)
descHtml += `<div style="font-size:11px;color:#f44336;margin-top:3px;">Active: ${tiers.join(' ')}</div>`;
}
document.getElementById('vv-fallback-desc').innerHTML = descHtml;
fbHtml += `</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>`;
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;
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>
</div>`;
});
});
}
document.getElementById('vv-fallback-body').innerHTML = fbHtml;
// ── Partner ─────────────────────────────────────────────────────────────
const pt = d.partner ?? {};