summaryrefslogtreecommitdiffstats
path: root/docs/refactor-node-ui.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-11 00:19:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-11 00:19:06 +0200
commitf059cb118c556d1f0279350507f74b8a47d5a98a (patch)
tree9a97762a844038a06134b4b7dcead1758477dfc1 /docs/refactor-node-ui.md
parentb045ba0010d69360b6a0265eb7c73a07900fe328 (diff)
downloadmeshbay-f059cb118c556d1f0279350507f74b8a47d5a98a.tar.gz
docs: remove the documents MESHBAY_DESIGN.md replaces
Twenty-four files, about 17 000 lines: the two architecture drafts, the three security reviews, eleven design notes, the roadmap, the decisions file, the v1–v4 archive, the deprecated user guide and the stale quickstart. Their content is in MESHBAY_DESIGN.md, and git history holds the originals. The reason to delete rather than keep bannered: a document that is superseded but present still gets read, and a reader cannot always tell which of two accounts of one mechanism is the live one. That was the argument for retiring the user guide rather than repairing it, and it applies to the whole set. What made this safe is the concordance. Roughly 290 comments and docstrings cite these files by section — `musicbay.md §6`, `mediacenter.md §5.5`, `draft-v6 §2.11` — and section 16 maps every one onto its replacement, so not a single comment needs editing to stay followable. It now says plainly that the files are gone and where to recover them, and it gained rows for the three reviews (their findings are section 13), and for the two guides. Four kept documents pointed into the set and were repointed first: `playlists.md` (nine references — it is a live proposal and must not dangle), `WINDOWS-PORT.md`, and CLAUDE.md's example. No dangling reference remains outside section 16. Two files were dropped from the list after checking what they hold. `HTTPS.md` is an operational runbook — Caddy, certificate renewal, DNS, troubleshooting — and MESHBAY_DESIGN.md deliberately covers no operations, so nothing would replace it; the versioned Caddyfile is the config, not the procedure. `cast-smart-tv.md` is the plan for the unbuilt DLNA phase of a feature whose first two phases ship, and section 11.4 summarises it in four lines rather than carrying the SSDP/UPnP work. There is no user guide now, and section 0.1 says so rather than leaving a reader to discover it. Suites green: 2258 passed, 4 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVoHVCcfBqud6ZjG4db3y7
Diffstat (limited to 'docs/refactor-node-ui.md')
-rw-r--r--docs/refactor-node-ui.md413
1 files changed, 0 insertions, 413 deletions
diff --git a/docs/refactor-node-ui.md b/docs/refactor-node-ui.md
deleted file mode 100644
index 7dbf3da..0000000
--- a/docs/refactor-node-ui.md
+++ /dev/null
@@ -1,413 +0,0 @@
-# Refactor: fold the node's local admin surface into the desktop Node page
-
-> **Superseded by `MESHBAY_DESIGN.md`.** This was the node admin surface refactor; its design
-> content now lives in §6.7.
->
-> It is kept because code comments, tests and other documents cite its
-> sections and its labels, and because it records reasoning a synthesis
-> compresses. **Where it disagrees with `MESHBAY_DESIGN.md`, the design
-> document is right; where either disagrees with the code, the code is.**
-> `MESHBAY_DESIGN.md` §16 maps every section reference here onto its
-> replacement, and §13 defines every label.
-
-> Status: **complete** (2026-09-01) — all six phases landed; see §11 for what
-> each did. This stays as the decision record.
-> Scope: the node's *own* administration surface. Group-level settings
-> (member-upload, apps, scan-settings) are not touched — they live in the group
-> UI and stay there.
->
-> **Net result:** `ui/app.py` is a JSON-only, ruff-clean loopback control API
-> (1074 → 453 lines); the server-rendered dashboard, the `/audit` HTML page,
-> the `meshbay-node ui` verb and three never-wired endpoints are gone; the
-> desktop client's Node page (`static/node-page.js`) gained an Overview tab,
-> node-wide Roster, Peers, a paginated Audit log with CSV export, index-cache
-> Maintenance, and an Unlink action, organised into six tabs; the pointless
-> `18000/tcp` firewall profiles were deleted. One capability is intentionally
-> gone: browser-based admin on a headless server — the CLI covers every
-> operation there.
-
----
-
-## 1. The distinction that matters
-
-"The local admin UI" is three things bundled in `packages/meshbay-node/src/meshbay_node/ui/`:
-
-| Component | Consumers today | Redundant with `node-page.js`? |
-|---|---|---|
-| **Loopback JSON API** `/api/*` (~35 routes), `127.0.0.1:{ui_port}`, per-run token | the **entire CLI** (`_daemon_api` — every verb is an HTTP call), `node-page.js` (via `platform.node.call`), `platform.node.detect()`, the create-group wizard's index-progress poll | **No.** This is the node's control plane. |
-| **Server-rendered HTML** — `GET /` (dashboard), `GET /audit` (`_DASHBOARD` / `_AUDIT_HTML` templates + `_render_*` helpers) | a human with a browser (`meshbay-node ui` prints a tokened URL; `ssh -L` from a remote host) | **Yes, entirely.** |
-| uvicorn server + middleware (token gate, CSP headers) | — | Stays — the API needs it. |
-
-**This refactor removes the HTML layer only.** Removing the module would break
-the CLI, node detection, and the Node page itself.
-
----
-
-## 2. Decision
-
-1. Enrich `packages/meshbay-hub/src/meshbay_hub/static/node-page.js` so it covers
- everything the HTML dashboard showed (peers, audit, index-cache maintenance,
- node-wide roster, node identity/QUIC/hub display).
-2. Add a **Unlink node** action (endpoint already exists, nothing surfaces it).
-3. Fix the audit view to show real usernames.
-4. Organise `node-page.js` into tabs.
-5. Delete `GET /` and `GET /audit` from `ui/app.py`, plus the templates, the
- `render` helpers, the `meshbay-node ui` CLI verb, and every doc/packaging
- reference to a browser admin UI.
-6. Keep the whole `/api/*` surface and the uvicorn server.
-
-**Accepted cost:** on a headless server (the meshbay.org model) the only admin
-path becomes the CLI. The CLI already covers every operation, so this is
-acceptable — but it is a real capability reduction and is recorded here as a
-conscious choice, not an oversight.
-
----
-
-## 3. Inventory — nothing is lost
-
-### 3a. HTML dashboard content → destination
-
-| Dashboard element | Backing endpoint | Destination |
-|---|---|---|
-| Node status badge, `needs` hints | `GET /api/status` | Node page header (already partly there) |
-| Stats: groups / files / peers / user | `GET /api/status` | Overview tab |
-| **Connected Peers** table | `GET /api/peers` | **Peers tab (new wiring)** |
-| Roster (node-wide) | `GET /api/roster?group_id=` | **Roster tab — extend the current per-group view to node-wide** |
-| Groups list | `GET /api/groups` | Groups tab (already there) |
-| Node Configuration: Hub URL, QUIC port, Node ID | `GET /api/status` (`endpoint_hint`, `quic_port`, `hub_url`) | Overview tab (new fields) |
-| Node Configuration: 5 `[node]` settings | `GET /api/node-settings` | Settings tab (already there) |
-| **Maintenance**: index-cache count + Prune | `GET /api/index-cache`, `POST /api/index-cache/prune` | **Settings/Maintenance tab (new wiring)** |
-| `/audit` page | `GET /api/audit` | **Audit tab (new wiring)** |
-
-### 3b. `/api/*` endpoints — consumer and disposition
-
-| Endpoint | Consumed by | Disposition |
-|---|---|---|
-| `GET /api/status` | Electron `node:detect`, CLI `status`, node-page | keep |
-| `DELETE /api/unlink` | *nothing* | keep + **surface in node-page** |
-| `GET /api/groups` | CLI, node-page | keep |
-| `POST /api/groups/attach` · `POST /api/groups/detach` | CLI, node-page (detach) | keep |
-| `DELETE /api/groups/{g}/files/{f}` | CLI `file rm`, group Files UI | keep |
-| `GET /api/groups/{g}/files` | CLI `file list`, HTML dashboard | keep (drop dashboard caller) |
-| `GET /api/denylist` · `POST /api/denylist/clear` | CLI, node-page | keep |
-| `GET /api/index-cache` · `POST /api/index-cache/prune` | **HTML dashboard only** | keep + **wire into node-page** |
-| `POST /api/groups/{g}/video/rematch` | CLI, group Videos UI | keep |
-| `GET /api/peers` | **HTML dashboard only** | keep + **wire into node-page** |
-| `GET /api/audit` | **HTML `/audit` only** | keep + **wire into node-page** + username fix (§5) |
-| `GET /api/config` | *nothing* | **triage — likely dead (§6b)** |
-| `POST /api/operator/pair` | CLI, node-page | keep |
-| `GET /api/roster` | CLI, node-page | keep (node-page uses node-wide mode too) |
-| `POST /api/groups/{g}/invites` · `GET /api/resolve` | CLI | keep |
-| `POST /api/members/{u}/revoke` | CLI | keep + optionally surface in node-page |
-| `POST /api/members/{u}/unpin` | CLI, node-page | keep |
-| `POST /api/groups/{g}/gek` | CLI `gek init/rotate`, node-page (rotate) | keep |
-| `POST /api/groups/{g}/roots` · `DELETE .../roots/{n}` | node-page | keep |
-| `GET /api/groups/{g}/index-status` | create-group wizard | keep |
-| `PUT /api/groups/{g}/member-upload` · `.../apps` · `.../scan-settings` | group Settings UI | keep (out of scope) |
-| `POST /api/reload` | CLI, node-page | keep |
-| `GET /api/node-settings` · `PUT /api/node-settings` | CLI `stun`, node-page | keep |
-| `GET /api/chat/history` · `WS /ws/chat` | *nothing* | **triage — likely dead (§6b)** |
-| `GET /` · `GET /audit` | browser only | **DELETE** |
-
-### 3c. CLI verbs
-
-`init reset status ui gek-init gek operator member group file video denylist stun
-reload restart-daemon calibrate-argon2`.
-
-Only **`ui`** goes away. `status` output drops its `admin UI meshbay-node ui`
-line. Nothing else changes — the CLI keeps talking to the same `/api/*`.
-
----
-
-## 4. Gap fills in `node-page.js`
-
-New sections/tabs, each a thin `nodeCall`:
-
-- **Peers** — `GET /api/peers` → table (user, IP, group, state). Poll ~5 s while visible.
-- **Audit** — `GET /api/audit` with the event/user/limit filters the HTML page had.
- Render every field as **text** (htm/preact escapes by default — never
- `dangerouslySetInnerHTML` on `detail`/`username`; `detail` carries
- member-chosen filenames, H2).
-- **Maintenance** — `GET /api/index-cache` (count) + `POST /api/index-cache/prune`.
-- **Roster (node-wide)** — call `GET /api/roster` with no `group_id`. Show every
- identity and its devices; keep the existing `unpin`, add `revoke_member` per
- member. (`revoke_device` for a single device is a later item — the endpoint
- does not exist yet.)
-- **Overview** — Node ID (`endpoint_hint`), QUIC port, hub URL, version, from
- `GET /api/status`.
-
-Tabs: **Overview · Groups · Roster · Peers · Audit · Settings**. Pure UI
-refactor of the current 749-line single scroll; no protocol change. Split into
-per-tab files if it grows (same move as `docs/apps.md` did for the group UI) —
-optional.
-
----
-
-## 5. Audit usernames
-
-**Symptom:** rows show `8f7e206e` (that is `user_id[:8]`, the fallback in the
-HTML template's `e.username || e.user_id.slice(0,8)`).
-
-**Cause:** the `username` column exists and `_audit()` already passes
-`username=self._username`, but it is blank for (a) legacy rows written before the
-column was populated and (b) events where the handshake payload carried no
-username (pre-handshake `auth_failed`, some token shapes).
-
-**Fix — resolve at read time, no migration:** in the audit endpoint (or a new
-`ops.get_audit`), build `{user_id: username}` from the roster
-(`roster.list_members()` + identities already expose `username` node-wide, same
-source `_render_roster` uses) and fill blanks:
-`row.username or roster_name.get(row.user_id) or row.user_id[:8]`.
-
-**Security:** this is **not** the stored-XSS concern. That concern is rendering
-attacker-influenced strings (`username` from the hub, `detail` filenames) as
-**markup**. Showing the real name is fine *as text*. The rule for the Preact
-Audit tab: interpolate, never inject HTML.
-
----
-
-## 6. Cleanup
-
-### 6a. Dead code / config once the HTML is gone
-
-- `ui/app.py`: `@app.get("/")`, `@app.get("/audit")`, `_DASHBOARD_*`/`_AUDIT_HTML`
- templates, `_render_dashboard`, `_render_node_settings`, `_render_roster` (HTML
- variant), `_render_audit_page`, and any `escape`-only helpers left unused.
-- `daemon.py`: the `ui` CLI verb + its `argparse` choice + help text + `quiet`
- list entry; the `_ui_url`/`ui` handler block; `log.info("Admin UI ready — open
- it with: meshbay-node ui")`; the `admin UI meshbay-node ui` line in
- `_print_status`.
-- `packaging/firewall/ufw/meshbay` and
- `packaging/firewall/firewalld/meshbay-node.xml`: drop the `18000/tcp` service.
- The UI binds `127.0.0.1` only (`daemon.py` `host="127.0.0.1"`), so these rules
- never did anything useful and become a footgun if the bind address ever
- changes. Remove regardless of the rest of this refactor.
-- Docs/man: `docs/QUICKSTART.md:110`, `QE/deploy/README.md:99`,
- `man/meshbay-node.1` (the `meshbay-node ui` entry), `packaging/deb/.../control`
- and `packaging/rpm/meshbay-node.spec` ("a local admin UI on localhost:18000").
-
-**Keep:** the uvicorn server, the token middleware, the CSP middleware (harmless
-and good hygiene on JSON responses), the `ui-token` file, `ui_port` in
-`node.toml`. Renaming `ui_*` → `localapi_*` is churn for no user value — deferred,
-noted only so the naming mismatch is a known thing.
-
-### 6b. Orphan endpoints — ✅ removed (phase 6)
-
-`GET /api/config`, `GET /api/chat/history`, `WS /ws/chat` (plus
-`broadcast_chat_to_ui` / `app.broadcast_chat` / `_chat_subscribers`) were a
-never-wired local operator chat view plus a superseded config dump. Confirmed
-dead against git history and removed. See phase 6 above.
-
----
-
-## 7. Security review
-
-The loopback API's model is unchanged and adequate: bind `127.0.0.1`; per-run
-token (`os.urandom(18)`, `ui-token` mode 0600, required as `?t=` or
-`X-MeshBay-Token`); CSP `default-src 'none'; connect-src 'self'`. Threats
-modelled: DNS-rebinding from the operator's browser, and any local process. The
-token closes both.
-
-- **Folding functions into `node-page.js` adds no surface** — every call goes
- through the same token-gated API; the Electron main process holds the token,
- the renderer never sees it (same model as `hub:fetch`).
-- **Do not shell out to the CLI from the Node page.** The CLI is itself an HTTP
- client of this API; spawning it would be a strictly worse `nodeCall` (process
- spawn, arg-injection, PATH assumptions, unstructured errors). If a function is
- missing from the API, add the endpoint. `main.js` shelling out to
- `systemctl --user` is a legitimate exception — OS service control has no API
- equivalent; node *operations* stay on the API.
-- **Audit tab:** render as text (§5).
-- **Unlink copy:** unlink clears only the hub's stored linking key
- (`DELETE /v1/users/me/node_key` → `user.pk_node_ed25519 = None`). It does **not**
- touch the roster, GEKs, pins, or on-disk group data — the node keeps serving
- anyone who reaches it directly over MNP. The UI must say "the hub stops
- brokering connections to this node", not imply a wipe. `reset` stays the
- destroy-everything path. After unlink, `POST /v1/nodes/auth` fails and the hub
- WebSocket drops — expected; show it as "offline from hub", not an error.
-
----
-
-## 8. Node-key linking — confirmed automatic (native)
-
-`create-group-page.js` `linkNodeKey(pk)` is called from `detectNode()` after
-`platform.node.detect()` returns the node's `pk_node_ed25519`; it
-`PUT /v1/users/me/node_key`. So in the desktop wizard the key links
-automatically on node detection. The manual paste box in `profile-page.js` is the
-fallback for the browser-only case (no bridge to read the node's key) and for
-re-linking. No change needed; the **unlink** counterpart is the only missing half
-(§2.2, §7).
-
----
-
-## 9. Draft correction
-
-`docs/meshbay-draft-v6.md` §2.11 and §2.12 describe the Node page as connecting
-**over MNP** (`node_status_ack`, `node_settings_set`). The code went
-loopback-HTTP instead (`node-page.js` header comment says so explicitly), which is
-better for a native client — no signaling round-trip, no GEK/session, works
-before any group is joined. The MNP handlers
-(`webrtc_server.py:_do_node_status`, `_do_node_settings_set`) are dormant.
-
-Update §2.11/§2.12 to state: the Node page uses the node's loopback API; the MNP
-`node_*` message types are reserved for a possible future browser-side (no
-Electron) path and are not the mechanism today.
-
----
-
-## 10. Tests
-
-- `test_cli_dispatch.py` — drop the `["ui", ...]` row; assert `ui` is no longer a
- parser choice.
-- New `ops`/endpoint test for audit username backfill from the roster.
-- Node-page: extend whatever source-reading test pins its structure (per the SPA
- testing posture) to cover the new tabs' `nodeCall` paths.
-- `test_packaging_units.py` / packaging tests — assert the firewall service files
- no longer open 18000.
-- Grep guard (or manual): no `meshbay-node ui` string left in `man/`, `docs/`,
- `packaging/`.
-
----
-
-## 11. Phases
-
-1. **Draft + firewall** — ✅ done 2026-09-01. Corrected draft-v6 §2.11/§2.12
- (loopback API, not MNP) with a 2026-09-01 amendment line. Deleted
- `packaging/firewall/firewalld/meshbay-node.xml`; stripped the `[MeshBay Node]`
- stanza from `packaging/firewall/ufw/meshbay` (the `[MeshBay Cast]` profile
- stays). Follow-on edits so the build still passes:
- `packaging/build/build-node.sh` (drop the firewalld copy),
- `packaging/rpm/meshbay-node.spec` `%files` (drop the `.xml` line),
- `docs/PACKAGING-GUIDE.md` (drop the "Node admin UI / TCP 18000" section). No
- behaviour change. The deb/spec "local admin UI on localhost:18000" description
- lines are left for phase 5.
-2. **node-page.js gap fills** — ✅ done 2026-09-01. Added five sections to the
- existing single-scroll page (tabs come in phase 4): **Overview** (version,
- node id, QUIC port, hub — from `GET /api/status`, fetched in
- `fetchStatus`/`refresh`), **Node roster** (`GET /api/roster` with no
- `group_id`, node-wide, with `unpin`), **Connected peers** (`GET /api/peers`),
- **Audit log** (`GET /api/audit` with event + limit filters, every field
- rendered as text), **Maintenance** (`GET /api/index-cache` +
- `POST /api/index-cache/prune`). Backend: `GET /api/audit` now backfills a
- blank `username` from `roster.list_identities()` at read time (no migration).
- ~31 `node.*` i18n keys added to all ten locale catalogues (English
- placeholders, matching the existing node-page convention). `test_locales.py`
- green; `node --check` green; node ops/audit/status/roster tests green.
- Per-member `revoke` in the node-wide view is deferred — revoke is
- group-scoped and the node-wide list has no group context.
-3. **Unlink** — ✅ done 2026-09-01. Added a danger-styled "Unlink from hub"
- section at the bottom of `node-page.js` (`unlinkNode` → `DELETE /api/unlink`,
- confirm + consequence copy per §7, hidden while `status ===
- 'waiting_for_node_key'`). Backend endpoint was already present and unchanged.
- 5 `node.unlink_*` i18n keys added to all ten locales; `.node-group-danger`
- added to `style.css`. `node --check` + `test_locales.py` green. Note: the
- endpoint 503s when the node has no hub session — the catch surfaces the
- bridge message, which is acceptable for that edge case.
-4. **Tabs** — ✅ done 2026-09-01. `node-page.js` now has a `tab` state and an
- `.admin-tabs` bar (reusing the existing admin-page tab styles) with six tabs:
- **Overview** (node info + Maintenance), **Groups** (per-group cards),
- **Roster** (node-wide roster), **Peers**, **Audit**, **Settings** (denylist +
- node settings + STUN + ICE + Unlink). Header, service panel, action message
- and the operator-pair banner stay above the bar, visible on every tab. Each
- section is gated `${tab === '<x>' && …}`; the groups IIFE is
- `${tab === 'groups' && (() => {…})()}`. 6 `node.tab_*` i18n keys added to all
- ten locales. Verified with a per-tab render probe (htm parses and the
- component executes cleanly for all six tabs) plus `node --check` and
- `test_locales.py`.
-5. **Remove HTML** — ✅ done 2026-09-01.
- - `ui/app.py`: deleted `GET /`, `GET /audit`, and everything below the
- `# ── HTML UI ──` marker (`_render_page` + inline dashboard template,
- `_render_roster`, `_render_node_settings`, `_render_audit_page`,
- `_AUDIT_HTML`, `_fmt_size`). ~500 lines gone. Dropped now-unused imports
- (`base64`, `time`, `html.escape`, `HTMLResponse`). Module docstring rewritten
- — it is a JSON control API, not a web UI. The uvicorn server, the token
- middleware and the CSP middleware stay.
- - `daemon.py`: removed the `ui` argparse choice + its help text + `quiet`
- entry + the `if args.command == "ui":` handler. Startup log line is now
- `Control API on 127.0.0.1:<port>`; `_print_status` drops the `admin UI`
- line; the "waiting_for_node_key" hint now points at `meshbay-node status`
- and the desktop client. `_daemon_api` docstring reworded. (Net effect on
- ruff: one F541 fewer; the file's pre-existing lint debt is untouched.)
- - Tests: `test_cli_dispatch.py` drops the `["ui"]` case and asserts `ui` is
- no longer a parser choice. The two `_render_page` H2 tests
- (`test_admin_ui_escapes_filenames`, `_roster_usernames`) are replaced by
- `test_node_control_api_serves_no_html`, which pins the routes at 404 and
- the render helpers as absent. Full node suite: 749 passed, same 2
- pre-existing failures (`test_packaging_units` bad split logic;
- `test_webrtc_transport` aiortc flake).
- - Docs/packaging: `docs/QUICKSTART.md`, `man/meshbay-node.1` (the `ui` verb
- entry deleted, "admin UI" → "control API" throughout),
- `packaging/deb/.../control`, `packaging/rpm/meshbay-node.spec`,
- `devel-phases-next.md` (14.1b struck through), `docs/invite-pairing-v1.md`
- (`_render_roster` pointer → `node-page.js`), `CLAUDE.md` key-modules row,
- `config.py` example-config comment. `QE/deploy/README.md` also touched but
- it is gitignored.
-6. **Orphan-endpoint triage** — ✅ done 2026-09-01. All three confirmed dead and
- removed from `ui/app.py`:
- - `GET /api/config` — no consumer (the CLI, SPA and Electron never call it);
- its payload is a strict subset of `/api/status` + `/api/groups` +
- `/api/node-settings`. `test_security_regressions.py` used it only as a
- sample path for the token-gate check → switched to `/api/groups`.
- - `GET /api/chat/history` — reads `state["chat_store"]`, a **global** store
- that the daemon deliberately never sets (per-group stores since finding
- H1), so it always returned `{"messages": []}`; the message shape was stale
- too. The real chat-history path is the MNP `chat_history` handler in
- `webrtc_server.py` (unchanged, `test_transport_contracts.py` still green).
- - `WS /ws/chat` + `_chat_subscribers` + `broadcast_chat_to_ui` +
- `app.broadcast_chat` — added in the Phase-7 "Node v2" commit and never
- wired: `git grep broadcast_chat` finds only the definition and the
- assignment, nothing invokes it, so the socket's subscribers received
- nothing.
- - Fallout: dropped now-unused imports (`json`, `WebSocket`,
- `WebSocketDisconnect`) and ran `ruff --fix` for the file's remaining
- pre-existing import debt (`Path`, `DEFAULT_CONFIG_PATH`, `generate_gek`,
- `wrap_gek_aes`, `ROLE_MEMBER`, `ROLE_OPERATOR`, the function-local
- `Roster`). **`ui/app.py` now passes `ruff check` with no errors** (1074 →
- 453 lines across phases 5–6).
- - Tightened the `_security_headers` CSP to `default-src 'none'; frame-ancestors
- 'none'; base-uri 'none'` — the JSON-only API has no HTML, inline script or
- stylesheet to allow, so the old `'unsafe-inline'` / `connect-src 'self'`
- grants were dead weight. No test asserted the string. Middleware docstrings
- refreshed ("admin UI" → "control API").
-
-4b. **Node page UI polish** — ✅ done 2026-09-01 (operator feedback on phase 4):
- - Overview/Maintenance, Roster, Peers and Audit tabs **auto-load on open** —
- the per-tab "Load" buttons are gone (`useEffect` on `[tab, status]`, audit
- also on filter/page). Per-group roster and denylist keep their buttons.
- - **Names, not ids**: `ui/app.py` gained `_display_names(state)` →
- `(user_id→username from roster, group_id→name from node.toml)`. `/api/peers`
- resolves both (`username`, new `group_name`); `/api/audit` adds
- `group_name` and keeps the username backfill. The SPA renders the name and
- falls back to a shortened id.
- - **Audit pagination**: `get_entries` gained `offset`; `/api/audit` takes
- `offset`, clamps `limit` to 1000, and fetches `limit+1` to report
- `has_more` without a count. The tab has Previous/Next + "Page N", newest
- first, page size 50/100/200/500. `test_audit.py::test_pagination_newest_first`.
- - **Audit overflow**: table wrapped in `.node-table-scroll` (`overflow-x:auto`);
- `.node-table-audit` keeps time/event/user/ip/group on one line (IP never
- clipped) and wraps only the detail column.
- - **CSV export**: `Export CSV` button → `exportAuditCsv` walks the whole log
- for the current filter in 1000-row pages (de-duped by id so an event
- written mid-export cannot duplicate a row; 1M-row hard stop) and
- `saveCsv()` writes every matching entry — native Save As on the desktop
- (`platform.nativeSave`), blob download link otherwise. RFC-4180 quoting.
- - Settings tab section order: node settings → STUN → ICE → **denylist** →
- **Unlink** (denylist sits directly above the danger zone).
- - 6 new i18n keys (`node.loading`, `node.audit_export/_exporting/_prev/_next/_page`),
- 4 dead `*_load` keys removed, across all ten locales. Per-tab render probe +
- `node --check` + `test_locales.py` + node test subset (193) green.
-
-Phases 1–4 lose nothing and can land independently. Phase 5 is the one that
-removes a capability (browser admin on a headless box) and should be its own
-reviewed commit.
-
----
-
-## 12. Out of scope
-
-- Group-level settings UI (member-upload, apps, scan-settings) — stays in the
- group UI.
-- Renaming `ui_port` / `ui-token` / `create_ui_app`.
-- `revoke_device` (single-device revocation) — needs a new endpoint; track
- separately.
-- Any MNP browser-side node admin path.