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>
This commit is contained in:
2026-05-13 16:09:28 +00:00
parent 2cf5bf47d3
commit 737901c962
5 changed files with 153 additions and 42 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"""
+25 -8
View File
@@ -109,22 +109,22 @@
Dashboard Dashboard
</a> </a>
{# Fleet — single sidebar entry for all device-type pages. {# Devices — single sidebar entry covering all device-type
The tab strip on each underlying page (Seismographs / pages. Lands on /roster (the unified all-devices view);
Sound Level Meters / Modems / All Devices) handles the tab strip on each underlying page lets the operator
navigation between the device-type-specific layouts. drill into seismograph / SLM / modem specifics.
Active when on any /seismographs, /sound-level-meters, Active when on any /seismographs, /sound-level-meters,
/modems, /roster, /pair-devices, /unit/* page. #} /modems, /roster, /pair-devices, /unit/* page. #}
{% set _is_fleet = ( {% set _is_devices = (
request.url.path in ('/seismographs', '/sound-level-meters', '/modems', '/roster', '/pair-devices') request.url.path in ('/seismographs', '/sound-level-meters', '/modems', '/roster', '/pair-devices')
or request.url.path.startswith('/unit/') or request.url.path.startswith('/unit/')
or request.url.path.startswith('/slm/') or request.url.path.startswith('/slm/')
) %} ) %}
<a href="/seismographs" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if _is_fleet %}bg-gray-100 dark:bg-gray-700{% endif %}"> <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 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> <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>
Fleet Devices
</a> </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 %}">
@@ -134,6 +134,23 @@
Projects Projects
</a> </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>
+7 -7
View File
@@ -14,6 +14,13 @@ Usage at top of any Fleet-section template:
<div class="mb-6 border-b border-gray-200 dark:border-gray-700"> <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"> <div class="flex items-end justify-between flex-wrap gap-3 mb-0">
<nav class="flex gap-1"> <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" <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 %}"> 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"> <svg class="w-4 h-4 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -35,13 +42,6 @@ Usage at top of any Fleet-section template:
</svg> </svg>
Modems Modems
</a> </a>
<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>
</nav> </nav>
<a href="/pair-devices" <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"> 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">
+3 -27
View File
@@ -575,33 +575,9 @@
</a> </a>
</div> </div>
<!-- Metadata Backfill (Phase 5a) --> {# 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">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) -->
<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">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>
+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 %}