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>
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>
"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>
Three additions to the shared event-detail modal, closing the gap
versus the standalone SFM webapp:
(1) "Show Event Report PDF" button toggles an inline iframe inside
the modal (no second-layer modal, no new tab). Lazy-loaded — src
isn't set until first reveal, so closing the modal without opening
the PDF never spends bandwidth. Sibling "Download PDF" link for
direct save. Iframe sized to 80vh / min 600px so the typical
letter-portrait single-page report fits with browser-native zoom
controls available.
(2) "Original .TXT report" download link, rendered only when
sidecar.source.txt_filename is present (post-2026-05-27 ingest
events). Hidden for legacy events to avoid 404 dead links.
(3) Inline Review form — false_trigger checkbox + reviewer text
input + notes textarea + Save button. PATCH /api/sfm/db/events/{id}/sidecar
with {"review": {...}}. On save, fires a CustomEvent
'sfm-event-review-saved' on window so table-owning pages
(/sfm, /unit/{id}, /admin/events, /projects/{p}/nrl/{l}) can
listen and refresh their FT badges without reload. Status line
shows the last-reviewed timestamp + Save success/failure feedback.
Smoke-tested end-to-end against a real BE12599 histogram event:
PATCH round-trip lands in the sidecar, GET reflects the change,
no 500s on /report.pdf or /sidecar paths through the proxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds inline waveform plots to the shared event-detail modal, ported
from sfm/sfm_webapp.html:2555-2880. The standalone SFM webapp's
plot logic moves into event-modal.js with Tailwind-friendly grid +
tick colors (theme-aware via the `dark` class on <html>).
Channels render in BW Event Report order — MicL on top, Tran on
bottom. Mic channel auto-converts psi → dB(L) when the operator's
mic_unit_pref is "dBL" (the default), using _psiToDblForChart with
a MIC_DBL_FLOOR=60 floor so the chart shows an SPL-vs-time curve
instead of a sparse pattern of "moments above floor".
Histograms render as bars with HH:MM:SS x-axis labels when the
sidecar carries time_axis.interval_times (events ingested with the
v0.20 parser); falls back to interval index for older events.
Geo + mic histogram channels enforce minimum Y ranges (0.05 in/s
and 0.001 psi respectively) so quiet events don't fill the panel.
Waveform events get the trigger-line + zero-baseline overlay; the
histogram branch suppresses it (no trigger concept). Downsampling
kicks in at >3000 samples to keep render time bounded.
Modal partial widened max-w-3xl → max-w-5xl to fit the chart panels
without horizontal clipping. Chart.js 4.4.1 loaded from cdn.jsdelivr
at the bottom of the partial, matching the standalone webapp's
reference version pin.
Side-yard: docker-compose bind-mounts ../seismo-relay-prod-snap into
the SFM container so the symlinked DB + waveform store inside
bridges/captures resolve. Without it SFM 500s on every /db/* call
because the symlink target was outside the container's filesystem
view. Read-write (not :ro) because SFM opens the DB in WAL mode
which requires creating -wal and -shm sidecar files even for reads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Event modal (event-modal.js):
- Record Type now derived from Blastware filename's last-char code
(H=Histogram, W=Waveform, M=Manual, E=Event, C=Combo). Falls back to
whatever SFM reported if the code isn't recognized. Client-side
workaround — SFM still hardcodes "Waveform" server-side and needs a
proper fix in its sidecar parser.
- PSI mic tile dropped; mic section now renders 3 tiles (dB(L), ZC
Frequency, Time of Peak) instead of 4.
- New "View JSON" toggle exposes a prettified inline JSON viewer with
a Copy-to-clipboard button alongside the existing "Download sidecar
JSON" link.
- "Project Info" section header renamed to "User Notes" to reflect
that these are operator-typed fields, not the terra-view project
assignment.
Sortable tables (sfm.html + unit_detail.html):
- Both Events tables now have clickable column headers with ↕/↓/↑
indicators. Default sort is Timestamp DESC. Clicking the same
column toggles direction; clicking a different column switches and
resets to DESC. Sort is purely client-side over the cached rowset,
so no extra fetches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new action buttons at the top of the Source File section of the
event-detail modal:
1. Download Blastware file — primary orange button. Pulls the raw .AB0
/.G10/.6R0/etc. binary from SFM (/db/events/{id}/blastware_file) via
terra-view's /api/sfm proxy. The browser saves it with the original
on-disk filename (using the HTML5 `download` attribute pointed at
sidecar.blastware.filename). Operator can then open the file
directly in Blastware on a Windows box for full waveform analysis,
archive it, or attach it to a compliance report.
Greyed-out "Blastware file unavailable" placeholder shown when
sidecar.blastware.available is false (rare — would mean SFM stored
the metadata but lost the binary).
2. Download sidecar JSON — secondary outlined button. Pulls the same
.sfm.json the modal renders from. Saved as <binary>.sfm.json.
Useful for ops/diagnostics and for the future metadata-driven
project parser (Phase 5) which can chew on these directly.
End-to-end verified through the proxy: 8882-byte Blastware binary
intact with "Instantel" magic header preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clicking any event row in any of the three event tables (/sfm Events,
project-location Events tab, unit detail SFM Events) now opens a modal
populated from the SFM .sfm.json sidecar. Previously the /sfm page had
a basic inline modal showing only the columns already in the table;
this rebuilds it as a shared component and exposes the rich fields
that the BW ASCII report unlocks.
Shared component:
- backend/static/event-modal.js — single ~250-line module. Public API:
showEventDetail(eventId) fetches /api/sfm/db/events/{id}/sidecar
live (no extra terra-view caching) and renders sections for:
• Event (serial, timestamp, record type, sample rate, rec time,
waveform key)
• Project Info (operator-typed user notes — project / client /
operator / sensor_location — flagged in the UI as "as typed
into the seismograph at session start", not the terra-view
assignment)
• Peak Particle Velocity (per-channel + vector sum, with the
time-of-vector-sum-peak when bw_report is available)
• Microphone (Peak dB(L) + psi, ZC frequency, time of peak)
• Sensor Self-Check table (per-channel freq + ratio/amplitude +
pass/fail)
• Device & Recording Metadata (firmware, battery, calibration
date + by-whom, geo range, stop mode, units)
• Source File (Blastware filename, size, SHA-256, capture time)
closeEventDetailModal() closes; Escape key also closes.
- templates/partials/event_detail_modal.html — modal shell partial
(sticky title bar, scrollable body, click-outside-to-close).
Wired into three pages:
- templates/sfm.html: removed the old inline modal + showEventDetail /
ppvCard / closeEventModal functions (replaced by the shared module).
Row onclick now passes just the event id instead of the full JSON.
- templates/vibration_location_detail.html: row click on the Events
tab opens the modal. The /unit/{serial} link inside the row has
event.stopPropagation() so the link navigates instead of opening
the modal.
- templates/unit_detail.html: row click on the SFM Events table opens
the modal. The attribution-cell project/location links also got
stopPropagation.
Graceful degradation: older events forwarded before the watcher's
_ASCII.TXT pairing fix don't have a bw_report block in their sidecar.
The modal renders an amber banner explaining that and shows just the
event + project_info + peak_values + source-file sections.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Created complete frontend structure with Jinja2 templates
- Implemented three main pages: Dashboard, Fleet Roster, and Unit Detail
- Added HTMX auto-refresh for real-time updates (10s interval)
- Integrated dark/light mode toggle with localStorage persistence
- Built responsive card-based UI with sidebar navigation
- Created API endpoints for status snapshot, roster, unit details, and photos
- Added mock data service for development (emit_status_snapshot)
- Implemented tabbed interface on unit detail page (Photos, Map, History)
- Integrated Leaflet maps for unit location visualization
- Configured static file serving and photo management
- Updated requirements.txt with Jinja2 and aiofiles
- Reorganized backend structure into routers and services
- Added comprehensive FRONTEND_README.md documentation
Frontend features:
- Auto-refreshing dashboard with fleet summary and alerts
- Sortable fleet roster table (prioritizes Missing > Pending > OK)
- Unit detail view with status, deployment info, and notes
- Photo gallery with thumbnail navigation
- Interactive maps showing unit coordinates
- Consistent styling with brand colors (orange, navy, burgundy)
Ready for integration with real Series3 emitter data.