28 KiB
Mk7 "PineAP Open Access Point" Card Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Rebuild the Open AP tab (#/pineap/open, views.pineap_open) into the genuine Mark 7 Pineapple's "PineAP Open Access Point" card — Open SSID / BSSID / Channel / Current Country / Hidden / Respond-to-all-probes toggles, filter notice boxes, and a Save that writes real device config.
Architecture: Backend extends the two existing wifi/get_ap + wifi/set_ap handlers to expose and persist the Open AP's SSID, BSSID (macaddr), hidden, channel, and country (channel/country applied to wireless.radio0 with a wifi reload). Frontend replaces views.pineap_open with the Mk7 card; karma ("Respond to all probe requests") saves via the existing /api/pineap/mimic and is session-tracked (the daemon cannot report karma). Filter notice boxes reuse the existing action-based filter API.
Tech Stack: Python (server.py, unittest), vanilla JS (views.js, app.css), existing h()/btn()/PagerAPI/App.toast helpers.
Global Constraints
- Backend is UCI-driven (
_uci_wifi_iface,_uci_section,device_run); the daemon's/api/settings/wifi/set_appersistsssid/hidden/bssid→macaddr/channel/enabledtowireless.wlan0openbut its iface-levelchannelwrite is inert for the actual radio — the radio channel/country must be written towireless.radio0+uci commit wireless+wifi reload. - The daemon exposes no readable karma/mimic or broadcast/advertise state — the karma toggle is session-tracked (module-level flag, default off, updated on Save); the info line omits the "Spoofed SSID Pool will be advertised" clause.
- Open AP
enabledis preserved as-is (the Mk7 card has no Enabled control); the frontend always sendsenabled= value loaded fromget_ap. - Filter mutations use the existing
actionAPI:POST /api/pineap/filters/ssid{action:'add'|'delete', value}andPOST /api/pineap/filters/client{action:'set_mode', mode:'deny'}. - No new routes; no daemon changes. Commit messages follow repo style (
feat:,fix:,docs:). - JS verification uses
C:\Users\root\AppData\Local\Temp\opencode\js_balance.py(no node available). Python:$env:LOCALAPPDATA\Programs\Python\Python311\python.exe. - Deploy: from repo root,
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "<PAGER_PASSWORD>", then/etc/init.d/pagerwebui restartover sshpass.
Task 1: Backend — expose and save Open AP network settings
Files:
- Modify:
payload/user/general/pager-webui/server.py(h_pineap_wifi_get_apat ~1479,h_pineap_wifi_set_apat ~1505, add helper_apply_open_radiojust beforeh_pineap_wifi_set_ap) - Test:
tests/test_pineap_proxy.py
Interfaces:
-
Consumes:
_uci_wifi_iface(name)(runsuci show wireless.<name>, returns dict),_uci_section(section),device_run(args),daemon_sock_call(method, path, body). -
Produces:
h_pineap_wifi_get_apopenpayload now{enabled, ssid, bssid, target, hidden, channel, country};h_pineap_wifi_set_apacceptsopen: {ssid, bssid, hidden, enabled, channel, country}. Task 2 consumes these exact field names. -
Step 1: Write the failing tests (extend
test_wifi_get_ap_reads_uci_wireless; add a new set_ap test)
Replace the test_wifi_get_ap_reads_uci_wireless body's fake_run with the version below and add the new assertions; append test_wifi_set_ap_open_bssid_channel_and_country to the PineapProxyTest class:
def test_wifi_get_ap_reads_uci_wireless(self):
def fake_run(args):
cmd = args[0]
if cmd == 'uci' and len(args) == 3:
sec = args[2]
if sec == 'wireless.wlan0wpa':
return 0, "wireless.wlan0wpa.ifname='wlan0wpa'\nwireless.wlan0wpa.ssid='Evil1'\nwireless.wlan0wpa.encryption='psk2'\nwireless.wlan0wpa.key='sekret'\nwireless.wlan0wpa.disabled='0'\nwireless.wlan0wpa.hidden='0'\n", ''
if sec == 'wireless.wlan0open':
return 0, "wireless.wlan0open.disabled='1'\nwireless.wlan0open.ssid='pager-open'\nwireless.wlan0open.macaddr='DE:AD:BE:EF:00:01'\nwireless.wlan0open.hidden='1'\n", ''
if sec == 'wireless.radio0':
return 0, "wireless.radio0.channel='6'\nwireless.radio0.country='US'\n", ''
if sec.startswith('pineapd.@ssidpool'):
return 0, "pineapd.@ssidpool[0].bssid='auto'\npineapd.@ssidpool[0].target='broadcast'\n", ''
return 0, '', ''
def fake_sock(method, path, body=None, timeout=10):
if path == '/api/pineap/hostapd/get_config':
return 200, {'pineape_disabled': False}
if path == '/api/pineap/get_config':
return 200, {'autossidpool': True}
return 200, {}
server.device_run = fake_run
server.daemon_sock_call = fake_sock
status, payload = server.h_pineap_wifi_get_ap(ctx())
self.assertEqual(status, 200)
self.assertEqual(payload['wpa'], {'ssid': 'Evil1', 'passphrase': 'sekret', 'enctype': 'psk2',
'hidden': False, 'enabled': True})
self.assertEqual(payload['open']['enabled'], False)
self.assertEqual(payload['open']['ssid'], 'pager-open')
self.assertEqual(payload['open']['bssid'], 'DE:AD:BE:EF:00:01')
self.assertEqual(payload['open']['hidden'], True)
self.assertEqual(payload['open']['channel'], 6)
self.assertEqual(payload['open']['country'], 'US')
self.assertEqual(payload['open']['target'], 'broadcast')
self.assertEqual(payload['enterprise']['enabled'], True)
self.assertEqual(payload['pool']['collecting'], True)
def test_wifi_set_ap_open_bssid_channel_and_country(self):
sock_calls = []
run_calls = []
def fake_sock(method, path, body=None, timeout=10):
sock_calls.append((method, path, body))
return (200, {'success': True})
def fake_run(args):
run_calls.append(args)
if args[0] == 'uci' and args[1] == 'show':
return 0, "wireless.radio0.channel='1'\n", ''
return 0, '', ''
server.daemon_sock_call = fake_sock
server.device_run = fake_run
status, _ = server.h_pineap_wifi_set_ap(ctx({'open': {
'ssid': 'Open', 'bssid': 'DE:AD:BE:EF:00:02', 'hidden': True,
'channel': 6, 'country': 'US', 'enabled': True}}))
self.assertEqual(status, 200)
method, path, body = sock_calls[0]
self.assertEqual(method, 'PUT')
self.assertEqual(path, '/api/settings/wifi/set_ap')
conf = body['configs'][0]
self.assertEqual(conf['interface'], 'wlan0open')
self.assertEqual(conf['ssid'], 'Open')
self.assertEqual(conf['bssid'], 'DE:AD:BE:EF:00:02')
self.assertEqual(conf['hidden'], True)
self.assertEqual(conf['channel'], 6)
self.assertEqual(conf['enabled'], True)
sets = [a for a in run_calls if a[:2] == ['uci', 'set']]
self.assertEqual(sets, [['uci', 'set', 'wireless.radio0.channel=6'],
['uci', 'set', 'wireless.radio0.country=US']])
self.assertIn(['uci', 'commit', 'wireless'], run_calls)
self.assertIn(['wifi', 'reload'], run_calls)
- Step 2: Run the tests to verify they fail
Run: & "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe" -m unittest tests.test_pineap_proxy
Expected: both tests FAIL (get_ap returns no ssid/bssid/hidden/channel/country; set_ap drops bssid/channel and never runs uci set wireless.radio0.*).
- Step 3: Implement the backend changes
In server.py:
h_pineap_wifi_get_ap — add the radio read and the new open fields (replace the current open_cfg = _uci_wifi_iface('wlan0open') line block and the 'open' dict):
def h_pineap_wifi_get_ap(ctx):
open_cfg = _uci_wifi_iface('wlan0open')
radio_cfg = _uci_wifi_iface('radio0')
wpa_cfg = _uci_wifi_iface('wlan0wpa')
status, data = daemon_sock_call('GET', '/api/pineap/hostapd/get_config')
host = data if status == 200 and isinstance(data, dict) else {}
status2, data2 = daemon_sock_call('GET', '/api/pineap/get_config')
pinecfg = data2 if status2 == 200 and isinstance(data2, dict) else {}
pool = _uci_section('pineapd.@ssidpool[0]')
channel = radio_cfg.get('channel') or ''
try:
channel = int(channel)
except (TypeError, ValueError):
channel = None
return 200, {
'open': {
'enabled': open_cfg.get('disabled') == '0',
'ssid': open_cfg.get('ssid') or '',
'bssid': open_cfg.get('macaddr') or '',
'target': pool.get('target') or None,
'hidden': open_cfg.get('hidden') == '1',
'channel': channel,
'country': radio_cfg.get('country') or '',
},
'wpa': {
'ssid': wpa_cfg.get('ssid') or '',
'passphrase': wpa_cfg.get('key') or '',
'enctype': wpa_cfg.get('encryption') or '',
'hidden': wpa_cfg.get('hidden') == '1',
'enabled': wpa_cfg.get('disabled') == '0',
},
'enterprise': {'enabled': not host.get('pineape_disabled', True)},
'pool': {'disabled': None, 'collecting': bool(pinecfg.get('autossidpool'))},
}
Add this helper immediately before h_pineap_wifi_set_ap:
def _apply_open_radio(openap):
"""Persist the Open AP's radio channel/country to wireless.radio0. The
daemon's iface-level channel write does not affect the actual radio, so
apply channel/country here and reload wifi when they change."""
changed = False
for key in ('channel', 'country'):
value = openap.get(key)
if value is None:
continue
current = _uci_wifi_iface('radio0').get(key) or ''
if str(value) != current:
device_run(['uci', 'set', 'wireless.radio0.%s=%s' % (key, value)])
changed = True
if changed:
device_run(['uci', 'commit', 'wireless'])
device_run(['wifi', 'reload'])
h_pineap_wifi_set_ap — replace the open branch and add the call after the daemon call (replace the current 'channel': 1 open config and the return 200, {'ok': True} line):
if openap.get('ssid') or openap.get('enabled') is not None:
configs.append({
'interface': 'wlan0open',
'ssid': openap.get('ssid', ''),
'enctype': 'none',
'enabled': bool(openap.get('enabled', True)),
'hidden': bool(openap.get('hidden', False)),
'channel': int(openap['channel']) if openap.get('channel') is not None else 1,
'bssid': openap.get('bssid') or '',
})
if not configs:
return 400, {'error': 'no configuration provided'}
status, data = daemon_sock_call('PUT', '/api/settings/wifi/set_ap', body={'configs': configs}, timeout=45)
if status != 200:
return (502 if status == 0 else status), {'error': 'daemon failed', 'detail': data}
_apply_open_radio(openap)
return 200, {'ok': True}
- Step 4: Run the tests to verify they pass
Run: & "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe" -m unittest tests.test_pineap_proxy
Expected: all PASS.
- Step 5: Run the full unittest loop
Run from repo root:
$py = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"; Get-ChildItem tests\test_*.py | ForEach-Object { $mod = "tests." + [IO.Path]::GetFileNameWithoutExtension($_.Name); $out = & $py -m unittest $mod 2>&1; ($out | Select-Object -Last 1) }
Expected: every module reports OK (recon may show skipped=1).
- Step 6: Commit
git add payload/user/general/pager-webui/server.py tests/test_pineap_proxy.py
git commit -m "feat: expose and save Open AP ssid/bssid/hidden/channel/country"
Task 2: Frontend — Mk7 "PineAP Open Access Point" card
Files:
- Modify:
payload/user/general/pager-webui/www/js/views.js(addOPEN_CHANNELS/OPEN_COUNTRIESconstants +let OPEN_KARMAbeforeviews.pineap_open; replace the wholeviews.pineap_openfunction, currently lines 327-409, just beforeconst EVIL_ENC) - Modify:
payload/user/general/pager-webui/www/css/app.css(append infobox styles at end)
Interfaces:
-
Consumes: Task 1's
openfields (ssid,bssid,hidden,channel,country,enabled) fromPOST /api/pineap/wifi/get_ap;set_apopenbody keys; existing/api/pineap/mimic,/api/pineap/get_config,GET /api/pineap/filters/{ssid,client}→{mode, entries}; existingactionfilter mutations. -
Produces: the Mk7 Open card. No later task consumes these names.
-
Step 1: Add the constants and module state before
views.pineap_open
Insert immediately before views.pineap_open = (root) => {:
const OPEN_CHANNELS = Array.from({ length: 11 }, (_, i) => {
const c = i + 1;
return [c, 'Channel ' + c + ' (' + (2412 + (c - 1) * 5) + ' MHz)'];
});
const OPEN_COUNTRIES = [
['US', 'United States'], ['DZ', 'Algeria'], ['AR', 'Argentina'], ['AU', 'Australia'],
['AT', 'Austria'], ['BH', 'Bahrain'], ['BM', 'Bermuda'], ['BO', 'Bolivia'], ['BR', 'Brazil'],
['BG', 'Bulgaria'], ['CA', 'Canada'], ['CL', 'Chile'], ['CN', 'China'], ['CO', 'Colombia'],
['CR', 'Costa Rica'], ['CS', 'Cyprus'], ['CZ', 'Czech Republic'], ['DK', 'Denmark'],
['DO', 'Dominican Republic'], ['EC', 'Ecuador'], ['EG', 'Egypt'], ['SV', 'El Salvador'],
['EE', 'Estonia'], ['FI', 'Finland'], ['FR', 'France'], ['DE', 'Germany'], ['GR', 'Greece'],
['GT', 'Guatemala'], ['HN', 'Honduras'], ['HK', 'Hong Kong'], ['IS', 'Iceland'], ['IN', 'India'],
['ID', 'Indonesia'], ['IE', 'Ireland'], ['PK', 'Islamic Republic of Pakistan'], ['IL', 'Israel'],
['IT', 'Italy'], ['JM', 'Jamaica'], ['JO', 'Jordan'], ['KE', 'Kenya'], ['KW', 'Kuwait'],
['LB', 'Lebanon'], ['LI', 'Liechtenstein'], ['LT', 'Lithuania'], ['LU', 'Luxembourg'],
['MU', 'Mauritius'], ['MX', 'Mexico'], ['MA', 'Morocco'], ['NL', 'Netherlands'], ['NZ', 'New Zealand'],
['NO', 'Norway'], ['OM', 'Oman'], ['PA', 'Panama'], ['PE', 'Peru'], ['PH', 'Philippines'],
['PL', 'Poland'], ['PT', 'Portuagal'], ['PR', 'Puerto Rico'], ['QA', 'Qatar'],
['KR', 'Republic of Korea (South Korea)'], ['RO', 'Romania'], ['RU', 'Russia'], ['SA', 'Saudi Arabia'],
['SG', 'Singapore'], ['SI', 'Slovenia'], ['SK', 'Slovak Republic'], ['ZA', 'South Africa'],
['ES', 'Spain'], ['LK', 'Sri Lanka'], ['SE', 'Sweden'], ['CH', 'Switzerland'], ['TW', 'Taiwan'],
['TH', 'Thailand'], ['TT', 'Trinidad and Tobago'], ['TN', 'Tunisia'], ['TR', 'Turkey'],
['UA', 'Ukraine'], ['AE', 'United Arab Emirates'], ['GB', 'United Kingdom'], ['UY', 'Uraguay'],
['VE', 'Venezuela'], ['VN', 'Vietnam']
];
let OPEN_KARMA = false;
- Step 2: Replace the whole
views.pineap_openfunction
Replace everything from views.pineap_open = (root) => { through its closing }; (current lines 327-409, i.e. just before const EVIL_ENC) with:
views.pineap_open = (root) => {
const box = pineapShell(root, '#/pineap/open');
const card = h('div', { class: 'pineap-title-card' });
box.appendChild(card);
card.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAP Open Access Point'));
const subtitle = h('div', { class: 'pineap-card-subtitle' });
card.appendChild(subtitle);
const ssidIn = h('input', { id: 'oa-ssid' });
const bssidIn = h('input', { id: 'oa-bssid' });
const chSel = h('select', { id: 'oa-channel' });
OPEN_CHANNELS.forEach(([v, l]) => chSel.appendChild(h('option', { value: v, text: l })));
const coSel = h('select', { id: 'oa-country' });
OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l })));
const hiddenCb = h('input', { type: 'checkbox', id: 'oa-hidden' });
const karmaCb = h('input', { type: 'checkbox', id: 'oa-karma' });
card.appendChild(h('div', { class: 'row' },
h('div', {}, h('label', {}, 'Open SSID', ssidIn)),
h('div', {}, h('label', {}, 'BSSID', bssidIn))));
card.appendChild(h('div', { class: 'row' },
h('div', {}, h('label', {}, 'Channel', chSel)),
h('div', {}, h('label', {}, 'Current Country', coSel))));
card.appendChild(h('div', { class: 'row' },
h('div', {}, h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), ' Hidden')),
h('div', {}, h('label', { class: 'switch' }, karmaCb, h('span', { class: 'track' }), ' Respond to all probe requests (impersonate all networks)'))));
const info = h('div', { class: 'muted', style: 'margin-top:10px;font-size:13px' });
card.appendChild(info);
const boxes = h('div', {});
card.appendChild(boxes);
card.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
h('div', {}, btn('Save', save)),
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' }, 'Applying reconfigures the radio — you may be disconnected briefly.')));
const state = {};
function cfgLink() {
return h('a', { href: '#/pineap/filtering', style: 'color:var(--primary);cursor:pointer', onclick: (e) => { e.preventDefault(); App.go('#/pineap/filtering'); } }, 'filter configuration');
}
function filterBtn() {
return h('a', { class: 'btn', href: '#/pineap/filtering', style: 'text-decoration:none;display:inline-block', onclick: (e) => { e.preventDefault(); App.go('#/pineap/filtering'); } }, 'Change Filters');
}
function infobox(severity, text, ...actions) {
return h('div', { class: 'pineap-infobox ' + severity },
h('span', { text }),
h('div', { class: 'pineap-infobox-actions' }, actions));
}
function filterSentence(sm, cm) {
if (sm === 'allow' && cm === 'allow') return 'any client in the filter configuration may connect to any SSID in the filter configuration.';
if (sm === 'deny' && cm === 'allow') return 'any client not in the filter configuration may connect to any SSID in the filter configuration.';
if (sm === 'allow' && cm === 'deny') return 'any client in the filter configuration may connect to any SSID not in the filter configuration.';
return 'any client not in the filter configuration may connect to any SSID not in the filter configuration.';
}
function save() {
Promise.allSettled([
PagerAPI.post('/api/pineap/wifi/set_ap', {
open: {
ssid: ssidIn.value,
bssid: bssidIn.value.trim(),
hidden: hiddenCb.checked,
enabled: !!state.enabled,
channel: chSel.value ? parseInt(chSel.value, 10) : null,
country: coSel.value
}
}),
PagerAPI.post('/api/pineap/mimic', { enable: karmaCb.checked })
]).then((results) => {
const ok = results.every((r) => r.status === 'fulfilled');
OPEN_KARMA = karmaCb.checked;
App.toast(ok ? 'Open AP saved' : 'Some settings failed', ok ? '' : 'error');
load();
});
}
function render() {
const sm = state.ssidMode || 'deny';
const cm = state.clientMode || 'deny';
subtitle.textContent = '';
subtitle.appendChild(document.createTextNode('The Open SSID is advertised without encryption. When client association is enabled, '));
subtitle.appendChild(cfgLink());
subtitle.appendChild(document.createTextNode(' ' + filterSentence(sm, cm)));
const hidden = hiddenCb.checked;
const karma = karmaCb.checked;
let t = 'The Open access point will be ' + (hidden ? 'hidden' : 'advertised');
if (!karma) {
t += '.';
} else {
if (sm === 'allow' && cm === 'allow') t += ', and clients in the allowed client filter list will be able to connect to any SSID in the allowed SSID filter.';
else if (sm === 'allow' && cm === 'deny') t += ', and clients in the allowed client filter list will be able to connect to any SSID not blocked by the SSID filter.';
else if (sm === 'deny' && cm === 'allow') t += ', and clients not in the denied client filter list will be able to connect to any SSID in the allowed SSID filter.';
else t += ', and clients not in the denied client filter list will be able to connect to any SSID not blocked by the SSID filter.';
}
info.textContent = t;
boxes.innerHTML = '';
const openSsid = ssidIn.value;
const ssidList = state.ssidList || [];
const clientList = state.clientList || [];
if (state.ssidFetched && sm === 'allow' && openSsid && ssidList.indexOf(openSsid) === -1) {
boxes.appendChild(infobox('error',
'The open SSID "' + openSsid + '" is not included in the filter allow list, clients will not be able to connect.',
btn('Add Allowed', () => PagerAPI.post('/api/pineap/filters/ssid', { action: 'add', value: openSsid }).then(load).catch(() => App.toast('Failed', 'error')))));
}
if (state.ssidFetched && sm === 'deny' && openSsid && ssidList.indexOf(openSsid) !== -1) {
boxes.appendChild(infobox('error',
'The open SSID "' + openSsid + '" is included in the filter deny list, clients will not be able to connect.',
btn('Remove Filter', () => PagerAPI.post('/api/pineap/filters/ssid', { action: 'delete', value: openSsid }).then(load).catch(() => App.toast('Failed', 'error')))));
}
if (sm === 'allow' && ssidList.length > 0 && karmaCb.checked) {
boxes.appendChild(infobox('info',
'Remember to add SSIDs you wish to impersonate to the PineAP SSID filter, or change to "Deny" mode to allow responding to all requested networks!',
filterBtn()));
}
if (state.clientFetched && cm === 'allow' && clientList.length === 0) {
boxes.appendChild(infobox('error',
'The PineAP Client filter is set to "allow", but no clients are listed; no clients will be able to connect!',
btn('Change Mode', () => PagerAPI.post('/api/pineap/filters/client', { action: 'set_mode', mode: 'deny' }).then(load).catch(() => App.toast('Failed', 'error'))),
filterBtn()));
}
}
function load() {
Promise.all([
PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })),
PagerAPI.get('/api/pineap/get_config').catch(() => ({ data: {} })),
PagerAPI.get('/api/pineap/filters/ssid').catch(() => ({ data: {} })),
PagerAPI.get('/api/pineap/filters/client').catch(() => ({ data: {} }))
]).then(([ap, cfg, sf, cf]) => {
const a = ap.data || {}, c = cfg.data || {};
const open = a.open || {};
ssidIn.value = open.ssid || '';
bssidIn.value = open.bssid || '';
if (open.channel != null) chSel.value = String(open.channel);
if (open.country) coSel.value = open.country;
hiddenCb.checked = !!open.hidden;
state.enabled = !!open.enabled;
karmaCb.checked = OPEN_KARMA;
const sd = sf.data || {}, cd = cf.data || {};
state.ssidFetched = !!sd.mode;
state.clientFetched = !!cd.mode;
state.ssidMode = sd.mode;
state.clientMode = cd.mode;
state.ssidList = sd.entries || [];
state.clientList = cd.entries || [];
render();
});
}
load();
return { destroy: () => {} };
};
- Step 3: Append the infobox CSS to
app.css
Append to the end of payload/user/general/pager-webui/www/css/app.css:
/* ---- Open AP: Mk7 filter notice boxes ---- */
.pineap-infobox { border-radius: 2px; padding: 10px 12px; margin-top: 10px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
.pineap-infobox.error { background: #fdecea; color: #b71c1c; border: 1px solid #f5c6cb; }
.pineap-infobox.info { background: #e3f2fd; color: #0d47a1; border: 1px solid #90caf9; }
.pineap-infobox-actions { display: flex; gap: 8px; flex-wrap: wrap; }
html.dark .pineap-infobox.error { background: #4a2020; color: #ffb4a9; border-color: #6b2d2d; }
html.dark .pineap-infobox.info { background: #10263a; color: #9cc7f0; border-color: #1d3a54; }
- Step 4: Run the JS delimiter balance check
Run: & "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe" "C:\Users\root\AppData\Local\Temp\opencode\js_balance.py" "C:\Users\root\Documents\Pineapple\pager-webui\payload\user\general\pager-webui\www\js\views.js"
Expected: ...views.js: delimiter balance OK
- Step 5: Run the full unittest loop (backend must stay green)
Run from repo root:
$py = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"; Get-ChildItem tests\test_*.py | ForEach-Object { $mod = "tests." + [IO.Path]::GetFileNameWithoutExtension($_.Name); $out = & $py -m unittest $mod 2>&1; ($out | Select-Object -Last 1) }
Expected: every module reports OK (recon may show skipped=1).
- Step 6: Commit
git add payload/user/general/pager-webui/www/js/views.js payload/user/general/pager-webui/www/css/app.css
git commit -m "feat: Mk7 PineAP Open Access Point card for Open AP tab"
Task 3: Deploy and verify on device
Files: none (verification only; no commit).
Interfaces:
-
Consumes: Tasks 1-2 output (deployed via
scripts/deploy.ps1). -
Step 1: Deploy the payload and restart the webui
Run from C:\Users\root\Documents\Pineapple\pager-webui:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "<PAGER_PASSWORD>"
Then restart and confirm the port is up (over sshpass SSH):
sshpass -p "<PAGER_PASSWORD>" ssh root@172.16.52.1 "/etc/init.d/pagerwebui restart; sleep 4; curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/api_ping"
Expected: 401 (auth required = running).
- Step 2: Confirm the deployed files contain the new code
sshpass -p "<PAGER_PASSWORD>" ssh root@172.16.52.1 "grep -c 'PineAP Open Access Point' /root/payloads/user/general/pager-webui/www/js/views.js; grep -c 'pineap-infobox' /root/payloads/user/general/pager-webui/www/css/app.css; grep -c 'radio0' /root/payloads/user/general/pager-webui/server.py"
Expected: all counts greater than zero.
- Step 3: On-device save-path round-trip (write current values, verify UCI, restore)
Record the current wireless.wlan0open + wireless.radio0 state first, then PUT the same values back through set_ap (idempotent), then verify and confirm the config is unchanged:
sshpass -p "<PAGER_PASSWORD>" ssh root@172.16.52.1 "uci show wireless.wlan0open; uci show wireless.radio0 | grep -E 'channel|country'"
Then, over sshpass SSH, base64 a script and run it via echo ... | base64 -d | sh (avoids shell-quoting mangling) that:
- Logs in to the WebUI (
POST /api/loginwith the root credentials, saves cookie). POST /api/pineap/wifi/get_ap→ confirm the response contains"open"withssid,bssid,hidden,channel,countrykeys.POST /api/pineap/wifi/set_apwith{open:{ssid:<current>, bssid:<current>, hidden:<current>, enabled:true, channel:<current>, country:<current>}}(the values just read) → expect{"ok":true}.uci show wireless.wlan0openagain → confirm ssid/hidden/macaddr unchanged. Expected: get_ap returns the new fields; set_ap returns ok; UCI unchanged (idempotent write).
- Step 4: Report for user UI walk
Tell the user the Open AP tab is now the Mk7 "PineAP Open Access Point" card: Open SSID / BSSID / Channel (1-11) / Current Country / Hidden / "Respond to all probe requests (impersonate all networks)" switches, filter notice boxes with Add Allowed / Change Mode / Change Filters actions, and a Save button. Note the two documented limitations: the karma toggle is session-tracked (the daemon cannot report it), and the "SSIDs from the Spoofed SSID Pool will be advertised" clause is omitted (no readable broadcast state). Ask them to refresh http://172.16.52.1:8080/#/pineap/open, edit the Open SSID and Save, and confirm the toast + that uci show wireless.wlan0open reflects the change.