From 17344aae135c585430d79ced87e77307b65d188c Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Wed, 28 Oct 2026 21:30:11 +0100 Subject: [PATCH] Monitor: quota con soli NaN resta in elenco con media null e fuori tolleranza Refs #118 Co-Authored-By: Claude Sonnet 5.5 --- src/backend/models/api/monitor.py | 2 +- src/backend/services/monitor_service.py | 16 ++++++++------- src/backend/tests/test_monitor.py | 26 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/backend/models/api/monitor.py b/src/backend/models/api/monitor.py index bca53bb..bbfaf00 100644 --- a/src/backend/models/api/monitor.py +++ b/src/backend/models/api/monitor.py @@ -12,7 +12,7 @@ class MonitorSubtask(BaseModel): description: str unit: str count: int - mean: float + mean: Optional[float] = None # #118: None = solo quote NaN ltl: Optional[float] = None utl: Optional[float] = None # True when ltl <= mean <= utl (a missing limit does not constrain). diff --git a/src/backend/services/monitor_service.py b/src/backend/services/monitor_service.py index 7944cb1..ea57ef2 100644 --- a/src/backend/services/monitor_service.py +++ b/src/backend/services/monitor_service.py @@ -96,19 +96,21 @@ async def list_open_runs(db: AsyncSession) -> list[MonitorRun]: per_sub[row.subtask_id].append(row) subtasks = [] for sid, group in sorted(per_sub.items()): - # #118: le quote NaN (value None) non entrano nella media; senza - # nessun valore numerico la quota non ha una media da mostrare + # #118: le quote NaN (value None) non entrano nella media; con soli + # NaN la quota resta in elenco con mean None ed e' fuori tolleranza numeric = [float(g.value) for g in group if g.value is not None] - if not numeric: - continue - mean = sum(numeric) / len(numeric) + mean = sum(numeric) / len(numeric) if numeric else None first = group[0] subtasks.append(MonitorSubtask( subtask_id=sid, description=first.description, unit=first.unit, - count=len(group), mean=round(mean, 6), + count=len(group), + mean=round(mean, 6) if mean is not None else None, # #118 ltl=float(first.ltl) if first.ltl is not None else None, utl=float(first.utl) if first.utl is not None else None, - in_tolerance=_in_tolerance(mean, first.ltl, first.utl), + in_tolerance=( + _in_tolerance(mean, first.ltl, first.utl) + if mean is not None else False # #118: tutte NaN = fail + ), )) timer = production_service.describe(run) ev = last_event.get(run.id) diff --git a/src/backend/tests/test_monitor.py b/src/backend/tests/test_monitor.py index 2d63e52..26cd90b 100644 --- a/src/backend/tests/test_monitor.py +++ b/src/backend/tests/test_monitor.py @@ -81,3 +81,29 @@ async def test_open_lists_only_open_runs_with_means(client, db_session, admin_us async def test_open_empty_for_admin(client, admin_user): resp = await client.get("/api/production-runs/open", headers=auth_headers(admin_user)) assert resp.status_code == 200 and resp.json() == [] + + +async def test_nan_quotes_in_the_monitor(client, db_session, admin_user): + """#118: solo NaN -> la quota resta, mean null e fuori tolleranza; mista -> media dei numeri.""" + sup = await _create_user(db_session, "capo", roles=["Supervisor"]) + r1, s1, v1 = await _run(db_session, admin_user, "N1") + r2, s2, v2 = await _run(db_session, admin_user, "N2") + for i in range(2): + db_session.add(_m(r1, s1, v1, admin_user, None, "fail", i)) + for i, (val, pf) in enumerate([(None, "fail"), (10.0, "pass"), (10.2, "pass")]): + db_session.add(_m(r2, s2, v2, admin_user, val, pf, i)) + await db_session.commit() + + resp = await client.get("/api/production-runs/open", headers=auth_headers(sup)) + assert resp.status_code == 200 + body = {r["station_code"]: r for r in resp.json()} + only_nan = body["ST-N1"]["subtasks"] + assert len(only_nan) == 1 + assert only_nan[0]["mean"] is None + assert only_nan[0]["count"] == 2 + assert only_nan[0]["in_tolerance"] is False + assert body["ST-N1"]["fail_count"] == 2 + mixed = body["ST-N2"]["subtasks"][0] + assert mixed["count"] == 3 + assert round(mixed["mean"], 6) == 10.1 + assert mixed["in_tolerance"] is True