Files
ducoterra a08e693698 phase: 10_dev_direct_dial
**Phase 10 final pass — repo green; live criteria blocked off-repo.**

- Verified: `gateway` runs `dashboards-agent` (active/enabled, `10.1.1.27:9120` → `/health` 200, `/snapshot` 401 no-token / 200 bearer on-host, token matches `/etc/dashboards-agent.token`); `driveripper`/`deskwork` inactive, `sudo -n` → "a password is required", `:9120` empty.
- New measurement (transient runtime rule on gateway `firewall-cmd --add-port=9120/tcp`, **reverted** — runtime+permanent port lists verified back to the original set, no rich rules): laptop still gets no SYN through, while `:22`/`:80` on the same address answer in ms ⇒ the block is the lab gateway `10.2.0.1` port filter, not this app/host firewall/agent. No other live change made (no sudoers, no upsd, no UPS command).
- Tests/lint (exact): `bash .agents/validate.sh` → **EXIT 0** — `uv run pytest --cov --cov-fail-under=94` **793 passed, 11 skipped, coverage 98.33 %**; `ruff check src tests agent` clean; `pyright` 0 errors; `node --check` clean; hygiene clean; `uv pip list --outdated` empty (bumped fastapi 0.143.3→0.143.5, `uv.lock` only).
- `uv run pytest -m live --live` (`.env.dev` sourced): **4 passed, 1 skipped, 6 failed** — all six are `:9120`/`:3051` connect timeouts; zero parse/contract failures.
- Dev server restarted on current code: engines `ok`; 3×`host` + 3×`disks` + `nut` `DOWN` with real reasons (designed degradation, stub rows intact, no credential leakage).
- Completion criteria: screenshots ✓ (`data/logs/phase10-{index,system,disks,llm,ups}-live.png`); tunnel-vocabulary grep ✓ outside `.agents/` history (PLAN.md locked line, phase overviews, past reports; fixtures/`uv.lock` hits are digit substrings — `tests/test_deploy_scripts.py` guards operational files); no page-payload change ✓ (contract tests). **Not met, operator-side:** agents on all three hosts, `LIST UPS` from the laptop, `dev-server.sh status` all-ok, live suite green.
- Fixed in this pass: `sources/nut.py` unreachable-upsd message no longer prints a bare `(TimeoutError: )` (now `(timed out after 4s)` / `(ConnectionRefusedError: …)`), pinned by a new test + extended refused-port test.
- Docs: README verified consistent (every env var in `config.py` documented, routes/flags/`--with-llm`/pager sizes checked) — no changes needed. AGENTS.md fixed 4 inconsistencies: per-host sudo reality (gateway grants `NOPASSWD ALL`, which is why it installed without `agent-sudoers`; driveripper scoped; deskwork unrelated), the now-proven firewall finding, `/system` table typo ("agent per host"), gap-4 DB/WAL sizes + test count. `.env.example` `upsd.users` sample now one `instcmds` per line (it contradicted README and the in-app copy). `.env.dev` (gitignored) comment corrected to match the evidence.
- Flagged, untouched (locked): `.agents/PLAN.md` L92–95 still says upsd is "forwarded to `127.0.0.1:3494` in dev" — false after this phase; evidence + exact operator commands appended to `data/logs/NOTES-verify.md` §11.
- Next pending phase: `11_admin_login`.
2026-10-18 05:44:13 -04:00

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 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