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
6.3 KiB
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
opsuser (not root). The repo documents the simplest possible working sudoers addition indeploy/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.devso 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
01_sudoers_grant_and_installer_hint.md—deploy/agent-sudoers(the minimal working grant- install instructions, in the
nut-sudoershouse style);install-agent.shrun without root prints the exact line to add.
- install instructions, in the
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.03_upsd_acl_for_laptop.md— driveripper: append ACL lines for the host range and the laptop range toupsd.conf(read-first,cmp-after, never blind-write), restartnut-server, verify the NUT wire from the laptop.04_env_dev_direct_dial.md— stop the tunnels; rewrite the local.env.devto direct targets (userinfo tokens,NUT_HOST/NUT_PORT=3051); restart the dev server; sources ok.05_retire_tunnels_update_scripts.md— deletescripts/dev-tunnels.sh; rewrite.env.dev.exampledirect-shaped; fixcapture-fixtures.sh, the live suite's docstring and skip messages,ups_contract.py, thealerts.pydocstring,test_config.py,.env.example's NUT note, and the dev-workflow sections ofREADME.md/AGENTS.md.06_verify_direct_dial_live.md— full live verification:/api/healthok over direct dial,uv run pytest -m live --livegreen, browser screenshots of/+/system+/ups+/llmon 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 -nondeploy/install-agent.shandscripts/capture-fixtures.shafter their edits.- The real proof of this phase is the live path:
uv run pytest -m live --liveagainst the directly-dialed fleet (the same production code paths over the prod-shaped transport) plus a browser screenshot perAGENTS.mdrule 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.shholds.
Documentation & Consistency
- The final pass reviews
README.mdandAGENTS.mdin 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 ofdev-tunnels.sh,DEV_TUNNELS,DEV_FORWARDS,9121/9122/9123or3494). - Task 05 updates
README.md/AGENTS.mdimmediately for the user-visible dev-workflow change. PLAN.mdis locked and must not be edited: its Data-Model note ("forwarded to127.0.0.1:3494in 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-agentis 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 UPSfrom 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 statusshows every sourceokover direct dialuv run pytest -m live --live(with.env.devsourced) is greendata/logs/holdsphase10-*.pngscreenshots of/,/system,/ups,/llmon live data over direct dial- full test suite 100% passing, coverage gate (≥94%) holds,
ruff+pyrightclean README.mdandAGENTS.mddescribe 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