feat(locations): show event count on vibration cards instead of sessions

For vibration projects, "Sessions: 0" on every location card was
misleading — monitoring sessions don't exist under the watcher-forward
pipeline.  The relevant number is how many SFM events have been
attributed to the location.

get_project_locations now fans out events_for_location() concurrently
across all vibration locations in the project (via asyncio.gather) and
injects event_count into each item's payload.  Sound locations are
unchanged — they still show session_count.

The template already had the conditional rendering ready from the
previous commit:

    {% if item.event_count is defined and item.location.location_type == 'vibration' %}
        <span><strong>{{ event_count }}</strong> events</span>
    {% else %}
        <span>Sessions: {{ session_count }}</span>
    {% endif %}

so this commit is purely the data-layer change that activates it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 05:25:19 +00:00
parent 52dd6c3e32
commit d297412d8a
+20
View File
@@ -154,6 +154,24 @@ async def get_project_locations(
# Order by operator-set sort_order, then name as a stable tie-breaker. # Order by operator-set sort_order, then name as a stable tie-breaker.
locations = query.order_by(MonitoringLocation.sort_order, MonitoringLocation.name).all() locations = query.order_by(MonitoringLocation.sort_order, MonitoringLocation.name).all()
# For vibration locations, fan out event counts via SFM concurrently
# so the card layout can show "{N} events" instead of "Sessions: 0"
# (sessions don't really exist for the watcher-forward pipeline).
# Sound locations skip this and keep showing session counts.
event_counts: dict[str, int] = {}
vibration_locations = [l for l in locations if l.location_type == "vibration"]
if vibration_locations:
import asyncio
from backend.services.sfm_events import events_for_location
results = await asyncio.gather(
*(events_for_location(db, l.id, limit=1) for l in vibration_locations),
return_exceptions=True,
)
for loc, res in zip(vibration_locations, results):
if isinstance(res, Exception):
continue # leave event_counts[loc.id] unset → template falls back
event_counts[loc.id] = (res.get("stats") or {}).get("event_count", 0) or 0
# Enrich with assignment info, splitting active vs removed. # Enrich with assignment info, splitting active vs removed.
active_data: list = [] active_data: list = []
removed_data: list = [] removed_data: list = []
@@ -184,6 +202,8 @@ async def get_project_locations(
"assigned_unit": assigned_unit, "assigned_unit": assigned_unit,
"session_count": session_count, "session_count": session_count,
} }
if location.id in event_counts:
item["event_count"] = event_counts[location.id]
if location.removed_at is None: if location.removed_at is None:
active_data.append(item) active_data.append(item)
else: else: