diff --git a/src/frontend/flask_app/app.py b/src/frontend/flask_app/app.py index d1942be..6a096b0 100644 --- a/src/frontend/flask_app/app.py +++ b/src/frontend/flask_app/app.py @@ -74,12 +74,14 @@ def create_app() -> Flask: from blueprints.maker import maker_bp from blueprints.statistics import statistics_bp from blueprints.admin import admin_bp + from blueprints.monitor import monitor_bp # #57 live station monitor app.register_blueprint(auth_bp) app.register_blueprint(measure_bp, url_prefix="/measure") app.register_blueprint(maker_bp, url_prefix="/maker") app.register_blueprint(statistics_bp, url_prefix="/statistics") app.register_blueprint(admin_bp, url_prefix="/admin") + app.register_blueprint(monitor_bp, url_prefix="/monitor") # #57 @app.before_request def _check_secret_key(): diff --git a/src/frontend/flask_app/blueprints/monitor.py b/src/frontend/flask_app/blueprints/monitor.py new file mode 100644 index 0000000..4db419d --- /dev/null +++ b/src/frontend/flask_app/blueprints/monitor.py @@ -0,0 +1,45 @@ +"""Live monitor of the open production runs (#57). + +Page at /monitor for admin and Supervisor (capoturno); the page polls a JSON proxy +of GET /api/production-runs/open every 10 seconds. +""" +from functools import wraps + +from flask import Blueprint, abort, jsonify, render_template, session + +from blueprints.auth import login_required +from services.api_client import api_client + +monitor_bp = Blueprint("monitor", __name__) + + +def monitor_required(f): + """Admin or Supervisor only: the same people the backend lets read /open.""" + @wraps(f) + def decorated(*args, **kwargs): + user = session.get("user", {}) + if not (user.get("is_admin") or "Supervisor" in user.get("roles", [])): + abort(403) + return f(*args, **kwargs) + return decorated + + +@monitor_bp.route("", strict_slashes=False) +@login_required +@monitor_required +def index(): + return render_template("monitor/index.html") + + +@monitor_bp.route("/api/runs") +@login_required +@monitor_required +def api_runs(): + """JSON proxy polled by the page.""" + resp = api_client.get("/api/production-runs/open") + if isinstance(resp, dict) and resp.get("error"): + body = {"error": True, "detail": resp.get("detail", "")} + if resp.get("session_expired"): + body["session_expired"] = True + return jsonify(body), resp.get("status_code", 500) + return jsonify(resp) diff --git a/src/frontend/flask_app/templates/monitor/index.html b/src/frontend/flask_app/templates/monitor/index.html new file mode 100644 index 0000000..3960be6 --- /dev/null +++ b/src/frontend/flask_app/templates/monitor/index.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Monitor stazioni") }} — TieMeasureFlow{% endblock %} + +{% block content %} +
+
+
+

{{ _("Monitor stazioni") }}

+

{{ _("Produzioni aperte, aggiornate ogni 10 secondi") }}

+
+
+
+ +
+ +
+ {{ _("Nessuna stazione in produzione") }} +
+ +
+ +
+
+ + +{% endblock %} diff --git a/src/frontend/flask_app/tests/test_monitor.py b/src/frontend/flask_app/tests/test_monitor.py new file mode 100644 index 0000000..6adfd32 --- /dev/null +++ b/src/frontend/flask_app/tests/test_monitor.py @@ -0,0 +1,46 @@ +"""Live station monitor page and JSON proxy (#57).""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_api(): + with patch("blueprints.monitor.api_client") as m: + yield m + + +def _login(client, roles, is_admin=False): + with client.session_transaction() as sess: + sess["api_key"] = "k" + sess["user"] = {"id": 2, "username": "u", "display_name": "U", "roles": roles, + "is_admin": is_admin, "language_pref": "en", "theme_pref": "light", + "active": True} + sess["language"] = "en" + + +def test_page_ok_for_supervisor(client, mock_api): + _login(client, ["Supervisor"]) + resp = client.get("/monitor") + assert resp.status_code == 200 and b"stationMonitor" in resp.data + + +def test_page_forbidden_for_operator(client, mock_api): + _login(client, ["MeasurementTec"]) + assert client.get("/monitor").status_code == 403 + assert client.get("/monitor/api/runs").status_code == 403 + + +def test_proxy_returns_backend_json(client, mock_api): + _login(client, ["Supervisor"]) + mock_api.get.return_value = [{"id": 1, "station_code": "ST-1"}] + resp = client.get("/monitor/api/runs") + assert resp.status_code == 200 and resp.get_json()[0]["station_code"] == "ST-1" + mock_api.get.assert_called_once_with("/api/production-runs/open") + + +def test_proxy_passes_backend_error(client, mock_api): + _login(client, [], is_admin=True) + mock_api.get.return_value = {"error": True, "status_code": 403, "detail": "no"} + resp = client.get("/monitor/api/runs") + assert resp.status_code == 403 and resp.get_json()["error"] is True