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 ─────────────────────────────────────────────────────────────────