11 KiB
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.pyortests/. - 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_configfields (loghandshake,logpartialhandshake,logpcap,logwigle,logrecon,autossidpool) go in ONEPOST /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 restartover 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 wholeviews.pineap_openfunction, 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/.trackCSS and.pineap-*layout classes from prior work. -
Produces:
views.pineap_openrendering the Mk7 settings card;staged/dirtymaps;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:
/* ---- 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_openfunction inviews.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:
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:
$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
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 -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-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:
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.