From 94354da61190bfb40d41bdc046ac40bc278bda41 Mon Sep 17 00:00:00 2001 From: serversdwn Date: Fri, 9 Jan 2026 20:02:05 +0000 Subject: [PATCH] seismo fleet roster repaired and visible. --- app/main.py | 2 + app/seismo/routers/partials.py | 140 +++++++++++++++++++++++++ app/seismo/routers/seismo_dashboard.py | 2 +- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 app/seismo/routers/partials.py diff --git a/app/main.py b/app/main.py index 68c581e..5e10fc2 100644 --- a/app/main.py +++ b/app/main.py @@ -34,6 +34,7 @@ from app.seismo.routers import ( activity as seismo_activity, seismo_dashboard as seismo_seismo_dashboard, settings as seismo_settings, + partials as seismo_partials, ) from app.seismo import routes as seismo_legacy_routes @@ -104,6 +105,7 @@ app.include_router(seismo_dashboard_tabs.router) app.include_router(seismo_activity.router) app.include_router(seismo_seismo_dashboard.router) app.include_router(seismo_settings.router) +app.include_router(seismo_partials.router, prefix="/partials") app.include_router(seismo_legacy_routes.router) # SLM Feature Module APIs diff --git a/app/seismo/routers/partials.py b/app/seismo/routers/partials.py new file mode 100644 index 0000000..81f4715 --- /dev/null +++ b/app/seismo/routers/partials.py @@ -0,0 +1,140 @@ +""" +Partial routes for HTMX dynamic content loading. +These routes return HTML fragments that are loaded into the page via HTMX. +""" +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from app.seismo.services.snapshot import emit_status_snapshot + +router = APIRouter() +templates = Jinja2Templates(directory="app/ui/templates") + + +@router.get("/unknown-emitters", response_class=HTMLResponse) +async def get_unknown_emitters(request: Request): + """ + Returns HTML partial with unknown emitters (units reporting but not in roster). + Called periodically via HTMX (every 10s) from the roster page. + """ + snapshot = emit_status_snapshot() + + # Convert unknown units dict to list and add required fields + unknown_list = [] + for unit_id, unit_data in snapshot.get("unknown", {}).items(): + unknown_list.append({ + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "fname": unit_data.get("fname", ""), + }) + + # Sort by ID for consistent display + unknown_list.sort(key=lambda x: x["id"]) + + return templates.TemplateResponse( + "partials/unknown_emitters.html", + { + "request": request, + "unknown_units": unknown_list + } + ) + + +@router.get("/devices-all", response_class=HTMLResponse) +async def get_all_devices(request: Request): + """ + Returns HTML partial with all devices (deployed, benched, retired, ignored). + Called on page load and when filters are applied. + """ + snapshot = emit_status_snapshot() + + # Combine all units from different buckets + all_units = [] + + # Add active units (deployed) + for unit_id, unit_data in snapshot.get("active", {}).items(): + unit_info = { + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "last_seen": unit_data.get("last", ""), + "fname": unit_data.get("fname", ""), + "deployed": True, + "retired": False, + "ignored": False, + "note": unit_data.get("note", ""), + "device_type": unit_data.get("device_type", "seismograph"), + "location": unit_data.get("location", ""), + "address": unit_data.get("address", ""), + "coordinates": unit_data.get("coordinates", ""), + "last_calibrated": unit_data.get("last_calibrated"), + "next_calibration_due": unit_data.get("next_calibration_due"), + "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"), + "ip_address": unit_data.get("ip_address"), + "phone_number": unit_data.get("phone_number"), + "hardware_model": unit_data.get("hardware_model"), + } + all_units.append(unit_info) + + # Add benched units (not deployed, not retired) + for unit_id, unit_data in snapshot.get("benched", {}).items(): + unit_info = { + "id": unit_id, + "status": unit_data["status"], + "age": unit_data["age"], + "last_seen": unit_data.get("last", ""), + "fname": unit_data.get("fname", ""), + "deployed": False, + "retired": False, + "ignored": False, + "note": unit_data.get("note", ""), + "device_type": unit_data.get("device_type", "seismograph"), + "location": unit_data.get("location", ""), + "address": unit_data.get("address", ""), + "coordinates": unit_data.get("coordinates", ""), + "last_calibrated": unit_data.get("last_calibrated"), + "next_calibration_due": unit_data.get("next_calibration_due"), + "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"), + "ip_address": unit_data.get("ip_address"), + "phone_number": unit_data.get("phone_number"), + "hardware_model": unit_data.get("hardware_model"), + } + all_units.append(unit_info) + + # Add retired units + for unit_id, unit_data in snapshot.get("retired", {}).items(): + unit_info = { + "id": unit_id, + "status": "Retired", + "age": unit_data["age"], + "last_seen": unit_data.get("last", ""), + "fname": unit_data.get("fname", ""), + "deployed": False, + "retired": True, + "ignored": False, + "note": unit_data.get("note", ""), + "device_type": unit_data.get("device_type", "seismograph"), + "location": unit_data.get("location", ""), + "address": unit_data.get("address", ""), + "coordinates": unit_data.get("coordinates", ""), + "last_calibrated": unit_data.get("last_calibrated"), + "next_calibration_due": unit_data.get("next_calibration_due"), + "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"), + "ip_address": unit_data.get("ip_address"), + "phone_number": unit_data.get("phone_number"), + "hardware_model": unit_data.get("hardware_model"), + } + all_units.append(unit_info) + + # Sort by ID for consistent display + all_units.sort(key=lambda x: x["id"]) + + return templates.TemplateResponse( + "partials/devices_table.html", + { + "request": request, + "units": all_units + } + ) diff --git a/app/seismo/routers/seismo_dashboard.py b/app/seismo/routers/seismo_dashboard.py index 1bf55f1..881ea81 100644 --- a/app/seismo/routers/seismo_dashboard.py +++ b/app/seismo/routers/seismo_dashboard.py @@ -11,7 +11,7 @@ from app.seismo.database import get_db from app.seismo.models import RosterUnit router = APIRouter(prefix="/api/seismo-dashboard", tags=["seismo-dashboard"]) -templates = Jinja2Templates(directory="templates") +templates = Jinja2Templates(directory="app/ui/templates") @router.get("/stats", response_class=HTMLResponse)