8 Commits

Author SHA1 Message Date
serversdown 0e2086d6bb Merge pull request 'v0.13.2 - s4 event pipeline complete' (#56) from dev into main
Reviewed-on: #56
2026-06-01 17:41:18 -04:00
serversdown 623ef648b7 release: v0.13.2 — PWA cache fix so mobile gets the v0.13.x modal
Mobile operators were never seeing the inline PDF preview, .TXT
download, or Review form that v0.13.0 added — every feature was
working on desktop browsers but invisible in the PWA.

Root cause: backend/static/sw.js had CACHE_VERSION = 'v1', unchanged
since v0.12.x.  The activate handler deletes any cache not matching
CACHE_VERSION, so without a bump the stale sfm-static-v1 cache (with
the pre-v0.13.0 event-modal.js) stayed authoritative.  cacheFirst
strategy served it forever; mobile users effectively saw the v0.12.x
modal regardless of how many times we rebuilt the image.

Fix:
- CACHE_VERSION bumped to 'v0.13.2' (matches backend/main.py VERSION).
  Comment in sw.js documents the convention: any release touching a
  static asset must bump this string.
- event-modal.js added to the precache list so its lifecycle is
  explicitly tied to the SW version bump (installed fresh on activate
  rather than landing via the cacheFirst-then-cached pattern).

Mobile users get the new modal on next page nav: SW update check
picks up the bumped sw.js, skipWaiting installs it, activate evicts
the v1 caches, controllerchange fires, page reloads, fresh
event-modal.js loads.  Worst case ~1h delay from
registration.update() interval; operators can force-refresh by
closing + reopening the PWA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:11:07 +00:00
serversdown 5ed00bf70e release: v0.13.1 — mic chart defaults to psi (matches PDF)
v0.13.0 shipped the mic_unit_pref default as "dBL", which made the
website chart's mic axis inconsistent with the PDF report (which
renders psi).  Original brief was always "psi on charts, dBL on
peaks" — I implemented the default backwards.  Operator caught it
within an hour of rollout.

