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

200 lines
11 KiB
Markdown

# 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 "<PAGER_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 "<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-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":"<PAGER_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.