From 1734d81e4830f793b105883b6f187baab6d8f39f Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 19 Oct 2026 07:45:44 -0400 Subject: [PATCH] phase: 11_admin_login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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`. --- .../11_admin_login/00_phase.md | 0 .../11_admin_login/01_auth_primitives.md | 0 .../02_verifier_config_fail_closed.md | 0 .../03_session_gate_and_public_surface.md | 0 .../11_admin_login/04_change_password.md | 0 .../11_admin_login/05_login_page.md | 0 .../11_admin_login/06_settings_page.md | 0 ...07_dev_config_docs_browser_verification.md | 0 .../11_admin_login__00_phase.a1.err | 0 .../11_admin_login__00_phase.a1.md | 15 + .../11_admin_login__00_phase.a1.md.status | 1 + .../11_admin_login__00_phase.a1.validate | 61 + .../11_admin_login__01_auth_primitives.a1.err | 0 .../11_admin_login__01_auth_primitives.a1.md | 17 + ...min_login__01_auth_primitives.a1.md.status | 1 + ...dmin_login__01_auth_primitives.a1.validate | 54 + ...gin__02_verifier_config_fail_closed.a1.err | 0 ...ogin__02_verifier_config_fail_closed.a1.md | 24 + ...2_verifier_config_fail_closed.a1.md.status | 1 + ...02_verifier_config_fail_closed.a1.validate | 55 + ..._03_session_gate_and_public_surface.a1.err | 0 ...__03_session_gate_and_public_surface.a1.md | 19 + ...ssion_gate_and_public_surface.a1.md.status | 1 + ...ession_gate_and_public_surface.a1.validate | 61 + .../11_admin_login__04_change_password.a1.err | 0 .../11_admin_login__04_change_password.a1.md | 18 + ...min_login__04_change_password.a1.md.status | 1 + ...dmin_login__04_change_password.a1.validate | 61 + .../11_admin_login__05_login_page.a1.err | 0 .../11_admin_login__05_login_page.a1.md | 15 + ...11_admin_login__05_login_page.a1.md.status | 1 + .../11_admin_login__05_login_page.a1.validate | 61 + .../11_admin_login__06_settings_page.a1.err | 0 .../11_admin_login__06_settings_page.a1.md | 17 + ...admin_login__06_settings_page.a1.md.status | 1 + ..._admin_login__06_settings_page.a1.validate | 61 + ...ev_config_docs_browser_verification.a1.err | 0 ...dev_config_docs_browser_verification.a1.md | 12 + ...fig_docs_browser_verification.a1.md.status | 1 + ...nfig_docs_browser_verification.a1.validate | 61 + .env.dev.example | 22 + .env.example | 28 + AGENTS.md | 420 +++- README.md | 295 ++- scripts/dev-server.sh | 3 + src/dashboards/__main__.py | 8 +- src/dashboards/api.py | 363 +++- src/dashboards/app.py | 245 ++- src/dashboards/auth.py | 752 +++++++ src/dashboards/config.py | 24 + src/dashboards/store.py | 42 +- src/dashboards/web/pages/disks.html | 1 + src/dashboards/web/pages/index.html | 1 + src/dashboards/web/pages/llm.html | 1 + src/dashboards/web/pages/login.html | 47 + src/dashboards/web/pages/settings.html | 97 + src/dashboards/web/pages/system.html | 1 + src/dashboards/web/pages/ups.html | 1 + src/dashboards/web/static/app.js | 148 +- src/dashboards/web/static/dash.css | 63 +- src/dashboards/web/static/page-login.js | 145 ++ src/dashboards/web/static/page-settings.js | 175 ++ tests/auth_client.py | 48 + tests/auth_contract.py | 38 + tests/test_api.py | 311 ++- tests/test_auth.py | 629 ++++++ tests/test_auth_api.py | 1781 +++++++++++++++++ tests/test_auth_bootstrap.py | 306 +++ tests/test_auth_docs.py | 265 +++ tests/test_config.py | 74 + tests/test_llm_slots_contract.py | 9 +- tests/test_main.py | 10 +- tests/test_store.py | 74 + tests/test_system_contract.py | 11 +- tests/test_ups_contract.py | 4 +- 75 files changed, 6941 insertions(+), 121 deletions(-) rename .agents/phases/{todo => complete}/11_admin_login/00_phase.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/01_auth_primitives.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/02_verifier_config_fail_closed.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/03_session_gate_and_public_surface.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/04_change_password.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/05_login_page.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/06_settings_page.md (100%) rename .agents/phases/{todo => complete}/11_admin_login/07_dev_config_docs_browser_verification.md (100%) create mode 100644 .agents/reports/11_admin_login/11_admin_login__00_phase.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__00_phase.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__00_phase.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__00_phase.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__04_change_password.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__04_change_password.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__05_login_page.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__05_login_page.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.validate create mode 100644 .agents/reports/11_admin_login/11_admin_login__07_dev_config_docs_browser_verification.a1.err create mode 100644 .agents/reports/11_admin_login/11_admin_login__07_dev_config_docs_browser_verification.a1.md create mode 100644 .agents/reports/11_admin_login/11_admin_login__07_dev_config_docs_browser_verification.a1.md.status create mode 100644 .agents/reports/11_admin_login/11_admin_login__07_dev_config_docs_browser_verification.a1.validate create mode 100644 src/dashboards/auth.py create mode 100644 src/dashboards/web/pages/login.html create mode 100644 src/dashboards/web/pages/settings.html create mode 100644 src/dashboards/web/static/page-login.js create mode 100644 src/dashboards/web/static/page-settings.js create mode 100644 tests/auth_client.py create mode 100644 tests/auth_contract.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_auth_api.py create mode 100644 tests/test_auth_bootstrap.py create mode 100644 tests/test_auth_docs.py diff --git a/.agents/phases/todo/11_admin_login/00_phase.md b/.agents/phases/complete/11_admin_login/00_phase.md similarity index 100% rename from .agents/phases/todo/11_admin_login/00_phase.md rename to .agents/phases/complete/11_admin_login/00_phase.md diff --git a/.agents/phases/todo/11_admin_login/01_auth_primitives.md b/.agents/phases/complete/11_admin_login/01_auth_primitives.md similarity index 100% rename from .agents/phases/todo/11_admin_login/01_auth_primitives.md rename to .agents/phases/complete/11_admin_login/01_auth_primitives.md diff --git a/.agents/phases/todo/11_admin_login/02_verifier_config_fail_closed.md b/.agents/phases/complete/11_admin_login/02_verifier_config_fail_closed.md similarity index 100% rename from .agents/phases/todo/11_admin_login/02_verifier_config_fail_closed.md rename to .agents/phases/complete/11_admin_login/02_verifier_config_fail_closed.md diff --git a/.agents/phases/todo/11_admin_login/03_session_gate_and_public_surface.md b/.agents/phases/complete/11_admin_login/03_session_gate_and_public_surface.md similarity index 100% rename from .agents/phases/todo/11_admin_login/03_session_gate_and_public_surface.md rename to .agents/phases/complete/11_admin_login/03_session_gate_and_public_surface.md diff --git a/.agents/phases/todo/11_admin_login/04_change_password.md b/.agents/phases/complete/11_admin_login/04_change_password.md similarity index 100% rename from .agents/phases/todo/11_admin_login/04_change_password.md rename to .agents/phases/complete/11_admin_login/04_change_password.md diff --git a/.agents/phases/todo/11_admin_login/05_login_page.md b/.agents/phases/complete/11_admin_login/05_login_page.md similarity index 100% rename from .agents/phases/todo/11_admin_login/05_login_page.md rename to .agents/phases/complete/11_admin_login/05_login_page.md diff --git a/.agents/phases/todo/11_admin_login/06_settings_page.md b/.agents/phases/complete/11_admin_login/06_settings_page.md similarity index 100% rename from .agents/phases/todo/11_admin_login/06_settings_page.md rename to .agents/phases/complete/11_admin_login/06_settings_page.md diff --git a/.agents/phases/todo/11_admin_login/07_dev_config_docs_browser_verification.md b/.agents/phases/complete/11_admin_login/07_dev_config_docs_browser_verification.md similarity index 100% rename from .agents/phases/todo/11_admin_login/07_dev_config_docs_browser_verification.md rename to .agents/phases/complete/11_admin_login/07_dev_config_docs_browser_verification.md diff --git a/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.err b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md new file mode 100644 index 0000000..b3cbb6b --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md @@ -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`. diff --git a/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.validate b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.validate new file mode 100644 index 0000000..d05a78f --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__00_phase.a1.validate @@ -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 diff --git a/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.err b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md new file mode 100644 index 0000000..902b72d --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md @@ -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$$$`, 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` diff --git a/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.validate b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.validate new file mode 100644 index 0000000..3ccdf36 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__01_auth_primitives.a1.validate @@ -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 == diff --git a/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.err b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md new file mode 100644 index 0000000..cab91c0 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md @@ -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 (`` 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` diff --git a/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.validate b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.validate new file mode 100644 index 0000000..34c9b6b --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__02_verifier_config_fail_closed.a1.validate @@ -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 == diff --git a/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.err b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md new file mode 100644 index 0000000..1912ef0 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md @@ -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` diff --git a/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.validate b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.validate new file mode 100644 index 0000000..8739bd1 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__03_session_gate_and_public_surface.a1.validate @@ -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 diff --git a/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.err b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md new file mode 100644 index 0000000..06d0ac3 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md @@ -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:
` (`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`. diff --git a/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.validate b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.validate new file mode 100644 index 0000000..8413fee --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__04_change_password.a1.validate @@ -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 diff --git a/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.err b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md new file mode 100644 index 0000000..d9618ec --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md @@ -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. diff --git a/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md.status b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.validate b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.validate new file mode 100644 index 0000000..4149579 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__05_login_page.a1.validate @@ -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 diff --git a/.agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.err b/.agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.md b/.agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.md new file mode 100644 index 0000000..5ad6d10 --- /dev/null +++ b/.agents/reports/11_admin_login/11_admin_login__06_settings_page.a1.md @@ -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 `
` (name / local expiry timestamp / `in 30d`) + **Sign out**; Change-password form (current / new / confirm, real `