approve_node never rotates an existing key; node update-check endpoint; real Update Node button (node-26#14, #15)

- approve_node: reuses node_keys/{id}.api_key when one exists (still
  re-syncs dynsec with it, still never overwrites node_keys). Re-clicking
  Approve can no longer lock a live node out. Reissue stays explicit.
- POST /nodes/{id}/request-update (admin): sets update_requested_at on the
  node doc AND sends MQTT node_update best-effort, so offline/locked-out
  nodes still get it.
- POST /nodes/{id}/update-check (node, inline auth: Bearer api_key for THAT
  node or X-Pickup-Secret): host updater's HTTPS check-in. Returns only
  update_requested_at; records updater_last_check_at / sha / applied result.
  Unknown node -> 401 (no node_id oracle). C2 cannot name a ref, image or
  command.
- Frontend: "Update Node Software" (admin only) calls request-update, honest
  confirm text; node page shows updater check-in and last update state.

Tests: tests/test_node_provisioning.py (12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-04 01:09:52 -04:00
co-authored by Claude Opus 5.5
parent fb299008f8
commit 7fa50f24d2
6 changed files with 352 additions and 16 deletions
+7
View File
@@ -72,6 +72,13 @@ export interface NodeRecord {
is_overridden?: boolean;
override_system_id?: string | null;
override_timeout_at?: string | null;
/** node-26#15 host updater: last "Update Node" request, and what the node did with it. */
update_requested_at?: string | null;
update_applied_at?: string | null;
update_result?: "ok" | "failed" | null;
/** Last HTTPS check-in from the host updater — alive even when MQTT is not. */
updater_last_check_at?: string | null;
updater_sha?: string | null;
}
export interface SdrDevice {