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`.
This commit is contained in:
2026-10-18 05:44:13 -04:00
parent fefbeb8227
commit a08e693698
55 changed files with 1259 additions and 299 deletions
+7 -7
View File
@@ -5,9 +5,9 @@ bridge to reality — they re-run the *production* code paths (`collectors.fetch
`NutMonitor.collect`) against real targets, which is what catches an upstream that
renamed a series or a driver that started reporting a temperature in kelvin.
They read exactly the env vars the app reads, so the dev loop is:
They read exactly the env vars the app reads, so the dev loop is direct dial — the same
addresses the deployed dashboard dials, nothing forwarded:
./scripts/dev-tunnels.sh start # agents + upsd forwarded to localhost
set -a; . .env.dev; set +a
uv run pytest -m live --live
@@ -55,7 +55,7 @@ async def test_every_host_agent_answers_and_parses(settings: Settings,
host_collector: HostCollector) -> None:
"""Each HOST_TARGETS entry must answer /snapshot and yield the metrics the page renders."""
if not settings.host_targets:
pytest.skip("HOST_TARGETS is unset — start ./scripts/dev-tunnels.sh")
pytest.skip("HOST_TARGETS is unset")
required = {"cpu.usage_pct", "mem.used_pct", "disk.used_pct"}
async with httpx.AsyncClient(timeout=settings.host_timeout) as client:
for target in settings.host_targets:
@@ -84,7 +84,7 @@ async def test_every_host_serves_the_hero_card_contract(settings: Settings,
upgrade renaming a field. Hermetic twin: `tests/test_system_contract.py`.
"""
if not settings.host_targets:
pytest.skip("HOST_TARGETS is unset — start ./scripts/dev-tunnels.sh")
pytest.skip("HOST_TARGETS is unset")
async with httpx.AsyncClient(timeout=settings.host_timeout) as client:
for target in settings.host_targets:
await collectors.fetch_host_snapshot(client, target, host_collector)
@@ -107,7 +107,7 @@ async def test_agent_disk_rows_carry_the_smart_shape(settings: Settings) -> None
operator's call, not a unit test.
"""
if not settings.host_targets:
pytest.skip("HOST_TARGETS is unset — start ./scripts/dev-tunnels.sh")
pytest.skip("HOST_TARGETS is unset")
seen_smart = 0
async with httpx.AsyncClient(timeout=settings.host_timeout) as client:
for target in settings.host_targets:
@@ -147,7 +147,7 @@ async def test_disks_source_charts_the_physical_drives_the_agent_reports(
disk, `disk.*@<dev>` series only for values that exist, and no invented numbers.
"""
if not settings.host_targets:
pytest.skip("HOST_TARGETS is unset — start ./scripts/dev-tunnels.sh")
pytest.skip("HOST_TARGETS is unset")
collector = DisksCollector()
checked = 0
async with httpx.AsyncClient(timeout=settings.disks_timeout) as client:
@@ -177,7 +177,7 @@ async def test_disks_source_charts_the_physical_drives_the_agent_reports(
if any(row["smart"] for row in payload["disks"]):
assert payload["smartctl_available"] is True
if not checked:
pytest.skip("no host agent answered — start ./scripts/dev-tunnels.sh")
pytest.skip("no host agent answered")
@pytest.mark.parametrize("engine", ["gufo", "vllm"])