fix: never re-enable SSID pool broadcast (crash guard) + top-bar health chip

Mode 'active' and the advertise toggle could re-enable the SSID-pool
broadcast that segfaults pineapd. active preset now skips ssidpool/enable
(advertise stays false), the advertise endpoint refuses with an
explanation when the pool is disabled, get_ap reports the real pool
state, and the PineAP overview disables the toggle with a notice. Added
a top-bar health chip (PINEAP OK / POOL OFF / PINEAPD DOWN) polled every
15s.
This commit is contained in:
2026-08-18 20:16:41 -05:00
parent 0cd9956c4c
commit d10fba9d1b
6 changed files with 69 additions and 13 deletions
@@ -187,6 +187,13 @@ body {
.badge.off { background: #fff3e0; color: #e65100; }
.badge.warn { background: #fff8e1; color: #f57f17; }
.badge.unknown { background: #eeeeee; color: #616161; }
.health-chip { font-size: 11px; font-weight: 600; letter-spacing: .04em; padding: 2px 8px; border-radius: 10px; margin-left: 10px; align-self: center; }
.health-chip.good { background: #e8f5e9; color: #2e7d32; }
.health-chip.warn { background: #fff8e1; color: #f57f17; }
.health-chip.bad { background: #fdecea; color: #b71c1c; }
html.dark .health-chip.good { background: #1b3a24; color: #81c784; }
html.dark .health-chip.warn { background: #3d3313; color: #ffd54f; }
html.dark .health-chip.bad { background: #4a2020; color: #ffb4a9; }
.btn {
background: var(--primary); color: #fff; border: 0; border-radius: 2px;
padding: 8px 14px; font-size: 14px; cursor: pointer;
@@ -26,6 +26,7 @@
<span id="brand-text" class="brand-text">Mark VIII</span>
<span class="toolbar-spacer"></span>
<span id="live-status"></span>
<span id="health-status" class="health-chip"></span>
<div class="toolbar-action">
<button id="notifications-btn" class="toolbar-icon-btn" type="button" title="Notifications"
aria-label="Notifications" aria-haspopup="menu" aria-controls="notifications-menu" aria-expanded="false"></button>
@@ -434,6 +434,7 @@ const Live = (() => {
let ws = null;
let ever = false;
let poll = null;
let pollHealthTimer = null;
const subs = [];
let timer = null;
function stopPoll() {
@@ -442,6 +443,10 @@ const Live = (() => {
function start() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
stopPoll();
if (!pollHealthTimer) {
pollHealthTimer = setInterval(pollHealth, 15000);
pollHealth();
}
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
catch (e) { fallback(); return; }
ws.onopen = () => { ever = true; };
@@ -489,6 +494,23 @@ const Live = (() => {
const el = document.getElementById('live-status');
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
}
function pollHealth() {
fetch(API_BASE + '/api/health', { credentials: 'include' }).then((r) => r.json())
.then((h) => {
const el = document.getElementById('health-status');
if (!el) return;
if (h.pineap_up === false) {
el.textContent = 'PINEAPD DOWN';
el.className = 'health-chip bad';
} else if (h.pool_disabled) {
el.textContent = 'POOL OFF';
el.className = 'health-chip warn';
} else if (h.pineap_up) {
el.textContent = 'PINEAP OK';
el.className = 'health-chip good';
}
}).catch(() => {});
}
function onTick(fn) {
subs.push(fn);
return () => {
@@ -276,6 +276,9 @@ views.pineap = (root) => {
h('div', { class: 'pineap-card-title' }, 'Quick Settings'));
quickCard.appendChild(h('label', { class: 'switch' }, quick.collect, h('span', { class: 'track' }), 'Capture SSIDs to Pool'));
quickCard.appendChild(h('label', { class: 'switch' }, quick.advertise, h('span', { class: 'track' }), 'Advertise AP Impersonation Pool'));
const poolNotice = h('div', { class: 'pineap-infobox warn', style: 'margin-top:8px',
text: 'Pool broadcast disabled: it segfaults pineapd on this firmware (crash-loop fix).' });
quickCard.appendChild(poolNotice);
quickCard.appendChild(h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' },
'Client connect/disconnect notifications are handled by the Pager alert payload system.'));
@@ -305,10 +308,10 @@ views.pineap = (root) => {
on(requested).then(() => {
if (remember) remember(requested);
load();
}).catch(() => {
}).catch((e) => {
cb.checked = !requested;
load();
App.toast('Failed', 'error');
App.toast((e && e.message) || 'Failed', 'error');
});
});
}
@@ -332,7 +335,7 @@ views.pineap = (root) => {
}
const descriptions = {
passive: ['Capture SSIDs to the impersonation pool', 'Do not broadcast the pool', 'Keep the PineAP response engine disabled'],
active: ['Capture SSIDs to the impersonation pool', 'Broadcast the impersonation pool', 'Enable the PineAP response engine']
active: ['Capture SSIDs to the impersonation pool', 'Enable the PineAP response engine', 'Pool broadcast stays disabled (firmware crash fix)']
};
if (m === 'advanced') {
modeInfo.textContent = 'All supported PineAP features are individually customizable from Quick Settings and the PineAP tabs.';
@@ -401,6 +404,10 @@ views.pineap = (root) => {
: Object.prototype.hasOwnProperty.call(c, 'autossidpool') ? !!c.autossidpool : null;
setKnownCheckbox(quick.collect, collect);
const pool = a.pool || {};
const poolDisabled = pool.disabled === true;
poolNotice.classList.toggle('hidden', !poolDisabled);
quick.advertise.disabled = poolDisabled;
quick.advertise.title = poolDisabled ? 'Disabled by firmware crash fix' : '';
if (pool.disabled != null) PINEAP_SESSION.advertise = pool.disabled === false;
setKnownCheckbox(quick.advertise, PINEAP_SESSION.advertise);
cards.karma.value.textContent = PINEAP_SESSION.karma == null ? 'Unknown' : (PINEAP_SESSION.karma ? 'On' : 'Off');