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,274 @@
# Handshakes Mark VII Parity — Design Spec
- **Date:** 2026-08-11
- **Status:** Draft (pending user review)
- **Owner:** WiFi Pineapple Pager expansion project
- **Applies to:** `payload/user/general/pager-webui/server.py`,
`payload/user/general/pager-webui/www/`, `tests/`
- **Supercedes §3.3 of:** `docs/superpowers/specs/2026-08-11-recon-markvii-design.md`
(that section restyled the file-listing table; this spec replaces it with the
real Mark VII handshake table)
## 1. Goal
Rework the Pager WebUI **Recon → Handshakes** tab (`#/recon/handshakes`) so it
looks and behaves like the stock Mark VII handshakes page: the same table
(**BSSID, Client, Source, Type, Captured, Message 14, Beacon Frame**, action
icons) with the same **Download** and **Delete** behavior, plus the **settings
dialog** (handshake location + "Delete All Handshakes").
Built **entirely from Pager-local data** (loot files in `/root/loot/handshakes`
+ the recon.db `handshake`/`wifi_device` tables). The Pager's Go daemon does
**not** expose `/api/pineap/handshakes` (verified 404 on-device +
`docs/specs/2026-08-11-pineapple-ui-clone-design.md`), so the Mark VII shape is
synthesized by `server.py`, matching how every other PineAP/Recon endpoint is
already implemented.
## 2. Mark VII reference (captured 2026-08-11)
Source of truth: live `GET /api/pineap/handshakes` on `172.16.42.1:1471`
(hak5pineapple) and the handshakes component in the cached Angular bundle
`%TEMP%\opencode\oldui-main.js`.
### 2.1 API shape
`GET /api/pineap/handshakes``{ "handshakes": [ { ... } ] }`. Each record:
```json
{ "file_exists": true,
"mac": "86:18:98:BE:CF:A5", // AP/BSSID (colon MAC, case as stored)
"client": "D6:9B:56:ED:28:82", // client MAC, or an Evil-Twin label
"source": "Recon", // "Recon" | "Evil WPA/2 Twin"
"type": "full", // "full" | "eviltwin"
"timestamp": "2023-12-05T16:03:18Z",
"in_db": true, // handshake also present in recon DB
"part_mask": 15, // bit 1=m1, 2=m2, 4=m3, 8=m4
"beacon": true,
"extension": "pcap", // pcap | 22000
"location": "/root/handshakes/86-18-98-BE-CF-A5_D6-9B-56-ED-28-82_full.pcap" }
```
### 2.2 Page
- Card title row: **"Captured WPA Handshakes"** (left) + `settings` gear icon
button (right). Gear opens a dialog: handshake **location** (from
`GET /api/pineap/handshakes/location`) and a **Delete All Handshakes** button
(`DELETE /api/pineap/handshakes/`). On success/error a small green check /
red X flashes beside the title for ~3s / ~5s.
- Table columns: `BSSID, Client, Source, Type, Captured, Message 1, Message 2,
Message 3, Message 4, Beacon Frame, [action]`.
- **BSSID / Client**: colon MACs as returned.
- **Source**: `"Recon"` or `"Evil WPA/2 Twin"`.
- **Type**: `{{type|titlecase}} {{format}}` where `format` derives from
`extension` (`pcap`→`PCAP`, `22000`→`Hashcat`, else `Unknown`) — e.g.
`Full PCAP`, `Hashcat`.
- **Captured**: localized date-time of `timestamp`.
- **Message 14 / Beacon Frame**: when `in_db` is true → green **check** if
the bit (`m1..m4`, `beacon`) is set, red **X** if not. When `in_db` is
false → grey **?** icon with tooltip "This information isn't available.
This is common when a handshake file has been found, but the associated
Recon scan has been lost or deleted."
- **[action]**: `file_download` icon button + `delete` icon button (warn
color).
- Empty state: `"No Handshakes Available"`.
- Component logic (from bundle): `getHandshakes()` maps each record's
`extension`→`format` and computes `m1=part_mask&1 … m4=part_mask&8`;
`deleteHandshake(hs)` calls `DELETE /api/pineap/handshakes/delete` with the
record as body, then reloads and flashes success/error; `downloadHandshake`
saves the file as `<mac>_<client>_<type>.<extension>`.
## 3. Pager data model
- **Loot files:** `/root/loot/handshakes/` (Pager native path; matches
pager-webui `LOOT_HS_DIR`). Native naming inferred from on-device payloads
(`handshake_sanitiser`, `deduplicate`, `handshake_2_usb`) and `pineapd`
strings:
- full: `<unix_ts>_<AP_MAC>_<CLIENT_MAC>_handshake.pcap|.22000`
- partial: `<unix_ts>_<AP_MAC>_<CLIENT_MAC>_handshake_partial.pcap|.22000`
- incomplete: `…_handshake_incomplete.pcap|.22000`
- MACs colon-separated (may be uppercase/lowercase); Windows-normalised
copies use dashes. The parser must accept both.
- **recon.db `handshake` table:** `hash, scan, stahash, aphash, time,
beacon BLOB, hs1 BLOB, hs2 BLOB, hs3 BLOB, hs4 BLOB`. The pager-webui
resolves `aphash/stahash` → MACs via `wifi_device` (pattern already used by
`recon_scan_data`). This supplies `part_mask`, `beacon`, `in_db`, and a
fallback timestamp.
- **Sources:** the Pager has no Evil-WPA/2-Twin mode, so `source` is always
`"Recon"` and `type` is `full` / `partial` / `incomplete`.
## 4. Backend (`server.py`)
### 4.1 `handshakes_data()` → `{ files, handshakes }`
Keep the existing `files` array unchanged (the dashboard reads it). Add a
`handshakes` array in the Mark VII shape. Build order:
1. `os.listdir(LOOT_HS_DIR)` + `os.stat` per file (existing).
2. **If no files → return `{files, handshakes: []}` immediately — zero DB
reads** (the compute mitigation).
3. Parse each filename (see §4.2) → `{ap, client, kind, ext, ts}`.
4. **One batched correlation read** (only when files exist):
`SELECT h.time, (h.hs1 IS NOT NULL AND length(h.hs1)>0) AS m1,
(h.hs2 IS NOT NULL AND length(h.hs2)>0) AS m2,
(h.hs3 IS NOT NULL AND length(h.hs3)>0) AS m3,
(h.hs4 IS NOT NULL AND length(h.hs4)>0) AS m4,
(h.beacon IS NOT NULL AND length(h.beacon)>0) AS beacon,
w1.mac AS ap, w2.mac AS sta
FROM handshake h
JOIN wifi_device w1 ON w1.hash = h.aphash
JOIN wifi_device w2 ON w2.hash = h.stahash
WHERE h.time >= <oldest parsed file ts>
ORDER BY h.time`
(a single `_db_rows` call; `handshake.time` is epoch seconds). Build a dict
keyed by normalized `(ap, sta)` keeping the latest row.
5. Compose one record per file (§4.3). Pure Python dict lookups — O(1) per
file. The join resolves hashes to MACs directly, so exactly **one** sqlite
read is needed.
Total added cost per request: at most 1 `_db_rows` call, only when the loot
dir is non-empty; filename parsing is microseconds.
### 4.2 Filename parser
Tolerant regex, accepts colon or dash MACs, optional `_handshake` token,
optional quality suffix, any extension:
```
^(\d+)_([0-9a-fA-F-:]+)_([0-9a-fA-F-:]+)_handshake(?:_(full|partial|incomplete))?\.([A-Za-z0-9]+)$
```
Fallback: any unparseable file still appears as a row with
`mac:'--', client:'--', type:'full', source:'Recon'`, `in_db:false` — it
keeps Download/Delete and shows "?" glyphs (matches Mark VII's
file-without-DB-record presentation).
### 4.3 Record composition (per file)
| field | source |
|---|---|
| `mac` | parsed AP MAC (colon, case as stored) |
| `client` | parsed client MAC (or `--`) |
| `source` | `"Recon"` |
| `type` | `full` / `partial` / `incomplete` (from filename; default `full`) |
| `timestamp` | epoch seconds (parsed `ts` prefix; fallback `st_mtime`; DB `time` wins if present) |
| `in_db` | bool — matching `(ap,sta)` row in the correlation dict |
| `part_mask` | `m1|m2|m3|m4` from DB row (0 when not in DB) |
| `beacon` | bool from DB row (false when not in DB) |
| `extension` | file extension |
| `name` | bare filename (pager-webui convenience field; used for download/delete) |
| `location` | `LOOT_HS_DIR + '/' + name` |
| `file_exists` | true |
### 4.4 New routes
- `GET /api/pineap/handshakes/location` → `{location: LOOT_HS_DIR}`
(mirrors Mark VII; read-only).
- `DELETE /api/pineap/handshakes/all` → remove every file in `LOOT_HS_DIR`,
return updated `handshakes_data()`. Mirrors Mark VII "delete all".
- Existing `GET /api/pineap/handshakes/{name}` (download) and
`DELETE /api/pineap/handshakes` `{name}` (per-row delete) unchanged.
Optionally set the download's `Content-Disposition` filename to the Mark VII
`<mac>_<client>_<type>.<ext>` form when the name parses.
## 5. Frontend (`www/`)
### 5.1 `views.recon_handshakes` (rewrite of `views.js:911`)
Keep: page title "Recon", `RECON_TABS` tab bar, card title row
"Captured WPA Handshakes" + gear, and the Pager-specific
`Download all (zip)` / `Archive` / `Refresh` row (user decision: keep, don't
match Mark VII pixel-for-pixel there). Replace the file table with the Mark
VII table:
- Custom table build (the generic `table()` helper only renders text; this
needs glyphs + icon buttons), header row exactly:
`BSSID, Client, Source, Type, Captured, Message 1, Message 2, Message 3,
Message 4, Beacon Frame, ""`.
- Cells:
- **BSSID / Client**: text.
- **Source**: `"Recon"`.
- **Type**: `${title(type)} ${format}` via existing `hsType()` (PCAP /
Hashcat / Unknown).
- **Captured**: `fmtTime(timestamp)`.
- **Message 14**: `!in_db` → `?` icon with `title` tooltip (Mark VII
wording); else check icon when `part_mask & bit`, X icon when not.
- **Beacon Frame**: `!in_db` → `?` icon; else check when `beacon`, X when
not.
- **Action**: `file_download` icon btn → `window.location =
apiBase + '/api/pineap/handshakes/' + encodeURIComponent(name)`; `delete`
icon btn (warn) → `PagerAPI.del('/api/pineap/handshakes', {name})` then
reload — no confirm (matches Mark VII).
- Delete / delete-all success → green check flash; failure → red X + error
text flash (replaces the current silent `.catch`).
- Empty state text: `"No Handshakes Available"`.
- Settings gear opens the dialog (§5.2). The previous "toast" behavior is
removed.
### 5.2 Settings dialog
Simple modal overlay (Mark VII uses a 600px mat-dialog; replicate visually):
- Title "Handshake Settings" (approximation of the Mark VII dialog).
- **Handshake Location**: read-only value from
`GET /api/pineap/handshakes/location`.
- **Delete All Handshakes** button → `PagerAPI.del('/api/pineap/handshakes/all')`
→ success flash + reload table + close.
- Close button / click-outside to dismiss.
### 5.3 `js/icons.js`
Add inline Material SVG paths: `check`, `close`, `question_mark` (the Mark VII
uses `assets/icons/question-mark.svg`; an inline `help` glyph is equivalent).
`settings`, `file_download`, `delete`, `refresh` already exist.
### 5.4 `css/app.css`
Add (light + `html.dark`): `.hs-cell-center` (M14/Beacon centering),
`.hs-ok` (green check), `.hs-bad` (red X), `.hs-na` (grey `?`),
`.hs-flash` / `.hs-flash-ok` / `.hs-flash-error` (title-row indicator),
`.modal-overlay`, `.modal`, `.modal-title`, `.modal-actions`, `.modal-body`.
Reuse existing `--surface`/`--border`/`--primary` tokens.
## 6. Compute-cost mitigation (user requirement)
- No handshake files → **no** DB reads, no subprocess spawns.
- Files exist → **exactly 1** `_db_rows` call, scoped by `WHERE h.time >=
<oldest file ts>` so a sparse capture never scans the full recon.db history.
- Correlation is dict-based (O(1) per file); filename parsing is pure string
work.
- No polling added to the view — loads on mount + manual Refresh (unchanged).
- Context: the recon scanning view already issues ~6 `_db_rows` calls every
10s; two extra on page load is negligible.
## 7. Verification notes (implementation-time, not blockers)
- **File naming / quality suffix**: inferred from on-device payloads, not yet
observed live (no handshakes captured). The tolerant parser + `--`/`?`
fallbacks keep the UI correct for any naming variant; a real capture should
be sanity-checked if one can be produced.
- **hs1hs4/beacon population**: recon.db schema includes them; if the daemon
leaves them NULL the table still renders (all X for in-DB rows).
- `handshake.time` epoch-seconds assumption matches the existing recon reads.
## 8. Testing
- Python (`tests/test_recon.py`, same mock style as existing):
- `handshakes_data()` returns `{files, handshakes}`; empty dir → no
DB-helper calls (assert mocked `_db_rows` not invoked).
- Filename parser: full/partial/incomplete × pcap/22000, colon and dash
MACs, unparseable fallback.
- Correlation: canned handshake/wifi_device rows → correct `part_mask`,
`beacon`, `in_db`, `timestamp`.
- `GET …/handshakes/location` returns the loot dir; `DELETE
…/handshakes/all` removes all files and returns updated data.
- Front-end: manual on-device pass — table renders, glyphs in both
`in_db` states, download filename, delete flash, settings dialog,
delete-all, empty state, dark theme, dashboard unchanged (still `files`).
- Deploy via `scripts/deploy.ps1`; `curl` the changed JS/CSS for 200.
## 9. Out of scope
- Evil WPA/2 Twin handshakes (`source:"Evil WPA/2 Twin"`) — no Pager mode.
- Editable handshake location (`PUT /api/pineap/handshakes/location`).
- PMKID / `hostap_handshake` rows in this table (Pager's `loghandshake` is
EAPOL; PMKID handling is separate).
- Changing the dashboard's handshake card/table (it keeps using `files`).
@@ -0,0 +1,103 @@
# Design: Open AP tab — Mark VII "PineAP Settings" card
Date: 2026-08-11
Status: Approved
## Problem
The PineAP **Open AP** tab (`#/pineap/open`) renders as a flat list of nine
plain checkboxes inside a single `.pineap-title-card`. The Mark VII PineAP page's
settings card ("PineAP") uses a titled card with a subtitle, iOS-style slide
toggles (`mat-slide-toggle`) grouped into sections, and a Save button. The Open
AP tab should be restyled to that anatomy, with behavior switched from
immediate-apply to a batched Mk7-style Save.
## Reference (Mark VII)
The Mk7 "PineAP" settings card (from the firmware bundle):
- 20px card title **"PineAP"** + muted subtitle **"Quickly set the general
behavior of PineAP"**
- The Passive/Active/Advanced button group (already on our Overview tab — not
duplicated here)
- Settings as `mat-slide-toggle` iOS switches
- A small accent **save** button (`savePineAPSettings`)
## Design
### Layout
A full-width `.pineap-card-settings` card containing:
- **Title row** (`.pineap-card-title-flex`): title **"PineAP"**, muted subtitle
**"Quickly set the general behavior of PineAP"**, and a **Save** button on the
right (accent/primary `.btn`).
- **Grouped slide-toggle settings** using the repo's existing `.switch` CSS
(iOS-style slider; defined in `app.css` but currently unused):
- **Karma**: Enable PineAP, Karma
- **SSID Pool**: Capture SSIDs to Pool, Advertise AP Impersonation Pool
- **Logging**: Log Handshakes, Log Partial Handshakes, Log PCAP, Log WiGLE,
Log Recon
- **Footer** (muted): `PineAP MAC: X Target MAC: Y`
### Behavior (Mk7-style Save, batched)
- Flipping a switch **stages** the value locally and marks the field dirty — no
API call is made until Save.
- **Save** applies only the dirty fields, batched per backend route:
- Logging + Capture SSIDs (set_config fields `loghandshake`,
`logpartialhandshake`, `logpcap`, `logwigle`, `logrecon`, `autossidpool`) →
one `POST /api/pineap/set_config` with the dirty fields. The backend merges
over the current daemon config, so untouched settings keep their values.
- `pineap_disabled` (Enable PineAP) → `POST /api/pineap/enable {enable}` if
dirty.
- `karma``POST /api/pineap/mimic {enable}` if dirty.
- `advertise``POST /api/pineap/ssidpool/advertise {enable}` if dirty.
- All dirty routes fire together (`Promise.allSettled`); toast "Settings
saved" if all fulfilled, else "Some settings failed" (error).
- Dirty-only apply preserves the unreadable live Karma state: the Pager daemon
cannot report Karma, so leaving the switch untouched means Save does not
change it.
- `load()` runs once on entry and after a successful Save to populate the
switches from the daemon (`get_config`, `hostapd`, `wifi/get_ap`). No polling
on this tab (unchanged), so staged values are never clobbered.
- If nothing is dirty, Save toasts "No changes".
### Mapping table (field → route, dirty-only)
| Switch | Backend call | Dirty flag |
|---|---|---|
| Enable PineAP | `POST /api/pineap/enable {enable}` | `pineap_disabled` |
| Karma | `POST /api/pineap/mimic {enable}` | `karma` |
| Capture SSIDs to Pool | `POST /api/pineap/set_config {autossidpool}` | `autossidpool` |
| Advertise AP Impersonation Pool | `POST /api/pineap/ssidpool/advertise {enable}` | `advertise` |
| Log Handshakes | `POST /api/pineap/set_config {loghandshake}` | `loghandshake` |
| Log Partial Handshakes | `POST /api/pineap/set_config {logpartialhandshake}` | `logpartialhandshake` |
| Log PCAP | `POST /api/pineap/set_config {logpcap}` | `logpcap` |
| Log WiGLE | `POST /api/pineap/set_config {logwigle}` | `logwigle` |
| Log Recon | `POST /api/pineap/set_config {logrecon}` | `logrecon` |
## CSS additions (`app.css`)
- `.pineap-card-subtitle` — muted, ~13px subtitle under the card title.
- `.pineap-settings-section` — small muted group label (e.g. "Karma", "SSID
Pool", "Logging"), 13px, with margin, used between the toggle groups.
- Reuse the existing `.switch` slider CSS (no change to it).
## Files touched
- `payload/user/general/pager-webui/www/css/app.css` — two new classes.
- `payload/user/general/pager-webui/www/js/views.js` — rewrite
`views.pineap_open` (layout + staged-save behavior).
No backend changes. No test-module changes.
## Verification
1. JS delimiter balance check (Python checker; no node available).
2. All 13 backend unittest modules pass (unchanged).
3. Deploy via `scripts/deploy.ps1` (password supplied securely), restart
`/etc/init.d/pagerwebui`.
4. On-device: Open AP tab shows the titled settings card with grouped slide
switches and Save; flipping a switch then Save applies the values (verify
via `get_config`/`hostapd` after Save); untouched toggles are not sent;
user walks the tab visually.
@@ -0,0 +1,86 @@
# Mk7 "PineAP Open Access Point" Card — Design
**Goal:** Rebuild the Open AP tab (`#/pineap/open`, `views.pineap_open`) to visually and functionally match the genuine Mark 7 Pineapple's `/PineAP/open` page: a single **"PineAP Open Access Point"** card with Open AP network settings and filter notices. The current grouped-toggles "PineAP settings" card is removed.
**Reference:** Verified against the real Mk7 firmware bundle served at `http://172.16.42.1:1471/` (`main.ce5a318adf590e170f6d.js`), component `app-open-wifi-view` (selector), template root `QAt`.
## 1. Frontend — `views.pineap_open`
Rendered inside `pineapShell(root, '#/pineap/open')`. A single `.pineap-title-card` (matching the clone's existing Mk7 card styling) containing, top to bottom:
- **Title:** "PineAP Open Access Point" (`.pineap-card-title`).
- **Subtitle:** "The Open SSID is advertised without encryption. When client association is enabled, " followed by a filter-dependent sentence selected by the SSID/client filter modes:
- ssid=allow ∧ client=allow → "any client in the filter configuration may connect to any SSID in the filter configuration."
- ssid=deny ∧ client=allow → "any client not in the filter configuration may connect to any SSID in the filter configuration."
- ssid=allow ∧ client=deny → "any client in the filter configuration may connect to any SSID not in the filter configuration."
- ssid=deny ∧ client=deny → "any client not in the filter configuration may connect to any SSID not in the filter configuration."
- ("filter configuration" links to `#/pineap/filtering`.)
- **Fields** (two `.row`s; labels above inputs per the clone's `label { display:block }` style):
1. **Open SSID** — text input.
2. **BSSID** — text input.
3. **Channel**`<select>` of channels 111, values `1`..`11`.
4. **Current Country**`<select>` of the Mk7 82-entry country list (see §3).
- **Switches** (existing `.switch` slider CSS):
- **Hidden** — bound to the Open AP `hidden` state.
- **Respond to all probe requests (impersonate all networks)** — bound to the PineAP `karma` state (the Mk7 binds this exact toggle to `PineAPSettings.karma`). The daemon exposes no readable karma/mimic state (verified: `get_config` has no `mimic`), so the toggle is **session-tracked** (module-level flag, default off, updated on Save) — consistent with the Overview's `karmaOn` handling. Save still POSTs `mimic`.
- **Info line** (below the switches, small muted text): "The Open access point will be " + ("hidden" if hidden else "advertised") + "." plus, when karma is on, the filter sentence from the subtitle's logic. The Mk7's ", and SSIDs from the Spoofed SSID Pool will be advertised" clause is **omitted**: the daemon exposes no readable broadcast/advertise state (pre-existing limitation; the Overview's advertise toggle has the same issue).
- **Filter notice boxes** (stacked, below the info line), mirroring the Mk7's infobox conditions and actions:
1. If SSID filter fetched ∧ Open SSID not in SSID-filter allow list ∧ mode=allow → error box: `The open SSID "<ssid>" is not included in the filter allow list, clients will not be able to connect.` + **Add Allowed** button → `POST /api/pineap/filters/ssid {action:'add', value: ssid}` then reload.
2. If SSID filter fetched ∧ Open SSID in SSID-filter list ∧ mode=deny → box: open SSID is blocked by the filter deny list + **Remove Filter** button → `POST /api/pineap/filters/ssid {action:'delete', value: ssid}` then reload.
3. If mode=allow ∧ SSID filter list non-empty ∧ karma → info box: "Remember to add SSIDs you wish to impersonate to the PineAP SSID filter (Deny)." + **Change Filters** link → `#/pineap/filtering`.
4. If client filter fetched ∧ client mode=allow ∧ client list empty → error box: "The PineAP Client filter is set to allow mode. When using this mode, " + the mode-dependent sentence + **Change Mode** button (→ `POST /api/pineap/filters/client {action:'set_mode', mode:'deny'}`, then reload) + **Change Filters** link.
- **Save button** (`.btn`) + muted note "Applying reconfigures the radio — you may be disconnected briefly."
- Save payload: `POST /api/pineap/wifi/set_ap` with `{open: {ssid, bssid, hidden, enabled, channel, country}}` where `enabled` is the value loaded from `get_ap` (preserved; the Mk7 card has no Enabled control), plus `POST /api/pineap/mimic {enable: <karma switch>}`. All via `Promise.allSettled`.
- Feedback via `App.toast('Open AP saved')` / `App.toast(..., 'error')`.
**Old card removed:** the "PineAP" grouped settings card (Enable PineAP / Karma / Capture SSIDs / Advertise / Logging toggles + footer MAC line + batched `saveCfg`) is deleted. Enable PineAP and Karma are covered by the Overview mode bar; SSID-pool capture/advertise by Overview Quick Settings; logging by Evil WPA / Logging. Nothing else in the app references the removed code.
## 2. Backend — `server.py`
Extend the two existing handlers; no new routes, no daemon changes.
### `h_pineap_wifi_get_ap` (currently ~line 1479)
The `open` object gains fields; reads remain UCI-based (`_uci_wifi_iface('wlan0open')`, radio config via `_uci_wifi_iface('radio0')` or equivalent):
- `ssid``open_cfg.get('ssid') or ''`.
- `bssid`**`open_cfg.get('macaddr') or ''`** (the Open AP interface MAC; changed from the ssidpool bssid). Verified: the only consumer of `open.bssid` is the old Open tab footer, which is being removed; the Overview uses only `open.enabled`.
- `hidden``open_cfg.get('hidden') == '1'`.
- `channel` — radio0 channel (2.4GHz radio hosting the open AP), as int if numeric, else `None`.
- `country` — radio0 country code or `''`.
- `enabled`, `target` unchanged (`enabled` = `open_cfg.get('disabled') == '0'`; `target` = pool target).
### `h_pineap_wifi_set_ap` (currently ~line 1505)
The `open` branch gains support:
- Pass `bssid` (→ `macaddr`), `hidden`, `channel` through to the daemon `PUT /api/settings/wifi/set_ap` open config (the daemon persists these to `wireless.wlan0open`, verified on-device: it writes `macaddr`, `hidden`, `channel`, `encryption`).
- **Country** and **radio channel** are applied directly: if `open.country` present, `uci set wireless.radio0.country=<code>`; if `open.channel` present, `uci set wireless.radio0.channel=<n>`; then `uci commit wireless` + `wifi reload`. (The daemon's iface-level channel write is inert for the actual radio — verified.) Only write/reload when the value changed.
- `enabled` semantics unchanged (preserve existing state when not supplied).
### Karma
"Respond to all probe requests" is saved by the frontend via the existing `POST /api/pineap/mimic {enable}`; no backend change.
## 3. Data lists (frontend constants)
- **Channels (2.4GHz):** 1..11 (labels "Channel N (24NN MHz)" matching Mk7; values 1..11).
- **Countries:** the Mk7's 82-entry list (US United States, DZ Algeria, AR Argentina, AU Australia, AT Austria, BH Bahrain, BM Bermuda, BO Bolivia, BR Brazil, BG Bulgaria, CA Canada, CL Chile, CN China, CO Colombia, CR Costa Rica, CS Cyprus, CZ Czech Republic, DK Denmark, DO Dominican Republic, EC Ecuador, EG Egypt, SV El Salvador, EE Estonia, FI Finland, FR France, DE Germany, GR Greece, GT Guatemala, HN Honduras, HK Hong Kong, IS Iceland, IN India, ID Indonesia, IE Ireland, PK Islamic Republic of Pakistan, IL Israel, IT Italy, JM Jamaica, JO Jordan, KE Kenya, KW Kuwait, LB Lebanon, LI Liechtenstein, LT Lithuania, LU Luxembourg, MU Mauritius, MX Mexico, MA Morocco, NL Netherlands, NZ New Zealand, NO Norway, OM Oman, PA Panama, PE Peru, PH Philippines, PL Poland, PT Portuagal, PR Puerto Rico, QA Qatar, KR Republic of Korea (South Korea), RO Romania, RU Russia, SA Saudi Arabia, SG Singapore, SI Slovenia, SK Slovak Republic, ZA South Africa, ES Spain, LK Sri Lanka, SE Sweden, CH Switzerland, TW Taiwan, TH Thailand, TT Trinidad and Tobago, TN Tunisia, TR Turkey, UA Ukraine, AE United Arab Emirates, GB United Kingdom, UY Uraguay, VE Venezuela, VN Vietnam).
## 4. Filter state data (frontend)
- SSID filter: `GET /api/pineap/filters/ssid``{mode, entries}` (existing handler). Mode `allow`/`deny`.
- Client filter: `GET /api/pineap/filters/client``{mode, entries}`.
- Mutations use the existing `action`-based API (matching `views.pineap_filtering`):
- Add Allowed / Remove Filter: `POST /api/pineap/filters/ssid` with `{action:'add'|'delete', value: ssid}`.
- Change Mode: `POST /api/pineap/filters/client` with `{action:'set_mode', mode:'deny'}`.
## 5. Error handling & UX
- All load fetches use `.catch(() => ({ data: {} }))` (existing pattern); the card still renders with empty fields if a fetch fails.
- Save = `Promise.allSettled([set_ap, mimic])`; toast "Open AP saved" only if all fulfilled, else "Some settings failed" (error); always re-run `load()`.
- Add Allowed / Remove Filter / Change Mode: fire, toast, re-run `load()`; failures toast 'Failed' (existing pattern).
## 6. Testing
- **Backend unit tests** (`tests/test_pineap_proxy.py`): extend the existing mocked-UCI tests —
- `test_wifi_get_ap_reads_uci_wireless`: mock `uci show wireless.wlan0open` with `ssid`/`macaddr`/`hidden`, and `uci show wireless.radio0` with `channel`/`country`; assert the new `open` fields.
- `test_wifi_set_ap_builds_configs`: open config now carries `bssid`/`channel`; country write triggers `uci set wireless.radio0.country` (mock `device_run`, assert the `uci set` + `uci commit` + `wifi reload` calls) only when country changed.
- Full unittest loop stays green.
- **JS:** delimiter-balance checker (`C:\Users\root\AppData\Local\Temp\opencode\js_balance.py`); no node available.
- **Deploy + on-device:** deploy via `scripts/deploy.ps1`; restart webui; curl save-path round-trip: set Open SSID/hidden via `wifi/set_ap`, verify `uci show wireless.wlan0open`, restore original values. Visual check against `http://172.16.42.1:1471/#/PineAP/open`.
@@ -0,0 +1,108 @@
# Design: PineAP pages — Mark VII layout
Date: 2026-08-11
Status: Approved
## Problem
The PineAP section of the Pager WebUI (8 tabs) uses the generic Pager WebUI card
layout (`.section`, `.card`, `.tabbar`). The user wants the PineAP pages to look
like the WiFi Pineapple Mark VII PineAP page: the `pineap-title-card-container`
title-card anatomy (20px card titles with clickable title links, centered 24px
values), a full-width Passive/Active/Advanced button group, and Mk7-style section
cards. Scope is limited to the PineAP pages (all 8 tabs); the rest of the app and
all behavior are unchanged.
## Reference (extracted from Mark VII firmware bundle)
The Mk7 PineAP page styles (verbatim class semantics from `mk7-main.js`):
```css
.pineap-title-card-container { display:flex; width:100%; flex-wrap:wrap; justify-content:space-between; }
.pineap-title-card { flex:1; }
.pineap-card-title { font-size:20px; display:flex; align-items:center; margin-bottom:10px; }
.pineap-card-title-link { color:inherit; text-decoration:none; } :visited inherit; :hover underline
.pineap-card-title-content { display:flex; justify-content:center; align-items:center; font-size:24px; }
.pineap-card-button-group { width:100%; height:30px; }
.pineap-card-button { width:100%; }
.pineap-card-settings, .pineap-card-pool, .pineap-card-handshakes, .pineap-card-inject { flex:1; }
.pineap-handshakes-none { display:flex; justify-content:center; font-style:italic; color:grey; }
```
The Recon page in this codebase already replicates this pattern as `recon-*`
(`.recon-title-card-container`, `.recon-title-card`, `.recon-card-title-link`,
`.recon-card-title-content`) — a proven precedent.
## Design
### 1. Layout vocabulary in `app.css`
Add the Mk7 `.pineap-*` classes above, using the codebase's design tokens where
the Mk7 used hardcoded colors (e.g., `.pineap-handshakes-none` uses `var(--muted)`
for the italic grey text). Card background/shadow come from the existing
`.card`-like surface treatment (`var(--surface)` + `var(--shadow)`), applied to a
new `.pineap-title-card` surface so title cards match the app's cards.
### 2. Overview tab (full Mk7 rebuild)
Three rows, each a `.pineap-title-card-container`:
- **Row 1 — stat title cards:** three `.pineap-title-card` cards.
Total SSIDs in Pool (link → `#/pineap/impersonation`), Clients Connected (link →
`#/pineap/clients`), Handshakes Captured (link → `#/pineap/evilwpa`). Each has a
20px `.pineap-card-title-link` and a centered 24px `.pineap-card-title-content`
value. Data and 5s polling unchanged from the current implementation.
- **Row 2 — mode + quick settings:** a wide mode card containing the mode badge,
the Passive/Active/Advanced `.pineap-card-button-group` (styled as the Mk7
30px group; reuses the existing `.seg-btn` logic) and the mode description
line; beside it a `.pineap-card-settings` Quick Settings card (Capture SSIDs
to Pool, Advertise AP Impersonation Pool).
Preset behavior unchanged (PineAP master + Karma only; `karmaOn` tracking;
`modePending` guard; karma-off wins precedence).
- **Row 3 — status title cards:** Karma, Open Network, Evil WPA, Evil Enterprise
as four `.pineap-title-card` cards with `.pineap-card-title` headings and
Configure links (to `#/pineap/open`, `#/pineap/evilwpa`, `#/pineap/enterprise`).
### 3. Other seven tabs
Same Mk7 card anatomy, existing tab bar retained as navigation:
- **Clients (`#/pineap/clients`):** a `.pineap-title-card-container` with a
"Clients Connected" count `.pineap-title-card` above the clients table card.
- **Impersonation (`#/pineap/impersonation`):** a "Total SSIDs in Pool" count
`.pineap-title-card` (link → pool section) plus the `.pineap-card-pool` pool
list card (add/clear, advertise/collect toggles, SSID table).
- **Open AP (`#/pineap/open`):** toggle list and info in a `.pineap-card-settings`
card with a `.pineap-card-title` heading.
- **Evil WPA (`#/pineap/evilwpa`):** config form in a card with `.pineap-card-title`;
the handshake capture list becomes `.pineap-card-handshakes` with the
`.pineap-handshakes-none` empty state.
- **Enterprise (`#/pineap/enterprise`):** enabled/auth toggles in a settings card;
Basic/Challenge data tables in `.pineap-card-inject`-style cards.
- **Filtering (`#/pineap/filtering`):** client/SSID filter cards with
`.pineap-card-title` headings (mode selector + entry tables).
- **APs (`#/pineap/aps`):** the AP scan table card with a `.pineap-card-title`
heading.
### 4. Behavior unchanged
No API or logic changes. All toggles, tables, preset/mode logic, and actions keep
their current behavior. The PineAP tab bar (8 tabs) remains the navigation.
## Files touched
- `payload/user/general/pager-webui/www/css/app.css` — add `.pineap-*` classes.
- `payload/user/general/pager-webui/www/js/views.js` — rebuild the markup of the
8 PineAP tab renderers to the Mk7 card anatomy.
No backend changes. No test-module changes.
## Verification
1. JS delimiter balance check (Python checker; no node available).
2. All 13 backend unittest modules pass (unchanged).
3. Deploy via `scripts/deploy.ps1` (password supplied securely), restart
`/etc/init.d/pagerwebui`.
4. On-device walk of all 8 PineAP tabs: title cards render with clickable links,
counts populate on the 5s poll, mode toggle still applies presets, tables and
forms still work.
@@ -0,0 +1,103 @@
# Design: PineAP Overview — Mark VII stats + mode quick toggle (Pager-supported)
Date: 2026-08-11
Status: Approved
## Problem
The Pager WebUI PineAP overview (`#/pineap`) currently shows only a mode badge, an
intro line, two Quick Settings checkboxes, and four status cards (Karma / Open
Network / Evil WPA / Evil Enterprise). Compared with the WiFi Pineapple Mark VII
PineAP overview it is missing:
1. The three overall dashboard counters: **Total SSIDs in Pool**, **Clients
Connected**, **Handshakes Captured**.
2. The **Passive / Active / Advanced** quick mode toggle.
All required data is already served by existing, on-device-verified webui
endpoints; the backend is not involved in this change.
## Data sources (all verified live)
| Card | Endpoint | Value | Link |
|---|---|---|---|
| Total SSIDs in Pool | `GET /api/pineap/ssids` | `data.ssids.length` (hak5cmd `PINEAPPLE_SSID_POOL_LIST`) | `#/pineap/impersonation` |
| Clients Connected | `GET /api/pineap/clients` | `data.count` (iwinfo assoclist) | `#/pineap/clients` |
| Handshakes Captured | `GET /api/pineap/handshakes` | `data.files.length` (loot dir scan) | `#/pineap/evilwpa` |
## Pager constraints (drives the design)
- **Karma (mimic) state is not readable.** The daemon exposes only
`mimic/enable` and `mimic/disable` (POST); `get_config` has no karma field and
there is no `mimic` GET route. The webui therefore tracks karma in a
view-local variable; on a fresh page load the true state is unknown.
- **SSID Pool Broadcasting cannot start** (`ssidpool/enable` returns 500 because
no `wlan0open` interface exists on the Pager). The mode presets do NOT touch
broadcast; it remains a manual Quick Settings toggle that surfaces the daemon
error.
## Design
### 1. Stats row (3 clickable cards)
A new `.cards` row placed above the existing status cards. Each card shows a
large numeric value via the existing `.card-label` / `.card-value` styles and a
`View`/`Configure` ghost button that navigates to the tab listed above. The row
is populated inside the existing 5-second `load()` loop by adding the three GETs
above to the current `Promise.all`.
### 2. Mode quick toggle (Passive / Active / Advanced)
A segmented control (new `.seg` / `.seg button` styles in `app.css`, modeled on
`.tabbar`) rendered directly under the mode badge in the header section. It has
three mutually exclusive buttons: Passive, Active, Advanced.
Preset actions (only Pager-supported features):
- **Passive**: `POST /api/pineap/enable {enable:true}` (PineAP on),
`POST /api/pineap/mimic {enable:false}` (Karma off).
- **Active**: `enable:true` + `mimic:true` (Karma on).
- **Advanced**: `enable:true` + `mimic:true`; Evil WPA / Evil Enterprise are left
as configured (customizable from their cards/tabs).
Apply flow: on click, disable the buttons, fire the preset requests (Promise.all),
then reload state. On any failure, revert the selection and toast "Failed". The
mode badge remains computed from readable state (`pineap_disabled`, Evil WPA /
Enterprise enabled) and the toggle highlight is synced to that computed mode.
Under the toggle, a short muted line describes the currently selected mode
(Mk7-style, adapted):
- 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."
### 3. Karma status card
Replaces the current always-`null` value with the locally-tracked karma state
('On' / 'Off'; '—' if never set this session).
### 4. Quick Settings
Unchanged: "Capture SSIDs to Pool" and "Advertise AP Impersonation Pool".
**Randomize Source MAC is explicitly out of scope** — the Pager has no backing
route.
## Files touched
- `payload/user/general/pager-webui/www/js/views.js``views.pineap` overview
section (stats row, mode toggle, karma card, feature-list line).
- `payload/user/general/pager-webui/www/css/app.css` — add `.seg` segmented
control styles.
No backend changes. No test-module changes (frontend-only; verified by JS
delimiter balance check, the 13-module unittest loop, deploy, and an on-device
walk).
## Verification
1. JS delimiter balance check (no node available; Python checker).
2. All 13 test modules still pass (unchanged).
3. Deploy via `scripts/deploy.ps1` (password supplied securely).
4. On-device: confirm the three stat endpoints return counts; user walks the
overview UI to confirm stats populate, cards navigate, and the mode toggle
applies presets (karma tracking, error toast on failure).
@@ -0,0 +1,162 @@
# Design: Mark VII PineAP Page Port for the Pager WebUI
Date: 2026-08-11
## Goal
Replace the current pager-webui PineAP page with a faithful replica of the WiFi
Pineapple Mark VII PineAP view, fully functional on the WiFi Pineapple Pager,
independent of campaign needs. Also fix the side-rail icon (location pin ->
wifi).
## Background / findings
- The current PineAP page (tabs: Open, Clients, Filtering, APs, Impersonation)
looks nothing like the Mark VII page and its backend is broken on the Pager:
- `server.py` reads Mark VII uci keys (`pineapd.pineapd.mimic`,
`collect_handshakes`, ...) that do not exist in the Pager's `pineapd`
config, so settings never persist (live `GET /api/pineap/settings` returns
only `{"bands":"2.4"}` plus raw).
- The filter-mode endpoints call `hak5cmd` commands without the required
argument, returning usage text instead of the mode.
- The Pager's daemon (`/pineapple/pineapple`, listens on `:1471`) natively
exposes a Mark VII-style PineAP REST API over a root-only unix socket at
`/tmp/api.sock` (raw HTTP/1.1, no auth - socket permission is the boundary).
pager-webui already uses this socket (`daemon_sock_call`, e.g. recon
start/stop). Confirmed live routes:
| Route | Method | Purpose |
|---|---|---|
| `/api/pineap/get_config`, `/set_config` | GET/POST | log flags, handshake path, ssidpool autocollect |
| `/api/pineap/hostapd/get_config`, `/set_config` | GET/POST | master PineAP on/off, Evil WPA (`wpa_ifaces`), Evil Enterprise (`pineape_*`) |
| `/api/pineap/hostapd/enable_pineap` | POST | master enable |
| `/api/pineap/hostapd/enable_pineape` | POST | evil enterprise enable |
| `/api/pineap/hostapd/enable_pineape_auth` | POST | enterprise auth-pass capture |
| `/api/pineap/mimic/enable`, `/disable` | POST | karma |
| `/api/pineap/examine/bssid`, `/reset` | POST | targeted handshake capture |
| `/api/pineap/ssidpool/list`, `/add`, `/clear`, `/disable`, `/disable_collect` | GET/POST | SSID pool + advertise/collect |
| `/api/pineap/ssidfilter/get_config`, `/set_config`, `/allow/*`, `/deny/*` | GET/POST | SSID filter mode + lists |
| `/api/pineap/macfilter/get_config`, `/set_config`, `/set_mode`, `/allow/*`, `/deny/*` | GET/POST | MAC/client filter mode + lists |
| `/api/pineap/interfaces/get`, `/set_interface`, `/set_interface_bands` | GET/POST | monitor interface hop/inject/bands |
| `/api/pineap/log/recon|pcap|wigle/start|stop` | POST | logging control |
| `/api/pineap/recon/new` | POST | new recon scan |
- The Pager natively supports Evil WPA (rogue AP on `wlan0wpa`: SSID, PSK,
encryption WPA2-PSK / WPA3-SAE / WPA3-OAE, hidden, enabled) and Evil
Enterprise (EAP, auth-pass capture), confirmed via the daemon binary strings
and the native (virtual) pager menu.
- The daemon lacks routes for: connected clients (use `iwinfo`), kick
(`hak5cmd PINEAPPLE_DEAUTH_CLIENT`), nearby-AP scan (`iwinfo scan`), and
handshake file listing (`/root/loot/handshakes/`). pager-webui already
implements these.
## Architecture
### Backend (`payload/user/general/pager-webui/server.py`)
1. Add a generic proxy handler `h_pineap_proxy` that forwards the browser
request (method + JSON body) verbatim to the daemon socket path
`/api/pineap/<subpath>` using the existing `daemon_sock_call`, and returns
the daemon's JSON response. Register it for the whole native tree
(`get_config`, `set_config`, `hostapd/*`, `mimic/*`, `examine/*`,
`ssidpool/*`, `ssidfilter/*`, `macfilter/*`, `interfaces/*`, `log/*`,
`recon/new`) so every native capability is reachable 1:1 through
pager-webui's authenticated `/api/pineap/*` namespace.
2. Keep the custom endpoints that have no daemon route: `clients` (iwinfo),
`clients/kick` (deauth), `aps` (iwinfo scan), `handshakes` (loot listing +
download/delete).
3. Add Enterprise data endpoints: list + clear of `hostap_basic` /
`hostap_challenge` rows from `recon.db` (via the sqlite3 CLI, the same
mechanism the recon page uses).
4. Delete the broken Mark VII-uci settings handlers (`SETTING_MAP`,
`_uci_map`, `h_pineap_settings_get/post`) and the filter handlers that
returned hak5 usage text (`h_filter_get/post`); replace with the proxy.
5. On daemon socket failure return 502 with a JSON error; never crash.
### Frontend (`www/js/app.js`, `www/js/views.js`, `www/css/app.css`)
1. Rail icon: change the PineAP rail item from `pineap` (map-marker) to the
existing `wifi` icon in `icons.js`/`app.js` rail definition.
2. Rebuild the PineAP page as 8 tabs matching the Mark VII structure (using
the existing vanilla-JS SPA design language - material-style cards,
toggles, tables, tab bar):
| Tab | Route | Content |
|---|---|---|
| PineAP | `#/pineap` | Mode badge (Passive/Active/Advanced from `pineap_disabled`+mimic+evilwpa/enterprise), description, quick toggles (Capture SSIDs to Pool, Advertise AP Impersonation Pool, Randomize Source MAC), alert-payload info note, 4 status cards (Karma / Open Network / Evil WPA / Evil Enterprise) with Configure links |
| Open AP | `#/pineap/open` | Enable PineAP (master), Karma, Logging group (handshakes/partial/pcap/wigle/recon), Capture SSIDs, Advertise Pool, Randomize MAC, AP Channel select, PineAP MAC + Target MAC (read-only) |
| Evil WPA | `#/pineap/evilwpa` | SSID, passphrase, encryption select (WPA2-PSK / WPA3-SAE / WPA3-OAE), Hidden, Enabled; handshake capture card (Examine BSSID + start/stop/reset); captured handshakes table |
| Enterprise | `#/pineap/enterprise` | Enabled, Auth Pass Capture; Basic Data + Challenge Data tables with Clear |
| Impersonation | `#/pineap/impersonation` | SSID pool textarea editor + Add + Clear; Advertise + Randomize toggles; pool start/stop/collect |
| Clients | `#/pineap/clients` | Connected clients table (MAC/interface/RSSI) + Kick, 5s auto-refresh |
| Filtering | `#/pineap/filtering` | Client Filter + SSID Filter cards: Allow/Deny mode + line-based list textareas with add/delete/clear |
| APs | `#/pineap/aps` | Kept as-is (iwinfo scan table, 10s refresh) |
3. Mark VII-only controls with no Pager equivalent (Autostart, Beacon
Responses, Beacon Intervals, enterprise cert generation) are omitted
rather than greyed out; Client Connect/Disconnect Notifications are shown
as an info note (the Pager handles these via alert payloads natively).
4. Register the new routes in the `routes` map and add the tab bar entries +
CSS.
## Data flow
Browser -> pager-webui `/api/pineap/*` (session-authenticated) ->
`daemon_sock_call` -> `/tmp/api.sock` -> Pager daemon -> `pineapd`/`hostapd`.
Mutating calls show a toast; failures surface a friendly error.
## Error handling
- Daemon socket unavailable -> HTTP 502 `{error}`; frontend shows a toast and
never renders a dead page.
- Unknown/unsupported subpath -> 404 through pager-webui.
- Enterprise tables empty -> "no data" empty states (same pattern as Recon).
## Testing
- Rewrite `tests/test_pineap_{settings,pool,clients,aps}.py` and add
`test_pineap_evilwpa.py` / `test_pineap_enterprise.py` /
`test_pineap_filtering.py` against the proxied shapes, mocking the daemon
socket call (module-level monkeypatch per the existing test conventions).
- Run the README's per-file unittest loop on Windows.
- On-device smoke test after `deploy.ps1`: walk every tab, verify toggles
persist across a reboot.
## Out of scope
- Firmware changes; the daemon API internals are used as-is.
- A PR to `hak5/wifipineapplepager-payloads` (packaging is drop-in ready).
## Open items (resolved during implementation)
- Exact `hostapd/set_config` Evil WPA field names (SSID/PSK/encryption/
hidden/enabled) - probe on the live device.
**Resolved:** Evil WPA lives in UCI `wireless.wlan0wpa`. The daemon route
`PUT /api/settings/wifi/set_ap` takes `{"configs":[{interface, ssid, enctype,
enabled, hidden, key, channel(int)}]}` (full replace; `channel` must be int).
`PUT /api/settings/wifi/get_ap` is a stub (always `{"interfaces":[]}`), so the
webui reads UCI `wireless.wlan0wpa`/`wlan0open` directly. `enctype` values:
`psk2`, `psk`, `sae`, `none` (not `wpa2`). Applying reconfigures the radio and
briefly drops the management connection.
- `ssidpool` list response shape (fallback: `hak5cmd PINEAPPLE_SSID_POOL_LIST`).
**Resolved:** `GET /api/pineap/ssidpool/list` is a stub (`{"success":true}`);
the pool is stored base64 in UCI `pineapd.@ssidpool[0].ssid`. `hak5cmd`
`PINEAPPLE_SSID_POOL_LIST/ADD/DELETE/CLEAR` all work and are used.
- Randomize-Source-MAC route (`GetPineAPRandomizeMAC` internal var) -
discover the backing route/field.
**Resolved (partial):** no readable daemon route for the pool advertise state
(`ssidpool/enable|disable` are POST; `enable` returns 500 natively because no
`wlan0open` interface exists on the Pager). The webui surfaces the daemon error.
- sqlite3 CLI availability for the Enterprise tables.
**Resolved:** sqlite3 CLI present; `hostap_basic`/`hostap_challenge` do not
exist yet in `recon.db`, so the Enterprise tab renders empty tables (webui
returns `rows: []`).
- Pineapd UCI required-field cycle: the daemon's `set_config`/`hostapd/set_config`
are full-replaces. If a write omits required fields (`pineapd.@pineapd[0].
reconpath` etc.), `get_config` then fails and later writes stay broken. The
webui merges each write over the current daemon config (or defaults when
`get_config` fails) so required fields are always preserved.
- Verified daemon methods: reads GET; `set_config`/`hostapd/set_config`/
`hostapd/enable_pineap`/`interfaces/set_interface`/`ssidpool/add`/`wifi/*` PUT;
toggles (`mimic/*`, `examine/*`, `ssidpool/enable|disable|enable_collect|
disable_collect`) POST. `ssidfilter`/`macfilter` `set_config` are PUT.
@@ -0,0 +1,215 @@
# Recon Mark VII Parity — Design Spec
- **Date:** 2026-08-11
- **Status:** Approved
- **Owner:** WiFi Pineapple Pager expansion project
- **Applies to:** `payload/user/general/pager-webui/www/` (front-end) and
`payload/user/general/pager-webui/server.py` (one backend change)
## 1. Goal
Rework the Pager WebUI Recon section so it looks and behaves as close as
possible to the stock Hak5 WiFi Pineapple (Mark VII) UI's Recon at
`http://172.16.42.1:1471/#/Recon`, using only data the Pager backend already
exposes plus one small optional-body change to `POST /api/recon/start`.
Source of truth for the Mark VII layout: the Angular bundle
`main.ce5a318adf590e170f6d.js` (captured 2026-08-11, cached at
`%TEMP%\opencode\oldui_main.js`). The scanning component (`app-scanning-view`)
template, component CSS, and Chart.js configs were extracted from it.
## 2. Mark VII Reference (extracted from bundle)
### Scanning page (`/Recon`, tab "Scanning")
```
div.recon-title-card-container (flex, wrap, justify: space-between, gap 10px)
div.recon-title-card-latest-overview
mat-card (height: 200px)
mat-card-title "Wireless Landscape" (font-size 20px)
mat-card-content > div > div.recon-wifidata-chart-container
canvas#landscapeChart (doughnut)
div.recon-center-text > span.recon-no-data " No wireless landscape data is available yet. "
div.recon-title-card-latest-overview "Channel Distribution" → canvas#channelChart (bar)
div.recon-title-card-latest-overview
mat-card (height: 200px)
mat-card-title > a.recon-card-title-link [routerLink /Recon/handshakes] " Handshakes "
content: <span font-size:32px><b>{totalHandshakes}</b></span>
<span color:grey>Handshakes Captured</span>
<mat-slide-toggle>Automatically Collect Any Handshakes</mat-slide-toggle>
div.recon-title-card-latest-overview
mat-card (height: 200px)
mat-card-title "Previous Scans"
content: mat-form-field (width 100%) > mat-label "Previous Scan" + mat-select
button[mat-icon-button] mat-icon "file_download"
button[mat-icon-button] mat-icon "delete"
mat-card (scan bar)
mat-card-content > div (height 48px, flex align-center)
mat-slide-toggle "Scan" (reconToggleState → startRecon/stopRecon)
div (margin-left 20px) mat-select (width 100px) scan_time
options: 30 Seconds/1 Minute/2 Minutes/5 Minutes/10 Minutes/Continuous
div (margin-left 20px) mat-select band (0 = 2.4GHz, 1 = 5GHz, 2 = 5GHz-DFS)
span[fxFlex] spacer
button[mat-icon-button] mat-icon "settings" (opens settings sidebar)
div (position: relative; min-height: 500px)
div.recon-scan-results-card
mat-card
mat-card-content
div (48px row) [AP table header: search mat-form-field "Search" + mat-paginator]
mat-table APs columns: SSID, MAC, Clients, OUI, Sec, WPS, MFP, Chan, Sign, FirstSeen, LastSeen
mat-card-content [Clients table: search + mat-paginator]
mat-table Clients columns: Client MAC, Signal, Channel, First seen, Last seen
```
Charts (from `generateLandscapeChart` / `generateChannelChart`):
- Landscape: `type:"doughnut"`, labels `["Access Points","Clients","Unassociated"]`,
backgroundColor `["#2ecc71","#2980b9","#8e44ad","#e74c3c"]`, `legend:{position:"bottom"}`.
- Channel: `type:"bar"`, one bar per channel sorted ascending, no legend, each bar
a color from a 64-color array, `yAxes:[{ticks:{reverse:!1,stepSize:1,min:0}}]`.
Component CSS (key rules, light theme):
- `.recon-title-card-container{display:flex;width:100%;flex-wrap:wrap;justify-content:space-between}`
- `.recon-title-card-latest-overview{flex:1;margin-bottom:1em}`
- `.recon-title-card-title{font-size:20px;margin-bottom:15px;display:flex;align-items:center}`
- `.recon-card-title-link{color:inherit;text-decoration:none}` `:hover{text-decoration:underline}`
- `.recon-title-card-content{display:flex;justify-content:center;align-items:center;height:70%}`
- `.recon-no-data{font-style:italic;color:#787878;display:flex;justify-content:center}`
- `.recon-scan-results-card{width:100%}`
- table rows cursor pointer; `.recon-table-row-active{background-color:#eaeaea}`
- dark: `.recon-table-row-active-dark{background-color:#565656}`
Tabs: Recon has exactly two tabs — `[Scanning → /Recon]`, `[Handshakes → /Recon/handshakes]`. No Events tab.
### Handshakes page (`/Recon/handshakes`)
Card "Captured WPA Handshakes" (title flex, settings icon button at right),
`table-container` with `mat-table`: columns `BSSID, Client, Source, Type, Captured,
Message 1, Message 2, Message 3, Message 4, Beacon Frame, [action]`. Rows show
M1M4 check/close/missing glyphs. Delete + download per row.
## 3. Design (Pager)
### 3.1 Tabs
`RECON_TABS` becomes two entries: **Scanning** (`#/recon`) and **Handshakes**
(`#/recon/handshakes`). Remove `views.recon_events`, the `#/recon/events`
route in `app.js`, and the old `views.recon_events` definition. Backend
`GET /api/recon/events` is left untouched.
### 3.2 Scanning view layout (`views.recon`)
Mark VII structure implemented with vanilla JS `h()` helpers and existing
`PagerAPI` endpoints. State per scan: `GET /api/recon/scans` → list;
`GET /api/recon/scans/{id}` → detail `{scan, aps[], clients[], handshakes[]}`.
- **Title cards row** (`.recon-title-card-container`, 200px cards):
1. **Wireless Landscape**`MiniChart.doughnut` with legend; segments
`[Access Points, Clients, Unassociated]` = `[aps.length, clients.length, 0]`,
colors `#2ecc71 / #2980b9 / #8e44ad`; placeholder text when no data.
2. **Channel Distribution**`MiniChart.bar`; per-channel counts from
detail APs (unknown channel grouped as `?`), Mark VII 64-color palette,
no legend, Y step 1.
3. **Handshakes Captured** — 32px bold count = selected scan handshakes
length; grey "Handshakes Captured" subtitle; "Automatically Collect Any
Handshakes" slide toggle wired to `GET/POST /api/pineap/settings`
(`collect_handshakes`); card title links to `#/recon/handshakes`.
4. **Previous Scans** — "Previous Scan" select (newest first, `Scan #id — time`
labels), `file_download` icon button → existing
`/api/recon/scans/{id}/download/json`, `delete` icon button → existing
`DELETE /api/recon/scans/{id}` (confirm dialog), refresh via poll.
- **Scan bar card** (48px row): "Scan" slide toggle (`POST /api/recon/start|stop`),
Duration select (30 Seconds / 1 Min / 2 Min / 5 Min / 10 Min / Continuous),
spacer, `settings` icon button → settings sidebar. No Band select (Pager
cannot single-band scan). Duration persisted in `localStorage`
(`pw_scan_duration`) and sent on start.
- **Results area** (`min-height:500px`), one card per table, each with a
Material-look "Search" field (client-side filter) and a paginator
(10/25/50/100, first/last/prev/next buttons, "110 of N" range label):
- **Access Points** — columns SSID, BSSID, Channel, Signal, Encryption,
Hidden; row click selects the AP (highlight) — no focus sidebar (backend
has no scan-AP deauth/capture APIs).
- **Clients** — columns MAC, Signal, Frequency, Packets.
- **Settings sidebar**: fixed right panel (top 64px, width 270px, shadow,
close button). Section "Access Points": Show SSID/MAC/Channel/Signal/
Encryption/Hidden toggles. Section "Clients": Show MAC/Signal/Frequency/
Packets toggles. Persisted as JSON in `localStorage` key `pw_recon_cols`.
- **Polling**: refresh scans/detail every 10s while the view is mounted
(destroy clears interval), same as today's behaviour (interval bumped from
15s → 10s to keep charts/table fresh).
### 3.3 Handshakes view (`views.recon_handshakes`)
Restyle to Mark VII look: card title "Captured WPA Handshakes" with a
`settings` icon button (non-functional on Pager; omitted or rendered as a
disabled hint — render it, clicking shows a toast "Settings not available on
Pager"). Table `File / Type / Size / Modified` plus per-row `Download` and
`Delete` actions (existing `/api/pineap/handshakes` endpoints + `/api/loot/zip`
for "Download all"). Keep existing zip/archive actions row.
### 3.4 Charts (`js/chart.js`)
- `MiniChart.doughnut(canvas, segments, { legend: true, height })` — redraw as
a true doughnut (inner hole via `arc` ring) and draw a legend row beneath
(color dot + label) when `legend` is set.
- `MiniChart.bar(canvas, items, opts)` — items `{label, value, color}`; draws
Y gridlines, bars from baseline, X labels, `maxY = ceil(max/1)`; Mark VII
palette cycles through `items[].color`.
- Keep existing `MiniChart.draw` (line) for the dashboard unchanged.
### 3.5 Icons (`js/icons.js`)
Add to `PineappleIcons`: `refresh`, `file_download`, `delete`, `settings`,
`search`, `first_page`, `last_page`, `chevron_left`, `chevron_right`
(Material Design path data; `wifi`/`extension`/`receipt` already present).
### 3.6 CSS (`css/app.css`)
Add Mark VII recon styles (light + `html.dark`): `.recon-title-card-container`,
`.recon-title-card`, `.recon-title-card-title`, `.recon-card-title-link`,
`.recon-title-card-content`, `.recon-no-data`, `.recon-chart-box`,
`.recon-scan-bar`, `.recon-scan-bar .sel`, `.recon-search`,
`.recon-paginator`, `.recon-settings-sidebar`, `.recon-settings-section`,
`.recon-settings-toggle`, `.recon-settings-close`, `.recon-row-selected`.
Card background follows `--surface`; title cards get the existing elevation
shadow; dark overrides via `html.dark`.
### 3.7 Backend (`server.py`)
`h_recon_start(ctx)` forwards an optional `scan_time` from the request body to
the daemon:
```python
def h_recon_start(ctx):
body = {}
scan_time = (getattr(ctx, 'body', None) or {}).get('scan_time')
if scan_time is not None:
body['scan_time'] = int(scan_time)
status, data = daemon_sock_call('POST', '/api/pineap/log/recon/start', body=body)
...
```
Default remains `{}` when no `scan_time` is sent, so the current behaviour is
unchanged. (Whether the Pager daemon honours `scan_time` is up to the daemon;
the UI persists and sends it either way.)
### 3.8 Routing (`js/app.js`)
Remove `'#/recon/events': 'recon_events'` from the `routes` map. No other
route changes.
## 4. Data flow
Unchanged API surface (except optional `scan_time` on recon start). The view
loads scans, auto-selects the newest, fetches the detail, renders charts +
tables, and re-fetches on a 10s interval. Search/pagination/column settings
are client-side state (`localStorage` keys `pw_scan_duration`, `pw_recon_cols`).
## 5. Testing
- Python: extend `tests/test_recon.py` `DaemonSockTest` with a case asserting
`h_recon_start` forwards `{'scan_time': 60}` when present and `{}` otherwise
(existing socket tests must stay green).
- Front-end: manual on-device smoke pass — every element of the scanning page,
tabs (2), scan start/stop, duration persistence, auto-handshake toggle,
previous-scan select + download + delete, table search/pagination, column
settings sidebar + persistence, handshakes page, dark theme, keyboard
shortcut `r`.
- Deploy via `scripts/deploy.ps1`; `curl` the served JS/CSS assets for 200.
## 6. Out of scope
- AP/client "focus" sidebars (deauth/capture/clone) — no backend support.
- 2D/3D cartography graph view — no AP↔client association data.
- Chart.js dependency — hand-rolled canvas rendering with identical palettes.
- Events tab, band select, "highlight active devices" aux settings.
- Backend removal of `/api/recon/events`.
@@ -0,0 +1,185 @@
# Virtual Pager Dock — Design Spec
- **Date:** 2026-08-11
- **Status:** Approved
- **Owner:** WiFi Pineapple Pager expansion project
- **Applies to:** `payload/user/general/pager-webui/www/` (front-end only) and
`scripts/dev_proxy.py` (dev config)
## 1. Goal
Add a **Virtual Pager** button in the topbar next to the existing **Terminal**
button in the Pager WebUI (`http://172.16.52.1:8080/`). Clicking it opens a
bottom-docked panel that shows a live, interactive replica of the Pager device:
the physical-device graphic with the real-time screen in the middle and
clickable directional/A/B buttons — matching the virtual pager view of the
Pager's stock UI at `http://172.16.52.1:1471/`.
Behavior mirrors the existing Terminal dock: open/close toggling, docked panel,
connect/disconnect of WebSockets on open/close.
## 2. Stock UI Reference (verified 2026-08-11)
Source: `http://172.16.52.1:1471/` (captured to
`%TEMP%\opencode\pager-virtual-pager.html`, 141,707 bytes).
### 2.1 Connections
- Screen: `ws://<host>:1471/api/pager/display/screen.ws` — streams binary RGBA
framebuffer frames. Requires the daemon `AUTH_<serverid>` cookie; returns
`401 Unauthorized` without it.
- Keys: `ws://<host>:1471/api/pager/input/keys.ws` — text frames; same cookie.
- Both handshakes verified returning `101 Switching Protocols` with a valid
`AUTH_<serverid>=<token>` cookie.
- Cookies are host-scoped (not port-scoped), so a browser that has logged into
`172.16.52.1:8080` already sends the cookie to `172.16.52.1:1471`. This is the
same mechanism the existing Terminal WS uses (direct `:1471` connection, no
backend relay), and requires **no backend changes**.
### 2.2 Screen framebuffer
- Resolution `480 × 222`, stride `480 * 4 = 1920`, expected
`1920 * 222 = 426,240` bytes per frame (4 bytes/pixel RGBA).
- Client renders each frame: copy RGBA into a hidden 480×222 canvas
(`createImageData`/`putImageData`), `canvas.toDataURL('image/png')` into the
`<img id="pager">` screen element.
### 2.3 Buttons / keys
Button image click sends these strings over the keys WS:
| Control | Sent key |
|-----------|------------------|
| LEFT | `ArrowLeft` |
| UP | `ArrowUp` |
| RIGHT | `ArrowRight` |
| DOWN | `ArrowDown` |
| A | `Enter` |
| B | `Escape` |
Physical keyboard: when the pager screen is focused (or body, with no other
control focused) the stock UI sends `e.key` on keydown (no repeats). Buttons
show a brief `.pressed` feedback (brightness filter).
### 2.4 Device graphic
`<table id="pager_ui">` (745 × 531) built from 24 image slices +
`spacer.gif`, button images `LEFT.png`, `UP.png`, `RIGHT.png`, `DOWN.png`,
`A_Button.png`, `B_Button.png`, and a 480×222 screen surface with hidden canvas
and a `#pager_error` overlay. Images are static and fetchable without auth from
`http://172.16.52.1:1471/images/`. Identical files already exist in
`wifipineapplepager/payloads/library/user/remote_access/nautilus/www/images/`
(sizes match byte-for-byte); they will be copied from there and hash-verified
against the stock UI.
Scaling: the stock UI scales the graphic with CSS `zoom` (`scale =
min(1, availableWidth / 745)`), `transform-origin: top center`.
## 3. Design
### 3.1 index.html
- Add `#pager-btn` ("Virtual Pager", `btn ghost`) next to `#terminal-btn`.
- Add `#pager-panel` dock after `#terminal-panel`, mirroring its structure:
- `#pager-bar` — title "Virtual Pager" + `#pager-close` button.
- `#pager` content area containing the pager graphic table with the live
screen, hidden canvas, and error/retry overlay.
- Load `js/pager.js` after `js/terminal.js`.
### 3.2 app.css
- `#pager-btn`: same style as `#terminal-btn`; `.active` state for both.
- `#pager-panel`: fixed bottom dock like `#terminal-panel` but `height: 540px`;
reuses the `term-up` slide-in animation and a shared `.dock` utility where
practical. Content centered; overflow hidden.
- Pager graphic scale-to-fit: a wrapper whose `zoom`/transform scales the
745×531 graphic to the available panel width, never upscaling
(`scale = min(1, availableWidth / 745)`), per §2.4.
- `.pager-btn` cursor/`user-select`/pressed feedback (stock rules).
- `#pager_error` overlay styles (centered, dark overlay, Reconnect button).
### 3.3 js/pager.js (new, mirrors js/terminal.js)
Module `Pager` with:
- `SCREEN_WIDTH = 480`, `SCREEN_HEIGHT = 222`, `FB_STRIDE = 1920`.
- `ensure()` — lazily grab `#pager-panel`, canvas, `<img id="pager">`, build the
graphic table (static innerHTML from bundled images), wire button clicks.
- `toggle()` — show/hide `#pager-panel`, toggle `#pager-btn.active`,
connect/disconnect.
- Screen WS: binary `event.data``new Uint8Array(buffer)`
`renderRGBAFrame(bytes)` (reject frames `< FB_STRIDE * SCREEN_HEIGHT`).
On error/close: hide screen, show error overlay with Reconnect.
- Keys WS: `sendKey(name)` helper; button clicks send the §2.3 strings.
- Keyboard: a `keydown` listener active only while the dock is open; skips when
focus is in an INPUT/TEXTAREA/SELECT or the terminal; captures
ArrowLeft/ArrowUp/ArrowRight/ArrowDown/Enter/Escape (preventDefault on the
arrows to stop page scroll) and sends them via keys WS. Letter shortcuts
(D/C/P/R/L/M) and backtick (terminal toggle) keep working — backtick closes
the pager dock via the replace-on-open behavior.
- `disconnect()` closes both WS.
- `window.addEventListener('resize')` re-applies scale-to-fit while open.
### 3.4 app.js
- Wire `#pager-btn` and `#pager-close` to `Pager.toggle()` (guarded with
`typeof Pager === 'undefined'` like the terminal).
- **Replace-on-open:** a small helper `showDock(name)` that, when opening
`pager`, calls `Term.close()` if `Term` is defined and its panel is open, and
vice versa. Refactor the terminal wiring to go through `showDock('terminal')`
so both directions use the same rule. (`Term` gains a `close()`/`isOpen()`
helper in `terminal.js`.)
- Keyboard shortcut `g` (or none) is not required; the button is the entry
point. Backtick toggles the terminal and closes the pager dock (per
replace-on-open).
### 3.5 config.js / dev_proxy.py
- `config.js`: add `pagerScreenWs: ''`, `pagerKeysWs: ''`.
- `app.js`: read `cfg.pagerScreenWs || ('ws://' + location.hostname +
':1471/api/pager/display/screen.ws')` and the keys equivalent; expose as
`App.pagerScreenWs` / `App.pagerKeysWs`.
- `dev_proxy.py` `_serve_dev_config`: emit both fields pointing at the pager
host (same shape as `terminalWs`).
### 3.6 Assets
- Copy from the nautilus payload into `www/assets/pager/`:
`virtual_pager_01..24.png`, `LEFT.png`, `UP.png`, `RIGHT.png`, `DOWN.png`,
`A_Button.png`, `B_Button.png`, `spacer.gif`.
- Hash-verify each against the stock UI (`http://172.16.52.1:1471/images/…`)
during implementation; if any differ, use the stock bytes instead.
### 3.7 Error handling
- Screen WS error/close: show `#pager_error` ("Lost connection to virtual
pager") with a Reconnect button that calls `connect()`.
- Keys WS is best-effort: if not OPEN, button presses are ignored (stock
behavior).
- If WS construction throws (unreachable daemon), write a message into the
error overlay rather than throwing in app.js.
## 4. Testing
- **Unit (Python):** none required — no backend changes.
- **Static review:** `pager.js` frame renderer bounds check; button→key mapping
table; keyboard guard conditions; replace-on-open logic.
- **On-device smoke (per §8 of the main webui spec):**
1. Log in at `http://172.16.52.1:8080/`; Virtual Pager button present next to
Terminal.
2. Open Virtual Pager: dock slides up, screen streams live, graphic scales to
fit width.
3. Click LEFT/UP/RIGHT/DOWN/A/B — screen responds; physical arrow/Enter/Escape
also drive it; typing in an input/terminal does not.
4. Open Terminal while Pager open → Pager dock closes (replace-on-open) and
vice versa.
5. Kill/reconnect network to the daemon → error overlay + Reconnect recovers.
6. Backtick still toggles the terminal and closes the Pager dock.
## 5. Out of scope
- The stock page's embedded shell terminal and terminal-shortcut grid (the
WebUI already has a Terminal dock).
- The Pager Skinner / virtual-pager theme features.
- Any backend relay for the pager WS (direct `:1471` connection, same as the
Terminal).