feat(nav,stats): Events sidebar entry + 'Overall Peak' excludes false triggers

Two related operator-facing improvements after the nav reorg.

1) Events as a top-level sidebar entry.

The /sfm page (fleet-wide event database) was demoted to Settings →
Developer in the previous reorg.  Bringing it back to main nav as
"Events" — operators do reach for the cross-project, sortable
event list, so it earns a top-level slot.

Sidebar now (7 items):
  Dashboard · Devices · Projects · Events · Tools · Job Planner · Settings

Settings → Developer card pointing at /sfm is removed.  /sfm page
title/subtitle updated from "SFM Event Data" to just "Events".  URL
unchanged.

2) "Peak PVS" KPI tile becomes "Overall Peak" and excludes false
   triggers from the calculation.

When operators ask "what's the biggest event at this location/unit/
project?" they mean the biggest REAL event, not the biggest sensor
glitch.  A single mis-flagged false trigger could otherwise dominate
the tile (the 14.13 in/s spike at Loc 1 was a prime example).

backend/services/sfm_events.py:
- _compute_stats() skips false_trigger=True events when computing
  peak_pvs / peak_pvs_at / peak_pvs_serial.  Continues counting them
  in false_trigger_count so the separate "False Triggers" tile still
  reflects what got filtered out.  last_event unchanged (recency, not
  magnitude).
- Same change automatically propagates to events_for_unit() and
  vibration_summary_for_project() — both call _compute_stats().

Templates: "Peak PVS" → "Overall Peak" in 3 KPI tile locations
(vibration_location_detail.html, partials/projects/vibration_summary
.html, unit_detail.html).  The physical-quantity name "Peak Vector
Sum" in the event-detail modal stays — that's the actual physics
term, not a summary stat.

