release: Mark VIII 1.0
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user