release: Mark VIII 1.0

This commit is contained in:
c4ch3c4d3
2026-08-11 20:24:24 -07:00
commit 3e1805dab8
86 changed files with 20678 additions and 0 deletions
@@ -0,0 +1,508 @@
# 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 "<PAGER_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.<name>`, 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 "<PAGER_PASSWORD>"
```
Then restart and confirm the port is up (over sshpass SSH):
```bash
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**
```bash
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:
```bash
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:
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:<current>, bssid:<current>, hidden:<current>, enabled:true, channel:<current>, country:<current>}}` (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.