# 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_ap` persists `ssid`/`hidden`/`bssid`→`macaddr`/`channel`/`enabled` to `wireless.wlan0open` but its iface-level `channel` write is **inert** for the actual radio — the radio channel/country must be written to `wireless.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 `enabled` is preserved as-is (the Mk7 card has no Enabled control); the frontend always sends `enabled` = value loaded from `get_ap`. - Filter mutations use the existing `action` API: `POST /api/pineap/filters/ssid` `{action:'add'|'delete', value}` and `POST /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 ""`, then `/etc/init.d/pagerwebui restart` over sshpass. --- ### Task 1: Backend — expose and save Open AP network settings **Files:** - Modify: `payload/user/general/pager-webui/server.py` (`h_pineap_wifi_get_ap` at ~1479, `h_pineap_wifi_set_ap` at ~1505, add helper `_apply_open_radio` just before `h_pineap_wifi_set_ap`) - Test: `tests/test_pineap_proxy.py` **Interfaces:** - Consumes: `_uci_wifi_iface(name)` (runs `uci show wireless.`, returns dict), `_uci_section(section)`, `device_run(args)`, `daemon_sock_call(method, path, body)`. - Produces: `h_pineap_wifi_get_ap` `open` payload now `{enabled, ssid, bssid, target, hidden, channel, country}`; `h_pineap_wifi_set_ap` accepts `open: {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: ```python 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): ```python 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`: ```python 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): ```python 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: ```powershell $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** ```bash 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` (add `OPEN_CHANNELS`/`OPEN_COUNTRIES` constants + `let OPEN_KARMA` before `views.pineap_open`; replace the whole `views.pineap_open` function, currently lines 327-409, just before `const EVIL_ENC`) - Modify: `payload/user/general/pager-webui/www/css/app.css` (append infobox styles at end) **Interfaces:** - Consumes: Task 1's `open` fields (`ssid`, `bssid`, `hidden`, `channel`, `country`, `enabled`) from `POST /api/pineap/wifi/get_ap`; `set_ap` `open` body keys; existing `/api/pineap/mimic`, `/api/pineap/get_config`, `GET /api/pineap/filters/{ssid,client}` → `{mode, entries}`; existing `action` filter 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) => {`: ```js 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_open` function** Replace everything from `views.pineap_open = (root) => {` through its closing `};` (current lines 327-409, i.e. just before `const EVIL_ENC`) with: ```js 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`: ```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: ```powershell $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** ```bash 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 powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "" ``` Then restart and confirm the port is up (over sshpass SSH): ```bash sshpass -p "" 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** ```bash sshpass -p "" 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: ```bash sshpass -p "" 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: 1. Logs in to the WebUI (`POST /api/login` with the root credentials, saves cookie). 2. `POST /api/pineap/wifi/get_ap` → confirm the response contains `"open"` with `ssid`, `bssid`, `hidden`, `channel`, `country` keys. 3. `POST /api/pineap/wifi/set_ap` with `{open:{ssid:, bssid:, hidden:, enabled:true, channel:, country:}}` (the values just read) → expect `{"ok":true}`. 4. `uci show wireless.wlan0open` again → 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.