"""Metrologist blueprint - SPC statistics dashboard and API proxies.""" import requests as http_requests from flask import Blueprint, jsonify, render_template, request, session from flask_babel import gettext as _ from blueprints.auth import login_required, role_required from config import Config 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 — loads recipes for filter dropdown.""" # #37.6: a single per_page=100 call silently dropped every recipe past # the 100th from the filter dropdown. 100 is the backend's max per_page # (routers/recipes.py), so a full listing needs a page loop, not a # bigger number. recipes = [] page = 1 while True: resp = api_client.get("/api/recipes", params={"per_page": 100, "page": page}) if isinstance(resp, dict) and resp.get("error"): break page_items = resp.get("items", []) if isinstance(resp, dict) else [] recipes.extend(page_items) total_pages = resp.get("pages", 1) if isinstance(resp, dict) else 1 if page >= total_pages or not page_items: break page += 1 return render_template("statistics/dashboard.html", recipes=recipes) # ============================================================================ # 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", "production_run_id", # #58 ]: 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"): error_body = {"error": True, "detail": resp.get("detail", "")} if resp.get("session_expired"): error_body["session_expired"] = True return jsonify(error_body), resp.get("status_code", 500) return jsonify(resp) @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("/api/capability") @login_required @role_required("Metrologist") def api_capability(): """Proxy: capability indices.""" return _proxy_statistics("capability") @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"): error_body = {"error": True, "detail": resp.get("detail", "")} if resp.get("session_expired"): error_body["session_expired"] = True return jsonify(error_body), resp.get("status_code", 500) # Response is a list if isinstance(resp, list): return jsonify(resp) return jsonify(resp) @statistics_bp.route("/api/runs") @login_required @role_required("Metrologist") def api_runs(): """Proxy: production runs of a recipe (#58, "Sessione" filter).""" params = {} recipe_id = request.args.get("recipe_id") if recipe_id: params["recipe_id"] = recipe_id resp = api_client.get("/api/statistics/runs", params=params) if isinstance(resp, dict) and resp.get("error"): error_body = {"error": True, "detail": resp.get("detail", "")} if resp.get("session_expired"): error_body["session_expired"] = True return jsonify(error_body), resp.get("status_code", 500) return jsonify(resp) # ============================================================================ # PROXY REPORT PDF → FastAPI # ============================================================================ def _proxy_report(endpoint: str): """Forward query params to a FastAPI reports endpoint and return PDF.""" params = {} for key in [ "recipe_id", "version_id", "subtask_id", "date_from", "date_to", "operator_id", "lot_number", "serial_number", "production_run_id", # #58 ]: val = request.args.get(key) if val is not None and val != "": params[key] = val base_url = Config.API_SERVER_URL.rstrip("/") headers = {} api_key = session.get("api_key") if api_key: headers["X-API-Key"] = api_key try: resp = http_requests.get( f"{base_url}/api/reports/{endpoint}", headers=headers, params=params, timeout=120, ) except (http_requests.ConnectionError, http_requests.Timeout) as e: return jsonify({"error": True, "detail": str(e)}), 502 if not resp.ok: try: err = resp.json() detail = err.get("detail", f"HTTP {resp.status_code}") except Exception: detail = f"HTTP {resp.status_code}" return jsonify({"error": True, "detail": detail}), resp.status_code from flask import Response as FlaskResponse return FlaskResponse( resp.content, mimetype="application/pdf", headers={ "Content-Disposition": resp.headers.get( "Content-Disposition", f'attachment; filename="report.pdf"' ) }, ) @statistics_bp.route("/api/report-spc") @login_required @role_required("Metrologist") def api_report_spc(): """Proxy: download SPC PDF report.""" return _proxy_report("spc") @statistics_bp.route("/api/report-measurements") @login_required @role_required("Metrologist") def api_report_measurements(): """Proxy: download measurements PDF report.""" return _proxy_report("measurements")