# PineAP Overview — Mark VII Stats + Mode Toggle 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:** Bring the Pager WebUI PineAP overview (`#/pineap`) to Mark VII parity — a clickable 3-card stats row (Total SSIDs in Pool / Clients Connected / Handshakes Captured) and a Passive/Active/Advanced quick mode toggle — using only functionality the Pager supports. **Architecture:** Frontend-only change to `views.pineap` in `www/js/views.js` plus a small `.seg` segmented-control style in `www/css/app.css`. All data comes from existing, on-device-verified webui endpoints; the backend and test modules are untouched. Karma (mimic) state is unreadable from the daemon, so it is tracked in a view-local variable. **Tech Stack:** Vanilla JS (hyperscript `h()` helper), existing `PagerAPI` client, existing `.cards`/`.card`/`.badge`/`.toggle` CSS, plain CSS additions. ## Global Constraints - No backend changes. No changes to `server.py` or `tests/`. - Karma state is NOT readable from the daemon (only `mimic/enable|disable`); track it client-side as `karmaOn` (null = unknown, defaults to `null` on page load). - Mode presets only apply Pager-supported features: `POST /api/pineap/enable` and `POST /api/pineap/mimic`. Do NOT touch `ssidpool/*` (broadcast cannot start natively — it stays a manual Quick Settings toggle). - 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: `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password ""`, then `/etc/init.d/pagerwebui restart` over sshpass SSH. - Commit messages follow repo style (`feat:`, `fix:`, `docs:`, `test:`). - Mode badge highlight refinement vs. the approved spec (intent-preserving): when `karmaOn` is tracked, prefer it over the unreadable daemon state so the toggle does not visually jump after a user applies a preset. --- ### Task 1: Rebuild the PineAP overview (stats row + mode toggle + karma tracking) **Files:** - Modify: `payload/user/general/pager-webui/www/css/app.css` (append `.seg` styles) - Modify: `payload/user/general/pager-webui/www/js/views.js` (replace the body of `views.pineap`, lines ~187-255) **Interfaces:** - Consumes: `pineapShell(root, hash)` (appends h1 + tab bar, returns content box), `pineapSetCard(card, label, value)` (sets `.card-label`/`.card-value` text), `h()`, `btn(label, onclick, variant)`, `PagerAPI.get/post`, `App.go(hash)`, `App.toast`. - Produces: `views.pineap` with (1) a `.seg` segmented control with three buttons, (2) a 3-card stats row keyed `stats.ssids`/`stats.clients`/`stats.handshakes`, (3) a view-local `karmaOn` variable consumed by `load()` for the Karma card and the computed mode. - [ ] **Step 1: Append the segmented-control CSS to `app.css`** Append to the end of `payload/user/general/pager-webui/www/css/app.css`: ```css .seg { display: inline-flex; margin-top: 8px; border: 1px solid var(--ink, #999); border-radius: 4px; overflow: hidden; } .seg-btn { background: transparent; border: none; padding: 5px 14px; font-size: 12px; cursor: pointer; color: var(--muted, #666); } .seg-btn + .seg-btn { border-left: 1px solid var(--ink, #999); } .seg-btn.active { background: var(--primary, #1976d2); color: #fff; } .seg-btn.busy { opacity: .5; pointer-events: none; } ``` - [ ] **Step 2: Replace the body of `views.pineap` in `views.js`** Replace everything from `views.pineap = (root) => {` through the closing `};` of that function (current lines ~187-255) with: ```js views.pineap = (root) => { const box = pineapShell(root, '#/pineap'); const mode = h('span', { class: 'badge', text: '-' }); const intro = h('p', { class: 'muted' }); const head = h('div', { class: 'section' }, h('h2', {}, 'PineAP'), h('div', { class: 'row' }, h('div', {}, mode)), intro); box.appendChild(head); const segBtns = {}; const modeBar = h('div', { class: 'seg' }); ['passive', 'active', 'advanced'].forEach((m) => { const b = h('button', { class: 'seg-btn', text: m[0].toUpperCase() + m.slice(1) }); b.addEventListener('click', () => applyMode(m)); modeBar.appendChild(b); segBtns[m] = b; }); const modeInfo = h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' }); head.appendChild(modeBar); head.appendChild(modeInfo); const quick = { collect: h('input', { type: 'checkbox', id: 'po-collect' }), advertise: h('input', { type: 'checkbox', id: 'po-advertise' }) }; const quickBox = h('div', { class: 'section' }, h('h2', {}, 'Quick Settings')); quickBox.appendChild(h('label', { class: 'toggle' }, quick.collect, ' Capture SSIDs to Pool')); quickBox.appendChild(h('label', { class: 'toggle' }, quick.advertise, ' Advertise AP Impersonation Pool')); quickBox.appendChild(h('div', { class: 'muted', style: 'margin-top:8px' }, 'Client connect/disconnect notifications are handled by the Pager alert payload system.')); box.appendChild(quickBox); const stats = {}; const statWrap = h('div', { class: 'cards' }); const statDefs = [ ['ssids', 'Total SSIDs in Pool', '#/pineap/impersonation'], ['clients', 'Clients Connected', '#/pineap/clients'], ['handshakes', 'Handshakes Captured', '#/pineap/evilwpa'] ]; statDefs.forEach(([k, label, hash]) => { const card = h('div', { class: 'card' }, h('div', { class: 'card-label' }), h('div', { class: 'card-value' }), h('div', { class: 'row' }, btn('View', () => App.go(hash), 'ghost'))); statWrap.appendChild(card); stats[k] = { label: card.querySelector('.card-label'), value: card.querySelector('.card-value') }; stats[k].label.textContent = label; }); box.appendChild(statWrap); const cards = { karma: {}, open: {}, wpa: {}, ent: {} }; const cardWrap = h('div', { class: 'cards' }); Object.keys(cards).forEach((k) => { const card = h('div', { class: 'card' }, h('div', { class: 'card-label' }), h('div', { class: 'card-value' }), h('div', { class: 'row' }, btn('Configure', () => App.go({ karma: '#/pineap/open', open: '#/pineap/open', wpa: '#/pineap/evilwpa', ent: '#/pineap/enterprise' }[k]), 'ghost'))); cardWrap.appendChild(card); cards[k].label = card.querySelector('.card-label'); cards[k].value = card.querySelector('.card-value'); }); box.appendChild(cardWrap); let karmaOn = null; function bind(cb, on) { cb.addEventListener('change', () => on(cb.checked).then(load).catch(() => { cb.checked = !cb.checked; App.toast('Failed', 'error'); })); } bind(quick.collect, (v) => PagerAPI.post('/api/pineap/ssidpool/collect', { enable: v })); bind(quick.advertise, (v) => PagerAPI.post('/api/pineap/ssidpool/advertise', { enable: v })); function setMode(m) { Object.keys(segBtns).forEach((k) => segBtns[k].classList.toggle('active', k === m)); modeInfo.textContent = { passive: 'PineAP is on; network impersonation (Karma) is off.', active: 'PineAP and Karma are on; the open network is impersonated.', advanced: 'All PineAP features are enabled and customizable.' }[m] || ''; } function applyMode(m) { const btn = segBtns[m]; if (!btn || btn.classList.contains('active')) return; const on = m !== 'passive'; btn.classList.add('busy'); Promise.all([ PagerAPI.post('/api/pineap/enable', { enable: true }), PagerAPI.post('/api/pineap/mimic', { enable: on }) ]).then(() => { karmaOn = on; setMode(m); App.toast('Mode: ' + m[0].toUpperCase() + m.slice(1)); load(); }).catch(() => App.toast('Failed', 'error')).finally(() => btn.classList.remove('busy')); } 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: {} })), PagerAPI.get('/api/pineap/ssids').catch(() => ({ data: {} })), PagerAPI.get('/api/pineap/clients').catch(() => ({ data: {} })), PagerAPI.get('/api/pineap/handshakes').catch(() => ({ data: {} })) ]).then(([cfg, host, ap, ss, cl, hs]) => { const c = cfg.data || {}, hh = host.data || {}, a = ap.data || {}; const disabled = !!hh.pineap_disabled; const wpa = a.wpa || {}, ent = a.enterprise || {}; const advanced = !disabled && (wpa.enabled || ent.enabled); const computed = disabled ? 'passive' : (karmaOn === false ? 'passive' : (advanced ? 'advanced' : 'active')); mode.textContent = computed[0].toUpperCase() + computed.slice(1); mode.className = 'badge ' + (disabled ? 'off' : 'on'); intro.textContent = disabled ? 'PineAP is disabled. Enable it from the Open AP tab to begin impersonating networks.' : 'The WiFi Pineapple will respond to probe requests and impersonate the Open, Evil WPA, and Evil Enterprise access points.'; setMode(computed); quick.collect.checked = !!c.autossidpool; const pool = a.pool || {}; quick.advertise.checked = pool.disabled === false; stats.ssids.value.textContent = (ss.data && Array.isArray(ss.data.ssids)) ? ss.data.ssids.length : '—'; stats.clients.value.textContent = (cl.data && typeof cl.data.count === 'number') ? cl.data.count : '—'; stats.handshakes.value.textContent = (hs.data && Array.isArray(hs.data.files)) ? hs.data.files.length : '—'; pineapSetCard(cards.karma, 'Karma', karmaOn == null ? null : (karmaOn ? 'On' : 'Off')); const open = a.open || {}; pineapSetCard(cards.open, 'Open Network', open.enabled == null ? '—' : (open.enabled ? 'On' : 'Off')); pineapSetCard(cards.wpa, 'Evil WPA', wpa.enabled ? 'On' : 'Off'); pineapSetCard(cards.ent, 'Evil Enterprise', ent.enabled ? 'On' : 'Off'); }); } load(); const iv = setInterval(load, 5000); return { destroy: () => clearInterval(iv) }; }; ``` - [ ] **Step 3: Run the JS delimiter balance check** Run: `python "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/js/views.js payload/user/general/pager-webui/www/css/app.css git commit -m "feat: PineAP overview stats cards + passive/active/advanced mode toggle" ``` --- ### 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: ```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: Verify the three stat endpoints return live data on device** Run (base64 the script then `echo ... | base64 -d | sh` over sshpass): ```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 ssids:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/ssids echo clients:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/clients echo handshakes:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/handshakes ``` Expected: `ssids` returns `{"ssids":[...]}`, `clients` returns `{"clients":[],"count":0}` (or a number), `handshakes` returns `{"files":[...],"handshakes":[...]}`. - [ ] **Step 3: Confirm the deployed files contain the new code** ```sh grep -c "seg-btn\|Total SSIDs in Pool\|Handshakes Captured" /root/payloads/user/general/pager-webui/www/js/views.js grep -c "\.seg" /root/payloads/user/general/pager-webui/www/css/app.css ``` Expected: counts greater than zero. - [ ] **Step 4: Report for user UI walk** Tell the user the overview now has: the three stat cards (numbers populate in the 5s poll; each `View` button navigates to its tab), the Passive/Active/Advanced segmented toggle (applies PineAP master + Karma; karma tracked client-side and shown on the Karma card; badge/description update; failures toast + revert), and unchanged Quick Settings. Ask them to refresh `http://172.16.52.1:8080/#/pineap` and confirm.