diff --git a/docs/superpowers/specs/2026-08-18-ui-hardening-design.md b/docs/superpowers/specs/2026-08-18-ui-hardening-design.md new file mode 100644 index 0000000..cdecb44 --- /dev/null +++ b/docs/superpowers/specs/2026-08-18-ui-hardening-design.md @@ -0,0 +1,123 @@ +# 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). diff --git a/payload/user/remote_access/pager-webui/www/css/app.css b/payload/user/remote_access/pager-webui/www/css/app.css index 852cbff..82242eb 100644 --- a/payload/user/remote_access/pager-webui/www/css/app.css +++ b/payload/user/remote_access/pager-webui/www/css/app.css @@ -301,23 +301,39 @@ html.dark .sel { background: #424242; border-color: #545454; color: #fff; } html.dark .muted { color: #bdbdbd; } /* ---- Recon (Mark VII parity) ---- */ -.recon-title-card-container { display: flex; width: 100%; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin: 8px 0 16px; } -.recon-title-card { flex: 1 1 220px; min-width: 220px; margin-bottom: 1em; } -.recon-card { background: var(--surface); border-radius: 2px; box-shadow: var(--shadow); height: 200px; padding: 12px 16px; display: flex; flex-direction: column; } -.recon-title-card-title { font-size: 20px; margin-bottom: 15px; display: flex; align-items: center; color: var(--text); } +.recon-title-card-container { display: flex; width: 100%; flex-wrap: wrap; gap: 10px; margin: 8px 0 16px; } +.recon-card { + flex: 1 1 0; min-width: 170px; height: 190px; + background: var(--surface); border-radius: 2px; box-shadow: var(--shadow); + padding: 12px 14px; display: flex; flex-direction: column; +} +.recon-card-title { + font-size: 12px; text-transform: uppercase; letter-spacing: .05em; + color: var(--muted); margin-bottom: 6px; display: flex; align-items: center; +} .recon-card-title-link { color: inherit; text-decoration: none; } .recon-card-title-link:visited { color: inherit; } -.recon-card-title-link:hover { text-decoration: underline; } -.recon-title-card-content { display: flex; justify-content: center; align-items: center; height: 70%; } -.recon-chart-box { width: 100%; height: 150px; position: relative; } -.recon-chart-box canvas { width: 100%; height: 100%; } -.recon-no-data { font-style: italic; color: #787878; display: flex; justify-content: center; padding: 12px; } -.recon-hs-col { display: flex; flex-direction: column; justify-content: center; align-items: center; } +.recon-card-title-link:hover { color: var(--primary); text-decoration: underline; } +.recon-card-body { display: flex; flex-direction: column; flex: 1; min-height: 0; } +.recon-card-value { + font-size: 26px; font-weight: 700; line-height: 1.15; color: var(--text); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.recon-card-sub { + font-size: 12px; color: var(--muted); margin: 1px 0 6px; min-height: 16px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.recon-chart-box { position: relative; flex: 1; min-height: 0; margin-top: auto; } +.recon-chart-box canvas { position: absolute; inset: 0; width: 100%; height: 100%; } +.recon-no-data { font-style: italic; color: #787878; display: flex; justify-content: center; align-items: center; padding: 8px; text-align: center; } .recon-hs-count { font-size: 32px; font-weight: 700; line-height: 1.1; } -.recon-hs-label { color: grey; margin: 2px 0 10px; } -.recon-toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text); margin: 0; cursor: pointer; } -.recon-ps-row { display: flex; align-items: center; width: 100%; gap: 4px; } -.recon-ps-row .sel { width: 100%; } +.recon-hs-label { color: var(--muted); font-size: 12px; margin: 1px 0 8px; } +.recon-toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); margin: 0; cursor: pointer; } +.recon-ps-row { display: flex; align-items: center; width: 100%; gap: 4px; margin-top: 2px; } +.recon-ps-row .sel { width: 100%; font-size: 12px; padding: 4px 6px; } +.recon-ps-actions { display: flex; align-items: center; gap: 2px; margin-top: 4px; } +.recon-ps-actions .icon-btn { width: 28px; height: 28px; } +.recon-ps-actions .icon-btn svg { width: 18px; height: 18px; } .icon-btn { background: transparent; color: var(--muted); border: 0; border-radius: 50%; width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; } .icon-btn:hover { background: var(--surface-alt); color: var(--text); } .icon-btn:disabled { opacity: .38; cursor: default; } @@ -362,6 +378,8 @@ html.dark .recon-row-compare td { background: rgba(25, 118, 210, .18); } .recon-focus-body { margin-top: 18px; } .recon-focus-body-title { font-size: 16px; margin-bottom: 8px; color: var(--text); } .recon-focus-action-button { width: 100%; margin-bottom: 5px; } +.recon-focus-twin { background: var(--ok); } +.recon-focus-twin:hover { background: #689f38; } .recon-focus-detail { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; border-bottom: 1px solid var(--border); font-size: 13px; } .recon-focus-detail-label { color: var(--muted); flex: none; } .recon-sort-arrow { color: var(--muted); font-size: 11px; } @@ -410,6 +428,16 @@ html.dark .recon-pill.on { background: #1b3a23; color: #81c784; } .recon-map-box { position: relative; } .recon-map-box canvas { display: block; } .recon-map-box .recon-no-data { min-height: 60px; } +.recon-map-tip { + position: absolute; z-index: 20; min-width: 220px; max-width: 260px; + background: var(--surface); border: 1px solid var(--border); border-radius: 3px; + box-shadow: var(--shadow); padding: 8px 10px; pointer-events: none; font-size: 12px; + color: var(--text); +} +.recon-map-tip-row { padding: 3px 0; border-bottom: 1px solid var(--border); } +.recon-map-tip-row:last-child { border-bottom: 0; } +.recon-map-tip-ssid { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.recon-map-tip-meta { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ---- Reports view ---- */ .wigle-warn { color: #ef6c00; font-size: 12px; } diff --git a/payload/user/remote_access/pager-webui/www/index.html b/payload/user/remote_access/pager-webui/www/index.html index 30d1822..8d9e1d6 100644 --- a/payload/user/remote_access/pager-webui/www/index.html +++ b/payload/user/remote_access/pager-webui/www/index.html @@ -6,7 +6,7 @@