v0.2.2-series4 endpoint added, dev branch set up at :1001

This commit is contained in:
serversdwn
2025-12-08 22:15:54 +00:00
parent 83593f7b33
commit 690669c697
8 changed files with 206 additions and 13 deletions

View File

@@ -1,3 +1,4 @@
import os
from fastapi import FastAPI, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
@@ -13,11 +14,15 @@ from backend.models import IgnoredUnit
# Create database tables
Base.metadata.create_all(bind=engine)
# Read environment (development or production)
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
# Initialize FastAPI app
VERSION = "0.2.2"
app = FastAPI(
title="Seismo Fleet Manager",
description="Backend API for managing seismograph fleet status",
version="0.1.1"
version=VERSION
)
# Configure CORS
@@ -35,6 +40,24 @@ app.mount("/static", StaticFiles(directory="backend/static"), name="static")
# Setup Jinja2 templates
templates = Jinja2Templates(directory="templates")
# Add custom context processor to inject environment variable into all templates
@app.middleware("http")
async def add_environment_to_context(request: Request, call_next):
"""Middleware to add environment variable to request state"""
request.state.environment = ENVIRONMENT
response = await call_next(request)
return response
# Override TemplateResponse to include environment and version in context
original_template_response = templates.TemplateResponse
def custom_template_response(name, context=None, *args, **kwargs):
if context is None:
context = {}
context["environment"] = ENVIRONMENT
context["version"] = VERSION
return original_template_response(name, context, *args, **kwargs)
templates.TemplateResponse = custom_template_response
# Include API routers
app.include_router(roster.router)
app.include_router(units.router)

View File

@@ -202,7 +202,7 @@ def set_retired(unit_id: str, retired: bool = Form(...), db: Session = Depends(g
def delete_roster_unit(unit_id: str, db: Session = Depends(get_db)):
"""
Permanently delete a unit from the database.
Checks both roster and emitters tables and deletes from any table where the unit exists.
Checks roster, emitters, and ignored_units tables and deletes from any table where the unit exists.
"""
deleted = False
@@ -218,7 +218,13 @@ def delete_roster_unit(unit_id: str, db: Session = Depends(get_db)):
db.delete(emitter)
deleted = True
# If not found in either table, return error
# Try to delete from ignored_units table
ignored_unit = db.query(IgnoredUnit).filter(IgnoredUnit.id == unit_id).first()
if ignored_unit:
db.delete(ignored_unit)
deleted = True
# If not found in any table, return error
if not deleted:
raise HTTPException(status_code=404, detail="Unit not found")

View File

@@ -10,6 +10,32 @@ from backend.models import Emitter
router = APIRouter()
# Helper function to detect unit type from unit ID
def detect_unit_type(unit_id: str) -> str:
"""
Automatically detect if a unit is Series 3 or Series 4 based on ID pattern.
Series 4 (Micromate) units have IDs starting with "UM" followed by digits (e.g., UM11719)
Series 3 units typically have other patterns
Returns:
"series4" if the unit ID matches Micromate pattern (UM#####)
"series3" otherwise
"""
if not unit_id:
return "unknown"
# Series 4 (Micromate) pattern: UM followed by digits
if unit_id.upper().startswith("UM") and len(unit_id) > 2:
# Check if remaining characters after "UM" are digits
rest = unit_id[2:]
if rest.isdigit():
return "series4"
# Default to series3 for other patterns
return "series3"
# Pydantic schemas for request/response validation
class EmitterReport(BaseModel):
unit: str
@@ -138,11 +164,16 @@ async def series3_heartbeat(request: Request, db: Session = Depends(get_db)):
emitter.last_seen = ts
emitter.last_file = last_file
emitter.status = status
# Update unit_type if it was incorrectly classified
detected_type = detect_unit_type(uid)
if emitter.unit_type != detected_type:
emitter.unit_type = detected_type
else:
# Insert new
# Insert new - auto-detect unit type from ID
detected_type = detect_unit_type(uid)
emitter = Emitter(
id=uid,
unit_type="series3",
unit_type=detected_type,
last_seen=ts,
last_file=last_file,
status=status
@@ -159,3 +190,97 @@ async def series3_heartbeat(request: Request, db: Session = Depends(get_db)):
"units_processed": len(results),
"results": results
}
# series4 (Micromate) Standardized Heartbeat Schema
@router.post("/api/series4/heartbeat", status_code=200)
async def series4_heartbeat(request: Request, db: Session = Depends(get_db)):
"""
Accepts a full telemetry payload from the Series4 (Micromate) emitter.
Updates or inserts each unit into the database.
Expected payload:
{
"source": "series4_emitter",
"generated_at": "2025-12-04T20:01:00",
"units": [
{
"unit_id": "UM11719",
"type": "micromate",
"project_hint": "Clearwater - ECMS 57940",
"last_call": "2025-12-04T19:30:42",
"status": "OK",
"age_days": 0.04,
"age_hours": 0.9,
"mlg_path": "C:\\THORDATA\\..."
}
]
}
"""
payload = await request.json()
source = payload.get("source", "series4_emitter")
units = payload.get("units", [])
print("\n=== Series 4 Heartbeat ===")
print("Source:", source)
print("Units received:", len(units))
print("==========================\n")
results = []
for u in units:
uid = u.get("unit_id")
last_call_str = u.get("last_call")
status = u.get("status", "Unknown")
mlg_path = u.get("mlg_path")
project_hint = u.get("project_hint")
# Parse last_call timestamp
try:
if last_call_str:
ts = datetime.fromisoformat(last_call_str.replace("Z", "+00:00"))
else:
ts = None
except:
ts = None
# Pull from DB
emitter = db.query(Emitter).filter(Emitter.id == uid).first()
if emitter:
# Update existing
emitter.last_seen = ts
emitter.last_file = mlg_path
emitter.status = status
# Update unit_type if it was incorrectly classified
detected_type = detect_unit_type(uid)
if emitter.unit_type != detected_type:
emitter.unit_type = detected_type
# Optionally update notes with project hint if it exists
if project_hint and not emitter.notes:
emitter.notes = f"Project: {project_hint}"
else:
# Insert new - auto-detect unit type from ID
detected_type = detect_unit_type(uid)
notes = f"Project: {project_hint}" if project_hint else None
emitter = Emitter(
id=uid,
unit_type=detected_type,
last_seen=ts,
last_file=mlg_path,
status=status,
notes=notes
)
db.add(emitter)
results.append({"unit": uid, "status": status})
db.commit()
return {
"message": "Heartbeat processed",
"source": source,
"units_processed": len(results),
"results": results
}

View File

@@ -113,12 +113,12 @@ def emit_status_snapshot():
# Separate buckets for UI
active_units = {
uid: u for uid, u in units.items()
if not u["retired"] and u["deployed"]
if not u["retired"] and u["deployed"] and uid not in ignored
}
benched_units = {
uid: u for uid, u in units.items()
if not u["retired"] and not u["deployed"]
if not u["retired"] and not u["deployed"] and uid not in ignored
}
retired_units = {
@@ -140,7 +140,7 @@ def emit_status_snapshot():
"retired": retired_units,
"unknown": unknown_units,
"summary": {
"total": len(units),
"total": len(active_units) + len(benched_units),
"active": len(active_units),
"benched": len(benched_units),
"retired": len(retired_units),