From 1e9d8bc75b96d7ae5c4f43859432c3bc90939df9 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 30 May 2026 17:24:55 -0400 Subject: [PATCH] Fallback card: rich state display with tiers, strikes, disabled/suspended MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit include/monitor.php: vv_fallback_state() now returns: enabled, check_interval, handback_strikes, handback_strikes_required, partnership_suspended, partner_lost_at, partnership_required, partnership_suspend_after — all read from state file + conf vars. pages/monitor.php: fallback card render rebuilt: Disabled: clear message with how to enable Suspended: partnership inactive with elapsed time FAILOVER: outage duration + T1/T2/T3/T4 tier badges (active=green, pending=dark) + handback strike progress dots (●●○) when remote comes back and strikes are counting NO_INTERNET/DARK: short contextual line NORMAL: quiet "monitoring · 30s · partnership gated" meta line Accent border: disabled/suspended → warn, NORMAL → ok, FAILOVER → err --- Plugin/unraid/include/monitor.php | 33 ++++-- Plugin/unraid/pages/monitor.php | 164 ++++++++++++++++++++++-------- 2 files changed, 146 insertions(+), 51 deletions(-) diff --git a/Plugin/unraid/include/monitor.php b/Plugin/unraid/include/monitor.php index 937a8c4..93d1a59 100644 --- a/Plugin/unraid/include/monitor.php +++ b/Plugin/unraid/include/monitor.php @@ -46,20 +46,39 @@ function vv_partner_state(): array { } function vv_fallback_state(): array { - // State file written by fallback.sh + $vars = vv_conf_vars(); + $enabled = ($vars['FALLBACK_ENABLED'] ?? 'false') === 'true'; + $interval = (int)($vars['FALLBACK_CHECK_INTERVAL'] ?? 30); + $reqStrikes = (int)($vars['FALLBACK_HANDBACK_STRIKES'] ?? 3); + $ptRequired = ($vars['FALLBACK_PARTNERSHIP_REQUIRED'] ?? 'true') === 'true'; + $suspendAfter = (int)($vars['FALLBACK_PARTNERSHIP_SUSPEND_AFTER'] ?? 120); + $stateFile = '/tmp/fallback_state.db'; - if (!file_exists($stateFile)) return ['state' => 'UNKNOWN']; + if (!file_exists($stateFile)) { + return ['state' => 'UNKNOWN', 'enabled' => $enabled, 'check_interval' => $interval, + 'handback_strikes' => 0, 'handback_strikes_required' => $reqStrikes, + 'partnership_required' => $ptRequired, 'partnership_suspended' => false, + 'partner_lost_at' => 0, 'partnership_suspend_after' => $suspendAfter]; + } $raw = []; foreach (file($stateFile) ?: [] as $line) { [$k, $v] = array_pad(explode('=', trim($line), 2), 2, ''); $raw[trim($k)] = trim($v); } return [ - 'state' => $raw['state'] ?? 'UNKNOWN', - 'failover_start' => $raw['failover_start'] ?? '0', - 'tier2_started' => $raw['tier2_started'] ?? 'false', - 'tier3_started' => $raw['tier3_started'] ?? 'false', - 'tier4_started' => $raw['tier4_started'] ?? 'false', + 'state' => $raw['state'] ?? 'UNKNOWN', + 'failover_start' => $raw['failover_start'] ?? '0', + 'tier2_started' => $raw['tier2_started'] ?? 'false', + 'tier3_started' => $raw['tier3_started'] ?? 'false', + 'tier4_started' => $raw['tier4_started'] ?? 'false', + 'handback_strikes' => (int)($raw['handback_strikes'] ?? 0), + 'partnership_suspended' => ($raw['partnership_suspended'] ?? 'false') === 'true', + 'partner_lost_at' => (int)($raw['partner_lost_at'] ?? 0), + 'enabled' => $enabled, + 'check_interval' => $interval, + 'handback_strikes_required' => $reqStrikes, + 'partnership_required' => $ptRequired, + 'partnership_suspend_after' => $suspendAfter, ]; } diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index cca6b68..969b622 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -693,51 +693,127 @@ function vvPollMonitor() { })(); // ── Fallback ────────────────────────────────────────────────────────────── - const fb = d.fallback ?? {}; - const fbActive = d.fallback_active ?? []; - const state = (fb.state ?? 'UNKNOWN').toUpperCase(); - const stateMap = { - NORMAL: ['#4caf50', '✓ Nominal'], - FAILOVER: ['#f44336', '⚠ Failover active'], - NO_INTERNET: ['#ff9800', '⚡ Internet lost'], - DARK: ['#9e9e9e', '◌ Dark mode'], - UNKNOWN: ['#444', '— No state file'], - }; - const [sColor, sLabel] = stateMap[state] ?? ['#444', state]; - - let fbHtml = `
${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`; - } - } - fbHtml += `
`; - - if (fbActive.length) { - fbActive.forEach(group => { - fbHtml += `
Running for ${group.hostname}
`; - group.containers.forEach(c => { - const img = c.image.includes('/') ? c.image.split('/').pop() : c.image; - fbHtml += `
- ${c.name} - ${img} -
`; - }); - }); - } else if (state === 'NORMAL') { - fbHtml += `
No containers active
`; - } - document.getElementById('vv-fallback-body').innerHTML = fbHtml; (function() { - const c = document.getElementById('vv-fallback'); - if (!c) return; - c.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err'); - if (state === 'NORMAL') c.classList.add('vv-accent-ok'); - else if (state === 'FAILOVER') c.classList.add('vv-accent-err'); - else if (state !== 'UNKNOWN') c.classList.add('vv-accent-warn'); + const fb = d.fallback ?? {}; + const fbActive = d.fallback_active ?? []; + const state = (fb.state ?? 'UNKNOWN').toUpperCase(); + const enabled = fb.enabled ?? false; + const strikes = fb.handback_strikes ?? 0; + const maxStrikes = fb.handback_strikes_required ?? 3; + const ptSuspended = fb.partnership_suspended ?? false; + const partnerLostAt = fb.partner_lost_at ?? 0; + const suspendAfter = fb.partnership_suspend_after ?? 120; + const interval = fb.check_interval ?? 30; + const ptRequired = fb.partnership_required ?? false; + + const stateMap = { + NORMAL: ['#4caf50', '✓ Nominal'], + FAILOVER: ['#f44336', '⚠ Failover active'], + NO_INTERNET: ['#ff9800', '⚡ Internet lost'], + DARK: ['#9e9e9e', '◌ Dark mode'], + UNKNOWN: ['#444', '— No state file'], + }; + const [sColor, sLabel] = stateMap[state] ?? ['#444', state]; + + // ── Disabled ────────────────────────────────────────────────────────── + if (!enabled) { + document.getElementById('vv-fallback-body').innerHTML = + `
○ Disabled
+
Set FALLBACK_ENABLED=true in master.conf to activate
`; + return; + } + + // ── Suspended (partnership gate) ─────────────────────────────────────── + if (ptSuspended) { + const lostMin = partnerLostAt > 0 ? Math.floor((Date.now()/1000 - partnerLostAt) / 60) : '?'; + document.getElementById('vv-fallback-body').innerHTML = + `
⊘ Suspended
+
Partnership inactive · ${lostMin}m elapsed
+
Resumes when partnership becomes active
`; + return; + } + + let fbHtml = ''; + + // ── State label + outage duration ────────────────────────────────────── + let stateExtra = ''; + if (state === 'FAILOVER' || state === 'DARK') { + const start = parseInt(fb.failover_start ?? 0); + if (start > 0) { + const sec = Math.floor(Date.now() / 1000) - start; + stateExtra = ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`; + } + } + fbHtml += `
${sLabel}${stateExtra}
`; + + // ── Handback strike progress ─────────────────────────────────────────── + if (state === 'FAILOVER' && strikes > 0) { + const dots = Array.from({length: maxStrikes}, (_,i) => + `` + ).join(''); + fbHtml += `
+ Handback + ${dots} + ${strikes}/${maxStrikes} +
`; + } + + // ── Tier badges (FAILOVER only) ──────────────────────────────────────── + if (state === 'FAILOVER') { + const t2 = fb.tier2_started === 'true'; + const t3 = fb.tier3_started === 'true'; + const t4 = fb.tier4_started === 'true'; + const tier = (n, on) => `T${n}`; + fbHtml += `
+ ${tier(1,true)} ${tier(2,t2)} ${tier(3,t3)} ${tier(4,t4)} +
`; + } + + // ── Active containers ────────────────────────────────────────────────── + if (fbActive.length) { + fbActive.forEach(group => { + fbHtml += `
+ COVERING ${group.hostname} +
`; + group.containers.forEach(c => { + const img = c.image.includes('/') ? c.image.split('/').pop() : c.image; + fbHtml += `
+ ${c.name} + ${img} +
`; + }); + }); + } + + // ── NORMAL quiet state ───────────────────────────────────────────────── + if (state === 'NORMAL' && !fbActive.length) { + let meta = `monitoring · ${interval}s`; + if (ptRequired) meta += ' · partnership gated'; + fbHtml += `
${meta}
`; + } + + // ── NO_INTERNET / DARK detail ────────────────────────────────────────── + if (state === 'NO_INTERNET') { + fbHtml += `
DDNS stopped · awaiting recovery
`; + } else if (state === 'DARK') { + fbHtml += `
Remote + internet both down
`; + } + + document.getElementById('vv-fallback-body').innerHTML = fbHtml; + + // Status accent border + const fbCard = document.getElementById('vv-fallback'); + if (fbCard) { + fbCard.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err'); + if (!enabled || ptSuspended) fbCard.classList.add('vv-accent-warn'); + else if (state === 'NORMAL') fbCard.classList.add('vv-accent-ok'); + else if (state === 'FAILOVER') fbCard.classList.add('vv-accent-err'); + else if (state !== 'UNKNOWN') fbCard.classList.add('vv-accent-warn'); + } })(); // ── CPU ─────────────────────────────────────────────────────────────────