chore(agent): add phase 10 — dev dials the fleet directly

Six tasks that make dev the prod topology: the documented `ops`-installable agent sudoers
grant, installing the agents as systemd services, the upsd ACL for the host and laptop
ranges, `.env.dev` pointing at the real hosts (token in the URL userinfo, `NUT_PORT=3051`),
retiring `scripts/dev-tunnels.sh`, and a live + browser verification pass.

No dashboard Python changes — deployment, config, scripts and docs only.

LLM-Generated: true
This commit is contained in:
2026-10-17 12:27:27 -04:00
parent 9015931860
commit 71900d903a
7 changed files with 448 additions and 0 deletions
@@ -0,0 +1,104 @@
# Phase 10 — Dev dials the fleet directly (no more SSH tunnels)
**Source:** user request (2026-10) — "I don't want this separation of architectures. My laptop can
talk to these hosts directly via local network or vpn already. Just have dev work like prod."
**Story:** n/a
**Context:** `AGENTS.md` (dev workflow: `scripts/dev-tunnels.sh`, `DEV_TUNNELS`/`DEV_FORWARDS`),
`deploy/install-agent.sh`, `deploy/nut-sudoers`, `scripts/dev-server.sh`,
`scripts/capture-fixtures.sh`, `tests/test_live_fleet.py`, `.env.dev.example`, `.env.example`,
`README.md`
## Objective
Dev becomes the prod topology: each host's agent runs as a **systemd service** (installed by
`ops` through a documented minimal sudoers grant), bound to the address the dashboard dials,
token in the URL userinfo; driveripper's upsd ACLs the host and laptop IP ranges on port 3051;
`.env.dev` points at the real hosts with no `DEV_TUNNELS`/`DEV_FORWARDS`; `scripts/dev-tunnels.sh`
is retired. The dashboard's Python is untouched — this phase changes deployment, config, scripts
and docs, and proves parity with the live suite plus browser screenshots.
## Decisions (user, 2026-10)
- The agent is a **systemd service, installed by the `ops` user** (not root). The repo documents
the **simplest possible working sudoers addition** in `deploy/agent-sudoers`, and the user adds
it to each host.
- The upsd ACL allows the **host's IP range and the laptop's IP range** — both real ranges,
derived at install time and commented in `.env.dev` so they can be re-derived when the laptop's
IP changes.
## Precondition (user action, before task 02)
The sudoers line needs root, and the command-scoped sudo on driveripper does not include
`visudo`. The user adds the one line from `deploy/agent-sudoers` on each host. If it is missing
when task 02 runs, that task must **stop cleanly and print the exact instructions** — never loop,
never wait, never attempt to edit sudoers itself.
## Dependencies
- `05_ups_hero_battery_glyph` (complete) — the last UI phase; its pages are what this phase
re-verifies against live data after the transport change.
- `06_system_page_gpu_verdicts`, `07_llm_monitor_style_pages`, `08_events_paging_and_copy`,
`09_disks_smartctl` (todo) — independent: this phase changes transport and ops, not payloads or
page behaviour, and they do not depend on the transport.
## Tasks
1. `01_sudoers_grant_and_installer_hint.md` — `deploy/agent-sudoers` (the minimal working grant
+ install instructions, in the `nut-sudoers` house style); `install-agent.sh` run without
root prints the exact line to add.
2. `02_install_agents_on_fleet.md` — on all three hosts: kill the ad-hoc agent, stage the
installer, install the systemd service bound to the dialed address, capture the printed
tokens, verify from the laptop.
3. `03_upsd_acl_for_laptop.md` — driveripper: append ACL lines for the host range and the laptop
range to `upsd.conf` (read-first, `cmp`-after, never blind-write), restart `nut-server`, verify
the NUT wire from the laptop.
4. `04_env_dev_direct_dial.md` — stop the tunnels; rewrite the local `.env.dev` to direct targets
(userinfo tokens, `NUT_HOST`/`NUT_PORT=3051`); restart the dev server; sources ok.
5. `05_retire_tunnels_update_scripts.md` — delete `scripts/dev-tunnels.sh`; rewrite
`.env.dev.example` direct-shaped; fix `capture-fixtures.sh`, the live suite's docstring and
skip messages, `ups_contract.py`, the `alerts.py` docstring, `test_config.py`, `.env.example`'s
NUT note, and the dev-workflow sections of `README.md`/`AGENTS.md`.
6. `06_verify_direct_dial_live.md` — full live verification: `/api/health` ok over direct dial,
`uv run pytest -m live --live` green, browser screenshots of `/` + `/system` + `/ups` + `/llm`
on live data, no tunnel port needed on the laptop.
## Testing & Quality
- The Python changes in this phase are test-file strings and one docstring — no new logic. The
full suite must stay 100% green and overall coverage must not drop (gate: `--cov-fail-under=94`).
- `bash -n` on `deploy/install-agent.sh` and `scripts/capture-fixtures.sh` after their edits.
- The real proof of this phase is the live path: `uv run pytest -m live --live` against the
directly-dialed fleet (the same production code paths over the prod-shaped transport) plus a
browser screenshot per `AGENTS.md` rule 10.
- The phase is "Complete" only when the **entire** project test suite passes 100% (every test in
the project, not just the phase's — pre-existing failures included) and the coverage gate in
`.agents/validate.sh` holds.
## Documentation & Consistency
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands
after this phase — fixing every inconsistency found, **including pre-existing ones not caused
by the phase** (including any remaining mention of `dev-tunnels.sh`, `DEV_TUNNELS`,
`DEV_FORWARDS`, `9121`/`9122`/`9123` or `3494`).
- Task 05 updates `README.md`/`AGENTS.md` immediately for the user-visible dev-workflow change.
- `PLAN.md` is locked and must not be edited: its Data-Model note ("forwarded to
`127.0.0.1:3494` in dev") goes stale after this phase — flag it in the final pass's report to
the user, and leave the file untouched.
## Completion Criteria
- [ ] `systemctl status dashboards-agent` is active on all three hosts, each bound to the address
the dashboard dials, answering `/health` (unauthenticated) and `/snapshot` (bearer) from the
laptop
- [ ] driveripper's upsd answers `LIST UPS` from the laptop on 3051 — not via the host's loopback
- [ ] `git grep -n "dev-tunnels\|DEV_TUNNELS\|DEV_FORWARDS\|9121\|9122\|9123\|3494"` matches
nothing outside `.agents/phases/complete/`
- [ ] `./scripts/dev-server.sh status` shows every source `ok` over direct dial
- [ ] `uv run pytest -m live --live` (with `.env.dev` sourced) is green
- [ ] `data/logs/` holds `phase10-*.png` screenshots of `/`, `/system`, `/ups`, `/llm` on live
data over direct dial
- [ ] full test suite **100% passing**, coverage gate (≥94%) holds, `ruff` + `pyright` clean
- [ ] `README.md` and `AGENTS.md` describe the direct-dial dev workflow and mention no tunnels
(verified in the final pass)
- [ ] no behavior change in completed phases: the four pages render the same payloads as before
the transport change