release: Mark VIII 1.0
This commit is contained in:
@@ -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 1–4, 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 1–4 / 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 1–4**: `!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` (M1–4/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.
|
||||
- **hs1–hs4/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 1–11, 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
|
||||
M1–M4 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, "1–10 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).
|
||||
Reference in New Issue
Block a user