2 Commits

Author SHA1 Message Date
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
8 changed files with 76 additions and 31 deletions
+23
View File
@@ -5,6 +5,29 @@ 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.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.
+1 -1
View File
@@ -1,4 +1,4 @@
# Terra-View v0.13.0
# Terra-View v0.13.1
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.13.0"
VERSION = "0.13.1"
if ENVIRONMENT == "development":
_build = os.getenv("BUILD_NUMBER", "0")
if _build and _build != "0":
+36 -18
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 'psi'"
)
# Backfill any rows where the column ended up NULL.
cur.execute(
"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).")
cur.execute(
"ALTER TABLE user_preferences "
"ADD COLUMN mic_unit_pref TEXT DEFAULT 'dBL'"
)
# Backfill the single row that should exist (id=1) to the default,
# in case the column ends up NULL on existing rows.
cur.execute(
"UPDATE user_preferences SET mic_unit_pref = 'dBL' "
"WHERE mic_unit_pref IS NULL"
)
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>`;
}
+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;