phase: 11_admin_login

**Verified (final pass, phase 11 — all 7 tasks already in `complete/`)**
- Gate from the wire on a restarted dev server: `/system`→`303 /login?next=/system` (`/llm?range=1h`→`/login?next=/llm%3Frange%3D1h`), `/api/summary`+`/api/config`→`401 {"ok":false,…}`, `/api/health`→`200`; signed-in `GET` of all six pages `200`, `/login`→`303 /`.
- Auth flows on a throwaway server (`:8199`, `/tmp` DB — dev `auth.verifier` untouched): wrong pw `401`, `testpass` `200`, password change `200`, **old cookie `401` / new cookie `200`**, old pw `401` / new pw `200`, short new `422`, wrong current `403`, cookie write without CSRF `403`, logout `200`; 5 failures then `429 retry-after: 60`; `login-denied`/`password-changed`/`password-denied` events; `meta` = the two auth keys.
- Fail-closed boot (no seed, empty DB): **exit 3**, message names `DASH_ADMIN_PASSWORD`, `auth.verifier`, and the `DELETE FROM meta …` reset. `dev-server.sh status` + the Containerfile `HEALTHCHECK` python one-liner both `200`/exit 0 unauthenticated.
- Live payloads unchanged (phases 01–10 walk): `/api/summary` keys + 8 alerts + hosts 3/2 live; UPS `simulated:false` with charge/load/runtime severity; deskwork `nvme0n1` 51 °C/19 159 h/11 % wear; gateway `smartctl_available:false`; driveripper `unreachable` stub.

**Tests/lint (all green)**: `bash .agents/validate.sh` → exit 0 — `1140 passed, 11 skipped`, coverage `98.62%` (floor 94), ruff `All checks passed!`, pyright `0 errors`, `node --check` clean, repo hygiene clean. `uv run pytest -m live --live` → `8 passed, 3 failed`, all `driveripper: 10.1.0.126:9120 … ConnectError` (pre-existing, auth-free; probe confirms nothing listening).

**Completion criteria**: all met except the live-suite caveat above — installing the driveripper agent needs root on that host (no `ops` grant), so per the live-infra rule I made **no live change**; task 07's five screenshots (`auth-login/-rejected/-locked-out/-signed-in-overview/-settings.png`) remain the browser evidence, and the login/rotation/lockout flows were re-proved over HTTP this pass.

**Docs**: README — fixed "only place the CSRF token is published" (login/password responses mint one), added the undocumented `DASH_RELOAD` row, stated that Sign out ends only this browser's cookie. `AGENTS.md` — new known gap 8 (stateless logout cannot revoke a copied token), gap-4 dev-store sizes dated, fleet section notes `data/` is gitignored. `api.py` — same `status` docstring imprecision fixed. Verified the 4 `DASH_*` rows, reset command, seed-wins rule and invariant 12 against `app.py`/`config.py`.

**Deviations**: no dependency bump (`uv pip list --outdated` advisory: certifi/fastapi/starlette — phase added none); verification notes appended to `data/logs/NOTES-auth.md` §10.

