Files
Mark-VIII/docs/superpowers/plans/2026-08-11-openap-mk7-card.md
T
2026-08-11 20:24:24 -07:00

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_ap persists ssid/hidden/bssidmacaddr/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:

    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 (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) => {:

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:

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:

  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.