3 Commits

Author SHA1 Message Date
serversdown e15481884a 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>
2026-05-13 16:13:37 +00:00
serversdown 737901c962 refactor(nav): rename Fleet→Devices, add Tools entry, move workflows to Tools
Sidebar evolved from "Fleet defaults to seismograph dashboard" to
"Devices defaults to unified roster" + a new "Tools" entry housing the
active operator workflows.

Sidebar (6 items):
  Dashboard · Devices · Projects · Tools · Job Planner · Settings

Changes:
- templates/base.html: renamed Fleet → Devices.  Default route changed
  from /seismographs to /roster — clicking Devices now lands on the
  unified all-devices view, then operators drill into type-specific
  layouts via the tab strip.  Tools entry added between Projects and
  Job Planner; highlights when on /tools or any of its linked workflow
  pages.
- templates/partials/fleet_tab_strip.html: reordered tabs so "All
  Devices" comes first (matches the new default landing).
  Seismographs → SLMs → Modems follow.
- templates/tools.html (new) + /tools route in main.py: card grid hub
  for active workflows.
    • Pair Devices — links to /pair-devices
    • Project Tidy — links to /settings/developer/project-tidy
    • Backfill from event metadata — /settings/developer/metadata-backfill
    • Reports — info card pointing to project detail pages where
      Excel report generation actually lives (per-project context)
    • Swap Detection — greyed-out placeholder for Phase 5c
- templates/settings.html: removed Project Tidy + Metadata Backfill
  cards from Settings → Developer.  They now live in Tools.  Settings
  → Developer retains the truly admin/dev surfaces (Watcher Manager,
  SFM Admin).

The workflow page URLs (/settings/developer/project-tidy,
/settings/developer/metadata-backfill) stay where they are — only the
nav entry point changes.  Bookmarks still work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 16:09:28 +00:00
serversdown 2cf5bf47d3 refactor(nav): collapse fleet/device pages into one sidebar entry with internal tab strip
The sidebar had 10 entries with 5 of them (Devices, Seismographs, Sound
Level Meters, Modems, Pair Devices) all about the physical fleet plus
SFM Events as a debug surface.  Operators kept asking "where do I find
BE11529?" without knowing whether it was a seismograph / SLM / modem.

This collapses those 5+1 into a single "Fleet" sidebar entry that opens
into a unified tab strip across the top of the four device pages.  Each
page keeps its existing custom layout (seismograph-specific
calibration/deployment columns, SLM live-status panel, modem pairing
view, all-devices roster).  The strip just provides the navigation +
the "Pair Devices" button as an action.

Sidebar before (10 items):
  Dashboard · Devices · Seismographs · SFM Events · Sound Level Meters
  Modems · Pair Devices · Projects · Job Planner · Settings

Sidebar after (5 items):
  Dashboard · Fleet · Projects · Job Planner · Settings

Changes:
- templates/partials/fleet_tab_strip.html (new): the shared tab strip.
  Auto-detects the active tab from request.url.path.  4 tabs
  (Seismographs / Sound Level Meters / Modems / All Devices) plus a
  "Pair Devices" button on the right.
- templates/{seismographs,sound_level_meters,modems,roster}.html: added
  {% include 'partials/fleet_tab_strip.html' %} as the first thing
  inside the content block.  No other changes to those templates'
  existing layouts.
