release: Mark VIII 1.0

This commit is contained in:
c4ch3c4d3
2026-08-11 20:24:24 -07:00
commit 3e1805dab8
86 changed files with 20678 additions and 0 deletions
@@ -0,0 +1,252 @@
# 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 "<PAGER_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 "<PAGER_PASSWORD>"
```
Then restart and confirm the port is up:
```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: 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":"<PAGER_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.