From 80464c6f11da0c8604f867831c48d32044decdaa Mon Sep 17 00:00:00 2001 From: serversdown Date: Mon, 22 Jun 2026 20:38:48 +0000 Subject: [PATCH] feat(projects): per-module stat breakdown on project cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single Locations/Units/Active row was confusing: "Active" collided with the green Active status badge and actually meant sound recording sessions, so vibration-only projects showed a meaningless "Active 0", and combined projects lumped both modules together with no split. Cards now show one stat line per module, each carrying its own identity + status badge (so the separate chip row is dropped as redundant): Vibration N locations · M units Sound N NRLs · M units · K recording - /list endpoint computes module_stats: locations (active, by type) and units counted via a join on the assigned location's type — so a module's unit count always reconciles with its location count (verified: sound+vibration units == total active assignments for every project). - "recording" (active sessions) shows only under Sound, where it's meaningful. - Projects with no modules fall back to a simple Locations/Units row. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015m9FuJvk65kJmmP3c9c6r1 --- backend/routers/projects.py | 43 +++++++++++- templates/partials/projects/project_list.html | 69 +++++++++++-------- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/backend/routers/projects.py b/backend/routers/projects.py index b564c7c..554f7d5 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -470,8 +470,10 @@ async def get_projects_list( for project in projects: # Get project type project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() + mods = _get_project_modules(project.id, db) - # Count locations + # Count locations (project-wide, includes removed — kept for back-compat + # with the compact list view). location_count = db.query(func.count(MonitoringLocation.id)).filter_by( project_id=project.id ).scalar() @@ -484,7 +486,7 @@ async def get_projects_list( ) ).scalar() - # Count active sessions + # Count active (recording) sessions — a sound-monitoring concept active_session_count = db.query(func.count(MonitoringSession.id)).filter( and_( MonitoringSession.project_id == project.id, @@ -492,11 +494,46 @@ async def get_projects_list( ) ).scalar() + # Per-module stats — each module shows only its own, relevant counts. + # Locations: active only (removed_at IS NULL). Units: active assignments + # counted by the TYPE OF LOCATION they sit on (join), so a module's unit + # count always lines up with its own location count — independent of the + # assignment's denormalized device_type. + def _module_loc_count(loc_type): + return db.query(func.count(MonitoringLocation.id)).filter( + MonitoringLocation.project_id == project.id, + MonitoringLocation.location_type == loc_type, + MonitoringLocation.removed_at.is_(None), + ).scalar() + + def _module_unit_count(loc_type): + return db.query(func.count(UnitAssignment.id)).join( + MonitoringLocation, MonitoringLocation.id == UnitAssignment.location_id + ).filter( + UnitAssignment.project_id == project.id, + UnitAssignment.assigned_until.is_(None), + MonitoringLocation.location_type == loc_type, + ).scalar() + + module_stats = {} + if "vibration_monitoring" in mods: + module_stats["vibration"] = { + "locations": _module_loc_count("vibration"), + "units": _module_unit_count("vibration"), + } + if "sound_monitoring" in mods: + module_stats["sound"] = { + "locations": _module_loc_count("sound"), + "units": _module_unit_count("sound"), + "recording": active_session_count, + } + projects_data.append({ "project": project, "project_type": project_type, - "modules": _get_project_modules(project.id, db), + "modules": mods, "module_status": _get_module_statuses(project.id, db), + "module_stats": module_stats, "location_count": location_count, "unit_count": unit_count, "active_session_count": active_session_count, diff --git a/templates/partials/projects/project_list.html b/templates/partials/projects/project_list.html index 071efaa..e2f6e46 100644 --- a/templates/partials/projects/project_list.html +++ b/templates/partials/projects/project_list.html @@ -39,36 +39,50 @@ {% endif %} - - {% if mods %} -
- {% for m in mods %} - {% set st = mstatus.get(m, 'active') %} - - {% if m == 'sound_monitoring' %} - - Sound - {% elif m == 'vibration_monitoring' %} - - Vibration - {% else %}{{ m }}{% endif %} - {% if st == 'completed' %} - {% elif st == 'on_hold' %}{% endif %} - - {% endfor %} -
- {% endif %} - {% if p.description %}

{{ p.description }}

{% endif %} - -
+ + {% set ms = item.module_stats %} + {% if ms %} +
+ {% if 'vibration' in ms %} + {% set vst = mstatus.get('vibration_monitoring', 'active') %} +
+ + + Vibration + {% if vst == 'completed' %}{% elif vst == 'on_hold' %}{% endif %} + + + {{ ms.vibration.locations }} location{{ '' if ms.vibration.locations == 1 else 's' }} + · {{ ms.vibration.units }} unit{{ '' if ms.vibration.units == 1 else 's' }} + +
+ {% endif %} + {% if 'sound' in ms %} + {% set sst = mstatus.get('sound_monitoring', 'active') %} +
+ + + Sound + {% if sst == 'completed' %}{% elif sst == 'on_hold' %}{% endif %} + + + {{ ms.sound.locations }} NRL{{ '' if ms.sound.locations == 1 else 's' }} + · {{ ms.sound.units }} unit{{ '' if ms.sound.units == 1 else 's' }} + · {% if ms.sound.recording > 0 %}{{ ms.sound.recording }} recording{% else %}0 recording{% endif %} + +
+ {% endif %} +
+ {% else %} + +

Locations

{{ item.location_count }}

@@ -77,11 +91,8 @@

Units

{{ item.unit_count }}

-
-

Active

-

{{ item.active_session_count }}

-
+ {% endif %}