feat(client): filter select_recipe by STATION_CODE with error fallback

Replace generic /api/recipes call with api_client.get_station_recipes(STATION_CODE).
Return 503 station_not_configured.html when STATION_CODE env var is unset.
Add station indicator to recipe selection page header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 08:28:46 +02:00
parent a4a849920f
commit 946264637b
5 changed files with 97 additions and 10 deletions
+13 -2
View File
@@ -22,8 +22,18 @@ measure_bp = Blueprint("measure", __name__)
@role_required("MeasurementTec") @role_required("MeasurementTec")
def select_recipe(): def select_recipe():
"""Recipe selection page with search and barcode support.""" """Recipe selection page with search and barcode support."""
# Load recipes from API # Fail-fast if STATION_CODE is not configured
resp = api_client.get("/api/recipes", params={"per_page": 100}) if not Config.STATION_CODE:
return render_template("errors/station_not_configured.html"), 503
# Load recipes filtered by station
try:
resp = api_client.get_station_recipes(Config.STATION_CODE)
except Exception as e:
return render_template(
"errors/station_not_configured.html", error=str(e),
), 502
if isinstance(resp, dict) and resp.get("error"): if isinstance(resp, dict) and resp.get("error"):
flash( flash(
_("Errore nel caricamento delle ricette: %(detail)s", _("Errore nel caricamento delle ricette: %(detail)s",
@@ -43,6 +53,7 @@ def select_recipe():
return render_template( return render_template(
"measure/select_recipe.html", "measure/select_recipe.html",
recipes=recipes, recipes=recipes,
station_code=Config.STATION_CODE,
auto_recipe_code=auto_recipe_code, auto_recipe_code=auto_recipe_code,
auto_lot=auto_lot, auto_lot=auto_lot,
auto_serial=auto_serial, auto_serial=auto_serial,
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{ _('Stazione non configurata') }} — TieMeasureFlow{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-2xl">
<div class="mt-20 p-8 bg-[var(--bg-card)] rounded-xl shadow-lg text-center border border-red-200 dark:border-red-800">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full
bg-red-50 dark:bg-red-900/30 mb-6">
<svg class="w-8 h-8 text-measure-fail" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
</svg>
</div>
<h1 class="text-2xl font-bold text-measure-fail mb-4">
{{ _('Stazione non configurata') }}
</h1>
<p class="mb-4 text-[var(--text-primary)]">
{{ _('Questo client non ha impostato la variabile di ambiente STATION_CODE.') }}
</p>
<p class="mb-6 text-sm text-[var(--text-secondary)]">
{{ _('Contattare il responsabile IT: il file .env del container deve contenere STATION_CODE con il codice della stazione assegnata.') }}
</p>
{% if error %}
<pre class="text-xs text-left text-[var(--text-secondary)] bg-[var(--bg-secondary)]
p-3 rounded-lg border border-[var(--border-color)] overflow-auto">{{ error }}</pre>
{% endif %}
</div>
</div>
{% endblock %}
@@ -75,6 +75,9 @@
<p class="mt-1 text-sm text-[var(--text-secondary)]"> <p class="mt-1 text-sm text-[var(--text-secondary)]">
{{ _('Scegli la ricetta di misura da eseguire') }} {{ _('Scegli la ricetta di misura da eseguire') }}
</p> </p>
<p class="mt-1 text-sm text-steel-500 dark:text-steel-400">
{{ _('Stazione') }}: <span class="font-mono font-bold">{{ station_code }}</span>
</p>
</div> </div>
</div> </div>
+11 -8
View File
@@ -8,15 +8,18 @@ import pytest
class TestSelectRecipe: class TestSelectRecipe:
"""GET /measure/select tests.""" """GET /measure/select tests."""
def test_select_recipe_renders(self, logged_in_client, mock_api_client): def test_select_recipe_renders(self, logged_in_client, mock_api_client, monkeypatch):
"""Recipe selection page renders for MeasurementTec role.""" """Recipe selection page renders for MeasurementTec role."""
mock_api_client.get.return_value = { monkeypatch.setenv("STATION_CODE", "ST-TEST")
"items": [ import config
{"id": 1, "code": "REC-001", "name": "Test Recipe"}, import importlib
], importlib.reload(config)
"total": 1, import blueprints.measure
"pages": 1, importlib.reload(blueprints.measure)
}
mock_api_client.get_station_recipes.return_value = [
{"id": 1, "code": "REC-001", "name": "Test Recipe"},
]
resp = logged_in_client.get("/measure/select") resp = logged_in_client.get("/measure/select")
assert resp.status_code == 200 assert resp.status_code == 200
@@ -0,0 +1,37 @@
"""Verify that /measure/select reads STATION_CODE and filters recipes via the server."""
import importlib
from unittest.mock import patch, MagicMock
def _reload_measure(monkeypatch, station_code=None):
"""Reload config and measure module under the given STATION_CODE env."""
if station_code is None:
monkeypatch.delenv("STATION_CODE", raising=False)
else:
monkeypatch.setenv("STATION_CODE", station_code)
import config
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
def test_select_recipe_calls_station_endpoint(logged_in_client, monkeypatch):
_reload_measure(monkeypatch, station_code="ST-TEST")
from blueprints import measure as measure_bp_mod
with patch.object(measure_bp_mod, "api_client") as mock_api:
mock_api.get_station_recipes.return_value = [
{"id": 1, "code": "R1", "name": "Recipe 1", "active": True},
]
resp = logged_in_client.get("/measure/select")
assert resp.status_code == 200
mock_api.get_station_recipes.assert_called_once()
args, kwargs = mock_api.get_station_recipes.call_args
assert args[0] == "ST-TEST" or kwargs.get("station_code") == "ST-TEST"
def test_select_recipe_without_station_code_shows_error(logged_in_client, monkeypatch):
_reload_measure(monkeypatch, station_code=None)
resp = logged_in_client.get("/measure/select")
assert resp.status_code == 503
body = resp.data.lower()
assert b"station_code" in body or b"stazione" in body