Two Overview improvements for projects that mix vibration + sound:
- Live monitoring now includes only live-mode (connected) NRLs. connection_mode
lives in the location's metadata JSON (default "connected"); offline/manual
NRLs are excluded, and since the section hides when the list is empty, it
disappears entirely when no NRL is a live SLM.
- The Overview location list is split into separate "Vibration Locations" and
"NRLs" sections (driven by enabled modules) instead of one mixed list.
Single-module projects still show just their one section. Live-chip repaint
listener updated for the per-type list ids.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
submitClassify()'s success path closes the modal and reloads the list but
never resets the submit button (only the error paths did), and
openClassifyModal() reset the form fields but not the button. So after a
successful classify, the next modal opened with the button stuck disabled on
"Classifying…" — only a full page refresh cleared it.
Reset the submit button to "Classify"/enabled in openClassifyModal so every
open starts clean regardless of how the previous one ended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The classify modal's _loadProjects() fetched /api/projects/list and called
.json() on it, but that endpoint returns HTML project cards (used by the
projects overview via htmx). Parsing HTML as JSON threw, the catch swallowed
it, and the Project dropdown came up empty — so deployments couldn't be
assigned to a project.
- Add GET /api/projects/list-json returning assignable projects (id, name,
status) as JSON, excluding deleted/archived/completed to match the default
/list view.
- Point the modal's _loadProjects() at the JSON endpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sessions could only be tagged day or night (weekday/weekend). 24/7 continuous
jobs had no fitting period type. Add "24-Hour" (full_24h) — a single full-day
period covering day + night.
UI (session_list.html):
- Full-width "24-Hour" button under the WD/WE x Day/Night grid; teal badge.
- Selecting it clears + disables the hour inputs (no window); reopening an
existing 24-Hour session opens with hours disabled. Badge current-period
kept in sync after save.
Backend (projects.py):
- full_24h added to VALID_PERIOD_TYPES and the session-label maps
("... - 24-Hour"). Operator-set only; never auto-derived.
- Combined report: include ALL rows for a 24-hour session (no day/night
window filter) and split them by hour into the three non-overlapping
buckets — Daytime 7-18:59, Evening 19-21:59, Nighttime 22:00-06:59. Empty
period columns are dropped downstream, so it shows whatever periods have data.
Scoped to the combined-report path; the older per-session single report still
uses the fixed Evening/Nighttime layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each live monitoring tile is now a clickable link to its NRL detail page
(/projects/{id}/nrl/{location_id}) — same target as the NRL card name — with
a hover border + lift affordance so it reads as clickable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a compact live-status chip to each NRL card in the location list, so the
inline list next to the map (Overview tab) and the Sound > NRLs tab show live
state alongside the new live tiles. Both surfaces share location_list.html, so
this lands in both.
- Chip shows "● <Leq> dB" when measuring (tinted green/amber/red at the same
55/70 thresholds as the live tiles), else Stopped / Offline / Wedged.
Hidden for cards with no assigned unit and for vibration locations.
- Painted by the existing 15s Overview poller (no extra requests). Repaints on
htmx:afterSwap so the chips survive the NRL list reloading (e.g. the 30s
dashboard swap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client portal has a live dashboard but the internal project page only
showed static counts. Add a portal-style live section to the Overview tab
so operators can see real-time sound levels at a glance.
Backend:
- New GET /api/projects/{id}/live-stats — resolves each sound NRL to its
active SLM unit and returns SLMM's cached /status snapshot (concurrent
fetch). Internal-rich: includes battery/power/reachability the portal
scrubs. Degrades to no_device/unreachable/no_data per location.
Frontend (project detail Overview tab):
- Rollup strip (live / offline / loudest-now) + a live tile per NRL with a
Live/Stopped/Offline/Wedged badge, color-coded Leq (55/70 thresholds),
Lp/Lmax, last-seen, and battery/power.
- Self-refreshes every 15s, pauses when the browser tab is hidden, and sits
outside the 30s htmx dashboard swap so it never flickers. Polls only for
projects with the sound module.
Reuses the same SLMM /status source as the portal; no SLMM changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Starting a measurement could pop "Error: Unknown error" in the browser
even though the device started recording fine. Two causes: the proxy's
10s timeout was shorter than a real device start over cellular, and on
an httpx timeout str(e) is empty, so the relayed detail was "" -> the
frontend's `result.detail || 'Unknown error'` rendered "Unknown error".
- Raise the control proxy timeout to 30s so a healthy start isn't cut off.
- Surface SLMM's own error detail on non-200 responses.
- Add an explicit, honest timeout message.
- Never return an empty detail (which rendered as "Unknown error").
Pairs with the SLMM-side fix that makes /start confirm promptly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- operator_users router now depends on _require_auth_enabled, which raises
404 when OPERATOR_AUTH_ENABLED is false — prevents world-open pre-seeding
of a superadmin while the flag is off (the default). Flag is read as a
live module attribute (operator_auth.OPERATOR_AUTH_ENABLED) so monkeypatching
in tests and a runtime flip both take effect.
- operator_gate passes OPTIONS requests through immediately before the exempt-
path check, so CORS preflight reaches CORSMiddleware rather than being
303/401'd by the gate.
- Two new tests: test_admin_surface_404s_when_flag_off (test_operator_users)
and test_options_preflight_passes_through_gate (test_operator_gate).
Full suite: 90 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/admin/users page and /api/admin/users/* JSON CRUD endpoints, all behind
require_role("superadmin"). Temp passwords are returned once on create/reset
and never stored in plaintext. Admins get 403; password_hash is never leaked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds operator_gate Starlette HTTP middleware that gates every route
except an explicit allow-list. Flag defaults OFF so all existing
behaviour and tests are unchanged. wire_operator_auth helper in
conftest lets tests monkeypatch the module-global SessionLocal and
flag, keeping the gate's own DB session pointed at the test engine.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OperatorUser SQLAlchemy model (operator_users table, auto-created by
create_all) with email uniqueness, default active/must_change_password/
failed_login_count, and sessions_valid_from truncated to whole seconds.
Add backend/operator_auth.py with feature flag, cookie constants, _ROLE_RANK
map, role_at_least(), and _norm_email() helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cut [0.14.0] consolidating SLM live monitoring, the FTP night-report
pipeline (was missing from the changelog entirely), the client portal,
and portal auth Phase 1 under one entry. Bump VERSION + README to 0.14.0
and add the sound-monitoring / night-report / client-portal features to
the README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If the post-restart DOD check shows the meter isn't measuring, retry once with start_recording (a plain start that does NOT re-index, unlike start_cycle) and re-verify before raising the schedule-failed alert. Retry fires only on a confident not-measuring reading — never on a failed/inconclusive DOD read — so a flaky read can't disrupt an already-running measurement or split the night across two store folders. Turns a transient restart hiccup into a self-heal instead of a meter left stopped overnight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- delete dead magic-link helpers (resolve_token, ensure_project_client,
mint_link_token, provision_preview_session) + now-unused datetime import
- key brute-force lockout on link_token alone (IP term only enabled a
source-IP-rotation bypass; behind the proxy all clients share one IP)
- drop unused PORTAL_BASE_URL from the retired CLI
- add WebSocket ownership tests (unauth + cross-project both close 1008)
Modules own raw device data; Terra-View owns fleet/project/session/report context. Documents the SFM (read-through) vs SLMM (Terra-View-stored) asymmetry, the rule new modules must follow, and grandfathers SLMM as a deliberate-future-realignment exception. Establishes the docs/adr/ convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Caught by adversarial review of the scope test: portal_client_for_project minted a
dedicated client but never set project.client_id, so the client-scoped routes found
no projects — every location 404'd, including the client's own (empty portal). Now
links the project + adds a positive-case test.