feat(devices): latest build tracking + update/reinstall actions with live-action guard #2

Merged
HexC0d3rz merged 2 commits from feat/device-build-and-reinstall into main 2026-06-13 22:17:28 -04:00
HexC0d3rz commented 2026-06-13 22:00:05 -04:00 (Migrated from github.com)

Summary

Adds the build-tracking and reinstall primitives for the device detail page:

  • "Latest build on main" metadata row — alongside the existing "Agent
    build" row, the device detail page now shows the build CI most recently
    published for the device's OS (latest_build_id /
    latest_build_published_at on DeviceSummary, both in the list and
    single-device API responses), with an "up to date" / "outdated" chip.
  • Always-available "Update agent" button — no longer hidden behind
    agent_outdated, so an operator can re-push the current build (e.g. to
    recover a corrupted install).
  • New "Reinstall agent" button with a dialog offering:
    • Standard reinstall — agent uninstalls itself; device row is kept;
      response includes the installer URL to re-run manually.
    • Clean install — same uninstall, plus the device row is wiped and a
      fresh one-time enrollment token (1h expiry, reuses EnrollmentCode) is
      issued. Requires typing the device hostname to confirm.
  • POST /api/devices/{id}/update — 404 if no build is published for
    the OS, 409 { "error": "device_offline" } if the agent isn't connected,
    otherwise pushes ServerMessage::Update and returns
    { "queued": true, "build_id": ... }. Audit-logged as device.update_agent.
  • POST /api/devices/{id}/reinstall — requires confirm: true; sends
    ServerMessage::Uninstall if the agent is connected. Audit-logged as
    device.reinstall / device.reinstall_clean.
  • Live-action safety guard — pushing Update/Uninstall to a
    connected agent now additionally requires ZENO_ALLOW_LIVE_AGENT_ACTIONS=1
    on the server and confirm_live: true in the request body, returning
    403 { "error": "live_actions_disabled", "hint": ... } otherwise. This
    was added after an earlier validation pass against this branch's endpoints
    accidentally triggered a real self-update + uninstall on a live connected
    agent (Alonzo-PC, since re-enrolled).

Tests

  • cargo test -p zeno-server — 52 passed, including new unit tests for
    is_agent_outdated, latest_published_build (mtime fallback for
    published_at), ReinstallRequest confirm validation, enrollment-code
    uniqueness/format, and the live-action guard's env-var + flag combinations.
  • node --test crates/zeno-server/tests/*.test.mjs — including new
    device_build_status.test.mjs covering the "Latest build on main"
    chip/text logic.
  • cargo build -p zeno-server clean; Docker dev image rebuilt and the
    container is healthy on port 8090.
  • Not done: real-browser smoke test of the new dialog/buttons (no live
    agent actions were performed in this pass per the new guard).

Files changed

  • crates/zeno-server/src/agent_updates.rsPublishedBuild.published_at
    (with mtime fallback) + tests
  • crates/zeno-server/src/api/devices.rslatest_build_* fields,
    /update and /reinstall endpoints, live-action guard, tests
  • crates/zeno-server/src/api/mod.rs — route registration
    (/devices/{id}/update, /devices/{id}/reinstall)
  • crates/zeno-server/src/auth.rs — enrollment code uniqueness test
  • crates/zeno-server/src/db/models.rsAuditEntry::record
  • crates/zeno-server/src/web/templates.rs +
    templates/device_detail.html — "Latest build on main" row, always-on
    Update button, new Reinstall button
  • crates/zeno-server/src/web/static/js/device_detail.js — build-status
    rendering (ZenoDeviceBuild.buildStatus), /update call with
    confirm_live: true
  • crates/zeno-server/src/web/static/js/device-reinstall.js — new reinstall
    dialog (standard vs. clean, hostname confirmation)
  • crates/zeno-server/tests/device_build_status.test.mjs — new
  • crates/zeno-server/Cargo.tomltempfile dev-dependency
## Summary Adds the build-tracking and reinstall primitives for the device detail page: - **"Latest build on main" metadata row** — alongside the existing "Agent build" row, the device detail page now shows the build CI most recently published for the device's OS (`latest_build_id` / `latest_build_published_at` on `DeviceSummary`, both in the list and single-device API responses), with an "up to date" / "outdated" chip. - **Always-available "Update agent" button** — no longer hidden behind `agent_outdated`, so an operator can re-push the current build (e.g. to recover a corrupted install). - **New "Reinstall agent" button** with a dialog offering: - *Standard reinstall* — agent uninstalls itself; device row is kept; response includes the installer URL to re-run manually. - *Clean install* — same uninstall, plus the device row is wiped and a fresh one-time enrollment token (1h expiry, reuses `EnrollmentCode`) is issued. Requires typing the device hostname to confirm. - **`POST /api/devices/{id}/update`** — 404 if no build is published for the OS, 409 `{ "error": "device_offline" }` if the agent isn't connected, otherwise pushes `ServerMessage::Update` and returns `{ "queued": true, "build_id": ... }`. Audit-logged as `device.update_agent`. - **`POST /api/devices/{id}/reinstall`** — requires `confirm: true`; sends `ServerMessage::Uninstall` if the agent is connected. Audit-logged as `device.reinstall` / `device.reinstall_clean`. - **Live-action safety guard** — pushing `Update`/`Uninstall` to a *connected* agent now additionally requires `ZENO_ALLOW_LIVE_AGENT_ACTIONS=1` on the server **and** `confirm_live: true` in the request body, returning `403 { "error": "live_actions_disabled", "hint": ... }` otherwise. This was added after an earlier validation pass against this branch's endpoints accidentally triggered a real self-update + uninstall on a live connected agent (`Alonzo-PC`, since re-enrolled). ## Tests - `cargo test -p zeno-server` — 52 passed, including new unit tests for `is_agent_outdated`, `latest_published_build` (mtime fallback for `published_at`), `ReinstallRequest` confirm validation, enrollment-code uniqueness/format, and the live-action guard's env-var + flag combinations. - `node --test crates/zeno-server/tests/*.test.mjs` — including new `device_build_status.test.mjs` covering the "Latest build on main" chip/text logic. - `cargo build -p zeno-server` clean; Docker dev image rebuilt and the container is healthy on port 8090. - Not done: real-browser smoke test of the new dialog/buttons (no live agent actions were performed in this pass per the new guard). ## Files changed - `crates/zeno-server/src/agent_updates.rs` — `PublishedBuild.published_at` (with mtime fallback) + tests - `crates/zeno-server/src/api/devices.rs` — `latest_build_*` fields, `/update` and `/reinstall` endpoints, live-action guard, tests - `crates/zeno-server/src/api/mod.rs` — route registration (`/devices/{id}/update`, `/devices/{id}/reinstall`) - `crates/zeno-server/src/auth.rs` — enrollment code uniqueness test - `crates/zeno-server/src/db/models.rs` — `AuditEntry::record` - `crates/zeno-server/src/web/templates.rs` + `templates/device_detail.html` — "Latest build on main" row, always-on Update button, new Reinstall button - `crates/zeno-server/src/web/static/js/device_detail.js` — build-status rendering (`ZenoDeviceBuild.buildStatus`), `/update` call with `confirm_live: true` - `crates/zeno-server/src/web/static/js/device-reinstall.js` — new reinstall dialog (standard vs. clean, hostname confirmation) - `crates/zeno-server/tests/device_build_status.test.mjs` — new - `crates/zeno-server/Cargo.toml` — `tempfile` dev-dependency
Sign in to join this conversation.
No description provided.