feat(measure): il fuori tolleranza si autorizza, non si chiude

Il punto 5 c'era a meta': la schermata apriva il modale del capoturno e bloccava
l'avanzamento automatico, ma il modale si chiudeva con Annulla o con un click
sullo sfondo, la misura era gia' salvata, e `pendingAdvance` veniva impostato e
non letto da nessuno. Era una conferma, non uno sbarramento.

Ora l'autorizzazione finisce sulla misura: due colonne, chi ha autorizzato e
quando. Finche' una quota e' fuori tolleranza e nessuno l'ha autorizzata, il
server rifiuta la misura della quota successiva e la chiusura del ciclo. Non e'
la schermata a impedirlo: la schermata risparmia solo il viaggio.

Rimisurare la stessa quota resta possibile — il calibro scivola, il pezzo si
riposiziona — e una seconda lettura in tolleranza libera il blocco, perche' conta
l'ultima lettura di ogni quota. Quante volte si possa riprovare e' il punto 4, e
tutte le letture restano comunque a registro. Un warning non blocca: fuori dai
limiti di attenzione ma dentro la tolleranza e' dentro la tolleranza.

Ricaricare la pagina era il modo piu' semplice per scavalcare il vecchio gate.
Non lo e' piu': la schermata chiede al server, all'apertura, se una quota sta
aspettando, e si ritrova davanti lo stesso blocco.

Sparisce /validate-supervisor, che verificava le credenziali e buttava via la
risposta. Al suo posto un endpoint che le credenziali le usa per scrivere
l'approvazione dove serve. Il controllo del capoturno si sposta in auth_service,
accanto al resto delle credenziali: fermo linea, chiusura e fuori tolleranza
fanno la stessa domanda, e solo una delle tre riguarda la produzione.

Il file di statistica guadagna authorised_by e authorised_at: mostrava il
fallimento e non la decisione, che e' la meta' che un auditor chiede.

Migrazione 010: due colonne nullable sulle misure. Le righe esistenti restano
nulle — retrodatare un'autorizzazione mai avvenuta sarebbe inventarsi un record
di audit, e quelle produzioni sono chiuse da un pezzo.

Test: +18 (309). Coprono il rifiuto della quota successiva, la rimisura ammessa,
il rilascio del blocco con una lettura buona, il ciclo che non si chiude, il
capoturno registrato sulla misura, le credenziali sbagliate e chi capoturno non
e', il file di statistica, e il fatto che una produzione chiusa non blocchi la
successiva. Aggiunto uploads/statistics/ al gitignore: i test che chiudono una
produzione scrivevano nel repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-28 20:54:24 +00:00
parent 5aa3d595ad
commit e2aae88858
17 changed files with 1341 additions and 441 deletions
+41 -19
View File
@@ -397,31 +397,53 @@ def save_measurement():
# ---------------------------------------------------------------------------
# Route: Validate supervisor credentials (AJAX)
# Routes: Out-of-tolerance authorisation (point 5)
# ---------------------------------------------------------------------------
@measure_bp.route("/validate-supervisor", methods=["POST"])
@measure_bp.route("/api/measurements/pending-authorisation", methods=["GET"])
@login_required
@role_required("MeasurementTec")
def validate_supervisor():
"""Validate supervisor (capoturno) credentials for out-of-tolerance authorization."""
def api_pending_authorisation():
"""Proxy: the out-of-tolerance measurement holding this operator up, or null.
Asked when the measurement screen loads, so reloading the page finds the same
block the server enforces rather than a clean slate.
"""
params = {"version_id": request.args.get("version_id")}
run_id = request.args.get("production_run_id")
if run_id:
params["production_run_id"] = run_id
resp = api_client.get("/api/measurements/pending-authorisation", params=params)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 200
@measure_bp.route("/api/measurements/<int:measurement_id>/authorise", methods=["POST"])
@login_required
@role_required("MeasurementTec")
def api_authorise_measurement(measurement_id: int):
"""Proxy: the capoturno lets an out-of-tolerance value stand.
The credentials go to the endpoint that records the approval on the
measurement. Checking them separately, as this used to, left the approval
written down nowhere and nothing depending on it.
"""
data = request.get_json(silent=True) or {}
username = data.get("username", "").strip()
password = data.get("password", "")
username = (data.get("supervisor_username") or "").strip()
password = data.get("supervisor_password") or ""
if not username or not password:
return jsonify({"error": True, "detail": _("Username e password richiesti")}), 400
return jsonify({
"error": True, "detail": _("Username e password richiesti"),
}), 400
resp = api_client.post("/api/auth/login", data={"username": username, "password": password})
if resp.get("error"):
return jsonify({"error": True, "detail": _("Credenziali non valide")}), 401
user = resp.get("user", {})
is_supervisor = "Supervisor" in (user.get("roles") or [])
if not (is_supervisor or user.get("is_admin")):
return jsonify({"error": True, "detail": _("Utente non autorizzato (richiesto capoturno)")}), 403
return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200
resp = api_client.post(
f"/api/measurements/{measurement_id}/authorise",
data={"supervisor_username": username, "supervisor_password": password},
)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 200
# ---------------------------------------------------------------------------