Commit Graph

382 Commits

Author SHA1 Message Date
serversdown 9b3b2189d0 Merge pull request 'update to 0.16.0' (#72) from dev into main
Reviewed-on: #72
v0.16.0
2026-06-23 00:59:44 -04:00
serversdown ff4b7f3d86 chore(release): 0.16.0 — modular projects & live Overview
Version bump 0.15.0 → 0.16.0 across main.py VERSION, sw.js CACHE_VERSION
(evicts stale PWA caches), README (header + highlights + version section),
ROADMAP stamp, and the CHANGELOG 0.16.0 entry.

Covers everything since 0.15.0: per-module status (independent sound/vibration
lifecycle, new project_modules.status column + migration), live monitoring on
the internal project Overview, browsable vibration events (Events sub-tab +
location filter + sortable columns), 24-Hour session period type, redesigned
project cards + per-module quick-open, the module-folder header restructure, and
five fixes (SLM start false-error, classify-modal dropdown + stuck button,
deployment GPS on existing locations, event date filters).

Deploy: run backend/migrate_add_module_status.py on prod; ships with SLMM v0.4.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
2026-06-23 01:02:48 +00:00
serversdown 2f5f6a2fce chore: add redeploy script 2026-06-22 23:04:42 +00:00
serversdown 80464c6f11 feat(projects): per-module stat breakdown on project cards
The single Locations/Units/Active row was confusing: "Active" collided with
the green Active status badge and actually meant sound recording sessions, so
vibration-only projects showed a meaningless "Active 0", and combined projects
lumped both modules together with no split.

Cards now show one stat line per module, each carrying its own identity +
status badge (so the separate chip row is dropped as redundant):
  Vibration   N locations · M units
  Sound       N NRLs · M units · K recording

- /list endpoint computes module_stats: locations (active, by type) and units
  counted via a join on the assigned location's type — so a module's unit
  count always reconciles with its location count (verified: sound+vibration
  units == total active assignments for every project).
- "recording" (active sessions) shows only under Sound, where it's meaningful.
- Projects with no modules fall back to a simple Locations/Units row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
2026-06-22 20:38:48 +00:00
serversdown f54c62b332 feat(projects): projects page overhaul — events, header, module toolbars, cards
Addresses a batch of projects-page UX issues:

1. Vibration Events sub-tab: add a Location filter + clickable column
   sorting (Timestamp/Location/Serial/Tran/Vert/Long/PVS/Mic). Events are
   cached client-side so location-filter and sort are instant (no SFM refetch).

3. Drop the misleading single-module "Sound Monitoring" subtitle on the
   Overview card (combined projects have multiple modules); show the
   project number · client identity instead.

4. Header cleanup: move the sound-only actions (Generate Combined Report,
   Night Report, Report Settings) and the Manual/Remote chip out of the
   global project header and into the Sound tab's module toolbar. The header
   now carries project-level concerns only (status, modules, merge). The
   Night Report / Report Settings modals stay defined in the header partial
   (global), so the relocated buttons still call them.

2. Per-module status UI: each module tab gets a status dropdown
   (active/on_hold/completed) wired to the new endpoint; the header module
   chips show a "✓ Done" / "On hold" badge.

5. Project cards redesigned: module mix accent strip, Sound/Vibration chips
   with per-module status, project number · client identity, and per-module
   "Sound"/"Vibration" quick-open buttons that deep-link into that module's
   tab (#sound / #vibration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
2026-06-22 20:24:40 +00:00
serversdown 092b72f63c feat(projects): per-module status (independent sound/vibration lifecycle)
Each ProjectModule now carries its own status (active|on_hold|completed)
so one half of a combined project can wrap up while the other keeps
running — e.g. mark Sound "completed" while Vibration stays "active",
without archiving the whole project.

- models.py: ProjectModule.status column (default 'active')
- migrate_add_module_status.py: idempotent ALTER (run on prod before deploy)
- projects.py: _get_module_statuses() helper, MODULE_STATUSES, and a
  PUT /{id}/modules/{type}/status endpoint; module_status now included in
  the project GET, header, and /list contexts so the UI can render it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1
2026-06-22 20:24:27 +00:00
serversdown 5dc0aa4064 feat: Vibration Events sub-tab + last-event on location cards
Two additions to the project Vibration tab:

- Events sub-tab (next to Locations): a project-wide events table across all
  vibration locations. New GET /api/projects/{id}/vibration-events fans
  events_for_location across the project's vibration locations, tags each event
  with its location, and merges newest-first (From/To date filters, Real/FT
  filter, limit). Table columns Timestamp/Location/Serial/Tran/Vert/Long/PVS/
  Mic/Flags; rows open the shared event-detail modal (Chart.js + event-modal.js
  come from the modal partial). Lazy-loads on first open; refreshes on
  sfm-event-review-saved.
- Last event per location card: thread last_event (already in
  events_for_location stats) through the locations endpoint and show
  "Last event: …" on vibration cards.

Reuses the same event source + modal as the per-location Events tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:48:55 +00:00
serversdown c45fcd7804 fix: event date filters unusable (datetime-local, no apply)
The unit and vibration-location event tabs used datetime-local From/To inputs
with onchange. Picking a date but leaving the time blank left the value empty,
so nothing applied — and there was no Apply button, so date-only filtering was
impossible.

Switch From/To to plain date inputs (apply immediately on selection) and send
a full inclusive day to the backend (From 00:00:00 → To 23:59:59).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:39:31 +00:00
serversdown 93f01be471 fix: deployment capture coords now reach existing locations
The /deploy classify "Assign to existing location" path dropped the captured
GPS — only "Create new location" applied it — so units assigned to pre-existing
coordless locations left those locations without a pin.

- Classify (promote) now backfills the captured GPS onto an existing location
  that has no coordinates (doesn't clobber operator-set coords).
- Add "Reforward info" button on Assigned deployment cards + endpoint
  POST /pending/{id}/resync-location that re-pushes a capture's GPS onto its
  assigned location (explicit action, overwrites). Fixes already-classified
  locations and guards against this recurring. Logged to unit history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:12:56 +00:00
serversdown ee6062f9fb feat: Overview live-mode-only NRLs + split locations by type
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>
2026-06-22 18:12:45 +00:00
serversdown 85a64c83f8 fix: classify button stuck on "Classifying…" on modal reopen
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>
2026-06-22 16:55:45 +00:00
serversdown 03f3dca243 fix: empty project dropdown in pending-deployment classify modal
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>
2026-06-22 16:43:51 +00:00
serversdown ceb893a54c feat: add 24-Hour (full-day) session period type
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>
2026-06-22 16:37:38 +00:00
serversdown 5b70dcf071 feat: make Overview live tiles link to NRL detail
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>
2026-06-21 22:01:12 +00:00
serversdown bb5b407c98 feat: live status chip on NRL list cards
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>
2026-06-21 21:30:54 +00:00
serversdown 76330f6137 feat: live monitoring section on internal project Overview
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>
2026-06-21 21:15:23 +00:00
serversdown c049ac8a41 fix: SLM control no longer shows false "Unknown error" on start
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>
2026-06-21 20:22:52 +00:00
serversdown c9bb25e7e1 chore(release): 0.15.0 — operator authentication (version, CHANGELOG split, SW cache, test isolation) 2026-06-18 22:04:16 +00:00
serversdown fdcb1ca532 Merge pull request 'Operator-Auth full implementation.' (#70) from feat/operator-auth into dev
Reviewed-on: #70
2026-06-18 16:35:59 -04:00
serversdown c5ffa5c8ea docs(deploy): add .env.example documenting SECRET_KEY / COOKIE_SECURE / OPERATOR_AUTH_ENABLED 2026-06-18 20:03:35 +00:00
serversdown 3f0b53c46c Merge pull request '0.14.0 update from dev - client portal, SLMM expansion, multistream support.' (#67) from dev into main
Reviewed-on: #67
v0.14.0
2026-06-17 16:41:10 -04:00
serversdown 68161298a4 fix(auth): hide /admin/users when flag off; pass OPTIONS preflight through gate
- 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>
2026-06-17 20:27:01 +00:00
serversdown 054eebe68b chore(auth): wire OPERATOR_AUTH_ENABLED into compose + changelog 2026-06-17 20:17:01 +00:00
serversdown dc95a59dfa test(auth): regression guard — gate never blocks machine endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:14:30 +00:00
serversdown 7a4453108a feat(auth): operator admin/break-glass CLI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:50:37 +00:00
serversdown bff9a4af4a feat(auth): superadmin user-management page + CRUD
/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>
2026-06-17 19:48:13 +00:00
serversdown 41ab900c33 feat(auth): login/logout/change-password routes + pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:38:55 +00:00
serversdown 2879abb355 feat(auth): deny-by-default gate middleware + require_role
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>
2026-06-17 19:27:41 +00:00
serversdown e8fe4845aa feat(auth): authenticate + lockout + operator data helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:19:36 +00:00
serversdown a6e1cb4f87 feat(auth): operator session cookie + current_operator DB re-validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:11:41 +00:00
serversdown 4abfcbc293 feat(auth): OperatorUser model + role ladder
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>
2026-06-17 19:08:18 +00:00
serversdown 8e817ec48d feat(auth): generic HMAC signed-cookie module for operator auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:05:56 +00:00
serversdown 37e6ca55c1 docs: operator-auth implementation plan (10 TDD tasks) 2026-06-17 18:57:26 +00:00
serversdown 59c19291ca docs: operator-auth design spec (v1 password login + roles + easy reset; 2FA/operator-role deferred)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:39:49 +00:00
serversdown abdb3869bd docs: consolidate changelog into 0.14.0 + refresh README
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>
2026-06-17 01:29:32 +00:00
serversdown 0beba9bfbc Merge pull request 'Sound night-report pipeline (v1): automated FTP capture → ingest → morning report' (#66) from feat/ftp-report-pipeline into dev
Reviewed-on: #66
2026-06-16 20:17:09 -04:00
serversdown b0e5dcdc52 Merge branch 'dev' into feat/ftp-report-pipeline 2026-06-16 21:52:06 +00:00
serversdown 4ca167de5e Merge pull request 'chore: wire SECRET_KEY + COOKIE_SECURE env pass-throughs in compose; flesh out changelog upgrade notes' (#64) from feat/portal-auth into dev
Reviewed-on: #64
2026-06-16 16:55:23 -04:00
serversdown 98bbbcfa86 Merge pull request 'Client portal auth (Phase 1): per-project link + password gate' (#63) from feat/portal-auth into dev
Reviewed-on: #63
2026-06-16 14:59:57 -04:00
serversdown b536877566 chore: wire SECRET_KEY + COOKIE_SECURE env pass-throughs in compose; flesh out changelog upgrade notes 2026-06-16 18:53:42 +00:00
serversdown 3c5e830f9c feat(reports): one safe restart-retry in the cycle before alerting
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>
2026-06-16 02:54:37 +00:00
serversdown 766f64f35f refactor: final-review cleanup
- 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)
2026-06-16 00:28:23 +00:00
serversdown da128f6173 docs: changelog + portal-auth Phase 1 notes 2026-06-16 00:19:33 +00:00
serversdown 76f16a2aba docs(adr): establish device data ownership principle (ADR 0001)
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>
2026-06-16 00:17:00 +00:00
serversdown 20f62a5c0a feat: env-driven Secure flag on portal session cookie
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:16:54 +00:00
serversdown 01180d5725 fix: retire portal_admin mint-link (dead /portal/enter URL); refresh docstrings; assert revoke route gone 2026-06-16 00:15:09 +00:00
serversdown f0a13ea2ff refactor: retire interim magic-link/open-link in favor of password gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:06:02 +00:00
serversdown 0394f4b0c8 fix: error handling + robust state in Portal access panel JS (per review) 2026-06-16 00:02:33 +00:00
serversdown eb91441904 feat: operator Portal access panel (enable + password + link)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:59:41 +00:00
serversdown 25a4a28433 feat: operator portal-access endpoints (enable/password/disable/state)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:55:10 +00:00