# 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