From 5684484fdf76f7b8172daa190081894a1c5948a4 Mon Sep 17 00:00:00 2001 From: kitos Date: Wed, 3 Jun 2026 10:59:58 +0200 Subject: [PATCH] fix(metrics): prevent 0.0 falsy bug for sub-hour timing values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: avg times were ~2-3 minutes (< 1h). round(0.033, 1) = 0.0 which is falsy in JS, so the frontend showed N/A instead of the value. Fix (backend): _safe_stats() and team metrics now convert to minutes when avg < 1 hour, adding a 'unit' field ('min' or 'hrs'). Fix (frontend): use != null instead of truthy check for avg_completion_hours, MTTD, MTTR — correctly shows 0.0 and uses the unit field to show 'min' or 'hrs'. --- .../services/operational_metrics_service.py | 22 +++++++++++++++---- frontend/src/pages/ExecutiveDashboardPage.tsx | 20 ++++++++--------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/backend/app/services/operational_metrics_service.py b/backend/app/services/operational_metrics_service.py index 569e238..1a29d00 100644 --- a/backend/app/services/operational_metrics_service.py +++ b/backend/app/services/operational_metrics_service.py @@ -17,13 +17,19 @@ from app.models.enums import TestState, TestResult def _safe_stats(values: list[float]) -> dict: - """Compute mean, median, min, max from a list of floats.""" + """Compute mean, median, min, max from a list of floats (in hours). + For sub-hour averages, mean_hours is stored as minutes to avoid + rounding to 0.0 which is falsy in JavaScript.""" if not values: return None sorted_vals = sorted(values) n = len(sorted_vals) + mean = sum(sorted_vals) / n + # Use minutes for sub-hour values to avoid JS falsy 0.0 + mean_display = round(mean * 60, 1) if mean < 1 else round(mean, 1) return { - "mean_hours": round(sum(sorted_vals) / n, 1), + "mean_hours": mean_display, + "unit": "min" if mean < 1 else "hrs", "median_hours": round(sorted_vals[n // 2], 1), "min_hours": round(sorted_vals[0], 1), "max_hours": round(sorted_vals[-1], 1), @@ -416,7 +422,9 @@ def get_metrics_by_team(db: Session) -> dict: if net > 0: red_times.append(net / 3600) if red_times: - red_avg_time = round(sum(red_times) / len(red_times), 1) + avg_hours = sum(red_times) / len(red_times) + # Use minutes for sub-hour values so rounding to 0.0 doesn't hide data + red_avg_time = round(avg_hours * 60, 1) if avg_hours < 1 else round(avg_hours, 1) # Blue team: count tests that reached the blue evaluation phase blue_tests_completed = ( @@ -449,17 +457,23 @@ def get_metrics_by_team(db: Session) -> dict: if net > 0: blue_times.append(net / 3600) if blue_times: - blue_avg_time = round(sum(blue_times) / len(blue_times), 1) + avg_hours = sum(blue_times) / len(blue_times) + blue_avg_time = round(avg_hours * 60, 1) if avg_hours < 1 else round(avg_hours, 1) + + red_avg_raw = sum(red_times) / len(red_times) if red_times else None + blue_avg_raw = sum(blue_times) / len(blue_times) if blue_times else None return { "red_team": { "tests_completed": red_tests_completed, "avg_completion_hours": red_avg_time, + "avg_unit": "min" if (red_avg_raw is not None and red_avg_raw < 1) else "hrs", "rejection_rate": calculate_rejection_rate(db)["by_red_lead"], }, "blue_team": { "tests_completed": blue_tests_completed, "avg_completion_hours": blue_avg_time, + "avg_unit": "min" if (blue_avg_raw is not None and blue_avg_raw < 1) else "hrs", "rejection_rate": calculate_rejection_rate(db)["by_blue_lead"], }, } diff --git a/frontend/src/pages/ExecutiveDashboardPage.tsx b/frontend/src/pages/ExecutiveDashboardPage.tsx index 187997f..9f9f42a 100644 --- a/frontend/src/pages/ExecutiveDashboardPage.tsx +++ b/frontend/src/pages/ExecutiveDashboardPage.tsx @@ -380,8 +380,8 @@ export default function ExecutiveDashboardPage() {

- {teamMetrics.red_team.avg_completion_hours - ? `${teamMetrics.red_team.avg_completion_hours}h` + {teamMetrics.red_team.avg_completion_hours != null + ? `${teamMetrics.red_team.avg_completion_hours}${(teamMetrics.red_team as any).avg_unit ?? "h"}` : "N/A"}

Avg Time

@@ -411,8 +411,8 @@ export default function ExecutiveDashboardPage() {

- {teamMetrics.blue_team.avg_completion_hours - ? `${teamMetrics.blue_team.avg_completion_hours}h` + {teamMetrics.blue_team.avg_completion_hours != null + ? `${teamMetrics.blue_team.avg_completion_hours}${(teamMetrics.blue_team as any).avg_unit ?? "h"}` : "N/A"}

Avg Time

@@ -573,15 +573,15 @@ export default function ExecutiveDashboardPage() {