feat: on-watch stats scope selector (page vs all)

Adds an icon selector to the "On watch" provider deck letting users
choose between page-scoped stats (legacy) and full-corpus stats that
aggregate across every event matching the current filters. Backend
returns a new provider_event_counts map alongside the paginated events.
This commit is contained in:
2026-05-12 14:12:59 +03:00
parent dfd7329177
commit dec0839853
6 changed files with 115 additions and 7 deletions
@@ -77,6 +77,32 @@ async def get_status(
count_query = select(func.count()).select_from(events_query.subquery())
total_events = (await session.exec(count_query)).one()
# Aggregate per-provider event counts across ALL matching events (ignoring
# offset/limit) so the "On watch" deck can show full-corpus stats when the
# user opts out of page-scoped stats. Sums ``assets_count`` (falling back to
# 1 per event) to mirror the frontend's per-page derivation.
provider_counts_query = (
select(
EventLog.provider_id,
EventLog.provider_name,
func.sum(func.coalesce(EventLog.assets_count, 1)).label("total"),
)
.where(EventLog.user_id == user.id)
.group_by(EventLog.provider_id, EventLog.provider_name)
)
if event_type:
provider_counts_query = provider_counts_query.where(EventLog.event_type == event_type)
if provider_id is not None:
provider_counts_query = provider_counts_query.where(EventLog.provider_id == provider_id)
if search:
provider_counts_query = provider_counts_query.where(
EventLog.collection_name.contains(search)
| EventLog.tracker_name.contains(search)
| EventLog.action_name.contains(search)
| EventLog.provider_name.contains(search)
)
provider_counts_rows = (await session.exec(provider_counts_query)).all()
# Sort
if sort == "oldest":
events_query = events_query.order_by(EventLog.created_at.asc())
@@ -98,8 +124,11 @@ async def get_status(
)).all()
tracker_name_map = {tid: tname for tid, tname in tracker_rows}
# Resolve live provider names similarly
# Resolve live provider names similarly. Includes IDs from the aggregated
# provider counts so the "all events" deck shows up-to-date names even for
# providers that don't appear on the current page.
provider_ids = {e.provider_id for e in event_rows if e.provider_id is not None}
provider_ids.update(pid for pid, _pname, _total in provider_counts_rows if pid is not None)
provider_name_map: dict[int, str] = {}
if provider_ids:
provider_rows = (await session.exec(
@@ -189,11 +218,26 @@ async def get_status(
return _display_action_name(e) or e.collection_name
return e.collection_name
# Build the provider event count map keyed by live provider name (matches
# the frontend's keying scheme). Falls back to the stored snapshot name
# when the provider has been deleted.
provider_event_counts: dict[str, int] = {}
for pid, pname, total in provider_counts_rows:
display_name = (
provider_name_map.get(pid) if pid is not None else None
) or pname or ""
if not display_name:
continue
provider_event_counts[display_name] = (
provider_event_counts.get(display_name, 0) + int(total or 0)
)
return {
"providers": providers_count,
"trackers": {"total": len(trackers), "active": active_count},
"targets": targets_count,
"total_events": total_events,
"provider_event_counts": provider_event_counts,
"recent_events": [
{
"id": e.id,