feat(sfm): project-level vibration events roll-up
Phase 3 of the SFM integration. Adds a "Project-wide vibration events"
KPI card to the Vibration tab of every project detail page, summarising
event activity across all of that project's vibration MonitoringLocations.
Backend:
- backend/services/sfm_events.py: vibration_summary_for_project() helper.
Concurrently fans out events_for_location() across every vibration
location in the project; aggregates total events, peak PVS (with the
location it occurred at), last-event timestamp, false-trigger count;
and produces a per-location breakdown sorted by event count.
- backend/routers/project_locations.py: new GET /api/projects/{p}/
vibration_summary endpoint returning an HTML partial (HTMX-friendly,
matches the locations-list HTMX pattern already used on this page).
Frontend:
- templates/partials/projects/vibration_summary.html: new partial with
four KPI tiles (total, peak PVS + linked location + date, last event,
false triggers) and a "Top locations by activity" mini-list showing
the top 5 by event count. Empty-state copy when the project has no
vibration locations yet.
- templates/projects/detail.html: HTMX-load the new summary above the
locations list inside the Vibration tab.
Verified against terra-view-alpha: 24 events across "Loc 1 - 78 poop
street", peak PVS 14.1351 in/s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -776,6 +776,46 @@ async def get_nrl_sessions(
|
||||
})
|
||||
|
||||
|
||||
@router.get("/vibration_summary", response_class=HTMLResponse)
|
||||
async def get_project_vibration_summary(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
from_dt: Optional[datetime] = Query(None),
|
||||
to_dt: Optional[datetime] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Render a small HTML partial summarising vibration-event activity
|
||||
across every vibration MonitoringLocation in the project.
|
||||
|
||||
Returned to the Vibration tab of the project detail page via HTMX.
|
||||
Fans out concurrently across all locations (which in turn fan out
|
||||
across each location's UnitAssignment windows). Total queries to
|
||||
SFM = sum of assignments across the project.
|
||||
|
||||
404 if the project doesn't exist. Empty-state partial if the
|
||||
project has no vibration locations.
|
||||
"""
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found.")
|
||||
|
||||
from backend.services.sfm_events import vibration_summary_for_project
|
||||
|
||||
summary = await vibration_summary_for_project(
|
||||
db, project_id, from_dt=from_dt, to_dt=to_dt
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"partials/projects/vibration_summary.html",
|
||||
{
|
||||
"request": request,
|
||||
"project_id": project_id,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/locations/{location_id}/events", response_class=JSONResponse)
|
||||
async def get_location_events(
|
||||
project_id: str,
|
||||
|
||||
Reference in New Issue
Block a user