docs: implementation plan — encryption landscape card ring + key redesign

This commit is contained in:
2026-08-19 06:52:49 -05:00
parent 8bd560f8e9
commit 66a62ce234
@@ -0,0 +1,179 @@
# Encryption Landscape Card — Ring + Key Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the Encryption Landscape recon card's text headline + clipped canvas legend with a full-size plain-hole ring graph and a wrapping HTML legend (dot + label + count per encryption bucket).
**Architecture:** Single-file client-side change: the recon view in `views.js` drops the `encValue`/`encSub` text nodes, draws the doughnut with `legend:false` and a taller height, and populates a new HTML legend container from the existing `encCounts` bucket map. `chart.js`'s `MiniChart.doughnut` is unchanged (its canvas `legend` option is simply no longer used by the enc card). CSS adds flex-wrap legend styles.
**Tech Stack:** Vanilla JS (no framework), canvas via `MiniChart.doughnut` in `chart.js`, plain CSS in `app.css`. Device deploy via `scripts/deploy.sh --password '<device-password>'`. Tests: none exist for the frontend; verification is via the deployed device + backend test suite (must stay green).
## Global Constraints
- Do not alter data source, `reconEncBucket`, per-scan bucketing, re-sync, or the other four recon cards.
- Legend entries: colored dot + label + count, format `● WPA2 54`; buckets with zero APs hidden.
- Ring center hole stays plain (no text).
- Keep `MiniChart.doughnut`'s `legend` option in `chart.js` (used by no caller after this change, but harmless).
- Empty state text stays "No encryption data yet — run a scan."
- No comments added to code unless already present in the surrounding style.
- Deploy and verify on the device; backend `tests/` suite must stay green (291 tests).
---
### Task 1: Ring + HTML legend for Encryption Landscape card
**Files:**
- Modify: `payload/user/remote_access/pager-webui/www/js/views.js:1497-1507` (card markup)
- Modify: `payload/user/remote_access/pager-webui/www/js/views.js:2256-2322` (drawCharts enc block)
- Modify: `payload/user/remote_access/pager-webui/www/css/app.css` (after line 328)
- Test: none (frontend); verify via device sweep
**Interfaces:**
- Consumes: `RECON_ENC_BUCKETS` (6 names, `views.js:852`), `RECON_ENC_COLORS` (6 colors, `views.js:851`), `encCounts` object `{bucketName: count}`, `reconEncBucket(a.encryption)`.
- Produces: DOM `div#recon-enc-legend` under the enc card body, populated by `drawCharts`; `canvas#recon-encryption` redrawn with `{ legend: false, height: 120 }`. `encValue`/`encSub` variables are removed.
- [ ] **Step 1: Edit the enc card markup in views.js**
Replace lines 1497-1507:
```js
const encBody = titleCard('Encryption Landscape', null);
const encValue = h('div', { class: 'recon-card-value', text: '—' });
const encSub = h('div', { class: 'recon-card-sub', text: '' });
encBody.appendChild(encValue);
encBody.appendChild(encSub);
const encBox = h('div', { class: 'recon-chart-box' });
encBody.appendChild(encBox);
const encCanvas = h('canvas', { id: 'recon-encryption' });
encBox.appendChild(encCanvas);
const encEmpty = h('div', { class: 'recon-no-data', text: 'No encryption data yet — run a scan.' });
encBox.appendChild(encEmpty);
```
with:
```js
const encBody = titleCard('Encryption Landscape', null);
const encBox = h('div', { class: 'recon-chart-box' });
encBody.appendChild(encBox);
const encCanvas = h('canvas', { id: 'recon-encryption' });
encBox.appendChild(encCanvas);
const encEmpty = h('div', { class: 'recon-no-data', text: 'No encryption data yet — run a scan.' });
encBox.appendChild(encEmpty);
const encLegend = h('div', { class: 'recon-enc-legend' });
encBody.appendChild(encLegend);
```
- [ ] **Step 2: Remove the enc text-headline computation in drawCharts**
Replace lines 2256-2266 (the `encCounts` loop stays — only the `topEnc` block goes):
```js
let topEnc = null, topEncN = 0;
Object.keys(encCounts).forEach((k) => {
if (encCounts[k] > topEncN) { topEnc = k; topEncN = encCounts[k]; }
});
encValue.textContent = topEnc || '—';
encSub.textContent = topEnc ? topEncN + ' of ' + n + ' APs' : '';
```
with nothing (delete those lines). The `encCounts` computation immediately above must remain.
- [ ] **Step 3: Rewrite the enc chart block in drawCharts**
Replace lines 2308-2322:
```js
const enc = document.getElementById('recon-encryption');
if (enc && typeof MiniChart !== 'undefined' && MiniChart.doughnut) {
try {
if (aps.length) {
MiniChart.doughnut(enc, RECON_ENC_BUCKETS.map((k, i) => ({
label: k, value: encCounts[k] || 0, color: RECON_ENC_COLORS[i]
})), { legend: true, height: 66 });
enc.classList.remove('hidden');
encEmpty.classList.add('hidden');
} else {
enc.classList.add('hidden');
encEmpty.classList.remove('hidden');
}
} catch (e) {}
}
```
with:
```js
const enc = document.getElementById('recon-encryption');
if (enc && typeof MiniChart !== 'undefined' && MiniChart.doughnut) {
try {
if (aps.length) {
MiniChart.doughnut(enc, RECON_ENC_BUCKETS.map((k, i) => ({
label: k, value: encCounts[k] || 0, color: RECON_ENC_COLORS[i]
})), { legend: false, height: 120 });
enc.classList.remove('hidden');
encEmpty.classList.add('hidden');
} else {
enc.classList.add('hidden');
encEmpty.classList.remove('hidden');
}
} catch (e) {}
}
const encLegend = document.getElementById('recon-enc-legend');
if (encLegend) {
encLegend.innerHTML = '';
RECON_ENC_BUCKETS.forEach((k, i) => {
const c = encCounts[k] || 0;
if (!c) return;
const entry = h('div', { class: 'recon-enc-entry' });
const dot = h('span', { class: 'recon-enc-dot' });
dot.style.background = RECON_ENC_COLORS[i];
entry.appendChild(dot);
entry.appendChild(h('span', { class: 'recon-enc-label', text: k }));
entry.appendChild(h('span', { class: 'recon-enc-count', text: String(c) }));
encLegend.appendChild(entry);
});
}
```
- [ ] **Step 4: Add the legend CSS to app.css**
Insert after line 328 (the `.recon-no-data` rule):
```css
.recon-enc-legend { display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 4px; align-items: baseline; }
.recon-enc-entry { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text); }
.recon-enc-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.recon-enc-label { color: var(--muted); }
.recon-enc-count { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
```
- [ ] **Step 5: Syntax check both JS files**
Run: `node --check payload/user/remote_access/pager-webui/www/js/views.js`
Run: `node --check payload/user/remote_access/pager-webui/www/js/chart.js`
Expected: exit 0, no output.
- [ ] **Step 6: Run the backend test suite**
Run: `cd <repo> && python3 -m pytest tests/ -q 2>&1 | tail -3`
Expected: `291 passed` (or the current passing count) — no regressions from unrelated files.
- [ ] **Step 7: Deploy to the device**
Run: `cd <repo> && ./scripts/deploy.sh --password '<device-password>'`
Expected: deploy completes with `EXTRACT_OK` / success output.
- [ ] **Step 8: Verify the enc card on the device**
Recreate the CDP venv if absent (`python3 -m venv /tmp/cdpenv2 && /tmp/cdpenv2/bin/pip install -q websocket-client`), then drive headless Chrome against http://172.16.52.1:8080 (login `<device-password>`, go to `#/recon`, wait ~20s) and assert:
1. `document.getElementById('recon-encryption')` canvas has non-zero `width` attribute and the card is visible (not `.hidden`).
2. `document.getElementById('recon-enc-legend')` contains entries whose text matches `/WPA2/` and `/\d+/`, and no `recon-card-value` element exists inside the enc card.
3. Zero `Runtime.exceptionThrown` events.
Expected: all three pass; screenshots unavailable, text-state assertions only.
- [ ] **Step 9: Commit**
```bash
cd <repo> && git add payload/user/remote_access/pager-webui/www/js/views.js payload/user/remote_access/pager-webui/www/css/app.css && git commit -m "ui: encryption landscape card — ring + HTML legend with counts"
```