From d3e221b6b12fa03dbe7c5d1aa481e50101cb4a44 Mon Sep 17 00:00:00 2001 From: serversdown Date: Wed, 10 Jun 2026 21:41:13 +0000 Subject: [PATCH] =?UTF-8?q?feat(portal):=20M1=20pages=20=E2=80=94=20locati?= =?UTF-8?q?ons=20overview=20+=20read-only=20live=20location=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /portal overview: client's active sound locations as live tiles (current Lp + Live/Stopped badge + "updated Xm ago", polled from the scoped cache every 15s) plus a Leaflet map of locations with coordinates. /portal/location/{id}: 404-gated read-only live panel — Lp/Leq/Lmax/L1/L10 cards + a 4-line Chart.js trace (backfilled from /history) + measuring/freshness badge. Cache-only, 15s poll, no device controls, no refresh-from-device. _client_locations() feeds the overview. Verified: portal.py compiles; both inline scripts balance; all four portal templates parse in Jinja2. Co-Authored-By: Claude Opus 4.8 --- backend/routers/portal.py | 40 +++++++++- templates/portal/location.html | 138 +++++++++++++++++++++++++++++++++ templates/portal/overview.html | 87 +++++++++++++++++++-- 3 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 templates/portal/location.html diff --git a/backend/routers/portal.py b/backend/routers/portal.py index 1cc01c1..1382b45 100644 --- a/backend/routers/portal.py +++ b/backend/routers/portal.py @@ -66,6 +66,26 @@ def active_unit_for_location(location_id: str, db: Session): return asg.unit_id if asg else None +def _client_locations(client: Client, db: Session) -> list: + """The client's active sound locations (for the overview tiles + map).""" + pids = _client_project_ids(client, db) + if not pids: + return [] + projs = {p.id: p.name for p in + db.query(Project.id, Project.name).filter(Project.id.in_(pids)).all()} + locs = (db.query(MonitoringLocation) + .filter(MonitoringLocation.project_id.in_(pids), + MonitoringLocation.location_type == "sound", + MonitoringLocation.removed_at.is_(None)) + .order_by(MonitoringLocation.sort_order, MonitoringLocation.name).all()) + return [{ + "id": loc.id, "name": loc.name, + "address": loc.address, "coordinates": loc.coordinates, + "project_name": projs.get(loc.project_id), + "has_device": active_unit_for_location(loc.id, db) is not None, + } for loc in locs] + + @router.get("/enter/{token}") def portal_enter(token: str, request: Request, db: Session = Depends(get_db)): """Magic-URL entry: validate the token, mint a session cookie, land on /portal.""" @@ -101,14 +121,28 @@ def portal_access(request: Request): @router.get("") -def portal_home(request: Request, client: Client = Depends(get_current_client)): - """Client overview. (M1 task 4 fills in the scoped location list + map.)""" +def portal_home(request: Request, client: Client = Depends(get_current_client), + db: Session = Depends(get_db)): + """Client overview — their active sound locations with live tiles + a map.""" return templates.TemplateResponse( "portal/overview.html", - {"request": request, "client": client, "locations": []}, + {"request": request, "client": client, + "locations": _client_locations(client, db)}, ) +@router.get("/location/{location_id}") +def portal_location(location_id: str, request: Request, + client: Client = Depends(get_current_client), + db: Session = Depends(get_db)): + """Read-only live view for one of the client's locations (404 if not owned).""" + loc = resolve_client_location(client, location_id, db) + return templates.TemplateResponse("portal/location.html", { + "request": request, "client": client, "location": loc, + "has_device": active_unit_for_location(location_id, db) is not None, + }) + + # -- scoped data (cache reads only — never hits the device) ------------------ @router.get("/api/location/{location_id}/live") diff --git a/templates/portal/location.html b/templates/portal/location.html new file mode 100644 index 0000000..4bb107b --- /dev/null +++ b/templates/portal/location.html @@ -0,0 +1,138 @@ +{% extends "portal/base.html" %} +{% block title %}{{ location.name }}{% endblock %} +{% block head %} + +{% endblock %} +{% block content %} +← All locations +

{{ location.name }}

+
+ + +
+ +{% if not has_device %} +
+ No device is currently assigned to this location. +
+{% else %} +
+
+
Lp (Instant)
+
--
dB
+
+
+
Leq (Average)
+
--
dB
+
+
+
Lmax (Max)
+
--
dB
+
+
+
L1
+
--
dB
+
+
+
L10
+
--
dB
+
+
+ +
+ +
+{% endif %} +{% endblock %} + +{% block scripts %} +{% if has_device %} + +{% endif %} +{% endblock %} diff --git a/templates/portal/overview.html b/templates/portal/overview.html index fefc731..a5b0994 100644 --- a/templates/portal/overview.html +++ b/templates/portal/overview.html @@ -1,16 +1,29 @@ {% extends "portal/base.html" %} {% block title %}Your locations{% endblock %} +{% block head %} + +{% endblock %} {% block content %}

Your monitoring locations

-

Live sound levels for your active locations.

+

Live sound levels for your active locations. Read-only.

-{# M1 task 4 fleshes this out into location tiles + a map. #} {% if locations %} + + @@ -20,3 +33,67 @@ {% endif %} {% endblock %} + +{% block scripts %} + + +{% endblock %}