feat(production): dai una vita propria alla produzione, lato server

Punto 1 del documento modifiche del 28/07, il prerequisito su cui poggiano i
punti 3, 4 e 6.

Lo stato di una produzione viveva dentro una pagina del browser: timer, conteggio
cicli e flag "produzione avviata" erano variabili Alpine di task_execute.html, e
la navigazione fra task e' un ricaricamento completo, quindi cambiando task si
perdeva tutto. Da qui il loop di misura che non reggeva, il fermo linea che non
aveva nulla da fermare e l'assenza di storico.

Nuove tabelle production_runs e production_events (migrazione 005), endpoint REST
senza stato in memoria di processo - con un'app di stazione installata su ogni PC
il database e' l'unico posto condiviso - e il frontend che legge lo stato
all'apertura invece di tenerlo in memoria.

Tre scelte di modello:

- la scadenza e' un timestamp assoluto (next_measurement_at), non un contatore:
  il countdown si ricalcola da li' a ogni caricamento, e lasciarne andare la
  differenza sotto zero dara' gratis il contatore del ritardo del punto 3. Al
  client vanno i secondi gia' calcolati, non il timestamp: un datetime naive
  verrebbe letto nel fuso del browser e il conto sarebbe sfasato dell'offset UTC;
- l'intervallo di misura e' copiato sulla produzione, non referenziato: modificare
  la ricetta a produzione avviata non deve spostare una scadenza in corso;
- active_station_id rispecchia la stazione finche' la produzione e' aperta e va a
  NULL alla chiusura. Con un vincolo unico sopra, "una stazione = una produzione
  aperta" e' una garanzia del database e non un controllo soggetto a race; i NULL
  non collidono, quindi le produzioni chiuse si accumulano senza disturbo.

Il fermo linea congela il conto alla rovescia e alla ripresa la scadenza viene
traslata della durata del fermo, non ricalcolata: un fermo non regala ne' toglie
tempo all'operatore. L'autorizzazione del capoturno passa da authenticate_user e
non da un login, che rigenererebbe la sua API key buttando giu' la sessione che ha
aperta altrove.

La migrazione e' stata eseguita davvero, non solo scritta, su uno SQLite usa e
getta: upgrade e downgrade girano e le colonne coincidono con i modelli. La prova
ha trovato un difetto - create_unique_constraint dopo create_table e' un ALTER,
che SQLite rifiuta - ora il vincolo e' dichiarato dentro create_table.

Fuori da questo commit, per stare nei confini del punto 1: l'API espone gia'
pause, resume e close, ma i pulsanti fermo linea e fine produzione restano da
collegare (punto 6), e il rientro forzato sulla misura allo scadere e' il punto 3.

Corretti due difetti trovati strada facendo: env.py non importava ne' Station ne'
ProductionRun, quindi l'autogenerate di Alembic era gia' cieco sulle stazioni; e
task_execute.html, lo schermo con piu' JavaScript dell'applicazione, non era
coperto dal test di sintassi. Aggiungerlo ha richiesto di correggere l'helper, che
validava le espressioni Alpine solo come espressione singola e bocciava
@click="a = false; b = true", forma che Alpine accetta: ora prova entrambe le
letture e fallisce solo se cadono tutte e due.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-28 16:20:43 +00:00
parent e7889f80a3
commit 55e5e0153f
16 changed files with 1851 additions and 147 deletions
@@ -414,6 +414,67 @@ def validate_supervisor():
return jsonify({"authorized": True, "supervisor": user.get("display_name", username)}), 200
# ---------------------------------------------------------------------------
# Routes: Production run (state that must outlive the page)
# ---------------------------------------------------------------------------
@measure_bp.route("/api/production/current", methods=["GET"])
@login_required
@role_required("MeasurementTec")
def api_current_production():
"""Proxy: the production open at this station, or null.
Every page asks this on load. Before, the timer and the cycle count lived in the
Alpine component and a change of task - a full page load - wiped them.
"""
station_code, _overridden = _current_station()
if not station_code:
return jsonify({"error": True, "detail": _("Stazione non configurata")}), 503
resp = api_client.get(
"/api/production-runs/current", params={"station_code": station_code},
)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 200
@measure_bp.route("/api/production/start", methods=["POST"])
@login_required
@role_required("MeasurementTec")
def api_start_production():
"""Proxy: open a production at this station."""
station_code, _overridden = _current_station()
if not station_code:
return jsonify({"error": True, "detail": _("Stazione non configurata")}), 503
data = request.get_json(silent=True) or {}
payload = {
"station_code": station_code,
"recipe_id": data.get("recipe_id"),
"version_id": data.get("version_id"),
"lot_number": data.get("lot_number") or session.get("lot_number") or None,
"serial_number": data.get("serial_number") or session.get("serial_number") or None,
}
resp = api_client.post("/api/production-runs", data=payload)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 201
@measure_bp.route("/api/production/<int:run_id>/cycle", methods=["POST"])
@login_required
@role_required("MeasurementTec")
def api_complete_cycle(run_id: int):
"""Proxy: record a finished measurement cycle and restart the interval."""
data = request.get_json(silent=True) or {}
resp = api_client.post(
f"/api/production-runs/{run_id}/cycle", data={"note": data.get("note")},
)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 200
# ---------------------------------------------------------------------------
# Route: File proxy (browser can't send X-API-Key directly)
# ---------------------------------------------------------------------------
@@ -428,6 +428,26 @@
</div>
</div>
{# ================================================================
PRODUCTION ERROR — the server refused to record the production state.
Shown because the fallback keeps the operator working locally, and a timer
that is not backed by the server must never look like one that is.
================================================================ #}
<div x-show="productionError"
x-transition
x-cloak
class="shrink-0 bg-red-50 dark:bg-red-900/20 border-t border-red-300 dark:border-red-700 px-4 py-2">
<div class="flex items-center justify-center gap-2">
<svg class="w-4 h-4 text-red-600 shrink-0" fill="none" stroke="currentColor" stroke-width="2" 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>
<span class="text-sm text-red-800 dark:text-red-200">
{{ _('Produzione non registrata sul server') }}:
<span x-text="productionError"></span>
</span>
</div>
</div>
{# ================================================================
AVVIO PRODUZIONE — visible after first cycle, before production started
================================================================ #}
@@ -709,6 +729,8 @@ function taskExecute() {
showCompletionOverlay: false,
// ---- Measurement timer ----
// The interval is only used for display: the deadline itself lives on the server,
// so it survives the page. See loadProductionRun().
measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
timerActive: false,
timerRemaining: 0,
@@ -716,6 +738,12 @@ function taskExecute() {
cycleCount: 0,
productionStarted: false,
// ---- Production run (server-side state) ----
// Navigating between tasks is a full page load, so anything kept only here dies.
// This is read back from the server on every load instead.
productionRun: null,
productionError: '',
// ---- Cycle & workflow state ----
cycleConfirmed: false,
showSupervisorModal: false,
@@ -812,6 +840,39 @@ function taskExecute() {
init() {
this.subtasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
this.inputStartedAt = Date.now();
// Rejoin whatever production is already open at this station.
this.loadProductionRun();
},
// ---- Production run: read the state back from the server ----
async loadProductionRun() {
try {
const resp = await fetch('{{ url_for("measure.api_current_production") }}');
if (!resp.ok) return;
const run = await resp.json();
if (run && run.id) this.adoptProductionRun(run);
} catch (e) {
// Offline or server down: leave the page usable, just without the timer.
}
},
/* Take the server's word for the state of the production. Called on load and
after every action that changes it, so the page never drifts from the truth. */
adoptProductionRun(run) {
this.productionRun = run;
this.productionStarted = run.status !== 'closed';
this.cycleCount = run.cycle_count;
const seconds = run.seconds_to_next_measurement;
if (seconds === null || seconds === undefined || run.status !== 'running') {
this.stopMeasurementTimer();
return;
}
// The server hands over seconds already computed rather than a timestamp: a
// naive datetime would be read in the browser's timezone and the countdown
// would be off by the UTC offset.
this.startCountdownFrom(seconds);
},
// ---- Check if a subtask has been measured ----
@@ -953,21 +1014,57 @@ function taskExecute() {
},
// ---- Confirm measurement cycle (Fine ciclo misura) ----
confirmCycle() {
async confirmCycle() {
this.cycleConfirmed = true;
this.showCompletionOverlay = false;
this.cycleCount++;
// Start measurement timer if recipe has an interval
// Recorded server-side so the count and the next deadline outlive this page.
if (this.productionRun) {
const run = await this.postProduction(
'{{ url_for("measure.api_complete_cycle", run_id=0) }}'.replace('/0/', '/' + this.productionRun.id + '/'),
);
if (run) {
this.adoptProductionRun(run);
return;
}
}
// No production open (recipe run outside a production): keep the old local
// behaviour rather than leaving the operator without a timer.
this.cycleCount++;
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
this.startMeasurementTimer();
this.startCountdownFrom(this.measurementIntervalMinutes * 60);
}
},
/* POST to a production endpoint, returning the updated run or null on failure. */
async postProduction(url, body) {
this.productionError = '';
try {
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content || '';
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify(body || {}),
});
const data = await resp.json().catch(() => null);
if (!resp.ok) {
this.productionError = (data && data.detail) || '{{ _("Errore di comunicazione con il server") }}';
return null;
}
return data;
} catch (e) {
this.productionError = '{{ _("Errore di connessione") }}';
return null;
}
},
// ---- Measurement timer ----
startMeasurementTimer() {
/* Ticks locally for a smooth display, but the number it starts from always comes
from the server, and every page load resynchronises it. */
startCountdownFrom(seconds) {
this.stopMeasurementTimer();
this.timerRemaining = this.measurementIntervalMinutes * 60;
this.timerRemaining = seconds;
this.timerActive = true;
var self = this;
this._timerInterval = setInterval(function () {
@@ -1014,11 +1111,26 @@ function taskExecute() {
} catch (_) {}
},
// ---- Avvio Produzione (GAIA placeholder) ----
startProduction() {
this.productionStarted = true;
// TODO: integrazione GAIA — inviare segnale per avviare timer linea
// await fetch('/measure/api/gaia/start-production', { method: 'POST', ... });
// ---- Avvio Produzione ----
/* Opens a production run on the server. That row is what makes the timer, the
cycle count and the history survive a change of task.
The hand-off to the ERP (GAIA) plugs in on top of this, once the protocol is
agreed: everything before it works without waiting for that. */
async startProduction() {
const run = await this.postProduction(
'{{ url_for("measure.api_start_production") }}',
{
recipe_id: this.task.recipe_id,
version_id: this.task.version_id,
lot_number: this.lotNumber || null,
serial_number: this.serialNumber || null,
},
);
if (run) {
this.adoptProductionRun(run);
return true;
}
return false;
},
// Is the current task the last one in the recipe sequence?
@@ -1030,14 +1142,22 @@ function taskExecute() {
// Start production from the completion overlay (last START task) and begin
// the measurement-interval cycle if the recipe defines one.
startProductionFromOverlay() {
async startProductionFromOverlay() {
this.showCompletionOverlay = false;
this.startProduction();
this.cycleConfirmed = true;
this.cycleCount++;
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
this.startMeasurementTimer();
const opened = await this.startProduction();
if (!opened) {
// Server refused: keep the operator working rather than stranding them
// mid-shift, but productionError is on screen so it is not silent.
this.cycleConfirmed = true;
this.cycleCount++;
if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
this.startCountdownFrom(this.measurementIntervalMinutes * 60);
}
return;
}
// The START-phase measurements have just been taken: record them as the first
// cycle, which is also what starts the interval running.
await this.confirmCycle();
},
get timerDisplay() {
@@ -0,0 +1,162 @@
"""Tests for the production-run proxy routes.
These are what let task_execute.html read its state back from the server instead of
keeping the timer in an Alpine variable that a change of task destroys.
"""
import importlib
from unittest.mock import patch
import pytest
@pytest.fixture(autouse=True)
def _restore_measure_module():
"""Reload config/measure after each test so env tweaks don't leak."""
yield
import config
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
def _with_station(monkeypatch, station_code="ST-PROD"):
if station_code is None:
monkeypatch.delenv("STATION_CODE", raising=False)
else:
monkeypatch.setenv("STATION_CODE", station_code)
monkeypatch.setenv("STATION_SWITCH_ENABLED", "0")
import config
importlib.reload(config)
import blueprints.measure
importlib.reload(blueprints.measure)
from blueprints import measure as measure_mod
return measure_mod
RUN = {
"id": 7,
"station_id": 1,
"recipe_id": 3,
"version_id": 5,
"operator_id": 2,
"lot_number": "LOT-1",
"serial_number": None,
"status": "running",
"measurement_interval_minutes": 30,
"next_measurement_at": "2026-07-28T17:00:00",
"cycle_count": 2,
"started_at": "2026-07-28T16:00:00",
"paused_at": None,
"closed_at": None,
"closed_by": None,
"seconds_to_next_measurement": 1500,
"overdue": False,
"server_time": "2026-07-28T16:35:00",
}
def test_current_production_passes_the_station(logged_in_client, monkeypatch):
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.get.return_value = RUN
resp = logged_in_client.get("/measure/api/production/current")
assert resp.status_code == 200
assert resp.get_json()["id"] == 7
mock_api.get.assert_called_once_with(
"/api/production-runs/current", params={"station_code": "ST-PROD"},
)
def test_current_production_returns_null_when_idle(logged_in_client, monkeypatch):
"""No production open is a normal answer, not an error."""
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.get.return_value = None
resp = logged_in_client.get("/measure/api/production/current")
assert resp.status_code == 200
assert resp.get_json() is None
def test_current_production_without_station_is_503(logged_in_client, monkeypatch):
_with_station(monkeypatch, station_code=None)
resp = logged_in_client.get("/measure/api/production/current")
assert resp.status_code == 503
def test_start_production_sends_station_and_recipe(logged_in_client, monkeypatch):
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = RUN
resp = logged_in_client.post(
"/measure/api/production/start",
json={"recipe_id": 3, "version_id": 5, "lot_number": "LOT-1"},
)
assert resp.status_code == 201
endpoint, kwargs = mock_api.post.call_args
assert endpoint[0] == "/api/production-runs"
sent = kwargs["data"]
assert sent["station_code"] == "ST-PROD"
assert sent["recipe_id"] == 3
assert sent["version_id"] == 5
assert sent["lot_number"] == "LOT-1"
def test_start_production_falls_back_to_session_traceability(logged_in_client, monkeypatch):
"""Lot and serial already captured in the session must reach the run."""
measure_mod = _with_station(monkeypatch)
with logged_in_client.session_transaction() as sess:
sess["lot_number"] = "LOT-SESSION"
sess["serial_number"] = "SN-SESSION"
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = RUN
logged_in_client.post("/measure/api/production/start", json={"recipe_id": 3})
sent = mock_api.post.call_args[1]["data"]
assert sent["lot_number"] == "LOT-SESSION"
assert sent["serial_number"] == "SN-SESSION"
def test_start_production_propagates_conflict(logged_in_client, monkeypatch):
"""A station already running a production must not silently open a second."""
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = {
"error": True, "status_code": 409,
"detail": "Station 'ST-PROD' already has an open production run",
}
resp = logged_in_client.post(
"/measure/api/production/start", json={"recipe_id": 3},
)
assert resp.status_code == 409
def test_complete_cycle_hits_the_run(logged_in_client, monkeypatch):
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = {**RUN, "cycle_count": 3}
resp = logged_in_client.post("/measure/api/production/7/cycle", json={})
assert resp.status_code == 200
assert resp.get_json()["cycle_count"] == 3
endpoint, kwargs = mock_api.post.call_args
assert endpoint[0] == "/api/production-runs/7/cycle"
def test_complete_cycle_propagates_paused_conflict(logged_in_client, monkeypatch):
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = {
"error": True, "status_code": 409,
"detail": "Production run is paused: resume it before measuring",
}
resp = logged_in_client.post("/measure/api/production/7/cycle", json={})
assert resp.status_code == 409
def test_production_routes_require_login(client, monkeypatch):
_with_station(monkeypatch)
for method, url in (
("get", "/measure/api/production/current"),
("post", "/measure/api/production/start"),
("post", "/measure/api/production/7/cycle"),
):
resp = getattr(client, method)(url)
assert resp.status_code in (302, 401), f"{method} {url} -> {resp.status_code}"
@@ -79,10 +79,8 @@ def _alpine_attribute_expressions(html: str):
yield name, value
def _node_check(source: str, label: str) -> None:
"""Fail the test if `node --check` rejects `source`."""
if not source.strip():
return
def _node_syntax_error(source: str) -> str | None:
"""Return node's complaint about `source`, or None if it parses."""
node = shutil.which("node")
if node is None:
pytest.skip("node binary not found on PATH; cannot validate JS syntax")
@@ -100,31 +98,56 @@ def _node_check(source: str, label: str) -> None:
finally:
os.unlink(path)
if result.returncode != 0:
snippet = source.strip()
if len(snippet) > 600:
snippet = snippet[:600] + "\n…(truncated)…"
pytest.fail(
f"{label}: node rejected this code as invalid JS.\n"
f"--- node stderr ---\n{result.stderr.strip()}\n"
f"--- source (first 600 chars) ---\n{snippet}"
)
return None if result.returncode == 0 else result.stderr.strip()
def _fail(label: str, source: str, stderr: str) -> None:
snippet = source.strip()
if len(snippet) > 600:
snippet = snippet[:600] + "\n…(truncated)…"
pytest.fail(
f"{label}: node rejected this code as invalid JS.\n"
f"--- node stderr ---\n{stderr}\n"
f"--- source (first 600 chars) ---\n{snippet}"
)
def _node_check(source: str, label: str) -> None:
"""Fail the test if `node --check` rejects `source`."""
if not source.strip():
return
stderr = _node_syntax_error(source)
if stderr is not None:
_fail(label, source, stderr)
def _check_alpine_attributes(html: str, page_label: str) -> None:
"""Validate every Alpine expression attribute on the page.
Wraps each value in `void (…)` so node parses it as an expression rather
than a statement. Function-body forms like `async () => { … }` parse fine
inside that wrapper too.
Alpine accepts an attribute either as a single expression or as a sequence of
statements, so the check accepts a value that parses as either and only fails
when both readings are rejected. Neither form alone is enough:
:class="{ 'dark': $store.theme.dark }" only as an expression
@click="showOverlay = false; confirmed = true" only as statements
An unterminated string literal - the bug this file exists for - is a syntax
error under both, so the guard is unchanged.
"""
for name, value in _alpine_attribute_expressions(html):
# Some Alpine attrs accept a function call shorthand (e.g.
# x-data="myComponent(window.__x)"); those parse fine as expressions.
wrapper = f"void ({value});\n"
_node_check(
wrapper,
f"{page_label} attribute {name}=\"\" did not parse as JS",
as_expression = f"void (\n{value}\n);\n"
as_statements = f"(() => {{\n{value}\n}});\n"
expression_error = _node_syntax_error(as_expression)
if expression_error is None:
continue
if _node_syntax_error(as_statements) is None:
continue
_fail(
f"{page_label} attribute {name}=\"\" parsed neither as an expression "
"nor as statements",
as_expression,
expression_error,
)
@@ -192,3 +215,52 @@ def test_admin_users_inline_js_is_valid(logged_in_client, mock_admin_api):
_node_check(body, f"/admin/users script[{i}]")
_check_alpine_attributes(html, "/admin/users")
@pytest.fixture
def mock_measure_api():
"""Patch api_client used inside the measure blueprint."""
mock = MagicMock()
with patch("blueprints.measure.api_client", mock):
yield mock
def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
"""The measurement screen carries the most inline JS in the app.
It drives the numpad, the caliper burst detection, the supervisor modal and -
since the production run moved server-side - the countdown. A broken literal
here kills every binding on the screen the operator actually works in.
"""
_force_italian(logged_in_client)
task = {
"id": 11,
"recipe_id": 3,
"version_id": 5,
"title": "Quota d'ingresso",
"description": "Misura l'altezza",
"file_path": None,
"file_type": None,
"annotations_json": None,
"subtasks": [{
"id": 21, "marker_number": 1, "name": "Altezza", "order_index": 0,
"nominal": 10.0, "utl": 10.5, "uwl": 10.2, "lwl": 9.8, "ltl": 9.5,
"unit": "mm",
}],
}
mock_measure_api.get.side_effect = [
task, # /api/tasks/11
[{"id": 11, "order_index": 0}], # /api/recipes/3/tasks
{"id": 3, "measurement_interval_minutes": 30}, # /api/recipes/3
]
resp = logged_in_client.get("/measure/execute/11")
assert resp.status_code == 200
html = resp.get_data(as_text=True)
scripts = _INLINE_SCRIPT_RX.findall(html)
assert scripts, "expected at least one inline <script> on /measure/execute"
for i, body in enumerate(scripts):
_node_check(body, f"/measure/execute script[{i}]")
_check_alpine_attributes(html, "/measure/execute")
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: TieMeasureFlow 1.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-07-28 15:24+0000\n"
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@@ -169,6 +169,12 @@ msgstr "Username and password required"
msgid "Utente non autorizzato (richiesto capoturno)"
msgstr "User not authorized (shift supervisor required)"
#: blueprints/measure.py:431 blueprints/measure.py:448
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr "Station not configured"
#: templates/base.html:173
msgid "Sessione in scadenza"
msgstr "Session expiring"
@@ -370,7 +376,7 @@ msgstr "Optional notes"
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:664
#: templates/measure/task_execute.html:684
msgid "Annulla"
msgstr "Cancel"
@@ -515,7 +521,7 @@ msgstr "New User"
#: templates/admin/users.html:48 templates/admin/users.html:173
#: templates/admin/users.html:179 templates/auth/login.html:35
#: templates/auth/login.html:49 templates/auth/profile.html:36
#: templates/measure/task_execute.html:649
#: templates/measure/task_execute.html:669
msgid "Username"
msgstr "Username"
@@ -567,7 +573,7 @@ msgstr "Username cannot be changed"
#: templates/admin/users.html:206 templates/admin/users.html:214
#: templates/auth/login.html:57 templates/auth/login.html:71
#: templates/measure/task_execute.html:654
#: templates/measure/task_execute.html:674
msgid "Password"
msgstr "Password"
@@ -817,11 +823,6 @@ msgstr "Logout blocked during measurements"
msgid "Prossima misura"
msgstr "Next measurement"
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr "Station not configured"
#: templates/errors/station_not_configured.html:20
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
msgstr "This client has not set the STATION_CODE environment variable."
@@ -863,7 +864,7 @@ msgstr "Preview"
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
#: templates/measure/task_complete.html:168
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
msgid "Task"
msgstr "Task"
@@ -1012,7 +1013,8 @@ msgstr "Error during deletion"
# Recipe Selection Additional
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
#: templates/measure/task_execute.html:1121
#: templates/measure/task_execute.html:1057
#: templates/measure/task_execute.html:1241
msgid "Errore di connessione"
msgstr "Connection Error"
@@ -1610,7 +1612,7 @@ msgid "Misurazione aggiunta"
msgstr "Measurement added"
#: templates/maker/task_editor.html:1645
#: templates/measure/task_execute.html:870
#: templates/measure/task_execute.html:931
msgid "Errore nel salvataggio della misurazione"
msgstr "Error saving measurement"
@@ -1768,13 +1770,13 @@ msgstr "Search"
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:585
#: templates/measure/task_execute.html:605
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr "Summary"
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:562
#: templates/measure/task_execute.html:582
msgid "Misurazioni Complete"
msgstr "Measurements Complete"
@@ -1795,12 +1797,12 @@ msgid "Totale"
msgstr "Total"
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:570
#: templates/measure/task_execute.html:590
msgid "Conformi"
msgstr "Pass"
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:574
#: templates/measure/task_execute.html:594
msgid "Attenzione"
msgstr "Warning"
@@ -2000,77 +2002,85 @@ msgstr "Next measurement in"
msgid "Ciclo"
msgstr "Cycle"
#: templates/measure/task_execute.html:446
#: templates/measure/task_execute.html:612
#: templates/measure/task_execute.html:445
msgid "Produzione non registrata sul server"
msgstr "Production not recorded on the server"
#: templates/measure/task_execute.html:466
#: templates/measure/task_execute.html:632
msgid "Avvio Produzione"
msgstr "Production Start"
#: templates/measure/task_execute.html:450
#: templates/measure/task_execute.html:470
msgid "Invia segnale al gestionale per avviare il timer della linea"
msgstr "Send a signal to the ERP system to start the line timer"
#: templates/measure/task_execute.html:461
#: templates/measure/task_execute.html:481
msgid "Produzione avviata"
msgstr "Production started"
#: templates/measure/task_execute.html:506
#: templates/measure/task_execute.html:526
msgid "Fine ciclo misura"
msgstr "Measurement cycle complete"
#: templates/measure/task_execute.html:516
#: templates/measure/task_execute.html:530
#: templates/measure/task_execute.html:536
#: templates/measure/task_execute.html:550
msgid "Completato"
msgstr "Completed"
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "Tutte le"
msgstr "All"
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "misurazioni sono state registrate."
msgstr "measurements have been recorded."
#: templates/measure/task_execute.html:578
#: templates/measure/task_execute.html:598
msgid "Non Conf."
msgstr "Fail"
#: templates/measure/task_execute.html:592
#: templates/measure/task_execute.html:612
msgid "Conferma ciclo"
msgstr "Confirm cycle"
#: templates/measure/task_execute.html:602
#: templates/measure/task_execute.html:622
msgid "Task successivo"
msgstr "Next task"
#: templates/measure/task_execute.html:642
#: templates/measure/task_execute.html:662
msgid "Autorizzazione capoturno"
msgstr "Shift supervisor authorization"
#: templates/measure/task_execute.html:651
#: templates/measure/task_execute.html:671
msgid "Username capoturno"
msgstr "Supervisor username"
#: templates/measure/task_execute.html:673
#: templates/measure/task_execute.html:693
msgid "Autorizza"
msgstr "Authorize"
#: templates/measure/task_execute.html:914
#: templates/measure/task_execute.html:975
msgid "Errore di rete. Riprovare."
msgstr "Network error. Please retry."
#: templates/measure/task_execute.html:1078
#: templates/measure/task_execute.html:1052
msgid "Errore di comunicazione con il server"
msgstr "Error communicating with the server"
#: templates/measure/task_execute.html:1198
msgid "Misurazione fuori tolleranza"
msgstr "Measurement out of tolerance"
#: templates/measure/task_execute.html:1079
#: templates/measure/task_execute.html:1199
msgid "Fermo linea richiesto"
msgstr "Line stop requested"
#: templates/measure/task_execute.html:1080
#: templates/measure/task_execute.html:1200
msgid "Fine produzione richiesta"
msgstr "End of production requested"
#: templates/measure/task_execute.html:1099
#: templates/measure/task_execute.html:1219
msgid "Credenziali non valide o utente non autorizzato"
msgstr "Invalid credentials or unauthorized user"
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: TieMeasureFlow 1.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-07-28 15:24+0000\n"
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: it\n"
@@ -169,6 +169,12 @@ msgstr "Username e password richiesti"
msgid "Utente non autorizzato (richiesto capoturno)"
msgstr "Utente non autorizzato (richiesto capoturno)"
#: blueprints/measure.py:431 blueprints/measure.py:448
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr "Stazione non configurata"
#: templates/base.html:173
msgid "Sessione in scadenza"
msgstr "Sessione in scadenza"
@@ -372,7 +378,7 @@ msgstr "Note opzionali"
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:664
#: templates/measure/task_execute.html:684
msgid "Annulla"
msgstr "Annulla"
@@ -517,7 +523,7 @@ msgstr "Nuovo Utente"
#: templates/admin/users.html:48 templates/admin/users.html:173
#: templates/admin/users.html:179 templates/auth/login.html:35
#: templates/auth/login.html:49 templates/auth/profile.html:36
#: templates/measure/task_execute.html:649
#: templates/measure/task_execute.html:669
msgid "Username"
msgstr "Username"
@@ -569,7 +575,7 @@ msgstr "Il nome utente non può essere modificato"
#: templates/admin/users.html:206 templates/admin/users.html:214
#: templates/auth/login.html:57 templates/auth/login.html:71
#: templates/measure/task_execute.html:654
#: templates/measure/task_execute.html:674
msgid "Password"
msgstr "Password"
@@ -819,11 +825,6 @@ msgstr "Logout bloccato durante le misurazioni"
msgid "Prossima misura"
msgstr "Prossima misura"
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr "Stazione non configurata"
#: templates/errors/station_not_configured.html:20
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
msgstr "Questo client non ha impostato la variabile di ambiente STATION_CODE."
@@ -865,7 +866,7 @@ msgstr "Anteprima"
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
#: templates/measure/task_complete.html:168
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
msgid "Task"
msgstr "Task"
@@ -1014,7 +1015,8 @@ msgstr "Errore durante eliminazione"
# Recipe Selection Additional
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
#: templates/measure/task_execute.html:1121
#: templates/measure/task_execute.html:1057
#: templates/measure/task_execute.html:1241
msgid "Errore di connessione"
msgstr "Errore di connessione"
@@ -1612,7 +1614,7 @@ msgid "Misurazione aggiunta"
msgstr "Misurazione aggiunta"
#: templates/maker/task_editor.html:1645
#: templates/measure/task_execute.html:870
#: templates/measure/task_execute.html:931
msgid "Errore nel salvataggio della misurazione"
msgstr "Errore nel salvataggio della misurazione"
@@ -1772,13 +1774,13 @@ msgstr "Cerca"
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:585
#: templates/measure/task_execute.html:605
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr "Riepilogo"
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:562
#: templates/measure/task_execute.html:582
msgid "Misurazioni Complete"
msgstr "Misurazioni Complete"
@@ -1799,12 +1801,12 @@ msgid "Totale"
msgstr "Totale"
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:570
#: templates/measure/task_execute.html:590
msgid "Conformi"
msgstr "Conformi"
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:574
#: templates/measure/task_execute.html:594
msgid "Attenzione"
msgstr "Attenzione"
@@ -2004,77 +2006,85 @@ msgstr "Prossima misurazione tra"
msgid "Ciclo"
msgstr "Ciclo"
#: templates/measure/task_execute.html:446
#: templates/measure/task_execute.html:612
#: templates/measure/task_execute.html:445
msgid "Produzione non registrata sul server"
msgstr "Produzione non registrata sul server"
#: templates/measure/task_execute.html:466
#: templates/measure/task_execute.html:632
msgid "Avvio Produzione"
msgstr "Avvio Produzione"
#: templates/measure/task_execute.html:450
#: templates/measure/task_execute.html:470
msgid "Invia segnale al gestionale per avviare il timer della linea"
msgstr "Invia segnale al gestionale per avviare il timer della linea"
#: templates/measure/task_execute.html:461
#: templates/measure/task_execute.html:481
msgid "Produzione avviata"
msgstr "Produzione avviata"
#: templates/measure/task_execute.html:506
#: templates/measure/task_execute.html:526
msgid "Fine ciclo misura"
msgstr "Fine ciclo misura"
#: templates/measure/task_execute.html:516
#: templates/measure/task_execute.html:530
#: templates/measure/task_execute.html:536
#: templates/measure/task_execute.html:550
msgid "Completato"
msgstr "Completato"
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "Tutte le"
msgstr "Tutte le"
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "misurazioni sono state registrate."
msgstr "misurazioni sono state registrate."
#: templates/measure/task_execute.html:578
#: templates/measure/task_execute.html:598
msgid "Non Conf."
msgstr "Non Conf."
#: templates/measure/task_execute.html:592
#: templates/measure/task_execute.html:612
msgid "Conferma ciclo"
msgstr "Conferma ciclo"
#: templates/measure/task_execute.html:602
#: templates/measure/task_execute.html:622
msgid "Task successivo"
msgstr "Task successivo"
#: templates/measure/task_execute.html:642
#: templates/measure/task_execute.html:662
msgid "Autorizzazione capoturno"
msgstr "Autorizzazione capoturno"
#: templates/measure/task_execute.html:651
#: templates/measure/task_execute.html:671
msgid "Username capoturno"
msgstr "Username capoturno"
#: templates/measure/task_execute.html:673
#: templates/measure/task_execute.html:693
msgid "Autorizza"
msgstr "Autorizza"
#: templates/measure/task_execute.html:914
#: templates/measure/task_execute.html:975
msgid "Errore di rete. Riprovare."
msgstr "Errore di rete. Riprovare."
#: templates/measure/task_execute.html:1078
#: templates/measure/task_execute.html:1052
msgid "Errore di comunicazione con il server"
msgstr "Errore di comunicazione con il server"
#: templates/measure/task_execute.html:1198
msgid "Misurazione fuori tolleranza"
msgstr "Misurazione fuori tolleranza"
#: templates/measure/task_execute.html:1079
#: templates/measure/task_execute.html:1199
msgid "Fermo linea richiesto"
msgstr "Fermo linea richiesto"
#: templates/measure/task_execute.html:1080
#: templates/measure/task_execute.html:1200
msgid "Fine produzione richiesta"
msgstr "Fine produzione richiesta"
#: templates/measure/task_execute.html:1099
#: templates/measure/task_execute.html:1219
msgid "Credenziali non valide o utente non autorizzato"
msgstr "Credenziali non valide o utente non autorizzato"
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-07-28 15:24+0000\n"
"POT-Creation-Date: 2026-07-28 16:15+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -166,6 +166,12 @@ msgstr ""
msgid "Utente non autorizzato (richiesto capoturno)"
msgstr ""
#: blueprints/measure.py:431 blueprints/measure.py:448
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr ""
#: templates/base.html:173
msgid "Sessione in scadenza"
msgstr ""
@@ -363,7 +369,7 @@ msgstr ""
#: templates/maker/task_editor.html:747 templates/maker/task_editor.html:866
#: templates/maker/task_editor.html:931 templates/maker/task_editor.html:1036
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:664
#: templates/measure/task_execute.html:684
msgid "Annulla"
msgstr ""
@@ -507,7 +513,7 @@ msgstr ""
#: templates/admin/users.html:48 templates/admin/users.html:173
#: templates/admin/users.html:179 templates/auth/login.html:35
#: templates/auth/login.html:49 templates/auth/profile.html:36
#: templates/measure/task_execute.html:649
#: templates/measure/task_execute.html:669
msgid "Username"
msgstr ""
@@ -559,7 +565,7 @@ msgstr ""
#: templates/admin/users.html:206 templates/admin/users.html:214
#: templates/auth/login.html:57 templates/auth/login.html:71
#: templates/measure/task_execute.html:654
#: templates/measure/task_execute.html:674
msgid "Password"
msgstr ""
@@ -800,11 +806,6 @@ msgstr ""
msgid "Prossima misura"
msgstr ""
#: templates/errors/station_not_configured.html:2
#: templates/errors/station_not_configured.html:16
msgid "Stazione non configurata"
msgstr ""
#: templates/errors/station_not_configured.html:20
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
msgstr ""
@@ -842,7 +843,7 @@ msgstr ""
#: templates/maker/recipe_editor.html:128 templates/maker/recipe_list.html:246
#: templates/maker/task_drawing.html:106 templates/maker/task_editor.html:130
#: templates/measure/task_complete.html:168
#: templates/measure/task_execute.html:477 templates/measure/task_list.html:2
#: templates/measure/task_execute.html:497 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
msgid "Task"
msgstr ""
@@ -989,7 +990,8 @@ msgid "Errore durante eliminazione"
msgstr ""
#: templates/maker/recipe_list.html:55 templates/measure/select_recipe.html:55
#: templates/measure/task_execute.html:1121
#: templates/measure/task_execute.html:1057
#: templates/measure/task_execute.html:1241
msgid "Errore di connessione"
msgstr ""
@@ -1580,7 +1582,7 @@ msgid "Misurazione aggiunta"
msgstr ""
#: templates/maker/task_editor.html:1645
#: templates/measure/task_execute.html:870
#: templates/measure/task_execute.html:931
msgid "Errore nel salvataggio della misurazione"
msgstr ""
@@ -1734,13 +1736,13 @@ msgstr ""
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:585
#: templates/measure/task_execute.html:605
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr ""
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:562
#: templates/measure/task_execute.html:582
msgid "Misurazioni Complete"
msgstr ""
@@ -1760,12 +1762,12 @@ msgid "Totale"
msgstr ""
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:570
#: templates/measure/task_execute.html:590
msgid "Conformi"
msgstr ""
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:574
#: templates/measure/task_execute.html:594
msgid "Attenzione"
msgstr ""
@@ -1963,77 +1965,85 @@ msgstr ""
msgid "Ciclo"
msgstr ""
#: templates/measure/task_execute.html:446
#: templates/measure/task_execute.html:612
#: templates/measure/task_execute.html:445
msgid "Produzione non registrata sul server"
msgstr ""
#: templates/measure/task_execute.html:466
#: templates/measure/task_execute.html:632
msgid "Avvio Produzione"
msgstr ""
#: templates/measure/task_execute.html:450
#: templates/measure/task_execute.html:470
msgid "Invia segnale al gestionale per avviare il timer della linea"
msgstr ""
#: templates/measure/task_execute.html:461
#: templates/measure/task_execute.html:481
msgid "Produzione avviata"
msgstr ""
#: templates/measure/task_execute.html:506
#: templates/measure/task_execute.html:526
msgid "Fine ciclo misura"
msgstr ""
#: templates/measure/task_execute.html:516
#: templates/measure/task_execute.html:530
#: templates/measure/task_execute.html:536
#: templates/measure/task_execute.html:550
msgid "Completato"
msgstr ""
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "Tutte le"
msgstr ""
#: templates/measure/task_execute.html:564
#: templates/measure/task_execute.html:584
msgid "misurazioni sono state registrate."
msgstr ""
#: templates/measure/task_execute.html:578
#: templates/measure/task_execute.html:598
msgid "Non Conf."
msgstr ""
#: templates/measure/task_execute.html:592
#: templates/measure/task_execute.html:612
msgid "Conferma ciclo"
msgstr ""
#: templates/measure/task_execute.html:602
#: templates/measure/task_execute.html:622
msgid "Task successivo"
msgstr ""
#: templates/measure/task_execute.html:642
#: templates/measure/task_execute.html:662
msgid "Autorizzazione capoturno"
msgstr ""
#: templates/measure/task_execute.html:651
#: templates/measure/task_execute.html:671
msgid "Username capoturno"
msgstr ""
#: templates/measure/task_execute.html:673
#: templates/measure/task_execute.html:693
msgid "Autorizza"
msgstr ""
#: templates/measure/task_execute.html:914
#: templates/measure/task_execute.html:975
msgid "Errore di rete. Riprovare."
msgstr ""
#: templates/measure/task_execute.html:1078
#: templates/measure/task_execute.html:1052
msgid "Errore di comunicazione con il server"
msgstr ""
#: templates/measure/task_execute.html:1198
msgid "Misurazione fuori tolleranza"
msgstr ""
#: templates/measure/task_execute.html:1079
#: templates/measure/task_execute.html:1199
msgid "Fermo linea richiesto"
msgstr ""
#: templates/measure/task_execute.html:1080
#: templates/measure/task_execute.html:1200
msgid "Fine produzione richiesta"
msgstr ""
#: templates/measure/task_execute.html:1099
#: templates/measure/task_execute.html:1219
msgid "Credenziali non valide o utente non autorizzato"
msgstr ""