feat(ui): reliability panel events/counters, RF plan chip and controls

This commit is contained in:
2026-08-22 14:55:49 -06:00
parent bace45d6e4
commit 4c1144ab32
4 changed files with 200 additions and 4 deletions
@@ -448,6 +448,9 @@ const Live = (() => {
let poll = null;
let pollHealthTimer = null;
let pollEventsTimer = null;
let pollRfTimer = null;
let rfState = null;
let rfChannel = null;
const lastEvents = { hsSeen: {}, hsPrimed: false, creds: null, credsPrimed: false,
pineapUp: null, mon0: null, mon1: null };
const subs = [];
@@ -466,6 +469,10 @@ const Live = (() => {
pollEventsTimer = setInterval(pollEvents, 15000);
pollEvents();
}
if (!pollRfTimer) {
pollRfTimer = setInterval(pollRfplan, 15000);
pollRfplan();
}
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
catch (e) { fallback(); return; }
ws.onopen = () => { ever = true; };
@@ -512,6 +519,47 @@ const Live = (() => {
const n = (msg.clients || []).length;
const el = document.getElementById('live-status');
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
const wifi = (msg.status || {}).wifi;
if (Array.isArray(wifi)) {
const up = wifi.find((w) => w && w.iface === 'wlan1up');
rfChannel = up && up.channel != null ? Number(up.channel) : null;
renderRfChip();
}
}
function pollRfplan() {
fetch(App.apiBase + '/api/rfplan', { credentials: 'include' }).then((r) => r.json())
.then((d) => {
rfState = d && typeof d.role === 'string' ? d : null;
renderRfChip();
}).catch(() => {});
}
function renderRfChip() {
const el = document.getElementById('rf-chip');
if (!el) return;
if (!rfState) {
el.textContent = '';
el.title = '';
el.className = 'health-chip';
return;
}
let text;
let cls = '';
if (rfState.role === 'uplink') {
text = 'PHY1: UPLINK' + (rfState.assoc && rfChannel ? ' ch' + rfChannel : '');
cls = rfState.assoc ? 'good' : 'warn';
} else if (rfState.role === 'attack') {
text = 'PHY1: ATTACK';
cls = 'warn';
} else if (rfState.role === 'idle') {
text = 'PHY1: IDLE';
} else {
text = 'PHY1: ' + String(rfState.role).toUpperCase();
}
el.textContent = text;
el.className = 'health-chip' + (cls ? ' ' + cls : '');
el.title = 'radio1 role: ' + rfState.role +
' \u00b7 assoc ' + (rfState.assoc || 'none') +
' \u00b7 hop ' + (rfState.hop_paused == null ? 'unknown' : rfState.hop_paused ? 'paused' : 'running');
}
function pollHealth() {
fetch(App.apiBase + '/api/health', { credentials: 'include' }).then((r) => r.json())