Fallback card: rich state display with tiers, strikes, disabled/suspended
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
This commit is contained in:
+120
-44
@@ -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 = `<div style="font-size:11px;color:${sColor};margin-bottom:8px;">${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 += `</div>`;
|
||||
|
||||
if (fbActive.length) {
|
||||
fbActive.forEach(group => {
|
||||
fbHtml += `<div style="font-size:10px;color:#888;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>`;
|
||||
});
|
||||
});
|
||||
} else if (state === 'NORMAL') {
|
||||
fbHtml += `<div style="font-size:10px;color:#333;">No containers active</div>`;
|
||||
}
|
||||
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 =
|
||||
`<div style="font-size:11px;color:#444;margin-bottom:6px;">○ Disabled</div>
|
||||
<div style="font-size:10px;color:#333;">Set FALLBACK_ENABLED=true in master.conf to activate</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Suspended (partnership gate) ───────────────────────────────────────
|
||||
if (ptSuspended) {
|
||||
const lostMin = partnerLostAt > 0 ? Math.floor((Date.now()/1000 - partnerLostAt) / 60) : '?';
|
||||
document.getElementById('vv-fallback-body').innerHTML =
|
||||
`<div style="font-size:11px;color:#555;margin-bottom:6px;">⊘ Suspended</div>
|
||||
<div style="font-size:10px;color:#444;margin-bottom:3px;">Partnership inactive · ${lostMin}m elapsed</div>
|
||||
<div style="font-size:10px;color:#333;">Resumes when partnership becomes active</div>`;
|
||||
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 += `<div style="font-size:11px;color:${sColor};font-weight:500;margin-bottom:6px;">${sLabel}${stateExtra}</div>`;
|
||||
|
||||
// ── Handback strike progress ───────────────────────────────────────────
|
||||
if (state === 'FAILOVER' && strikes > 0) {
|
||||
const dots = Array.from({length: maxStrikes}, (_,i) =>
|
||||
`<span style="color:${i < strikes ? '#4caf50' : '#2a2a2a'};font-size:14px;line-height:1;">●</span>`
|
||||
).join('');
|
||||
fbHtml += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||||
<span style="font-size:10px;color:#555;">Handback</span>
|
||||
<span style="display:flex;gap:3px;">${dots}</span>
|
||||
<span style="font-size:10px;color:#555;">${strikes}/${maxStrikes}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 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) => `<span style="font-size:9px;padding:1px 6px;border-radius:3px;
|
||||
background:${on ? '#1a2a1a' : '#1a1a1a'};
|
||||
border:1px solid ${on ? '#2a5a2a' : '#252525'};
|
||||
color:${on ? '#4caf50' : '#333'};">T${n}</span>`;
|
||||
fbHtml += `<div style="display:flex;gap:4px;margin-bottom:8px;">
|
||||
${tier(1,true)} ${tier(2,t2)} ${tier(3,t3)} ${tier(4,t4)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Active containers ──────────────────────────────────────────────────
|
||||
if (fbActive.length) {
|
||||
fbActive.forEach(group => {
|
||||
fbHtml += `<div style="font-size:10px;color:#555;margin-bottom:4px;letter-spacing:.03em;">
|
||||
COVERING ${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:3px;">
|
||||
<span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
|
||||
<span style="color:#444;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
|
||||
</div>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── NORMAL quiet state ─────────────────────────────────────────────────
|
||||
if (state === 'NORMAL' && !fbActive.length) {
|
||||
let meta = `monitoring · ${interval}s`;
|
||||
if (ptRequired) meta += ' · partnership gated';
|
||||
fbHtml += `<div style="font-size:10px;color:#2a2a2a;">${meta}</div>`;
|
||||
}
|
||||
|
||||
// ── NO_INTERNET / DARK detail ──────────────────────────────────────────
|
||||
if (state === 'NO_INTERNET') {
|
||||
fbHtml += `<div style="font-size:10px;color:#555;">DDNS stopped · awaiting recovery</div>`;
|
||||
} else if (state === 'DARK') {
|
||||
fbHtml += `<div style="font-size:10px;color:#555;">Remote + internet both down</div>`;
|
||||
}
|
||||
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user