e15481884a
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>
77 lines
4.4 KiB
HTML
77 lines
4.4 KiB
HTML
{# Project-wide vibration events roll-up. Loaded via HTMX. #}
|
|
{% if summary.vibration_location_count == 0 %}
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4">
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
No vibration monitoring locations yet. Add one to start collecting events.
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-5">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
|
Project-wide vibration events
|
|
</h3>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
across {{ summary.vibration_location_count }} location{{ '' if summary.vibration_location_count == 1 else 's' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- KPI tiles -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
|
<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">Total Events</span>
|
|
<span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ "{:,}".format(summary.total_events) }}</span>
|
|
</div>
|
|
<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">Overall Peak</span>
|
|
{% 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>
|
|
<a href="/projects/{{ summary.project_id }}/nrl/{{ summary.peak_pvs_location_id }}"
|
|
class="text-xs text-seismo-orange hover:text-seismo-navy truncate mt-1"
|
|
title="{{ summary.peak_pvs_location_name }}">
|
|
{{ summary.peak_pvs_location_name }}
|
|
{% if summary.peak_pvs_at %} · {{ summary.peak_pvs_at[:10] }}{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
|
{% endif %}
|
|
</div>
|
|
<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">Last Event</span>
|
|
{% if summary.last_event %}
|
|
<span class="text-base font-bold text-gray-900 dark:text-white mt-1">{{ summary.last_event[:19].replace('T', ' ') }}</span>
|
|
{% else %}
|
|
<span class="text-2xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
|
{% endif %}
|
|
</div>
|
|
<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">False Triggers</span>
|
|
<span class="text-2xl font-bold {% if summary.false_trigger_count > 0 %}text-amber-600 dark:text-amber-400{% else %}text-gray-900 dark:text-white{% endif %} mt-1">{{ "{:,}".format(summary.false_trigger_count) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if summary.per_location and summary.total_events > 0 %}
|
|
<!-- Top locations by activity -->
|
|
<div>
|
|
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Top locations by activity</h4>
|
|
<div class="space-y-1.5">
|
|
{% for loc in summary.per_location[:5] %}
|
|
<a href="/projects/{{ summary.project_id }}/nrl/{{ loc.location_id }}"
|
|
class="flex items-center justify-between py-1.5 px-3 rounded hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
📍 {{ loc.location_name }}
|
|
</span>
|
|
<span class="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap ml-3">
|
|
<span>{{ "{:,}".format(loc.event_count) }} event{{ '' if loc.event_count == 1 else 's' }}</span>
|
|
{% if loc.peak_pvs is not none %}
|
|
<span class="text-xs text-gray-500">peak {{ "%.4f"|format(loc.peak_pvs) }}</span>
|
|
{% endif %}
|
|
</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|