feat: FASE 5b/6.1+6.2 - SPC Backend + Dashboard Metrologist (Plotly.js)

Aggiunge servizio SPC con calcoli Cp/Cpk/Pp/Ppk, carta di controllo (UCL/LCL),
istogramma con curva normale. Router FastAPI con 5 endpoint statistics, blueprint
Flask con proxy AJAX, dashboard interattiva Alpine.js + Plotly.js con filtri per
ricetta/subtask/date, riepilogo pass/fail, gauge Cpk e i18n IT/EN completo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 15:00:05 +01:00
parent e1f4ee73d0
commit bcd807e57d
14 changed files with 1567 additions and 242 deletions
+93 -30
View File
@@ -1,44 +1,107 @@
"""Metrologist blueprint - SPC statistics and dashboards."""
from flask import Blueprint, render_template, session, redirect, url_for
"""Metrologist blueprint - SPC statistics dashboard and API proxies."""
from flask import Blueprint, jsonify, render_template, request
from flask_babel import gettext as _
from blueprints.auth import login_required, role_required
from services.api_client import api_client
statistics_bp = Blueprint("statistics", __name__)
# ============================================================================
# PAGINE
# ============================================================================
@statistics_bp.route("/dashboard")
@login_required
@role_required("Metrologist")
def dashboard():
"""SPC dashboard overview."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/dashboard.html")
"""SPC dashboard overview — loads recipes for filter dropdown."""
resp = api_client.get("/api/recipes", params={"per_page": 100})
if isinstance(resp, dict) and resp.get("error"):
recipes = []
else:
recipes = resp.get("items", []) if isinstance(resp, dict) else []
return render_template("statistics/dashboard.html", recipes=recipes)
@statistics_bp.route("/control-chart")
def control_chart():
"""X-bar / R control chart."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/control_chart.html")
# ============================================================================
# PROXY API AJAX → FastAPI
# ============================================================================
def _proxy_statistics(endpoint: str):
"""Forward query params to a FastAPI statistics endpoint."""
params = {}
for key in [
"recipe_id", "version_id", "subtask_id",
"date_from", "date_to", "operator_id",
"lot_number", "serial_number", "n_bins",
]:
val = request.args.get(key)
if val is not None and val != "":
params[key] = val
resp = api_client.get(f"/api/statistics/{endpoint}", params=params)
if isinstance(resp, dict) and resp.get("error"):
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
return jsonify(resp)
@statistics_bp.route("/histogram")
def histogram():
"""Histogram with normal curve."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/histogram.html")
@statistics_bp.route("/api/summary")
@login_required
@role_required("Metrologist")
def api_summary():
"""Proxy: summary pass/fail/warning."""
return _proxy_statistics("summary")
@statistics_bp.route("/capability")
def capability():
"""Cp/Cpk/Pp/Ppk capability gauge."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/capability.html")
@statistics_bp.route("/api/capability")
@login_required
@role_required("Metrologist")
def api_capability():
"""Proxy: capability indices."""
return _proxy_statistics("capability")
@statistics_bp.route("/trend")
def trend():
"""Temporal trends and period comparison."""
if "user" not in session:
return redirect(url_for("auth.login"))
return render_template("statistics/trend.html")
@statistics_bp.route("/api/control-chart")
@login_required
@role_required("Metrologist")
def api_control_chart():
"""Proxy: control chart data."""
return _proxy_statistics("control-chart")
@statistics_bp.route("/api/histogram")
@login_required
@role_required("Metrologist")
def api_histogram():
"""Proxy: histogram data."""
return _proxy_statistics("histogram")
@statistics_bp.route("/api/subtasks")
@login_required
@role_required("Metrologist")
def api_subtasks():
"""Proxy: get subtasks for recipe filter."""
params = {}
recipe_id = request.args.get("recipe_id")
if recipe_id:
params["recipe_id"] = recipe_id
version_id = request.args.get("version_id")
if version_id:
params["version_id"] = version_id
resp = api_client.get("/api/statistics/subtasks", params=params)
if isinstance(resp, dict) and resp.get("error"):
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
# Response is a list
if isinstance(resp, list):
return jsonify(resp)
return jsonify(resp)