ui: recon card redesign, channel-map hover, Send to PineAP prefill, recon hardening

- Top 5 recon cards restyled as stat cards: value + sub-line + mini chart,
  consistent 190px anatomy; Handshakes links to the handshakes page;
  Previous Scans shows count/latest plus a compact picker + action row
- Channel map records lobe geometry (canvas.__reconLobes / __reconLobesHit);
  hovering lists the networks under the cursor, click pins the tooltip,
  mouseleave hides it; empty states hide the tooltip too
- Recon focus sidebar gains 'Send to PineAP — Twin this network': routes to
  Evil WPA (psk2/sae/owe from recon encryption) or OpenAP (with BSSID) and
  prefills the form; one-shot PineAPPrefill is consumed by the attack
  launcher and never auto-deploys or leaks into manual forms
- Existing Actions verified end-to-end and hardened: capture/stop handshake
  sync the auto-collect toggle, examine buttons disable while pending
- Recon resilience: loadDetail retries after failed render or 503 (no more
  permanently blank page), scan-list errors surface in the scan bar, poll
  guard prevents overlapping loads, chart draws are individually isolated
- Auto-collect toggle re-syncs from get_config on the slow poll so the UI
  tracks the pager's own settings
This commit is contained in:
2026-08-18 23:49:49 -05:00
parent 84ce19b2b3
commit d7ef0624f4
5 changed files with 537 additions and 148 deletions
@@ -161,6 +161,8 @@ const MiniChart = (() => {
// Channel map: each AP is a raised-cosine lobe at its reported center
// frequency with the peak at its signal strength. The radio does not report
// channel width, so every lobe assumes 20 MHz (half-width +-10 MHz).
// Lobe geometry (CSS px) is recorded on the canvas so the caller can
// hit-test pointer position against the networks under the cursor.
function channelMap(canvas, aps, opts) {
const o = opts || {};
const dpr = window.devicePixelRatio || 1;
@@ -171,6 +173,7 @@ const MiniChart = (() => {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = canvas.clientWidth, h = H;
ctx.clearRect(0, 0, w, h);
canvas.__reconLobes = [];
if (!aps || !aps.length) return;
const pts = aps
.map((a) => ({
@@ -211,6 +214,7 @@ const MiniChart = (() => {
const topY = y(a.signal);
const peakH = Math.max(2, baseY - topY);
const half = Math.max(4, (10 / (fMax - fMin)) * plotW);
canvas.__reconLobes.push({ ap: a, cx: cx, half: half, topY: topY, baseY: baseY });
ctx.beginPath();
for (let i = 0; i <= 28; i++) {
const t = -1 + i / 14;
@@ -227,6 +231,20 @@ const MiniChart = (() => {
ctx.lineWidth = 1.5;
ctx.stroke();
});
// Hit test: list networks whose lobe is under (mx, my) in CSS px.
canvas.__reconLobesHit = function (mx, my, padPx) {
const pad = padPx == null ? 4 : padPx;
const hits = [];
(this.__reconLobes || []).forEach((l) => {
const dx = mx - l.cx;
if (Math.abs(dx) > l.half + 2) return;
const t = Math.min(1, Math.max(-1, dx / l.half));
const lift = Math.max(0, 0.5 + 0.5 * Math.cos(Math.PI * t));
const lobeTop = l.baseY - lift * (l.baseY - l.topY);
if (my >= lobeTop - pad && my <= l.baseY + pad) hits.push(l.ap);
});
return hits;
};
let lastLabelX = -Infinity;
ctx.textAlign = 'center';
aps.forEach((a) => {