chore(release): 0.15.0 — operator authentication (version, CHANGELOG split, SW cache, test isolation)

This commit is contained in:
2026-06-18 20:43:29 +00:00
parent fdcb1ca532
commit c9bb25e7e1
5 changed files with 37 additions and 4 deletions
+20 -1
View File
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.15.0] - 2026-06-18
**Operator authentication** — the internal app gets a login. The operator-facing surface had **zero auth**; this adds a deny-by-default login gate and roles, the prerequisite that makes the app safe to put behind a public URL (office-deployment sequencing: auth → expose). Built test-first (10 tasks, 90 passing tests — the project's first auth test suite alongside the portal's).
### Added
- **Operator authentication (login + roles), shipped dark behind `OPERATOR_AUTH_ENABLED`.** The internal app gains a deny-by-default login gate (one Starlette middleware over every route except an allow-list: `/login` `/logout` `/health` `/static/*` `/portal/*` + the three machine heartbeat endpoints `/emitters/report` `/api/series3/heartbeat` `/api/series4/heartbeat`). Two roles — `superadmin` (account management at `/admin/users`) and `admin` (full app); `operator` reserved/deferred. Sessions are a 30-day HMAC-signed `tv_session` cookie re-validated against the DB each request (instant revoke via `active` / `sessions_valid_from`). Password reset is superadmin-driven: reset-anyone (temp shown once + forced change), self-service `/change-password`, and a `backend/operator_admin.py` seed/break-glass CLI. Brute-force lockout (5 tries / 15 min) + constant-time login (no user-enumeration). Reuses the portal's argon2 hasher + a new shared `backend/auth_cookies.py` HMAC signer. Spec: `docs/superpowers/specs/2026-06-17-operator-auth-design.md`, plan: `docs/superpowers/plans/2026-06-17-operator-auth.md`.
- **`.env.example`** documenting `SECRET_KEY` / `COOKIE_SECURE` / `OPERATOR_AUTH_ENABLED` for deployment.
### Known limitations
- **SLMM proxy WebSocket endpoints bypass the gate.** `/api/slmm/{id}/stream|live|monitor` are WebSocket upgrades, which a Starlette HTTP middleware never sees — they stay unauthenticated even with the gate on. Pre-existing (not a regression); close it (in-handler `tv_session` check, as the portal WS already does) before true internet exposure.
- **No TLS yet** — until served over HTTPS the login password crosses the wire in cleartext. Still a large improvement over the prior zero-auth exposure; real internet exposure needs the deployment-phase TLS.
### Upgrade Notes
- New `operator_users` table **auto-creates on startup — no migration**.
- Set a real `SECRET_KEY` (in a gitignored `.env`; template at `.env.example`) before internet exposure; set `COOKIE_SECURE=true` once on HTTPS (leave `false` on plain HTTP or the browser won't send the cookie).
- **Rollout (no self-lockout):** deploy with `OPERATOR_AUTH_ENABLED=false` (app behaves exactly as before) → seed a superadmin via `docker compose exec web-app python3 backend/operator_admin.py create-superadmin …` → confirm you can log in → set the flag `true` and `docker compose up -d web-app`. Flipping it back to `false` is the instant escape hatch.
## [0.14.0] - 2026-06-17
Rounds out **sound monitoring** and adds a **client-facing portal**, consolidating four threads since 0.13.x: SLM live monitoring (now on SLMM's shared, cached feed), an automated **FTP night-report pipeline**, a read-only **client portal**, and **per-project password auth** for it. Depends on the matching **SLMM `dev`** build — see Upgrade Notes at the end of each section.
@@ -17,7 +37,6 @@ SLM live monitoring — fan-out feed + cache-first reads. The throughline: the
#### Added
- **Operator authentication (login + roles), shipped dark behind `OPERATOR_AUTH_ENABLED`.** The internal app gains a deny-by-default login gate (one Starlette middleware over every route except an allow-list: `/login` `/logout` `/health` `/static/*` `/portal/*` + the three machine heartbeat endpoints). Two roles — `superadmin` (account management) and `admin` (full app); `operator` reserved. Sessions are a 30-day HMAC-signed `tv_session` cookie re-validated against the DB each request (instant revoke via `active` / `sessions_valid_from`). Password reset is superadmin-driven: reset-anyone (temp shown once + forced change), self-service `/change-password`, and a `backend/operator_admin.py` seed/break-glass CLI. Brute-force lockout (5 tries / 15 min) + constant-time login (no user-enumeration). New `operator_users` table auto-creates — no migration. Reuses the portal's argon2 hasher + a new shared `backend/auth_cookies.py` signer. Rollout: ship with the flag off (app unchanged), seed a superadmin, confirm login, then flip on — the flag is an instant escape hatch. Spec: `docs/superpowers/specs/2026-06-17-operator-auth-design.md`.
- **Fan-out `/monitor` feed consumption.** The unit live view (`partials/slm_live_view.html`) and the dashboard live tile (`sound_level_meters.html`) now subscribe to SLMM's shared per-device monitor over `WS /api/slmm/{unit}/monitor` instead of each opening its own device stream. Any number of clients attach without each consuming the NL-43's single connection — the "second viewer sees nothing" contention is gone. A WS proxy handler for `/monitor` was added to `backend/routers/slmm.py`.
- **L1/L10 percentile lines + cards.** Both the per-unit live chart and the dashboard card chart now plot L1 (purple) and L10 (orange) alongside Lp/Leq, and the KPI cards show L1/L10. Sourced from the DOD feed's `ln1`/`ln2` (DRD streaming can't carry percentiles, DOD can). Missing/`-.-` values leave a gap rather than dropping the line to 0.
- **Live-chart backfill on open.** Charts seed from SLMM's downsampled DOD trail (`GET /api/slmm/{unit}/history?hours=2`) so a viewer sees recent trend immediately instead of a blank chart that fills one point per second.
+1 -1
View File
@@ -30,7 +30,7 @@ Base.metadata.create_all(bind=engine)
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
# Initialize FastAPI app
VERSION = "0.14.0"
VERSION = "0.15.0"
if ENVIRONMENT == "development":
_build = os.getenv("BUILD_NUMBER", "0")
if _build and _build != "0":
+1 -1
View File
@@ -8,7 +8,7 @@
// PWA users actually receive the new bundles instead of being stuck on
// the pre-bump version. Convention: keep it in sync with the Terra-View
// version string in backend/main.py.
const CACHE_VERSION = 'v0.13.2';
const CACHE_VERSION = 'v0.15.0';
const STATIC_CACHE = `sfm-static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `sfm-dynamic-${CACHE_VERSION}`;
const DATA_CACHE = `sfm-data-${CACHE_VERSION}`;
+1 -1
View File
@@ -4,7 +4,7 @@ Living document — captures known deferred work, in-flight initiatives, and lon
Bump items up/down or strike them through as priorities shift. Source of truth for "what's next"
should be this file plus the `## Current Development Focus` block in `CLAUDE.md`.
Last updated: 2026-06-05 (Terra-View v0.13.3)
Last updated: 2026-06-18 (Terra-View v0.15.0)
---
+14
View File
@@ -50,6 +50,20 @@ def _reset_portal_lockout():
yield
@pytest.fixture(autouse=True)
def _operator_auth_off_by_default(monkeypatch):
"""Pin the operator-auth gate OFF for every test, so the suite is deterministic
regardless of the container's OPERATOR_AUTH_ENABLED env (the dev container may
have it ON for manual testing). Tests that exercise the gate opt in via
wire_operator_auth(enabled=True), which overrides this within the test body."""
try:
import backend.operator_auth as _oa
monkeypatch.setattr(_oa, "OPERATOR_AUTH_ENABLED", False, raising=False)
except Exception:
pass
yield
def make_project(db_session, name=None, **kwargs):
"""Insert and return a Project with a unique name."""
p = models.Project(