Verified end-to-end: Overall Peak renders on real data; peak event
false_trigger flag confirmed False.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 16:13:37 +00:00
parent 737901c962
commit e15481884a
7 changed files with 36 additions and 27 deletions
+14 -4
View File
@@ -548,7 +548,14 @@ def _empty_stats() -> dict:
def _compute_stats(events: list[dict]) -> dict: def _compute_stats(events: list[dict]) -> dict:
"""Roll up summary stats from a merged event list. Cheap O(N) pass.""" """Roll up summary stats from a merged event list. Cheap O(N) pass.
The "Overall Peak" stat (peak_pvs) EXCLUDES events flagged as false
triggers — operators care about the highest REAL event, not the
biggest sensor glitch. false_trigger_count still includes them so
operators can see how many were filtered out. last_event uses
every event regardless (it's about activity recency, not magnitude).
"""
if not events: if not events:
return _empty_stats() return _empty_stats()
@@ -559,6 +566,12 @@ def _compute_stats(events: list[dict]) -> dict:
false_trigger_count = 0 false_trigger_count = 0
for ev in events: for ev in events:
is_false_trigger = bool(ev.get("false_trigger"))
if is_false_trigger:
false_trigger_count += 1
# Peak calculation: skip flagged false triggers.
if not is_false_trigger:
pvs = ev.get("peak_vector_sum") pvs = ev.get("peak_vector_sum")
if pvs is not None and (peak_pvs is None or pvs > peak_pvs): if pvs is not None and (peak_pvs is None or pvs > peak_pvs):
peak_pvs = pvs peak_pvs = pvs
@@ -569,9 +582,6 @@ def _compute_stats(events: list[dict]) -> dict:
if ts and (last_event is None or ts > last_event): if ts and (last_event is None or ts > last_event):
last_event = ts last_event = ts
if ev.get("false_trigger"):
false_trigger_count += 1
return { return {
"event_count": len(events), "event_count": len(events),
"peak_pvs": peak_pvs, "peak_pvs": peak_pvs,
+11
View File
@@ -134,6 +134,17 @@
Projects Projects
</a> </a>
{# Events — fleet-wide event database (SFM). Cross-project
sortable/filterable event list. Day-to-day event browsing
for a specific location or unit lives on those detail
pages; this is the firehose for cross-cutting queries. #}
<a href="/sfm" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sfm' %}bg-gray-100 dark:bg-gray-700{% endif %}">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Events
</a>
{# Tools — operator workflow hub. Active when on /tools {# Tools — operator workflow hub. Active when on /tools
itself or any of the workflow pages it links into itself or any of the workflow pages it links into
(project tidy, metadata backfill, pair devices). #} (project tidy, metadata backfill, pair devices). #}
@@ -23,7 +23,7 @@
<span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ "{:,}".format(summary.total_events) }}</span> <span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ "{:,}".format(summary.total_events) }}</span>
</div> </div>
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3 flex flex-col"> <div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3 flex flex-col">
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Peak PVS</span> <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overall Peak</span>
{% if summary.peak_pvs is not none %} {% if summary.peak_pvs is not none %}
<span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ "%.4f"|format(summary.peak_pvs) }} <span class="text-sm font-normal">in/s</span></span> <span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ "%.4f"|format(summary.peak_pvs) }} <span class="text-sm font-normal">in/s</span></span>
<a href="/projects/{{ summary.project_id }}/nrl/{{ summary.peak_pvs_location_id }}" <a href="/projects/{{ summary.project_id }}/nrl/{{ summary.peak_pvs_location_id }}"
+1 -13
View File
@@ -561,19 +561,7 @@
</a> </a>
</div> </div>
<!-- SFM Admin (raw event database) --> {# SFM Admin moved back to main nav as "Events" — see sidebar. #}
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-slate-700 rounded-lg">
<div>
<div class="font-medium text-gray-900 dark:text-white">SFM Admin</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Raw event database from SFM — cross-project event search, file downloads, debug view. Day-to-day event browsing lives on project / location / unit pages.
</div>
</div>
<a href="/sfm"
class="ml-6 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap">
Open
</a>
</div>
{# Metadata Backfill + Project Tidy moved to Tools (they're {# Metadata Backfill + Project Tidy moved to Tools (they're
operator workflows, not admin/dev surfaces). Find them operator workflows, not admin/dev surfaces). Find them
+2 -2
View File
@@ -5,8 +5,8 @@
{% block content %} {% block content %}
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">SFM Event Data</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">Events</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Blastware ACH events forwarded by series3-watcher</p> <p class="text-gray-600 dark:text-gray-400 mt-1">Fleet-wide event database. Filter by serial, date, false-trigger, or browse the units roster.</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span id="sfm-status-badge" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400"> <span id="sfm-status-badge" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
+1 -1
View File
@@ -313,7 +313,7 @@
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">outside any assignment window</span> <span class="text-xs text-gray-500 dark:text-gray-400 mt-1">outside any assignment window</span>
</div> </div>
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3 flex flex-col"> <div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3 flex flex-col">
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Peak PVS</span> <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overall Peak</span>
<span id="ue-stat-peak" class="text-2xl font-bold text-gray-900 dark:text-white mt-1"></span> <span id="ue-stat-peak" class="text-2xl font-bold text-gray-900 dark:text-white mt-1"></span>
<span id="ue-stat-peak-when" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></span> <span id="ue-stat-peak-when" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></span>
</div> </div>
+1 -1
View File
@@ -199,7 +199,7 @@
<span id="ev-stat-count" class="text-3xl font-bold text-gray-900 dark:text-white mt-1"></span> <span id="ev-stat-count" class="text-3xl font-bold text-gray-900 dark:text-white mt-1"></span>
</div> </div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 flex flex-col"> <div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 flex flex-col">
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Peak PVS</span> <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overall Peak</span>
<span id="ev-stat-peak" class="text-3xl font-bold text-gray-900 dark:text-white mt-1"></span> <span id="ev-stat-peak" class="text-3xl font-bold text-gray-900 dark:text-white mt-1"></span>
<span id="ev-stat-peak-when" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></span> <span id="ev-stat-peak-when" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></span>
</div> </div>