# Open AP Tab — Mark VII "PineAP Settings" 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:** Restyle the PineAP Open AP tab (`#/pineap/open`) into the Mark VII "PineAP" settings card — titled card with subtitle, iOS-style slide switches grouped into sections, and a batched Mk7-style Save button — replacing the current flat checkbox list. **Architecture:** Frontend-only. Rewrite `views.pineap_open` in `www/js/views.js` to build a `.pineap-card-settings` card: a `.pineap-card-title-flex` title row ("PineAP" + subtitle + Save button), three grouped sections of `.switch` slide toggles, and a muted footer. Toggles stage values locally (dirty flags); Save applies only the dirty fields batched per backend route (`set_config` for logging/capture, `enable`, `mimic`, `ssidpool/advertise`). Two new CSS classes in `app.css`. No backend changes. **Tech Stack:** Vanilla JS (`h()`, `btn()`, `PagerAPI`, `App.toast`, `pineapShell`), the existing `.switch` slider CSS, existing `PagerAPI.post` routes. ## Global Constraints - No backend changes. No changes to `server.py` or `tests/`. - Toggles must NOT call the API on change — they stage locally and mark dirty. - Save applies ONLY dirty fields; untouched fields keep their daemon state (preserves the unreadable live Karma state). - Batching per route: `set_config` fields (`loghandshake`, `logpartialhandshake`, `logpcap`, `logwigle`, `logrecon`, `autossidpool`) go in ONE `POST /api/pineap/set_config`; `pineap_disabled` → `POST /api/pineap/enable {enable}`; `karma` → `POST /api/pineap/mimic {enable}`; `advertise` → `POST /api/pineap/ssidpool/advertise {enable}`. - `load()` runs once on entry and after a successful Save; it must not clobber staged-but-unsaved values. - JS verification uses the Python delimiter-balance checker at `C:\Users\root\AppData\Local\Temp\opencode\js_balance.py` (no node available). - Python for the unittest loop: `$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. - Commit messages follow repo style (`feat:`, `fix:`, `docs:`). --- ### Task 1: Open AP tab Mk7 settings card (CSS + rewrite) **Files:** - Modify: `payload/user/general/pager-webui/www/css/app.css` (append three classes) - Modify: `payload/user/general/pager-webui/www/js/views.js` (replace the whole `views.pineap_open` function, currently lines ~327-375) **Interfaces:** - Consumes: `h(tag, attrs, ...children)`, `btn(label, onclk, cls)`, `PagerAPI.get/post`, `App.toast`, `pineapShell(root, hash)`, `tabBar`; existing `.switch`/`.track` CSS and `.pineap-*` layout classes from prior work. - Produces: `views.pineap_open` rendering the Mk7 settings card; `staged`/`dirty` maps; `save()` batched apply; `load()` populating switches without clobbering staged values. - [ ] **Step 1: Append the three CSS classes to `app.css`** Append to the end of `payload/user/general/pager-webui/www/css/app.css`: ```css /* ---- Open AP: Mark VII PineAP settings card ---- */ .pineap-card-subtitle { color: var(--muted); font-size: 13px; margin: -8px 0 10px; } .pineap-settings-section { font-size: 13px; font-weight: 500; color: var(--muted); margin: 14px 0 4px; } .pineap-card-settings .switch { margin: 6px 0; } ``` - [ ] **Step 2: Replace the whole `views.pineap_open` function in `views.js`** Replace everything from `views.pineap_open = (root) => {` through the closing `};` of that function (current lines ~327-375, i.e. just before `const EVIL_ENC`) with: ```js views.pineap_open = (root) => { const box = pineapShell(root, '#/pineap/open'); const wrap = h('div', { class: 'pineap-title-card pineap-card-settings' }); wrap.appendChild(h('div', { class: 'pineap-card-title-flex' }, h('span', { text: 'PineAP' }), h('span', { class: 'toolbar-spacer' }), btn('Save', save, ''))); wrap.appendChild(h('div', { class: 'pineap-card-subtitle', text: 'Quickly set the general behavior of PineAP' })); const staged = {}; const dirty = {}; const toggles = {}; const groups = [ ['Karma', [ ['pineap_disabled', 'Enable PineAP'], ['karma', 'Karma'] ]], ['SSID Pool', [ ['autossidpool', 'Capture SSIDs to Pool'], ['advertise', 'Advertise AP Impersonation Pool'] ]], ['Logging', [ ['loghandshake', 'Log Handshakes'], ['logpartialhandshake', 'Log Partial Handshakes'], ['logpcap', 'Log PCAP'], ['logwigle', 'Log WiGLE'], ['logrecon', 'Log Recon'] ]] ]; groups.forEach(([section, items]) => { wrap.appendChild(h('div', { class: 'pineap-settings-section', text: section })); items.forEach(([k, label]) => { const cb = h('input', { type: 'checkbox', id: 'oap-' + k }); toggles[k] = cb; cb.addEventListener('change', () => { staged[k] = cb.checked; dirty[k] = true; }); wrap.appendChild(h('label', { class: 'switch' }, cb, h('span', { class: 'track' }), ' ' + label)); }); }); const info = h('div', { class: 'muted', style: 'margin-top:10px' }); wrap.appendChild(info); box.appendChild(wrap); function save() { const keys = Object.keys(dirty); if (!keys.length) { App.toast('No changes'); return; } const reqs = []; const setCfg = {}; keys.forEach((k) => { if (k === 'pineap_disabled') reqs.push(PagerAPI.post('/api/pineap/enable', { enable: staged[k] })); else if (k === 'karma') reqs.push(PagerAPI.post('/api/pineap/mimic', { enable: staged[k] })); else if (k === 'advertise') reqs.push(PagerAPI.post('/api/pineap/ssidpool/advertise', { enable: staged[k] })); else setCfg[k] = staged[k]; }); if (Object.keys(setCfg).length) reqs.push(PagerAPI.post('/api/pineap/set_config', setCfg)); Promise.allSettled(reqs).then((results) => { const ok = results.every((r) => r.status === 'fulfilled'); App.toast(ok ? 'Settings saved' : 'Some settings failed', ok ? '' : 'error'); Object.keys(dirty).forEach((k) => delete dirty[k]); load(); }); } function load() { Promise.all([ PagerAPI.get('/api/pineap/get_config').catch(() => ({ data: {} })), PagerAPI.get('/api/pineap/hostapd').catch(() => ({ data: {} })), PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })) ]).then(([cfg, host, ap]) => { const c = cfg.data || {}, hh = host.data || {}, a = ap.data || {}; if (!dirty.pineap_disabled) toggles.pineap_disabled.checked = !hh.pineap_disabled; if (!dirty.karma) toggles.karma.checked = !!c.mimic; ['loghandshake', 'logpartialhandshake', 'logpcap', 'logwigle', 'logrecon', 'autossidpool'] .forEach((k) => { if (!dirty[k]) toggles[k].checked = !!c[k]; }); const pool = a.pool || {}; if (!dirty.advertise) toggles.advertise.checked = pool.disabled === false; const o = a.open || {}; info.textContent = 'PineAP MAC: ' + (o.bssid || '—') + ' Target MAC: ' + (o.target || '—'); }); } load(); return { destroy: () => {} }; }; ``` - [ ] **Step 3: 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 4: Run the 13-module unittest loop (backend must stay green)** Run from `C:\Users\root\Documents\Pineapple\pager-webui`: ```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 5: Commit** ```bash git add payload/user/general/pager-webui/www/css/app.css payload/user/general/pager-webui/www/js/views.js git commit -m "feat: Mark VII PineAP settings card for Open AP tab with batched save" ``` --- ### Task 2: Deploy and verify on device **Files:** none (verification only; no commit). **Interfaces:** - Consumes: Task 1 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-settings-section' /root/payloads/user/general/pager-webui/www/css/app.css; grep -c 'Quickly set the general behavior of PineAP' /root/payloads/user/general/pager-webui/www/js/views.js" ``` Expected: both counts greater than zero. - [ ] **Step 3: On-device save-path check (read-only + one harmless write/restore)** Over sshpass SSH, base64 a script and run it via `echo ... | base64 -d | sh`: ```sh curl -s -c /tmp/pwj -X POST http://127.0.0.1:8080/api/login -H "Content-Type: application/json" -d '{"username":"root","password":""}' > /dev/null echo before:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*' curl -s -b /tmp/pwj -X POST http://127.0.0.1:8080/api/pineap/set_config -d '{"logrecon":true}' > /dev/null echo after-set-true:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*' curl -s -b /tmp/pwj -X POST http://127.0.0.1:8080/api/pineap/set_config -d '{"logrecon":false}' > /dev/null echo restored:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*' ``` Expected: `before` shows the current `logrecon` value, `after-set-true` shows `true`, `restored` shows the original value (verifies the batched `set_config` merge path the Save button uses). - [ ] **Step 4: Report for user UI walk** Tell the user the Open AP tab is now the Mk7 "PineAP" settings card: title + subtitle "Quickly set the general behavior of PineAP", grouped iOS-style slide switches (Karma / SSID Pool / Logging), and a Save button that batches only the changed toggles (unchanged Karma is left untouched). Ask them to refresh `http://172.16.52.1:8080/#/pineap/open`, flip a logging toggle, Save, and confirm the toast + that `get_config` reflects the change.