Same-day patch:
- backend/models.py: default "dBL" → "psi"
- migrate_add_mic_unit_pref.py: idempotent across both fresh DB
  ("add column with psi default") and v0.13.0 upgrade ("flip dBL
  rows to psi").  One-row table, freshness assumed.
- backend/routers/settings.py: GET/PUT fallback "dBL" → "psi"
- templates/settings.html: dropdown's `selected` flag moves to psi
  + reorders options + relabels with "(matches PDF report)" hint
- backend/static/event-modal.js: module-level fallback + branch
  conditions flip to make psi the unset/error default

Includes the "Captured at" → "Time received" relabel from earlier
in the day (already-shipped commit 43c804d) rolled into the
release notes.

Migration is idempotent + safe to re-run; rolled out on the dev
container during this commit's smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:57:32 +00:00
serversdown 43c804d0c4 event-modal: relabel "Captured at" → "Time received"
"Captured at" was easily misread as "when the device captured the
event" — but that's the event's Timestamp at the top of the modal
(unit-local trigger time).  source.captured_at is actually when SFM
received and stored the event.  New label avoids the ambiguity, and
the hover tooltip spells it out for anyone unsure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:58 +00:00
serversdown c1f995b4d3 release: v0.13.0 — SFM integration Phase 1
Phase 1 closes the read-only gap between Terra-View and the
standalone SFM webapp on port 8200.  Operators no longer need to
bounce between the two for routine event review.

Wraps up four commits shipped this iteration:
  db8d666  settings: add mic_unit_pref for event-report chart
  1d9fd00  event-modal: port 4-channel Chart.js waveform/histogram
           panels + docker-compose mount fix for SFM container
  4b2bb9a  event-modal: inline PDF preview + .TXT link + review form
  2905a32  admin_events: wire shared event-detail modal into the page

Highlights:
- Inline PDF preview via iframe (lazy-loaded; browser-native zoom)
- Chart.js 4-channel waveform/histogram in the modal
- Review form persisted to sidecar via PATCH
- /admin/events row click opens the modal (was port-8200-only)
- mic_unit_pref setting (dBL default, psi alternate; chart only)
- Cross-modal CustomEvent so host tables refresh on save

Phase 2 (device control: start/stop monitoring, push compliance,
erase) deferred pending SFM auth layer — see seismo-relay roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:22:55 +00:00
serversdown d0685baed5 Merge pull request 'v0.12.1 — Unit Swap wizard, editable timeline, roster/tz fixes' (#54) from dev into main
Reviewed-on: #54
docs+chore: v0.12.1 — Unit Swap wizard, editable timeline, roster/tz fixes

CHANGELOG entry for the five commits that landed after the v0.12.0 tag:
two features (Unit Swap wizard at /tools/unit-swap, editable deployment
timeline on /unit/{id}) and two correctness fixes (RosterUnit.deployed
now flips on swap/unassign/promote; deployment timeline now respects
user timezone for both display and edits).  No schema migrations.

README bumped to v0.12.1 with new bullets for the post-v0.12.0 features
and several already-shipped items that were missing from the list (SFM
Event DB Manager, Deployment-History calendar + Gantt tabs, reusable
location-map partial).  backend/main.py VERSION constant bumped too.
2026-05-20 11:44:47 -04:00
serversdown 275a168046 Merge pull request 'merge v0.12.0' (#51) from dev into main
Reviewed-on: #51
2026-05-17 19:44:56 -04:00
serversdown f4fd1c943d Merge pull request 'v0.11.0' (#50) from release/0.11.0 into main
## [0.11.0] - 2026-05-15

Operator-facing polish release.  All work builds on the v0.10.0 SFM integration foundation — this release is about making the day-to-day workflows (managing locations, cleaning up bad attributions, browsing deployments) faster and less error-prone.

### Added
- **Soft-remove monitoring locations** (`POST /api/projects/{p}/locations/{l}/remove` + `/restore`): mark a location as no longer actively monitored without destroying historical events.  Cascade-closes active unit assignments and cancels pending scheduled actions at the location.  Restored locations rejoin the active list (assignments are NOT auto-reopened — operator creates new ones if resuming).  Project page splits locations into Active and Removed sections; removed cards are greyed out, badged with the removal date + reason, and offer a Restore button.
- **Per-unit deployment Gantt chart** above the existing Deployment Timeline list on every seismograph unit detail page.  Plain-SVG rendering, color per location, today marker (orange dashed line), reduced-opacity bars for closed assignments, blue outlines on metadata-backfilled assignments, dashed blue underlines marking mergeable groups.  Click a bar to scroll the matching list row into view with a flash highlight.
- **Merge consecutive same-location assignments** (`POST /api/projects/{p}/assignments/merge`): operators often end up with several rows representing one continuous deployment (after remove/restore, or metadata-backfill adjacent to a manual record).  Now auto-detected and surfaceable in the timeline header — one click combines them into a single record.  Preserves the earliest record's notes + ingest source, writes an `assignment_merged` audit entry, deletes the others.
- **Delete assignment for mis-clicks** (`DELETE /api/projects/{p}/assignments/{a}`): hard-deletes a bogus assignment row that was never a real deployment.  Trash icon in each row of the location's Deployment History panel.  Refuses the delete if any `MonitoringSession` exists in the assignment's window — those should go through Unassign instead, which preserves audit history.  Writes an `assignment_deleted` UnitHistory row.
- **Drag-to-reorder location cards**: each active card has a six-dot drag handle on the left.  Drag/drop reorders the DOM and persists via `POST /api/projects/{p}/locations/reorder`.  Implementation uses native HTML5 drag-and-drop (no library).  New locations land at the end (`sort_order = max + 1`); removed locations stay sorted by removal date.
- **Three-dot kebab menu on location cards**: replaces the four inline pill buttons (Unassign / Edit / Remove / Delete) with a single ⋮ menu.  Click ⋮ to open; click outside or Escape to close; only one menu open at a time.
- **Event count on vibration location cards**: vibration cards now show "{N} events" sourced from SFM via concurrent fan-out, instead of "Sessions: 0" (sessions don't exist under the watcher-forward pipeline).  Sound locations still show session counts.
- **Project overview location map**: right column of every project's overview replaces the lightly-used Upcoming Actions panel with a Leaflet map.  One pin per active monitoring location (parsed from the `coordinates` field).  Click pin → scrolls + flashes the matching card.  Tooltip on hover.  Locations without coordinates surface as an inline hint below the map.  If the project has pending scheduled actions, a small "{N} upcoming actions →" link appears in the card header that switches to the Schedules tab.

### Changed
- **Backfill location fuzzy matcher is now stricter**: `rapidfuzz.WRatio` was over-confident on location names because their shared boilerplate vocabulary ("Area", "Loc", numbers) inflated scores.  Example false positive that prompted the change: `"Area 2 - Brookville Dam - Loc 2 East"` vs `"Area 1 - Loc 1 - 87 Jenks"` scored 86% via WRatio.  Now uses `token_set_ratio` as the base scorer plus a 0.30 penalty when the two strings have disjoint multi-digit numeric tokens.  Catches the "same project, different address number" case (`"68 Jenks"` vs `"87 Jenks"`) that pure token-set scoring still rated above 0.90.  Project matching keeps WRatio (where its leniency is desirable for typos like `1-80` vs `I-80`).

### Fixed
- **Three separate JSON.stringify quote-collision bugs**: any inline `onclick="...({...} | tojson)"` or `onclick="...${JSON.stringify(x)}..."` where `x` contained any character that JSON quotes (essentially every real-world string) broke the HTML attribute and silently un-bound the click handler.  Surfaced in three places this release; all fixed by switching to `data-*` attributes plus a trampoline function reading from `this.dataset`:
    - **Location Remove button** on the project page
    - **Metadata-backfill typeahead dropdown** (existing project + location pickers)
    - **Project-merge typeahead dropdown** (in the per-project header)
- **Project-merge modal too short to show typeahead options without scrolling**: modal body's `flex-1 overflow-y-auto` collapsed tight; added `min-height: 480px` to the modal container + `min-h-[320px]` to the body so the dropdown always has room.
- **Project location map covered modals**: Leaflet's internal panes carry z-indexes 200–800 by default and the map container didn't establish a stacking context, so those z-indexes leaked into the root and outranked modals' `z-50`.  Fixed by adding `isolation: isolate` to the map container.
- **`delete_assignment` crashed with `AttributeError`**: the safety check queried `MonitoringSession.start_time` but the actual column is `started_at`.  Every DELETE call to `/assignments/{id}` failed with 500 before doing anything.

### Migration Notes
Run on each database before deploying.  Both migrations are idempotent and non-destructive.

```bash
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_location_removed.py
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_location_sort_order.py
```

Or sweep all migrations at once (safe — already-applied ones no-op):

```bash
for f in backend/migrate_*.py; do
  docker exec terra-view-terra-view-1 python3 "/app/backend/$(basename $f)"
done
```

New columns added this release:
- `monitoring_locations.removed_at` (DATETIME, nullable) — NULL means active
- `monitoring_locations.removal_reason` (TEXT, nullable)
- `monitoring_locations.sort_order` (INTEGER, default 0) — seeded to alphabetical-index per project on first migration

**Deploy order matters**: migrations must run BEFORE the new code is up, otherwise the running app will throw 500s on the unrecognized columns.  Idempotent migrations make this recoverable but it's better avoided — the v0.11.0 deploy on prod hit this exact window after the v0.10.0 release.

---
2026-05-15 19:16:42 -04:00
9 changed files with 155 additions and 33 deletions
+91
View File
@@ -5,6 +5,97 @@ All notable changes to Terra-View will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.13.2] - 2026-05-30
PWA-cache fix for mobile operators. v0.13.0 added the inline PDF preview, `.TXT` download, and Review form to `event-modal.js`, but mobile devices using Terra-View as a PWA never saw any of it — the service worker had `CACHE_VERSION = 'v1'` (unchanged since v0.12.x), so the activate handler never evicted the stale cache and mobile users kept getting served the pre-v0.13.0 modal forever.
### Fixed
- **Service worker cache version bumped + tied to the app version**. `CACHE_VERSION` in `backend/static/sw.js` is now `'v0.13.2'`, which causes the SW's activate handler to delete the old `sfm-static-v1` / `sfm-dynamic-v1` / `sfm-data-v1` caches on first visit after the upgrade. Going forward the convention is: any release that touches a static asset must bump `CACHE_VERSION` to match `backend/main.py`'s `VERSION`. Comment in `sw.js` documents this.
- **`event-modal.js` precached** alongside `mobile.js` / `offline-db.js` etc. Lifecycle is now tied to the SW version bump explicitly — old modal JS gets evicted on activate, new modal JS is fetched and cached during install.
### What mobile users will see after deploy
On next page navigation the SW update check fires, the new SW installs (skipWaiting), activate evicts the v1 caches, `controllerchange` fires, the page reloads with the v0.13.x modal. On the worst-case device (no recent visit), it might take up to an hour for `registration.update()` to pick up the new SW — operators can force-refresh by closing and re-opening the PWA, or by clearing site data once.
---
## [0.13.1] - 2026-05-29
Same-day patch on top of v0.13.0. Fixes the mic-chart unit default — v0.13.0 shipped with `dBL` as the default, but the PDF report renders the mic axis in psi, so the website chart and the printed report didn't match. Operator caught it within an hour of rollout. Also relabels the modal's "Captured at" field to "Time received" so it isn't mistaken for the device's trigger time.
### Fixed
- **Event-detail modal: mic chart now defaults to psi**, matching the PDF report's mic axis. The waveform/histogram chart's mic channel now renders in raw psi by default; operators who specifically prefer dB(L) on charts can flip it via Settings → General → "Event Report — Mic Channel Units". Peaks everywhere else (table tiles, modal Peaks section, KPI summaries) stay in dB(L) as before — this is strictly a chart-axis change.
- **Modal label: "Captured at" → "Time received"** (+ tooltip clarifying it's the SFM ingestion time, not the unit-local trigger time at the top of the modal). Same change in seismo-relay's standalone webapp for consistency.
### Migration Notes
The bundled `backend/migrate_add_mic_unit_pref.py` is now idempotent across both the v0.13.0 "add column" path and the v0.13.0 → v0.13.1 default flip. Existing rows sitting at the original `'dBL'` default (i.e. nobody touched the setting yet — true for almost everyone) get bumped to `'psi'` on migration.
```bash
cd /home/serversdown/terra-view
docker compose build terra-view && docker compose up -d terra-view
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_mic_unit_pref.py
```
If you _did_ deliberately set the chart to dB(L) via Settings between v0.13.0 rollout and this patch, the migration will reset it — one click in Settings to restore. Trade-off considered acceptable given the very small user base and the freshness of the v0.13.0 release.
---
## [0.13.0] - 2026-05-29
The "SFM integration Phase 1" release. Closes the gap between Terra-View and the standalone SFM webapp on port 8200 — operators no longer need to bounce between the two for routine event review. The shared event-detail modal (used on `/sfm`, `/unit/{id}`, `/admin/events`, and `/projects/{p}/nrl/{l}`) gains a Chart.js waveform/histogram chart, inline PDF preview, original `.TXT` download, and a review form with false-trigger flag + reviewer + notes. `/admin/events` finally gets the modal too. A new Settings field controls the mic chart's display unit.
### Added — Event-detail modal: Chart.js waveform/histogram panels
- **4-channel stacked plots** (MicL → Long → Vert → Tran, matching BW Event Report layout) inside the existing `partials/event_detail_modal.html` shell. Ported from seismo-relay's standalone `sfm/sfm_webapp.html:2555-2880`; theme-aware grid + tick colors (light/dark mode via Tailwind's `dark` class on `<html>`).
- **Waveform mode**: line plot, symmetric Y-axis around zero for geo channels, dashed trigger overlay at `t=0` with triangle markers above and below, zero-baseline dashed line + "0.0" label on the right margin. Downsamples at >3000 samples to keep render time bounded.
- **Histogram mode**: bar plot, zero-anchored Y with minimum range (`0.05 in/s` geo, `0.001 psi` mic) so quiet events don't fill the panel. X-axis uses `time_axis.interval_times` (HH:MM:SS labels emitted by seismo-relay v0.20.0+) when available, otherwise falls back to interval index. Trigger/zero-baseline overlays suppressed (no trigger concept on histograms).
- **Mic conversion** — converts raw psi samples to dB(L) for the chart when the operator's `mic_unit_pref` is "dBL" (the default). Rectifies the AC waveform (`abs()`) and floors at `MIC_DBL_FLOOR = 60` so the chart reads as an SPL-vs-time curve instead of a sparse pattern of isolated spikes above the floor. Peak label uses the unrectified value.
- **Chart cleanup** — `_destroyCharts()` runs on modal close so repeated open/close doesn't leak Chart.js instances.
- Chart.js 4.4.1 pinned via cdn.jsdelivr at the bottom of the modal partial; matches the standalone webapp's reference version.
### Added — Event-detail modal: PDF preview + downloads + review form
- **"Show Event Report PDF"** toggle opens an inline iframe inside the modal (no second-layer modal, no new browser tab). Iframe lazy-loads on first reveal — closing the modal without opening the PDF never spends bandwidth on the fetch. Sized 80vh / 600px min so a typical letter-portrait single-page report fits with browser-native zoom + download + print controls available. Companion "Download PDF" button for direct save.
- **"Original .TXT report"** download link, rendered only when `sidecar.source.txt_filename` is present (events ingested with seismo-relay's `.TXT` preservation pattern, post-2026-05-27). Hidden for legacy events to avoid 404 dead links.
- **Inline Review form** — `false_trigger` checkbox + reviewer text input + notes textarea + Save button. Persists via `PATCH /api/sfm/db/events/{id}/sidecar` with `{review: {...}}`. Status line shows last-reviewed timestamp + save success/failure feedback. On save fires a `sfm-event-review-saved` `CustomEvent` on `window` so the host page's table can refresh without a full reload — wired up on `/sfm`, `/unit/{id}`, `/admin/events`, and `/projects/{p}/nrl/{l}`.
### Added — `/admin/events` row click opens the modal
- The SFM Event DB Manager at `/admin/events` previously had no detail view — admins had to copy an event ID and load the standalone webapp on port 8200. Now table rows are clickable: `onclick` on `<tr>` calls `showEventDetail(id)`, with `event.stopPropagation()` on the checkbox cell so bulk-selection clicks don't also open the modal.
- `partials/event_detail_modal.html` + `event-modal.js` are now included on this page, matching the existing pattern on `/sfm`, `/unit/{id}`, and `/projects/{p}/nrl/{l}`.
### Added — `mic_unit_pref` user setting (Settings → General)
- **New `user_preferences.mic_unit_pref` column**, "dBL" default with "psi" as the alternate value. Controls only the event-report modal's waveform chart mic axis — peak values in every other surface (event tables, KPI tiles, modal Peaks section) stay in dB(L) regardless.
- Surfaced as a single dropdown on Settings → General, below the auto-refresh interval. Round-trips through `GET/PUT /api/settings/preferences`.
- New `backend/migrate_add_mic_unit_pref.py` script for existing databases — idempotent ALTER TABLE.
### Fixed — Docker Compose: SFM container can finally read the DB
- `../seismo-relay-prod-snap` is now bind-mounted into the SFM container at the same absolute host path it had outside, so the symlinked `seismo_relay.db` + `waveforms/` directory inside `bridges/captures/` resolve. Without it, SFM 500'd on every `/db/*` proxy call because the symlink target wasn't visible from inside the container. Read-write (not `:ro`) because SFM opens the DB in WAL mode, which requires creating `-wal` and `-shm` sidecar files even for reads.
### Migration Notes
```bash
cd /home/serversdown/terra-view
# Apply the new column to the database — required. Idempotent.
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_mic_unit_pref.py
# Rebuild + restart both Terra-View and SFM (compose mounts changed).
docker compose build terra-view && docker compose up -d
```
Set Settings → General → "Event Report — Mic Channel Units" if "psi" is preferred over the default "dB(L)". Setting persists in the DB and is fetched once per modal open.
### What's NOT in this release
Device-control endpoints (`/device/*` — start/stop monitoring, push compliance config, erase events, etc.) remain unexposed in the Terra-View UI. They proxy through transparently but no page calls them. Phase 2 of the SFM integration will bring them online once the SFM auth layer lands (a hard prerequisite — anything reachable through Terra-View's URL needs to be gated against unauthenticated callers).
---
## [0.12.1] - 2026-05-20
Field-operations polish — three small features and two correctness fixes that smooth out the deployment workflow added in v0.12.0. The new Unit Swap wizard and editable deployment timeline are the operator-facing items; the swap/unassign/promote roster-flag fix closes a long-standing data-consistency hole.
+1 -1
View File
@@ -1,4 +1,4 @@
# Terra-View v0.12.1
# Terra-View v0.13.2
Backend API and HTMX-powered web interface for managing a mixed fleet of seismographs and field modems. Track deployments, monitor health in real time, merge roster intent with incoming telemetry, and control your fleet through a unified database and dashboard.
## Features
+1 -1
View File
@@ -30,7 +30,7 @@ Base.metadata.create_all(bind=engine)
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
# Initialize FastAPI app
VERSION = "0.12.1"
VERSION = "0.13.2"
if ENVIRONMENT == "development":
_build = os.getenv("BUILD_NUMBER", "0")
if _build and _build != "0":
+31 -13
View File
@@ -3,10 +3,19 @@
Database migration: Add mic_unit_pref column to user_preferences.
Adds a single field controlling the mic channel's unit on the event-
report waveform chart in the SFM event detail modal. "dBL" (default)
or "psi". Peaks and KPI tiles elsewhere are always dBL regardless.
report waveform chart in the SFM event detail modal. "psi" (default
matches the PDF report's mic axis) or "dBL". Peaks and KPI tiles
elsewhere are always dBL regardless.
Idempotent safe to re-run.
History: v0.13.0 originally shipped this with default "dBL", which
made the website chart inconsistent with the PDF. v0.13.1 flips the
default to "psi" so they match. This migration is idempotent and
covers three cases:
1. Fresh DB without the column adds it with default 'psi'.
2. DB upgraded from v0.13.0 (column exists, value 'dBL') flips to
'psi' on the assumption no operator deliberately picked 'dBL' yet.
3. DB upgraded from later flip step is a no-op for non-'dBL' values.
"""
import sqlite3
@@ -32,24 +41,33 @@ def migrate():
cur.execute("PRAGMA table_info(user_preferences)")
existing = {row[1] for row in cur.fetchall()}
if "mic_unit_pref" in existing:
print("mic_unit_pref already exists — nothing to do.")
conn.close()
return
if "mic_unit_pref" not in existing:
cur.execute(
"ALTER TABLE user_preferences "
"ADD COLUMN mic_unit_pref TEXT DEFAULT 'dBL'"
"ADD COLUMN mic_unit_pref TEXT DEFAULT 'psi'"
)
# Backfill the single row that should exist (id=1) to the default,
# in case the column ends up NULL on existing rows.
# Backfill any rows where the column ended up NULL.
cur.execute(
"UPDATE user_preferences SET mic_unit_pref = 'dBL' "
"UPDATE user_preferences SET mic_unit_pref = 'psi' "
"WHERE mic_unit_pref IS NULL"
)
print("Added mic_unit_pref column (default 'psi').")
else:
print("mic_unit_pref column already exists.")
# v0.13.0 → v0.13.1 default-flip: rows still sitting at the original
# 'dBL' default get bumped to 'psi'. If any operator deliberately
# chose 'dBL' through Settings before this migration runs they'd
# get reset — acceptable trade-off given the small user base and
# the fact the setting is one click to restore.
cur.execute("UPDATE user_preferences SET mic_unit_pref = 'psi' "
"WHERE mic_unit_pref = 'dBL'")
flipped = cur.rowcount
if flipped:
print(f"Flipped {flipped} row(s) from 'dBL' to 'psi' (v0.13.0 default).")
conn.commit()
conn.close()
print("Added mic_unit_pref to user_preferences (default 'dBL').")
if __name__ == "__main__":
+4 -2
View File
@@ -136,8 +136,10 @@ class UserPreferences(Base):
status_ok_threshold_hours = Column(Integer, default=12)
status_pending_threshold_hours = Column(Integer, default=24)
# Mic display units on the event-report waveform chart only — peaks
# and KPI tiles elsewhere are always dBL. "dBL" (default) or "psi".
mic_unit_pref = Column(String, default="dBL")
# and KPI tiles elsewhere are always dBL. "psi" (default — matches
# the PDF report) or "dBL". Default flipped in v0.13.1 after
# operator feedback that the chart should mirror the PDF.
mic_unit_pref = Column(String, default="psi")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+2 -2
View File
@@ -294,7 +294,7 @@ def get_preferences(db: Session = Depends(get_db)):
"calibration_warning_days": prefs.calibration_warning_days,
"status_ok_threshold_hours": prefs.status_ok_threshold_hours,
"status_pending_threshold_hours": prefs.status_pending_threshold_hours,
"mic_unit_pref": prefs.mic_unit_pref or "dBL",
"mic_unit_pref": prefs.mic_unit_pref or "psi",
"updated_at": prefs.updated_at.isoformat() if prefs.updated_at else None
}
@@ -336,7 +336,7 @@ def update_preferences(
"calibration_warning_days": prefs.calibration_warning_days,
"status_ok_threshold_hours": prefs.status_ok_threshold_hours,
"status_pending_threshold_hours": prefs.status_pending_threshold_hours,
"mic_unit_pref": prefs.mic_unit_pref or "dBL",
"mic_unit_pref": prefs.mic_unit_pref or "psi",
"updated_at": prefs.updated_at.isoformat() if prefs.updated_at else None
}
+6 -4
View File
@@ -46,7 +46,7 @@
const MIC_DBL_FLOOR = 60;
let _charts = {}; // ch → Chart instance
let _micUnitPref = 'dBL'; // refreshed via fetch on first chart render
let _micUnitPref = 'psi'; // refreshed via fetch on first chart render
let _micUnitPrefLoaded = false; // one-shot fetch guard
function _esc(s) {
@@ -294,10 +294,10 @@
const r = await fetch('/api/settings/preferences');
if (r.ok) {
const prefs = await r.json();
_micUnitPref = prefs.mic_unit_pref === 'psi' ? 'psi' : 'dBL';
_micUnitPref = prefs.mic_unit_pref === 'dBL' ? 'dBL' : 'psi';
}
} catch (e) {
// Network error → silent fall back to default 'dBL'.
// Network error → silent fall back to default 'psi'.
}
_micUnitPrefLoaded = true;
return _micUnitPref;
@@ -647,7 +647,9 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
<div class="sm:col-span-2"><span class="text-gray-500">Blastware file</span> <span class="font-mono text-xs ml-1">${_esc(bw.filename || '')}</span> ${sizeKb ? `<span class="text-xs text-gray-500 ml-2">(${sizeKb} KB)</span>` : ''}</div>
<div class="sm:col-span-2"><span class="text-gray-500">SHA-256</span> <span class="font-mono text-xs ml-1 break-all">${_esc(bw.sha256 || '')}</span></div>
<div><span class="text-gray-500">Captured at</span> <span class="font-medium ml-1">${_esc(src.captured_at ? src.captured_at.slice(0, 19).replace('T', ' ') : '')}</span></div>
<div title="When SFM received and stored this event — NOT the unit-local trigger time (see Timestamp at the top of the modal for that).">
<span class="text-gray-500">Time received</span> <span class="font-medium ml-1">${_esc(src.captured_at ? src.captured_at.slice(0, 19).replace('T', ' ') : '')}</span>
</div>
<div><span class="text-gray-500">Tool version</span> <span class="font-mono text-xs ml-1">${_esc(src.tool_version || '')}</span></div>
</div>`;
}
+11 -2
View File
@@ -1,18 +1,27 @@
/* Service Worker for Seismo Fleet Manager PWA */
/* Network-first strategy with cache fallback for real-time data */
const CACHE_VERSION = 'v1';
// IMPORTANT: bump this on every release that touches a precached or
// runtime-cached static asset (event-modal.js, mobile.js, style.css,
// templates served at /, etc.). The activate handler deletes any cache
// not matching CACHE_VERSION, so old SW caches get evicted and mobile
// 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 STATIC_CACHE = `sfm-static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `sfm-dynamic-${CACHE_VERSION}`;
const DATA_CACHE = `sfm-data-${CACHE_VERSION}`;
// Files to precache (critical app shell)
// Files to precache (critical app shell). event-modal.js is included
// so its cache lifecycle is tied to the SW version bump explicitly.
const STATIC_FILES = [
'/',
'/static/style.css',
'/static/mobile.css',
'/static/mobile.js',
'/static/offline-db.js',
'/static/event-modal.js',
'/static/manifest.json',
'https://cdn.tailwindcss.com',
'https://unpkg.com/htmx.org@1.9.10',
+3 -3
View File
@@ -130,8 +130,8 @@
</label>
<select id="mic-unit-pref"
class="w-full max-w-md px-4 py-2 text-gray-900 dark:text-gray-100 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-seismo-orange">
<option value="dBL" selected>dB(L) — sound pressure level</option>
<option value="psi">psi — raw pressure</option>
<option value="psi" selected>psi — raw pressure (matches PDF report)</option>
<option value="dBL">dB(L) — sound pressure level</option>
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Applies only to the waveform chart inside the event detail modal. Peak values everywhere else (tables, KPIs, modal summary) stay in dB(L) regardless.
@@ -787,7 +787,7 @@ async function loadPreferences() {
document.getElementById('refresh-interval').value = prefs.auto_refresh_interval || 10;
// Load event-report mic units
document.getElementById('mic-unit-pref').value = prefs.mic_unit_pref || 'dBL';
document.getElementById('mic-unit-pref').value = prefs.mic_unit_pref || 'psi';
// Load status thresholds
document.getElementById('ok-threshold').value = prefs.status_ok_threshold_hours || 12;