- templates/base.html: replaced the 6 device-related sidebar links with
  one "Fleet" link to /seismographs.  The Fleet entry is highlighted
  when the current URL is any of /seismographs, /sound-level-meters,
  /modems, /roster, /pair-devices, /unit/*, or /slm/*.
- templates/settings.html: SFM Events moved out of the main nav into a
  new "SFM Admin" card under Settings → Developer.  Daily event
  browsing already lives on project / location / unit pages (Phases
  1+2+3); the standalone /sfm page is now admin / cross-project debug
  surface only.

URLs unchanged — all bookmarks / deep links still work.  /sfm still
serves the standalone page, it's just no longer in the main nav.
Mobile bottom-nav unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 15:32:17 +00:00
14 changed files with 244 additions and 75 deletions
+8
View File
@@ -258,6 +258,14 @@ async def project_tidy_page(request: Request):
return templates.TemplateResponse("admin/project_tidy.html", {"request": request}) return templates.TemplateResponse("admin/project_tidy.html", {"request": request})
@app.get("/tools", response_class=HTMLResponse)
async def tools_page(request: Request):
"""Tools / workflow hub. Active operator workflows (device pairing,
project tidy, metadata backfill, future swap detection, report
generators) all live here in card form."""
return templates.TemplateResponse("tools.html", {"request": request})
@app.get("/modems", response_class=HTMLResponse) @app.get("/modems", response_class=HTMLResponse)
async def modems_page(request: Request): async def modems_page(request: Request):
"""Field modems management dashboard""" """Field modems management dashboard"""
+19 -9
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,19 +566,22 @@ def _compute_stats(events: list[dict]) -> dict:
false_trigger_count = 0 false_trigger_count = 0
for ev in events: for ev in events:
pvs = ev.get("peak_vector_sum") is_false_trigger = bool(ev.get("false_trigger"))
if pvs is not None and (peak_pvs is None or pvs > peak_pvs): if is_false_trigger:
peak_pvs = pvs false_trigger_count += 1
peak_pvs_at = ev.get("timestamp")
peak_pvs_serial = ev.get("serial") # Peak calculation: skip flagged false triggers.
if not is_false_trigger:
pvs = ev.get("peak_vector_sum")
if pvs is not None and (peak_pvs is None or pvs > peak_pvs):
peak_pvs = pvs
peak_pvs_at = ev.get("timestamp")
peak_pvs_serial = ev.get("serial")
ts = ev.get("timestamp") ts = ev.get("timestamp")
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,
+40 -35
View File
@@ -109,47 +109,24 @@
Dashboard Dashboard
</a> </a>
<a href="/roster" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/roster' %}bg-gray-100 dark:bg-gray-700{% endif %}"> {# Devices — single sidebar entry covering all device-type
pages. Lands on /roster (the unified all-devices view);
the tab strip on each underlying page lets the operator
drill into seismograph / SLM / modem specifics.
Active when on any /seismographs, /sound-level-meters,
/modems, /roster, /pair-devices, /unit/* page. #}
{% set _is_devices = (
request.url.path in ('/seismographs', '/sound-level-meters', '/modems', '/roster', '/pair-devices')
or request.url.path.startswith('/unit/')
or request.url.path.startswith('/slm/')
) %}
<a href="/roster" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if _is_devices %}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"> <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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg> </svg>
Devices Devices
</a> </a>
<a href="/seismographs" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/seismographs' %}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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
Seismographs
</a>
<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>
SFM Events
</a>
<a href="/sound-level-meters" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sound-level-meters' %}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="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
</svg>
Sound Level Meters
</a>
<a href="/modems" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/modems' %}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="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>
</svg>
Modems
</a>
<a href="/pair-devices" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/pair-devices' %}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.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
Pair Devices
</a>
<a href="/projects" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path.startswith('/projects') %}bg-gray-100 dark:bg-gray-700{% endif %}"> <a href="/projects" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path.startswith('/projects') %}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"> <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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
@@ -157,6 +134,34 @@
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
itself or any of the workflow pages it links into
(project tidy, metadata backfill, pair devices). #}
{% set _is_tools = (
request.url.path == '/tools'
or request.url.path == '/pair-devices'
or request.url.path == '/settings/developer/project-tidy'
or request.url.path == '/settings/developer/metadata-backfill'
) %}
<a href="/tools" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if _is_tools %}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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Tools
</a>
<a href="/fleet-calendar" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path.startswith('/fleet-calendar') %}bg-gray-100 dark:bg-gray-700{% endif %}"> <a href="/fleet-calendar" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path.startswith('/fleet-calendar') %}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"> <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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
+1
View File
@@ -3,6 +3,7 @@
{% block title %}Field Modems - Terra-View{% endblock %} {% block title %}Field Modems - Terra-View{% endblock %}
{% block content %} {% block content %}
{% include "partials/fleet_tab_strip.html" %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center"> <h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
<svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+54
View File
@@ -0,0 +1,54 @@
{# Fleet tab strip.
Shared header for every page under the "Fleet" sidebar section. Each
underlying page (/roster, /seismographs, /sound-level-meters, /modems)
keeps its own custom layout — this partial just provides the tab
navigation across the top so they feel like one logical area.
The active tab is detected from request.url.path so deep links work.
Usage at top of any Fleet-section template:
{% include 'partials/fleet_tab_strip.html' %}
#}
{% set _path = request.url.path %}
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-end justify-between flex-wrap gap-3 mb-0">
<nav class="flex gap-1">
<a href="/roster"
class="px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors {% if _path == '/roster' %}border-seismo-orange text-seismo-orange{% else %}border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600{% endif %}">
<svg class="w-4 h-4 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
All Devices
</a>
<a href="/seismographs"
class="px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors {% if _path == '/seismographs' %}border-seismo-orange text-seismo-orange{% else %}border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600{% endif %}">
<svg class="w-4 h-4 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
Seismographs
</a>
<a href="/sound-level-meters"
class="px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors {% if _path == '/sound-level-meters' %}border-seismo-orange text-seismo-orange{% else %}border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600{% endif %}">
<svg class="w-4 h-4 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M12 6v12M9 8.464a5 5 0 000 7.072"/>
</svg>
Sound Level Meters
</a>
<a href="/modems"
class="px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors {% if _path == '/modems' %}border-seismo-orange text-seismo-orange{% else %}border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600{% endif %}">
<svg class="w-4 h-4 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
</svg>
Modems
</a>
</nav>
<a href="/pair-devices"
class="mb-1 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
Pair Devices
</a>
</div>
</div>
@@ -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
View File
@@ -3,6 +3,7 @@
{% block title %}Devices - Seismo Fleet Manager{% endblock %} {% block title %}Devices - Seismo Fleet Manager{% endblock %}
{% block content %} {% block content %}
{% include "partials/fleet_tab_strip.html" %}
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
+1
View File
@@ -3,6 +3,7 @@
{% block title %}Seismographs - Seismo Fleet Manager{% endblock %} {% block title %}Seismographs - Seismo Fleet Manager{% endblock %}
{% block content %} {% block content %}
{% include "partials/fleet_tab_strip.html" %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Seismographs</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">Seismographs</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Manage and monitor seismograph units</p> <p class="text-gray-600 dark:text-gray-400 mt-1">Manage and monitor seismograph units</p>
+4 -26
View File
@@ -561,33 +561,11 @@
</a> </a>
</div> </div>
<!-- Metadata Backfill (Phase 5a) --> {# 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">Backfill from event metadata</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Auto-create projects, locations, and unit assignments from the operator-typed metadata baked into SFM events. Skip the manual entry.
</div>
</div>
<a href="/settings/developer/metadata-backfill"
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>
<!-- Project Tidy (Phase 5b) --> {# Metadata Backfill + Project Tidy moved to Tools (they're
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-slate-700 rounded-lg"> operator workflows, not admin/dev surfaces). Find them
<div> at /tools. #}
<div class="font-medium text-gray-900 dark:text-white">Project Tidy</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Find duplicate-looking projects via fuzzy name match (typos, abbreviations, spacing variations) and bulk-merge them.
</div>
</div>
<a href="/settings/developer/project-tidy"
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>
</div> </div>
</div> </div>
</div> </div>
+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
View File
@@ -3,6 +3,7 @@
{% block title %}Sound Level Meters - Seismo Fleet Manager{% endblock %} {% block title %}Sound Level Meters - Seismo Fleet Manager{% endblock %}
{% block content %} {% block content %}
{% include "partials/fleet_tab_strip.html" %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center"> <h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
<svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+110
View File
@@ -0,0 +1,110 @@
{% extends "base.html" %}
{% block title %}Tools - Seismo Fleet Manager{% endblock %}
{% block content %}
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Tools</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">
Active operator workflows. Pair devices, clean up duplicates, generate reports.
</p>
</div>
<!-- Card grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Pair Devices -->
<a href="/pair-devices"
class="block bg-white dark:bg-slate-800 rounded-xl shadow-lg p-5 hover:shadow-xl transition-shadow border border-transparent hover:border-seismo-orange">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/30 text-seismo-orange flex items-center justify-center shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-gray-900 dark:text-white">Pair Devices</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Bidirectionally link seismographs ↔ modems (or SLMs ↔ modems) so they ship out together as a deployed pair.
</p>
</div>
</div>
</a>
<!-- Project Tidy -->
<a href="/settings/developer/project-tidy"
class="block bg-white dark:bg-slate-800 rounded-xl shadow-lg p-5 hover:shadow-xl transition-shadow border border-transparent hover:border-seismo-orange">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 flex items-center justify-center shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-4H5m14 8H5m14 4H5"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-gray-900 dark:text-white">Project Tidy</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Find duplicate-looking projects via fuzzy name match (typos, abbreviations) and bulk-merge them. Useful after a metadata backfill run.
</p>
</div>
</div>
</a>
<!-- Metadata Backfill -->
<a href="/settings/developer/metadata-backfill"
class="block bg-white dark:bg-slate-800 rounded-xl shadow-lg p-5 hover:shadow-xl transition-shadow border border-transparent hover:border-seismo-orange">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 flex items-center justify-center shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-gray-900 dark:text-white">Backfill from event metadata</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Auto-create projects, locations, and unit assignments from the operator-typed metadata baked into SFM events. Skip the manual entry.
</p>
</div>
</div>
</a>
<!-- Reports (per-project) -->
<a href="/projects"
class="block bg-white dark:bg-slate-800 rounded-xl shadow-lg p-5 hover:shadow-xl transition-shadow border border-transparent hover:border-seismo-orange">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 flex items-center justify-center shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-gray-900 dark:text-white">Reports</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Excel report generation lives on each project's detail page. Open a project and use <em>Generate Combined Report</em> (for multi-location sound studies) or single-location export.
</p>
</div>
</div>
</a>
<!-- Swap Detection (Phase 5c — coming soon) -->
<div class="bg-gray-50 dark:bg-slate-800/50 rounded-xl shadow p-5 border border-dashed border-gray-300 dark:border-gray-700 cursor-not-allowed">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-400 flex items-center justify-center shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-semibold text-gray-500 dark:text-gray-400">Swap Detection</h3>
<span class="px-1.5 py-0.5 rounded text-xs bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400">soon</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-500">
Daily background job that auto-detects unit swaps in the field (BE12345 → BE67890 at the same project + location) from operator-typed metadata. Coming in Phase 5c.
</p>
</div>
</div>
</div>
</div>
{% endblock %}
+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>