**Next pending phase**: `12_nut_self_test_verdict`.
This commit is contained in:
2026-10-19 07:45:44 -04:00
parent eb4788cba1
commit 1734d81e48
75 changed files with 6941 additions and 121 deletions
@@ -0,0 +1,15 @@
**Verified (final pass, phase 11 — all 7 tasks already in `complete/`)**
- Gate from the wire on a restarted dev server: `/system`→`303 /login?next=/system` (`/llm?range=1h`→`/login?next=/llm%3Frange%3D1h`), `/api/summary`+`/api/config`→`401 {"ok":false,…}`, `/api/health`→`200`; signed-in `GET` of all six pages `200`, `/login`→`303 /`.
- Auth flows on a throwaway server (`:8199`, `/tmp` DB — dev `auth.verifier` untouched): wrong pw `401`, `testpass` `200`, password change `200`, **old cookie `401` / new cookie `200`**, old pw `401` / new pw `200`, short new `422`, wrong current `403`, cookie write without CSRF `403`, logout `200`; 5 failures then `429 retry-after: 60`; `login-denied`/`password-changed`/`password-denied` events; `meta` = the two auth keys.
- Fail-closed boot (no seed, empty DB): **exit 3**, message names `DASH_ADMIN_PASSWORD`, `auth.verifier`, and the `DELETE FROM meta …` reset. `dev-server.sh status` + the Containerfile `HEALTHCHECK` python one-liner both `200`/exit 0 unauthenticated.
- Live payloads unchanged (phases 01–10 walk): `/api/summary` keys + 8 alerts + hosts 3/2 live; UPS `simulated:false` with charge/load/runtime severity; deskwork `nvme0n1` 51 °C/19 159 h/11 % wear; gateway `smartctl_available:false`; driveripper `unreachable` stub.
**Tests/lint (all green)**: `bash .agents/validate.sh` → exit 0 — `1140 passed, 11 skipped`, coverage `98.62%` (floor 94), ruff `All checks passed!`, pyright `0 errors`, `node --check` clean, repo hygiene clean. `uv run pytest -m live --live` → `8 passed, 3 failed`, all `driveripper: 10.1.0.126:9120 … ConnectError` (pre-existing, auth-free; probe confirms nothing listening).
**Completion criteria**: all met except the live-suite caveat above — installing the driveripper agent needs root on that host (no `ops` grant), so per the live-infra rule I made **no live change**; task 07's five screenshots (`auth-login/-rejected/-locked-out/-signed-in-overview/-settings.png`) remain the browser evidence, and the login/rotation/lockout flows were re-proved over HTTP this pass.
**Docs**: README — fixed "only place the CSRF token is published" (login/password responses mint one), added the undocumented `DASH_RELOAD` row, stated that Sign out ends only this browser's cookie. `AGENTS.md` — new known gap 8 (stateless logout cannot revoke a copied token), gap-4 dev-store sizes dated, fleet section notes `data/` is gitignored. `api.py` — same `status` docstring imprecision fixed. Verified the 4 `DASH_*` rows, reset command, seed-wins rule and invariant 12 against `app.py`/`config.py`.
**Deviations**: no dependency bump (`uv pip list --outdated` advisory: certifi/fastapi/starlette — phase added none); verification notes appended to `data/logs/NOTES-auth.md` §10.
**Next pending phase**: `12_nut_self_test_verdict`.
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 12%]
........................................................................ [ 18%]
........................................................................ [ 25%]
........................................................................ [ 31%]
........................................................................ [ 37%]
........................................................................ [ 43%]
........................................................................ [ 50%]
........................................................................ [ 56%]
........................................................................ [ 62%]
........................................................................ [ 68%]
...............sssssssssss.............................................. [ 75%]
........................................................................ [ 81%]
........................................................................ [ 87%]
........................................................................ [ 93%]
....................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 348 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2820 39 99%
Required test coverage of 94% reached. Total coverage: 98.62%
1140 passed, 11 skipped, 1 warning in 61.66s (0:01:01)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.147.0 wheel
starlette 1.8.0 1.9.0 wheel
@@ -0,0 +1,17 @@
Task complete. Summary:
**Implemented**
- `src/dashboards/auth.py` — pure, stdlib-only auth primitives: `hash_password`/`verify_password`/`needs_rehash` (self-describing `pbkdf2_sha256$<iters>$<salt>$<hash>`, 600 000 default, `PBKDF2_MAX_ITERATIONS` DoS ceiling, `compare_digest`, **never raises**); `new_secret` + `Session` + `mint_session`/`read_session` (HMAC-signed stateless cookie, signature-before-expiry, rejects tampered username/expiry, rotated secret, non-UTF-8 payload, NaN/inf expiry); `mint_csrf`/`verify_csrf` bound to the session token; `LoginThrottle` (5 attempts → 60 s base, doubling, capped at 3600 s, `success()` clears, `prune()` + inline `THROTTLE_MAX_KEYS` size guard for spoofed-address swarms); `origin_matches` + `sec_fetch_site_allowed` (unknown → reject).
- `tests/test_auth.py` — 99 tests incl. 21-shape malformed-verifier table; structural pins: no third-party import, no log/stdout call, no `raise` anywhere.
**Verification**
- `uv run pytest tests/test_auth.py --cov=dashboards.auth --cov-branch` → 99 passed, **100 % stmts + branches** (194/194, 46/46).
- `bash .agents/validate.sh` → exit 0: **892 passed, 11 skipped**, coverage **98.46 %** (floor 94), ruff clean, pyright 0 errors, `node --check` clean, repo hygiene clean.
- `uv pip list --outdated` empty; `pyproject.toml`/`uv.lock` untouched (no new dependency).
- `uv run pytest -m live --live` → same 6 pre-existing transport failures documented in AGENTS.md (laptop cannot reach `:9120`/`:3051`); engines pass. No live change made or needed.
**Docs** — `AGENTS.md`: invariant 6 extended to the admin's own credential; `auth.py` added to the module-responsibility list (explicitly noting the gate itself arrives in tasks 03-04); new Known gap 5 for the peer-address throttle key; test/coverage counts refreshed. `README.md`: no doc impact yet (no env var, route, or page until tasks 02-07).
**Decisions** — `needs_rehash` returns `True` for off-shape/unknown-algo verifiers ("replace it"); `origin_matches("")` is `False` and the caller gates on header *presence* (documented, tested); base64url is decoded with `validate=True`; blocks never shorten on a failure during a lockout.
**Next pending task:** `.agents/phases/todo/11_admin_login/02_verifier_config_fail_closed.md`
@@ -0,0 +1,54 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 7%]
........................................................................ [ 15%]
........................................................................ [ 23%]
........................................................................ [ 31%]
........................................................................ [ 39%]
........................................................................ [ 47%]
........................................................................ [ 55%]
.............................................................sssssssssss [ 63%]
........................................................................ [ 71%]
........................................................................ [ 79%]
........................................................................ [ 87%]
........................................................................ [ 95%]
....................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 28
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 221 0 100%
src/dashboards/app.py 101 0 100%
src/dashboards/auth.py 194 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 115 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 178 2 99% 250-251
------------------------------------------------------------------
TOTAL 2530 39 98%
Required test coverage of 94% reached. Total coverage: 98.46%
892 passed, 11 skipped, 1 warning in 60.28s (0:01:00)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,24 @@
All green. Final state:
**Implemented**
- `store.py`: `get_meta`/`set_meta`/`delete_meta` (rowcount-returning, one key per call) over the existing `meta` table; `stats()`'s `last_maintain` now reads through `get_meta()` (outside its own non-reentrant lock), `maintain()`'s write stays inline.
- `config.py`: four settings — `DASH_ADMIN_PASSWORD` (seed, `_env` strips so `VAR=` = unset), `DASH_SESSION_HOURS` (720), `DASH_COOKIE_SECURE`, `DASH_PBKDF2_ITERATIONS` (imports `auth.PBKDF2_ITERATIONS`).
- `auth.py`: `MetaStore` Protocol, frozen `AuthState`, `load_auth_state()` (env seeds once → stored verifier wins → `env_ignored` + one fixed-wording warning; no row + no seed → `RuntimeError` naming the variable, the key and the `sqlite3 … DELETE FROM meta …` reset command; unusable secret is regenerated *and* persisted), `update_verifier()` (secret written first, empty/whitespace password refused).
- `app.py`/`api.py`: lifespan builds `ctx.auth` after the context and before the first poll (one call site for both store branches); `AppContext.auth: AuthState | None`. Fixed `__main__.py` to pass `SETTINGS` into `create_app()` so the served app is configured by the same object the bind address came from.
- Tests: 6 in `test_store.py`, 4 in `test_config.py`, new `tests/test_auth_bootstrap.py` (23), 2 in `test_api.py` (fail-closed start / seeded start); `tests/auth_contract.py` = `testpass` + `FAST_PBKDF2` applied to the 14 app-starting call sites; `test_auth.py`'s two structural pins now allow logging + exactly the two documented raise sites and prove neither interpolates a credential.
- Docs: README quickstart, config table, new *Admin login* section (reset procedure), architecture + Testing; AGENTS.md new invariant 11, `auth.py`/`config.py`/`store.py`/`app.py` responsibility lines, invariant 6 corrected; `.env.example` + `.env.dev.example` gained the four vars (empty — validate.sh 2b rejects a non-placeholder secret in a tracked template); appended `DASH_ADMIN_PASSWORD=testpass` to the gitignored `.env.dev` so the dev server keeps coming up.
**Verification**
- `bash .agents/validate.sh` → exit 0: `927 passed, 11 skipped`, coverage **98.50%** (floor 94; baseline 98.46), ruff clean, pyright `0 errors`, `node --check` clean, hygiene clean. Per-module: `auth.py` 100%, `config.py` 100%, `app.py` 100%, `store.py` 99% (2 pre-existing misses in `recent_events`).
- `uv run pytest tests/test_store.py tests/test_config.py tests/test_auth.py tests/test_auth_bootstrap.py -q` → 182 passed.
- Live probe A (`DASH_DB=/tmp/auth-probe.sqlite3`, `DASH_ADMIN_PASSWORD` unset): **exit 3**, `RuntimeError: no admin password configured — refusing to start a public dashboard open.` … `sqlite3 "/tmp/auth-probe.sqlite3" "DELETE FROM meta WHERE key IN ('auth.verifier', 'auth.session_secret')"` — put this line in the commit message.
- Live probe B (`DASH_ADMIN_PASSWORD=testpass`): boots, `/api/health` 200, `meta` holds `auth.verifier` = `pbkdf2_sha256$600000$…` + `auth.session_secret`, `grep -c testpass` = 0 (same checks on the real dev DB). No `sqlite3` CLI on this box, so the `SELECT key FROM meta` listing ran through Python's stdlib `sqlite3` — same query.
- `./scripts/dev-server.sh restart` → running, `/api/health` 200, `/` 200 (fleet still `degraded` on the known transport block, unchanged).
- `uv run pytest -m live --live`: the same 6 pre-existing transport failures (host agents + upsd unreachable from the laptop), no new ones.
**Decisions / deviations**
- `load_auth_state()` takes `db_path: str | None` instead of the task's `now: float | None` — nothing in the loader reads a clock (a session's expiry lives in its signed payload), and the real DB path makes the reset command copy-pasteable (`<db>` when unknown).
- Chose an explicit `admin_password`/`pbkdf2_iterations` at each test call site (via `tests/auth_contract.py`) rather than ambient env, so no test depends on the shell.
- No live infra changes were made or needed beyond the local dev server restart.
**Next pending task:** `.agents/phases/todo/11_admin_login/03_session_gate_and_public_surface.md`
@@ -0,0 +1,55 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 7%]
........................................................................ [ 15%]
........................................................................ [ 23%]
........................................................................ [ 30%]
........................................................................ [ 38%]
........................................................................ [ 46%]
........................................................................ [ 53%]
........................................................................ [ 61%]
..................sssssssssss........................................... [ 69%]
........................................................................ [ 76%]
........................................................................ [ 84%]
........................................................................ [ 92%]
........................................................................ [ 99%]
.. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 223 0 100%
src/dashboards/app.py 103 0 100%
src/dashboards/auth.py 247 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2602 39 99%
Required test coverage of 94% reached. Total coverage: 98.50%
927 passed, 11 skipped, 1 warning in 48.88s
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
@@ -0,0 +1,19 @@
**Implemented** (task 03 — session gate & public surface)
- Completed/verified the gate in `app.py`: `PUBLIC_PATHS`/`PUBLIC_PREFIXES` + `PAGES["/login"]`, `session_gate` middleware (503 without `ctx.auth` → router-404 passthrough → public → 303/401 → origin + CSRF on writes), `_own_origin` via `X-Forwarded-Proto`, `auth.safe_next`, `no-store` on every gate answer.
- `api.py`: `auth_router()` (`POST /api/auth/login` throttle-first, one `wrong password` sentence, 422 before hashing, cookie flags + `Secure` on proxied https; `GET /api/auth/status`; CSRF-gated `POST /api/auth/logout`), `AppContext.login_throttle` (one per process), `get_session`/`get_auth_state` refusals.
- `web/static/app.js`: memoised `ensureAuth()`, CSRF on every non-GET/HEAD `fetchJSON`, one-shot `bounceToLogin()`, exports `authStatus`/`ensureAuth`; `/login` page + `page-login.js` (posts without `fetchJSON`).
- Tests: renamed `test_auth_gate.py` → **`tests/test_auth_api.py`** (the name every code comment and the phase mandate use), dropped a duplicated helper, and added the missing required test `test_every_route_that_is_not_public_requires_a_session` (walks the *served* table via `walk_routes`, GET+POST, pins the public set exactly), the wire proof that a tokenless `POST /api/ups/command` reaches **no** driver while the tokened one does (`driver.killpower` still needs `DANGER`), non-object JSON body 422, 503-on-public-paths pin, route-404-while-signed-in, and a direct call of the login route's no-credential branch.
- Docs: README gained *What the session covers / Writes need the CSRF token / Scripting the API (bearer) / Lockout* plus testing notes; AGENTS.md gained invariant **12** (default-deny + the four-entry public surface), `api.py`/`app.py`/`web/`/`auth.py` line updates, the invariant-1 and invariant-11 forward references corrected, test/coverage figures, and a re-verified fleet/live-tally correction.
**Verification**
- `bash .agents/validate.sh` → exit 0: **1081 passed, 11 skipped**, coverage **98.59%** (floor 94); `api.py`, `app.py`, `auth.py` each **100%**; ruff `All checks passed!`; pyright `0 errors`; `node --check` clean.
- Live: `curl -si /system` → `303` + `location: /login?next=/system`; `/api/summary` & `/api/config` → `{"ok":false,"detail":"sign in required","login":"/login"}`; `/api/health`, `/static/dash.css`, `/login` → 200; login POST returns `ok:true` + `csrf` + `HttpOnly; SameSite=lax; Max-Age`; bearer token opens `/api/summary` (200); cookie write without `X-CSRF-Token` → `403 missing or invalid CSRF token`.
- Browser (headless Chromium, screenshots in `data/logs/auth-gate-*.png`): `/system?range=1h` → login form, wrong password shows inline "wrong password" without navigating, `testpass` lands back on `/system?range=1h` with live heroes, signed-in `/login` → `/`, no page JS errors.
- `uv run pytest -m live --live` → 8 passed / 3 failed, all three the documented `driveripper` agent `ConnectError` (no auth involvement).
**Notable decisions / deviations**
- Gate answers `503` for **all** paths (public included) when no `AuthState` exists — task text said "non-public"; `get_context` would otherwise 500 on `/api/health`, and "healthy" from a server that can't name its admin is the invariant-4 failure. Pinned by test + AGENTS invariant 12; the login route's own 503 branch is now driven directly.
- `_wants_page` treats `Accept: */*` (curl) as a page request so the phase's own acceptance check sees a 303, while `/api/*` always answers JSON.
- No live infra changes: lighting up `driveripper` needs its installer + firewall line (operator-only per AGENTS fleet notes), so the 3 live failures stay; validate.sh's dependency advisory (fastapi/starlette/certifi) left alone — this task touched no dependency, and a FastAPI bump would move the route-table shape the new audit walks.
**Next pending task:** `.agents/phases/todo/11_admin_login/04_change_password.md`
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 13%]
........................................................................ [ 19%]
........................................................................ [ 26%]
........................................................................ [ 32%]
........................................................................ [ 39%]
........................................................................ [ 46%]
........................................................................ [ 52%]
........................................................................ [ 59%]
........................................................................ [ 65%]
............................sssssssssss................................. [ 72%]
........................................................................ [ 79%]
........................................................................ [ 85%]
........................................................................ [ 92%]
........................................................................ [ 98%]
............ [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 287 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2759 39 99%
Required test coverage of 94% reached. Total coverage: 98.59%
1081 passed, 11 skipped, 1 warning in 61.09s (0:01:01)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.146.0 wheel
starlette 1.8.0 1.9.0 wheel
@@ -0,0 +1,18 @@
All green. Final report:
**Implemented** (`src/dashboards/api.py`)
- `POST /api/auth/password`: validates `new` first (`MIN_PASSWORD_LENGTH = 8`, non-empty once stripped, must differ from `current` → `422` naming the rule), throttles the *current* password on its own key `password:<address>` (`429`+`Retry-After`), wrong → `403` + `password-denied` event and the rows untouched, right → `auth.update_verifier` (verifier **and** secret), `ctx.auth = new_state`, throttle reset, `password-changed` event, response built from the **new** state with a fresh cookie + CSRF token.
- Shared `_session_answer()` so login and the change cannot disagree about cookie attributes (`HttpOnly`/`SameSite=lax`/`Path`/`Max-Age`/`Secure` via `DASH_COOKIE_SECURE` or `X-Forwarded-Proto`); `_password_body()` (`422` for any malformed body before hashing/throttle); `_new_password_problem()`.
- Login now upgrades a verifier minted below `DASH_PBKDF2_ITERATIONS` (`_upgrade_verifier`, secret kept); a refused store write is swallowed with one fixed-text log line so `database is locked` never costs a correct password its login.
**Tests / lint / coverage**
- `uv run pytest tests/test_auth_api.py` → 180 passed (26 new: refusal-leaves-rows-byte-identical, separate throttle key + sixth-with-correct-password `422`/`429`, the four `422` composition cases, 8 malformed bodies, both rows changed + old/other-browser cookie `401` + new cookie/CSRF reaching the scripted upsd wire, restart-with-seed, rehash + refused-rehash, cookie-attribute parity, POST-only/CSRF, no auth detail on `/api/health`|`/api/config`, no plaintext in the DB bytes).
- `bash .agents/validate.sh` → `1107 passed, 11 skipped`, coverage `98.62%` (floor 94; `api.py` 100%), `ruff` clean, `pyright` 0 errors, `node --check` clean.
- `uv run pytest -m live --live` → unchanged tally, 3 failures all the documented `driveripper` transport (`ConnectError 10.1.0.126:9120`), no regression.
- Live by hand on :8090 (server restarted): sign in → change → old cookie `401`, returned cookie `200`, its CSRF passes the gate, `meta.auth.verifier` is only `pbkdf2_sha256$600000$…`; password restored to `testpass` so later tasks keep working.
**Docs**: README — new *Changing the password* section, `DASH_PBKDF2_ITERATIONS` rehash note, "one write endpoint **that dials anything**", lockout section notes the second bucket. AGENTS.md — invariant 12 names the third gated write, `api.py`/`auth.py` bullets describe the route + revocation-by-rotation, Known gap 5 records both throttle keys (also fixed the `/api/config is redaction boundary` grammar slip).
**Deviations**: none of substance; deps untouched, so the advisory `uv pip list --outdated` (certifi/fastapi/starlette) was left alone rather than upgrading mid-phase.
**Next pending task**: `.agents/phases/todo/11_admin_login/05_login_page.md`.
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 12%]
........................................................................ [ 19%]
........................................................................ [ 25%]
........................................................................ [ 32%]
........................................................................ [ 38%]
........................................................................ [ 45%]
........................................................................ [ 51%]
........................................................................ [ 57%]
........................................................................ [ 64%]
......................................................sssssssssss....... [ 70%]
........................................................................ [ 77%]
........................................................................ [ 83%]
........................................................................ [ 90%]
........................................................................ [ 96%]
...................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 348 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2820 39 99%
Required test coverage of 94% reached. Total coverage: 98.62%
1107 passed, 11 skipped, 1 warning in 60.86s (0:01:00)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.146.0 wheel
starlette 1.8.0 1.9.0 wheel
@@ -0,0 +1,15 @@
**Task 05 complete.** Final report:
**Implemented**
- `web/pages/login.html` — `Homelab · sign in`, masthead brand link + theme toggle and **no** `.tabs`/dots/clock; one `.card`: `h1`, the `.sub` line, `label[for=admin-password]` + `input#admin-password[type=password autocomplete=current-password required]` + `button.action#sign-in`, `#login-error[role=alert][hidden]`, `#login-note.faint.mono[hidden]`, `app.js`→`page-login.js` both `defer`. No username field.
- `web/static/page-login.js` — house-style IIFE: `Dash.fetchJSON('POST /api/auth/login')` → `location.assign(target(body.next))`; `401` → inline `Wrong password.` + `field.select()`; `429` → the server `detail` + `Retry-After` ticking down with the button disabled until 0 (and the now-false refusal cleared with it); anything else prints the server's `detail`; all text via `textContent`; client copy of `safe_next`; `Dash.ensureAuth()` stops the form flash.
- `web/static/app.js` — `LOGIN_PATH` literal (pinned to `auth.LOGIN_PATH`) and `bounceToLogin()` returns when already on `/login`; `fetchJSON` now carries `response.headers` on the thrown `Error`, so the countdown is the server's number and not an invented one (rule 2).
- `dash.css` — `.login-card`/`.login-error`/`form`/`label` in the `.hero .label` vocabulary, existing `:root` tokens only; `input[type="text"]` extended to `input[type="password"]` instead of duplicated; the interim `.login-body/.login-panel/.login-form/.login-theme` rules deleted.
**Tests / gates** — 7 new-or-rewritten served-bytes tests in `tests/test_auth_api.py` (field/role/no-username, no tabs+dots+clock with the brand link present, posts *through* the shared fetch, the no-navigate guard, status-branched refusal + `Retry-After`, countdown/disable/clear, `next` judged before every `location.*`, CDN fragments) and the `/login` cross-reference in `tests/test_api.py`. `bash .agents/validate.sh` → **1113 passed, 11 skipped**, coverage **98.62 %** (floor 94), ruff clean, pyright 0 errors, `node --check` clean.
**Browser (live dev server :8090)** — anonymous `/` → `303 /login?next=/`; wrong password inline with no reload and the text selected; five wrong → lockout counting `Retry-After` down with a dimmed disabled button, then it cleared and `testpass` worked; `?next=/ups` landed on `/ups`; dots `hosts/disks=warn` (driveripper's agent is not installed — the documented state) `engines/UPS=ok`. Screenshots `data/logs/login-{dark,light,refusal,lockout,next-ups-landing,success-overview}.png` + `data/logs/NOTES-login-page.md`.
**Docs** — AGENTS.md `web/` bullet now describes the finished page and the bounce-guard rule (it previously claimed "no masthead" and "the only script that posts without `Dash.fetchJSON`"); README gained *The sign-in page*.
**Deviations** — the task-03 test "the login page posts without the shared fetch" was deliberately inverted (task 05 asks for `fetchJSON`; the guard makes it safe); `#login-note` carries one extra `login-note` class as the CSS hook. No new Python (coverage unchanged), no new deps (the advisory `uv pip list --outdated` is pre-existing), no task/phase/plan file touched, no git index changes; dev server left running.
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 12%]
........................................................................ [ 19%]
........................................................................ [ 25%]
........................................................................ [ 32%]
........................................................................ [ 38%]
........................................................................ [ 44%]
........................................................................ [ 51%]
........................................................................ [ 57%]
........................................................................ [ 64%]
............................................................sssssssssss. [ 70%]
........................................................................ [ 76%]
........................................................................ [ 83%]
........................................................................ [ 89%]
........................................................................ [ 96%]
............................................ [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 348 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2820 39 99%
Required test coverage of 94% reached. Total coverage: 98.62%
1113 passed, 11 skipped, 1 warning in 62.14s (0:01:02)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.146.0 wheel
starlette 1.8.0 1.9.0 wheel
@@ -0,0 +1,17 @@
Everything is green. Final state:
**Implemented (task 06 — `/settings`)**
- `app.py`: `"/settings": "settings.html"` in `PAGES` (gated by the middleware, not added to `PUBLIC_PATHS`).
- `web/pages/settings.html`: Session `<dl>` (name / local expiry timestamp / `in 30d`) + **Sign out**; Change-password form (current / new / confirm, real `<label for>`, shared `aria-describedby` hint naming the 8-char floor); `#settings-result` `role="status"`; static precedence paragraph; `.tabs` with **no** `aria-current`, no `#nav-status`.
- `web/static/page-settings.js`: status read → panel; logout → `resetAuth()` → `/login`; local pre-checks (non-empty current, ≥8 chars, confirm match, focus moved to the offending box) → `POST /api/auth/password`; `403`/`422`/`429` messages via `textContent`; on success `resetAuth()`, fields cleared, status re-read.
- `web/static/app.js`: exported `resetAuth()` (clears `authStatus` + in-flight promise, and an `authEpoch` guard so a status call started before a rotation can't repopulate a dead CSRF token); also called on the `401` bounce.
- `⚙ Account` link in `.masthead-right` of **all five** dashboards (task text predates `/disks`) — outside `.tabs`, no `id`; `dash.css` extends `button.ghost` → `button.ghost, a.ghost` + an `.settings-*` section using existing tokens only.
- Tests: 7 new served-bytes tests in `tests/test_api.py` (form fields, precedence copy, not-a-tab, anonymous `303`/`401`, `test_every_dashboard_page_links_to_settings`, `MIN_PASSWORD_LENGTH` ↔ JS parity, `resetAuth` contract); `/settings` added to `MARKUP_PAGES`/`ASSETS`/`PAGE_SCRIPTS` (orphan-`id` audit — mutation-checked) and to `PROTECTED_PAGES`/page-list assertions in `test_auth_api.py`/`test_api.py`.
**Checks**: `bash .agents/validate.sh` → **1121 passed, 11 skipped**, coverage **98.62 %** (floor 94), `ruff` clean, `pyright` 0 errors, `node --check` clean on all `web/static/*.js`. Live suite `-m live --live`: 8 passed / 3 failed — the pre-existing `driveripper` transport failures documented in AGENTS.md, unchanged.
**Docs**: README — new *The account page* section, `/login` + `/settings` in the intro, stale "task 06" pointer fixed; AGENTS.md — `web/` bullet rewritten for `/settings` + `resetAuth()`, test count 1081 → 1121.
**Live-infra decision**: the password-change flow was verified against a throwaway server (port 8091, own `/tmp` DB, `NUT_SIMULATE=1`), **not** the operator's dev DB or UPS. Proved in a real browser: success line, session kept, and a **same-page** `POST /api/ups/command` returning `{"ok":true,…}` after the rotation (i.e. `resetAuth()`), other sessions `401`, dead session → `/login?next=…`, and `/login?next=/settings` landing on `/settings`. Sim instance killed and its DB deleted; the dev server (8090) is up and its verifier still matches `testpass`. Screenshots: `data/logs/auth-settings.png` (dark), `auth-settings-light.png`, `auth-account-link.png` on `/system` with live data.
**Next pending task**: `.agents/phases/todo/11_admin_login/07_dev_config_docs_browser_verification.md`.
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 12%]
........................................................................ [ 19%]
........................................................................ [ 25%]
........................................................................ [ 31%]
........................................................................ [ 38%]
........................................................................ [ 44%]
........................................................................ [ 50%]
........................................................................ [ 57%]
........................................................................ [ 63%]
....................................................................ssss [ 69%]
sssssss................................................................. [ 76%]
........................................................................ [ 82%]
........................................................................ [ 89%]
........................................................................ [ 95%]
.................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 348 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2820 39 99%
Required test coverage of 94% reached. Total coverage: 98.62%
1121 passed, 11 skipped, 1 warning in 61.04s (0:01:01)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.147.0 wheel
starlette 1.8.0 1.9.0 wheel
@@ -0,0 +1,12 @@
**Implemented (task 07 — dev config, docs, live browser proof)**
- `.env.example` / `.env.dev.example`: `DASH_ADMIN_PASSWORD=change-me` (allow-listed placeholder), seed-vs-override + fail-closed prose, `testpass` named **only in comment prose**; `DASH_COOKIE_SECURE` deployment wording. `.env.dev` verified unchanged (`0600`, `testpass`, NUT creds intact).
- `scripts/dev-server.sh`: one sign-in hint line; probe + `status` still ride the public `/api/health`, no credential.
- README: `## Signing in` renamed **and moved before** *Configuration*, new HMAC-session paragraph, Quickstart dev-password line, deployment note (`DASH_COOKIE_SECURE=1`, quadlet env, nginx/HSTS live in the deployment repo), testing note.
- AGENTS.md: session-required prose in *What this is*, invariant 12 now says nothing may be added to `/api/health` without re-deciding, **two new known gaps** (single factor by design; per-process throttle/multi-worker), Commands/test-count updates, new testing-policy bullet.
- `tests/test_auth_docs.py` (19 tests): templates declare all four vars and carry no non-placeholder secret (hermetic twin of validate.sh 2b), dev script stays auth-free, README/AGENTS keep the public list, reset command, config rows and the three gaps.
- Screenshots in `data/logs/`: `auth-login`, `auth-login-rejected`, `auth-locked-out`, `auth-signed-in-overview`, `auth-settings`, `auth-account-link` (+4 walk shots), all live; `data/logs/NOTES-auth.md` records curl codes, the two `meta` keys (`pbkdf2_sha256$600000$…`, `auth.session_secret`, `testpass` = 0 hits in the DB), operator actions and the PLAN-anchor note.
- **Fleet drift found while verifying and corrected in AGENTS.md:** `deskwork` now runs the installed agent as `dashboards-agent` in `disk` (live SMART: `nvme0n1` 52 °C / 19 158 h / 11 % wear), gateway `:9120` and driveripper upsd `:3051` are reachable from the laptop (phase-10 inter-VLAN drop gone), driveripper still has no agent (the only live failure).
**Gates:** `bash .agents/validate.sh` → **1140 passed, 11 skipped**, coverage **98.62 %** (floor 94), ruff `All checks passed!`, pyright `0 errors`, `node --check` clean, repo hygiene + template scan silent-pass. `uv run pytest -m live --live` → **8 passed / 3 failed**, all `driveripper: 10.1.0.126:9120 unreachable` (unchanged, auth-free). Leak checks: `/system`→`303 /login?next=/system`; `/api/summary|config|events|system/history|ups/snapshot`→`401`; `/api/health`,`/login`,`/static/dash.css`→`200`; `dev-server.sh status` unauthenticated. `git status` shows no `.env*`/screenshot entries.
**Deviations:** NOTES written to `data/logs/NOTES-auth.md` (project convention; `.agents/phases/todo/` is off-limits). The task's `curl /login | grep -c 'http'` returns **1** — the inline favicon's SVG `xmlns`, never a request — so NOTES records the honest off-origin check (`(src|href)="https?://` = 0 on all seven pages, DOM-verified too).
@@ -0,0 +1,61 @@
== uv run pytest --cov (hermetic: no network, live tests skip) ==
........................................................................ [ 6%]
........................................................................ [ 12%]
........................................................................ [ 18%]
........................................................................ [ 25%]
........................................................................ [ 31%]
........................................................................ [ 37%]
........................................................................ [ 43%]
........................................................................ [ 50%]
........................................................................ [ 56%]
........................................................................ [ 62%]
........................................................................ [ 68%]
...............sssssssssss.............................................. [ 75%]
........................................................................ [ 81%]
........................................................................ [ 87%]
........................................................................ [ 93%]
....................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/dashboards/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.4-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------------------
src/dashboards/__init__.py 3 0 100%
src/dashboards/__main__.py 14 1 93% 32
src/dashboards/alerts.py 176 0 100%
src/dashboards/api.py 348 0 100%
src/dashboards/app.py 178 0 100%
src/dashboards/auth.py 265 0 100%
src/dashboards/collectors.py 155 5 97% 141-142, 146-147, 164
src/dashboards/config.py 120 0 100%
src/dashboards/debug.py 14 0 100%
src/dashboards/poller.py 165 3 98% 192-194, 279
src/dashboards/prom.py 105 5 95% 107, 144, 158, 161, 164
src/dashboards/rates.py 37 1 97% 51
src/dashboards/severity.py 49 0 100%
src/dashboards/sources/__init__.py 0 0 100%
src/dashboards/sources/disks.py 130 0 100%
src/dashboards/sources/host.py 199 1 99% 147
src/dashboards/sources/llm.py 150 1 99% 228
src/dashboards/sources/nut.py 390 20 95% 137, 151, 157, 169, 175, 187, 195, 208, 279-280, 282, 498, 510, 545, 578, 582, 639-640, 645, 657
src/dashboards/sources/nut_sim.py 134 0 100%
src/dashboards/store.py 188 2 99% 255-256
------------------------------------------------------------------
TOTAL 2820 39 99%
Required test coverage of 94% reached. Total coverage: 98.62%
1140 passed, 11 skipped, 1 warning in 61.73s (0:01:01)
== uv run ruff check src tests agent ==
All checks passed!
== uv run pyright ==
0 errors, 0 warnings, 0 informations
== node --check src/dashboards/web/static/*.js ==
advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):
certifi 2026.7.22 2026.10.18 wheel
fastapi 0.143.5 0.147.0 wheel
starlette 1.8.0 1.9.0 wheel