# Mark VIII UI Hardening + Recon Polish — Design Date: 2026-08-18 Status: Approved in advance by user (auto-approve; user unavailable for review) ## Context Mark VIII (web UI for the WiFi Pineapple Pager) has been through major dev changes. The user runs a live test tomorrow and needs the UI rock solid. Verified on device: - MCP server works: `tools/list` returns 16 tools; `device.state`, `recon.aps` respond correctly. - API endpoints used by the Recon "Actions" buttons work (`/api/recon/examine`, `/api/pineap/set_config`). - Recon detail endpoint is slow (~1.5 s) and can 503 when the sqlite DB is busy; the UI swallows errors silently and can stay blank. ## Findings 1. **Recon top cards (5)**: equal 200 px cards with 20 px titles, centered content, charts sized for wide cards. "Previous Scans" crams 5 icon buttons + a 50+-option select into a ~220 px card → buttons stack into three rows. "Channel Distribution" bar chart is unreadable at card width. Handshakes toggle text wraps awkwardly. 2. **Channel map**: canvas lobes render but have zero interactivity — no way to see which networks are under the cursor. 3. **Focus sidebar Actions**: "Capture WPA Handshakes" / "Stop Handshake Capture" / "Examine BSSID" / "Examine Channel" work at the API level but give weak feedback; there is no path from a recon target to the PineAP evil-twin form. 4. **Robustness**: `loadDetail()` in the recon view swallows render/network errors with `.catch(() => {})`; if rendering throws after `detailId` is set, the page stays blank forever (guard short-circuits). `load()` has no pending guard → overlapping polls. `hsAuto` (loghandshake) checkbox is only synced at view creation — it drifts from the pager's own settings when changed elsewhere (pager UI / another browser). ## Changes ### 1. Recon title cards — redesign (`views.js` + `app.css`) New card anatomy (all five consistent): - Compact header: 12 px uppercase muted title (clickable where it links). - Primary value: 28 px bold. - Sub-line: 12 px muted context text. - Optional mini chart: fixed 96 px tall. Cards (`flex: 1 1 0; min-width: 170px; height: 190px` — five fit one row even in a ~1000 px pane): 1. **Wireless Landscape** — value: total networks (APs); sub: "N clients · M unassociated"; mini doughnut (APs/Clients/Unassociated). 2. **Channel Distribution** — value: busiest channel "CH 6"; sub: "N of K APs on CH 6 · channels seen"; mini bar chart of channel counts. 3. **Encryption Landscape** — value: top encryption bucket; sub: "N of K APs"; mini doughnut of buckets. 4. **Handshakes** — value: handshake count (links to `#/recon/handshakes`); sub: "captured"; auto-collect toggle (restyled). 5. **Previous Scans** — value: scan count; sub: latest scan start time; compact select row + single row of icon buttons. Empty states get copy that matches the card ("No landscape data yet — run a scan"). ### 2. Channel map hover (`chart.js` + `views.js` + `app.css`) - `MiniChart.channelMap` records per-lobe geometry on the canvas: `canvas.__reconLobes = [{ ap, cx, half, topPx, basePx }]` (CSS px). - `views.js` attaches `mousemove`/`mouseleave`/`click` to the map canvas: - hit test: `t=(mx-cx)/half`, lift = `0.5+0.5·cos(πt)`, hovered if `my >= basePx - lift·peakPx - 4` and `my <= basePx + 8`; - tooltip div (absolute, inside `.recon-map-box`) lists every network under the cursor: SSID/MAC/channel/freq/signal/encryption/vendor; - click pins the tooltip until the next move or click; - `mouseleave` hides it. - Lobe geometry regenerates on every `renderChannelMap()` (redraw), so stale geometry is impossible. ### 3. Focus sidebar: Actions validation + Send to PineAP (`views.js`) - Existing buttons stay; toast feedback improved (already verified working at API level; re-verified end-to-end in browser). - New primary button **"Send to PineAP — Twin this network"**: - encryption bucket `Open` → navigate `#/pineap/open` with prefill `{ssid, hidden, channel, bssid}`; - anything else → navigate `#/pineap/evilwpa` with prefill `{ssid, hidden, channel, enctype}` where enctype maps from recon encryption: SAE→`sae`, OWE→`owe`, WPA3-only→`sae`, else `psk2`; - Enterprise networks still go to Evil WPA (psk2) — noted in the prefill banner. - Prefill mechanism: `window.PineAPPrefill = { set, consume }` (module singleton in `views.js`); `consume()` clears after use so a stale prefill never leaks into a manually opened form. - `attackLauncher()` consumes the prefill when building the form (SSID, hidden, channel via `chanSelect`, enctype, BSSID) and renders a muted banner: "Prefilled from Recon — verify, set the passphrase, then Deploy." Deploy is never triggered automatically (no attacks without an explicit user action). ### 4. Robustness / sync hardening (`views.js`) - `loadDetail()`: on fetch failure keep `detailId` unset so the poll retries; surface "Scan data unavailable — retrying…" in the scan status line; wrap the render body so one chart's exception cannot blank the table (each chart draw also wrapped individually). - `drawCharts()`: wrap each chart section in try/catch. - `load()`: `loadPending` guard against overlapping polls; surface scan-list errors in the status line. - `hsAuto` re-syncs from `/api/pineap/get_config` on the 30 s slow poll (stays in sync with the pager's own UI/settings changes). - Version bumps in `index.html` for `app.css`, `chart.js`, `views.js`. ### 5. Verification - Backend unchanged → existing `tests/` still pass (run the suite). - Deploy via `scripts/deploy.sh --password` (device: 172.16.52.1). - End-to-end in browser (device UI): - login; recon page: cards populated with scan data; no blank-page state; - click an AP row → focus sidebar → each Action button verified by reading back state (`get_config`) and API responses; - Send to PineAP → form pre-filled on the right tab (open vs WPA target); - channel map click → tooltip shows networks under cursor; - reboot-resilience spot check via service restart (pagerwebui restart).