feat(production): la misura torna a cercare l'operatore

Il conto alla rovescia non si ferma piu' a zero. Oltre la scadenza continua
nell'altro senso e dice da quanto la linea e' in ritardo, in rosso, su tutte le
schermate del percorso di misura: lista task, riepilogo, scelta ricetta. Un
ritardo va letto, non dedotto. Allo scadere dell'intervallo la misura si
ripropone da sola: cicalino, un avviso di cinque secondi, e l'operatore torna al
primo task di misura da qualunque schermata si trovi.

Un ciclo copre tutti i task di misura della ricetta, quindi solo l'ultimo lo
chiude e fa ripartire l'intervallo. Farlo ripartire al primo avrebbe fatto
misurare una ricetta con tre task di misura tre volte piu' spesso di come e'
configurata. E' il server a decidere quale task chiude il ciclo, perche' e' lui
a conoscere la sequenza: la lista dei task di misura viaggia con la produzione
(measurement_task_ids), che e' anche cio' che permette a una schermata qualsiasi
di sapere dove riportare l'operatore.

Aggiunta la rimisura: si gira il pezzo e si misura di nuovo dentro lo stesso
ciclo, senza chiudere niente e senza guadagnare tempo sulla scadenza. Le due
letture restano entrambe in statistica, che e' il motivo per cui si prendono.

Chi sta gia' misurando quando l'intervallo scade e' in ritardo, non perso: la
banda diventa rossa e lo si lascia lavorare. Portarlo altrove a meta' ciclo
cancellerebbe quote che ha davanti agli occhi senza guadagnare nulla.

Sistemato anche il pulsante "Conferma ciclo" dell'overlay: alzava una bandierina
locale e basta, quindi il ciclo non veniva mai registrato sul server da quella
strada. Ora passa da confirmCycle come il pulsante della barra.

Migrazione 008: il registro eventi della produzione impara task_measured e
remeasure, e la colonna task_id — un task_measured che non dice quale task non
registra niente di utile. I valori nuovi entrano tutti insieme perche' allargare
una enum MySQL riscrive la tabella, stesso ragionamento dei tipi di task in 007.
Verificata su SQLite (batch mode) e in MySQL con --sql.

La logica dell'orologio — come si legge un ritardo, quando suona, dove sta la
misura — vive in un solo posto (production-clock.js) e la schermata di misura la
usa invece di riscriverla.

Test: +10 (265). Coprono il ciclo che non riparte a meta', la rimisura che non
sposta la scadenza, il rifiuto a linea ferma, la sequenza dei task di misura
esposta dalla produzione, i proxy Flask e la validita' JS della lista task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-28 19:31:51 +00:00
parent 6fbff2fe76
commit bde8fafd77
20 changed files with 1586 additions and 529 deletions
+39 -13
View File
@@ -15,6 +15,7 @@ from src.backend.models.api.production import (
ProductionRunCreate,
ProductionRunResponse,
ProductionRunWithEventsResponse,
RemeasurePayload,
SupervisorAction,
)
from src.backend.models.orm.production import ProductionRun
@@ -23,13 +24,19 @@ from src.backend.services import production_service
router = APIRouter(prefix="/api/production-runs", tags=["production"])
_DERIVED = {
"seconds_to_next_measurement", "overdue", "server_time", "measurement_task_ids",
}
def _as_response(run: ProductionRun) -> ProductionRunResponse:
async def _as_response(db: AsyncSession, run: ProductionRun) -> ProductionRunResponse:
"""The stored run plus everything the client cannot work out on its own."""
return ProductionRunResponse(
**ProductionRunResponse.model_validate(run).model_dump(
exclude={"seconds_to_next_measurement", "overdue", "server_time"}
),
**ProductionRunResponse.model_validate(run).model_dump(exclude=_DERIVED),
**production_service.describe(run),
measurement_task_ids=await production_service.measurement_task_ids(
db, run.version_id,
),
)
@@ -41,7 +48,7 @@ async def open_production_run(
):
"""Start a production at a station."""
run = await production_service.open_run(db, data, user)
return _as_response(run)
return await _as_response(db, run)
@router.get("/current", response_model=ProductionRunResponse | None)
@@ -56,7 +63,7 @@ async def get_current_production_run(
"""
station = await production_service.get_station_by_code(db, station_code)
run = await production_service.get_open_run_for_station(db, station.id)
return _as_response(run) if run is not None else None
return await _as_response(db, run) if run is not None else None
@router.get("/{run_id}", response_model=ProductionRunWithEventsResponse)
@@ -69,7 +76,7 @@ async def get_production_run(
run = await production_service.get_run(db, run_id)
events = await production_service.list_run_events(db, run_id)
return ProductionRunWithEventsResponse(
**_as_response(run).model_dump(),
**(await _as_response(db, run)).model_dump(),
events=[ProductionEventResponse.model_validate(e) for e in events],
)
@@ -81,12 +88,31 @@ async def complete_measurement_cycle(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Record a finished measurement cycle and restart the interval."""
"""Record a finished measurement task; the last one restarts the interval."""
run = await production_service.get_run(db, run_id)
run = await production_service.complete_cycle(
db, run, user, note=payload.note if payload else None,
db, run, user,
task_id=payload.task_id if payload else None,
note=payload.note if payload else None,
)
return _as_response(run)
return await _as_response(db, run)
@router.post("/{run_id}/remeasure", response_model=ProductionRunResponse)
async def remeasure_in_cycle(
run_id: int,
payload: RemeasurePayload | None = None,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Turn the piece over and measure again, without closing the cycle."""
run = await production_service.get_run(db, run_id)
run = await production_service.remeasure(
db, run, user,
task_id=payload.task_id if payload else None,
note=payload.note if payload else None,
)
return await _as_response(db, run)
@router.post("/{run_id}/pause", response_model=ProductionRunResponse)
@@ -102,7 +128,7 @@ async def pause_production_run(
db, action.supervisor_username, action.supervisor_password,
)
run = await production_service.pause_run(db, run, user, supervisor, note=action.note)
return _as_response(run)
return await _as_response(db, run)
@router.post("/{run_id}/resume", response_model=ProductionRunResponse)
@@ -118,7 +144,7 @@ async def resume_production_run(
db, action.supervisor_username, action.supervisor_password,
)
run = await production_service.resume_run(db, run, user, supervisor, note=action.note)
return _as_response(run)
return await _as_response(db, run)
@router.post("/{run_id}/close", response_model=ProductionRunResponse)
@@ -134,4 +160,4 @@ async def close_production_run(
db, action.supervisor_username, action.supervisor_password,
)
run = await production_service.close_run(db, run, user, supervisor, note=action.note)
return _as_response(run)
return await _as_response(db, run)
@@ -0,0 +1,74 @@
"""widen the production event trace for the measurement loop
The loop needs to distinguish two things the trace could not say before: a
measurement task finished while the cycle is still running (task_measured), and the
piece turned over and measured again without closing the cycle (remeasure). Only
the last measurement task of a recipe still produces cycle_completed, because only
that one restarts the interval.
Both values are added in one go. Widening a MySQL enum rewrites the table, so the
values that are already planned go in now rather than one migration at a time -
the same reasoning as the task types in 007.
The events that happen inside a cycle also need to say which task they are about,
so the column comes with them: a task_measured that does not name its task records
nothing usable.
Revision ID: 008_loop_events
Revises: 007_task_type
Create Date: 2026-07-28
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '008_loop_events'
down_revision: Union[str, None] = '007_task_type'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
OLD_TYPES = ('start', 'cycle_completed', 'line_stop', 'resume', 'close')
NEW_TYPES = (
'start', 'cycle_completed', 'task_measured', 'remeasure',
'line_stop', 'resume', 'close',
)
def _alter(values: Sequence[str]) -> None:
"""Restate the column with a new set of allowed values.
On MySQL this is the ALTER that rewrites the enum. On SQLite, where an enum is
stored as a plain VARCHAR, batch mode rebuilds the table instead of emitting an
ALTER the engine does not support - the tests run there.
"""
with op.batch_alter_table('production_events') as batch:
batch.alter_column(
'event_type',
existing_type=sa.Enum(*OLD_TYPES, name='production_event_type_enum'),
type_=sa.Enum(*values, name='production_event_type_enum'),
existing_nullable=False,
)
def upgrade() -> None:
_alter(NEW_TYPES)
with op.batch_alter_table('production_events') as batch:
batch.add_column(sa.Column('task_id', sa.Integer(), nullable=True))
batch.create_foreign_key(
'fk_production_events_task_id', 'recipe_tasks', ['task_id'], ['id'],
)
def downgrade() -> None:
with op.batch_alter_table('production_events') as batch:
batch.drop_constraint('fk_production_events_task_id', type_='foreignkey')
batch.drop_column('task_id')
# The new kinds of event have no equivalent in the old vocabulary, and a row
# left with a value the column no longer accepts would fail the rebuild.
op.execute(
"DELETE FROM production_events "
"WHERE event_type IN ('task_measured', 'remeasure')"
)
_alter(OLD_TYPES)
+18 -1
View File
@@ -19,6 +19,7 @@ class ProductionEventResponse(BaseModel):
event_type: str
user_id: int
supervisor_id: Optional[int]
task_id: Optional[int] = None
note: Optional[str]
created_at: datetime
@@ -50,6 +51,11 @@ class ProductionRunResponse(BaseModel):
overdue: bool = False
server_time: Optional[datetime] = None
# The measurement tasks of this run's version, in execution order. The client
# needs them to bring the operator back to the measurement when the interval
# expires, whichever screen they are on, and to know which task closes a cycle.
measurement_task_ids: list[int] = Field(default_factory=list)
class ProductionRunWithEventsResponse(ProductionRunResponse):
events: list[ProductionEventResponse] = Field(default_factory=list)
@@ -64,9 +70,20 @@ class SupervisorAction(BaseModel):
class CycleCompletePayload(BaseModel):
# Which measurement task was just finished. The interval restarts only on the
# last one of the recipe; omitting it closes the cycle outright.
task_id: Optional[int] = Field(default=None, gt=0)
note: Optional[str] = None
class RemeasurePayload(BaseModel):
"""The piece was turned over: measure it again inside the same cycle."""
task_id: Optional[int] = Field(default=None, gt=0)
note: Optional[str] = None
ProductionEventType = Literal[
"start", "cycle_completed", "line_stop", "resume", "close",
"start", "cycle_completed", "task_measured", "remeasure",
"line_stop", "resume", "close",
]
+13 -1
View File
@@ -31,7 +31,13 @@ if TYPE_CHECKING:
RUN_STATUSES = ("running", "paused", "closed")
EVENT_TYPES = ("start", "cycle_completed", "line_stop", "resume", "close")
# task_measured marks a measurement task finished inside a cycle that is not over
# yet; only the last one produces cycle_completed and restarts the interval.
# remeasure is the piece being turned and measured again without closing the cycle.
EVENT_TYPES = (
"start", "cycle_completed", "task_measured", "remeasure",
"line_stop", "resume", "close",
)
class ProductionRun(Base):
@@ -139,6 +145,12 @@ class ProductionEvent(Base):
supervisor_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("users.id"), nullable=True
)
# Which task the event is about, for the ones that happen inside a cycle. A
# task_measured or a remeasure that does not say which task is not a record of
# anything; null for the events that concern the run as a whole.
task_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("recipe_tasks.id"), nullable=True
)
note: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.now(), index=True
+79 -5
View File
@@ -24,6 +24,7 @@ from src.backend.models.api.production import ProductionRunCreate
from src.backend.models.orm.production import ProductionEvent, ProductionRun
from src.backend.models.orm.recipe import Recipe, RecipeVersion
from src.backend.models.orm.station import Station
from src.backend.models.orm.task import MEASURING_TASK_TYPES, RecipeTask
from src.backend.models.orm.user import User
from src.backend.services import auth_service, production_export_service
@@ -103,6 +104,28 @@ async def get_station_by_code(db: AsyncSession, code: str) -> Station:
return station
async def measurement_task_ids(db: AsyncSession, version_id: int) -> list[int]:
"""Ids of the measurement tasks of a version, in the order they are executed.
This is the loop the operator lives in once production starts: the client uses
the first of these to bring the measurement back on screen when the interval
expires, and the last to know which task closes a cycle.
The type is read from the declared task_type (point 2), not from whether quotes
happen to be present: a measurement task with no quotes yet is still where the
operator has to be taken.
"""
result = await db.execute(
select(RecipeTask.id)
.where(
RecipeTask.version_id == version_id,
RecipeTask.task_type.in_(MEASURING_TASK_TYPES),
)
.order_by(RecipeTask.order_index, RecipeTask.id)
)
return list(result.scalars().all())
async def list_run_events(db: AsyncSession, run_id: int) -> list[ProductionEvent]:
result = await db.execute(
select(ProductionEvent)
@@ -149,6 +172,7 @@ def _add_event(
user: User,
supervisor: Optional[User] = None,
note: Optional[str] = None,
task_id: Optional[int] = None,
) -> ProductionEvent:
"""Append to the run's trace.
@@ -161,6 +185,7 @@ def _add_event(
user_id=user.id,
supervisor_id=supervisor.id if supervisor else None,
note=note,
task_id=task_id,
created_at=_now(),
)
db.add(event)
@@ -254,10 +279,7 @@ def _require_open(run: ProductionRun) -> None:
)
async def complete_cycle(
db: AsyncSession, run: ProductionRun, user: User, note: Optional[str] = None,
) -> ProductionRun:
"""Record a finished measurement cycle and restart the interval."""
def _require_measurable(run: ProductionRun) -> None:
_require_open(run)
if run.status == "paused":
raise HTTPException(
@@ -265,6 +287,36 @@ async def complete_cycle(
detail="Production run is paused: resume it before measuring",
)
async def complete_cycle(
db: AsyncSession,
run: ProductionRun,
user: User,
task_id: Optional[int] = None,
note: Optional[str] = None,
) -> ProductionRun:
"""Record a finished measurement task, and restart the interval if it was the last.
A cycle spans every measurement task of the recipe, so only the last one closes
it. Restarting the interval at the first would make a recipe with three
measurement tasks measure three times as often as it was set up to.
An unrecognised task_id - or none at all - closes the cycle: a caller that does
not know about the sequence gets the behaviour that existed before it.
"""
_require_measurable(run)
measure_ids = await measurement_task_ids(db, run.version_id)
closes_cycle = (
task_id is None or task_id not in measure_ids or task_id == measure_ids[-1]
)
if not closes_cycle:
_add_event(db, run, "task_measured", user, note=note, task_id=task_id)
await db.flush()
await db.refresh(run)
return run
now = _now()
run.cycle_count += 1
if run.measurement_interval_minutes and run.measurement_interval_minutes > 0:
@@ -273,7 +325,29 @@ async def complete_cycle(
run.next_measurement_at = now + timedelta(
minutes=run.measurement_interval_minutes
)
_add_event(db, run, "cycle_completed", user, note=note)
_add_event(db, run, "cycle_completed", user, note=note, task_id=task_id)
await db.flush()
await db.refresh(run)
return run
async def remeasure(
db: AsyncSession,
run: ProductionRun,
user: User,
task_id: Optional[int] = None,
note: Optional[str] = None,
) -> ProductionRun:
"""Measure the same task again without closing the cycle.
This is the piece being turned over. The deadline and the cycle count are left
exactly as they are - a second reading of the same part is not a new cycle, and
it must not buy the operator another interval's worth of time. The values
themselves are ordinary measurements: both readings stay in the statistics,
which is the point of taking them.
"""
_require_measurable(run)
_add_event(db, run, "remeasure", user, note=note, task_id=task_id)
await db.flush()
await db.refresh(run)
return run
+4
View File
@@ -243,6 +243,10 @@ async def create_test_recipe(
version_id=version.id,
order_index=0,
title="Test Task",
# Declared, like any task since the type stopped being deduced: this one has
# a quote under it, so a fixture that called it a note would be modelling
# exactly the inconsistency point 2 removed.
task_type="measure",
directive="Measure the part",
description="First measurement task",
)
+156
View File
@@ -13,6 +13,7 @@ from sqlalchemy import select
from src.backend.models.orm.production import ProductionRun
from src.backend.models.orm.recipe import Recipe
from src.backend.models.orm.station import Station
from src.backend.models.orm.task import RecipeTask
from src.backend.services import auth_service, production_service
from src.backend.tests.conftest import auth_headers, create_test_recipe
@@ -63,6 +64,29 @@ async def _advance_clock(db_session, run_id: int, delta: timedelta) -> None:
await db_session.commit()
async def _only_task(db_session, version_id: int):
"""The single task the fixture recipe comes with - a measurement one."""
row = await db_session.execute(
select(RecipeTask)
.where(RecipeTask.version_id == version_id)
.order_by(RecipeTask.order_index)
)
return row.scalars().first()
async def _add_task(
db_session, version_id: int, order_index: int, title: str, task_type: str = "measure",
) -> RecipeTask:
"""A further task in the sequence, of a declared type."""
task = RecipeTask(
version_id=version_id, order_index=order_index, title=title, task_type=task_type,
)
db_session.add(task)
await db_session.commit()
await db_session.refresh(task)
return task
async def _open(client, user, station, recipe, **extra):
return await client.post(
"/api/production-runs",
@@ -209,6 +233,138 @@ async def test_cycle_restarts_the_interval_and_counts(
assert 9 * 60 <= body["seconds_to_next_measurement"] <= 10 * 60
async def test_measurement_tasks_travel_with_the_run(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
"""Where the measurement is cannot be guessed by the client: the run says so.
This is what lets a screen bring the operator back to the measurement when the
interval expires, whatever page they wandered off to.
"""
station = await _station(db_session, admin_user.id, code="ST-SEQ")
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-SEQ")
opened = (await _open(client, measurement_tec_user, station, recipe)).json()
first = await _only_task(db_session, opened["version_id"])
# A drawing between the two measurements, to prove it is left out.
await _add_task(db_session, opened["version_id"], 1, "Disegno", "drawing")
second = await _add_task(db_session, opened["version_id"], 2, "Seconda misura")
body = (await client.get(
"/api/production-runs/current",
params={"station_code": station.code},
headers=auth_headers(measurement_tec_user),
)).json()
assert body["measurement_task_ids"] == [first.id, second.id]
async def test_cycle_closes_only_on_the_last_measurement_task(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
"""Restarting at the first would measure three times as often as configured."""
station = await _station(db_session, admin_user.id, code="ST-MULTI")
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-MULTI")
opened = (await _open(client, measurement_tec_user, station, recipe)).json()
run_id = opened["id"]
first = await _only_task(db_session, opened["version_id"])
second = await _add_task(db_session, opened["version_id"], 1, "Seconda misura")
headers = auth_headers(measurement_tec_user)
# Four minutes of the interval have gone by when the first task is finished.
await _advance_clock(db_session, run_id, timedelta(minutes=4))
mid = (await client.post(
f"/api/production-runs/{run_id}/cycle", headers=headers, json={"task_id": first.id},
)).json()
assert mid["cycle_count"] == 0, "il ciclo non e' ancora finito"
assert 5 * 60 <= mid["seconds_to_next_measurement"] <= 6 * 60, (
"l'intervallo non deve ripartire a meta' ciclo"
)
end = (await client.post(
f"/api/production-runs/{run_id}/cycle", headers=headers, json={"task_id": second.id},
)).json()
assert end["cycle_count"] == 1
assert 9 * 60 <= end["seconds_to_next_measurement"] <= 10 * 60
events = (await client.get(
f"/api/production-runs/{run_id}", headers=headers,
)).json()["events"]
by_type = {e["event_type"]: e for e in events}
assert by_type["task_measured"]["task_id"] == first.id
assert by_type["cycle_completed"]["task_id"] == second.id
async def test_cycle_without_a_task_closes_as_before(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
"""A caller that knows nothing of the sequence keeps the old behaviour."""
station = await _station(db_session, admin_user.id, code="ST-PLAIN")
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-PLAIN")
opened = (await _open(client, measurement_tec_user, station, recipe)).json()
await _add_task(db_session, opened["version_id"], 1, "Seconda misura")
body = (await client.post(
f"/api/production-runs/{opened['id']}/cycle",
headers=auth_headers(measurement_tec_user),
)).json()
assert body["cycle_count"] == 1
# ---------------------------------------------------------------------------
# Rimisura - turning the piece over
# ---------------------------------------------------------------------------
async def test_remeasure_does_not_buy_another_interval(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
"""A second reading of the same part is not a new cycle."""
station = await _station(db_session, admin_user.id, code="ST-AGAIN")
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-AGAIN")
opened = (await _open(client, measurement_tec_user, station, recipe)).json()
run_id = opened["id"]
task = await _only_task(db_session, opened["version_id"])
headers = auth_headers(measurement_tec_user)
await _advance_clock(db_session, run_id, timedelta(minutes=7))
body = (await client.post(
f"/api/production-runs/{run_id}/remeasure", headers=headers,
json={"task_id": task.id},
)).json()
assert body["cycle_count"] == 0
assert 2 * 60 <= body["seconds_to_next_measurement"] <= 3 * 60, (
"rigirare il pezzo non deve spostare la scadenza"
)
events = (await client.get(
f"/api/production-runs/{run_id}", headers=headers,
)).json()["events"]
remeasured = [e for e in events if e["event_type"] == "remeasure"]
assert len(remeasured) == 1
assert remeasured[0]["task_id"] == task.id
async def test_remeasure_refused_while_the_line_is_stopped(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
station = await _station(db_session, admin_user.id, code="ST-AGPAUSE")
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-AGPAUSE")
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
supervisor, password = await _supervisor(db_session, username="capo-again")
await client.post(
f"/api/production-runs/{run_id}/pause",
headers=auth_headers(measurement_tec_user),
json={"supervisor_username": supervisor.username, "supervisor_password": password},
)
resp = await client.post(
f"/api/production-runs/{run_id}/remeasure",
headers=auth_headers(measurement_tec_user),
)
assert resp.status_code == 409
async def test_overdue_run_reports_negative_seconds(
client: AsyncClient, measurement_tec_user, admin_user, db_session,
):
+22 -2
View File
@@ -470,10 +470,30 @@ def api_start_production():
@login_required
@role_required("MeasurementTec")
def api_complete_cycle(run_id: int):
"""Proxy: record a finished measurement cycle and restart the interval."""
"""Proxy: record a finished measurement task.
The task travels with the call: only the last measurement task of the recipe
closes the cycle and restarts the interval, and the server is what decides.
"""
data = request.get_json(silent=True) or {}
resp = api_client.post(
f"/api/production-runs/{run_id}/cycle", data={"note": data.get("note")},
f"/api/production-runs/{run_id}/cycle",
data={"task_id": data.get("task_id"), "note": data.get("note")},
)
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/<int:run_id>/remeasure", methods=["POST"])
@login_required
@role_required("MeasurementTec")
def api_remeasure(run_id: int):
"""Proxy: the piece was turned over - measure again inside the same cycle."""
data = request.get_json(silent=True) or {}
resp = api_client.post(
f"/api/production-runs/{run_id}/remeasure",
data={"task_id": data.get("task_id"), "note": data.get("note")},
)
if isinstance(resp, dict) and resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
@@ -0,0 +1,176 @@
/**
* The measurement clock, shared by every screen an operator can be on.
*
* Two things the shop floor asked for live here. First, the countdown does not stop
* at zero: it keeps going the other way, so how late a measurement is can be read
* off the screen instead of worked out. Second, when the interval expires the
* measurement comes back on its own, wherever the operator happens to be - the task
* list, the summary, the recipe picker.
*
* The number always comes from the server (seconds_to_next_measurement, already
* signed). Ticking locally only keeps the display smooth between round trips, and
* every resync overwrites it: a tab that was asleep, or a machine whose clock is
* off, still shows the same figure as the station next to it.
*
* ProductionClock holds the pure parts, used here and by the measurement screen,
* which has its own component but must not grow a second copy of these rules.
*/
(function (global) {
'use strict';
var ProductionClock = {
/* mm:ss of the distance from the deadline, in whichever direction. The sign is
carried by the wording around it ("tra" / "in ritardo di"), not by a minus. */
format: function (seconds) {
var total = Math.abs(Math.round(seconds || 0));
var m = Math.floor(total / 60);
var s = total % 60;
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
},
hasClock: function (seconds) {
return seconds !== null && seconds !== undefined;
},
isOverdue: function (seconds) {
return ProductionClock.hasClock(seconds) && seconds <= 0;
},
/* Three short beeps. Wrapped because a browser refuses to make noise before the
page has been touched, and a silent buzzer must not take the screen with it.
Whether a light column replaces this is question D-5. */
playBuzzer: function () {
try {
var ctx = new (global.AudioContext || global.webkitAudioContext)();
[0, 0.25, 0.5].forEach(function (delay) {
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 880;
osc.type = 'square';
gain.gain.value = 0.3;
osc.start(ctx.currentTime + delay);
osc.stop(ctx.currentTime + delay + 0.15);
});
} catch (_) {}
},
/* The task the operator has to be taken back to: the first measurement task of
the recipe. Null when the recipe has none, which is the whole reason this is
a lookup and not an assumption. */
measurementUrl: function (run, executeUrlTemplate) {
var ids = (run && run.measurement_task_ids) || [];
if (!ids.length) return null;
return executeUrlTemplate.replace('/0', '/' + ids[0]);
},
};
global.ProductionClock = ProductionClock;
/* How long the operator gets to see why the screen is about to change. Long
enough to read it, short enough that it is not a way of putting the
measurement off. */
var ANNOUNCE_SECONDS = 5;
/* The server is asked again on this cadence: the run may have been stopped or
closed from another screen, and a sleeping tab drifts. */
var RESYNC_MS = 60000;
/**
* Banner component for the screens that only watch the clock.
*
* options.currentUrl - endpoint returning the run open at this station, or null
* options.executeUrl - url of a task execution page with 0 as the task id
*/
global.productionClock = function (options) {
var opts = options || {};
return {
run: null,
seconds: null,
announced: false,
redirectIn: 0,
_tick: null,
_resync: null,
init: function () {
var self = this;
this.load();
this._tick = setInterval(function () { self.onSecond(); }, 1000);
this._resync = setInterval(function () { self.load(); }, RESYNC_MS);
},
destroy: function () {
if (this._tick) clearInterval(this._tick);
if (this._resync) clearInterval(this._resync);
},
load: async function () {
try {
var resp = await fetch(opts.currentUrl);
if (!resp.ok) return;
var run = await resp.json();
this.adopt(run && run.id ? run : null);
} catch (e) {
// Offline or server down: the page stays usable, just without the clock.
}
},
adopt: function (run) {
this.run = run;
this.seconds = run ? run.seconds_to_next_measurement : null;
if (!run || run.status !== 'running') {
// Closed or stopped: nothing is due, and an announcement left on screen
// would be telling the operator to go and measure a stopped line.
this.announced = false;
this.redirectIn = 0;
return;
}
// Arriving on a page that is already late is the same event as going late
// while sitting on it.
if (this.isDue) this.announce();
},
get running() { return !!this.run && this.run.status === 'running'; },
get paused() { return !!this.run && this.run.status === 'paused'; },
get visible() {
return !!this.run && ProductionClock.hasClock(this.seconds);
},
get isDue() { return this.running && ProductionClock.isOverdue(this.seconds); },
get display() { return ProductionClock.format(this.seconds); },
get measurementUrl() {
return ProductionClock.measurementUrl(this.run, opts.executeUrl || '');
},
onSecond: function () {
var wasAnnounced = this.announced;
if (this.running && ProductionClock.hasClock(this.seconds)) {
// Past zero it keeps counting, into the negative: that is the figure the
// operator reads as "how long I am late by".
this.seconds--;
if (this.seconds <= 0) this.announce();
}
if (wasAnnounced && this.redirectIn > 0) {
this.redirectIn--;
if (this.redirectIn === 0) this.goToMeasurement();
}
},
announce: function () {
if (this.announced) return;
this.announced = true;
ProductionClock.playBuzzer();
// Without a measurement task to go to there is nothing to announce beyond
// the banner going red, and a redirect to nowhere would be a broken link.
this.redirectIn = this.measurementUrl ? ANNOUNCE_SECONDS : 0;
},
goToMeasurement: function () {
var url = this.measurementUrl;
if (url) global.location.href = url;
},
};
};
})(window);
@@ -0,0 +1,108 @@
{#
The measurement clock, for every screen that is not the measurement itself.
Two requirements of 28/07 are in here. The countdown is always on screen while a
production is open, and past zero it turns round and shows how long the line has
been over the interval - in red, because a late measurement that looks like an
early one is how it stays late. And when the interval expires the measurement
comes back on its own: the operator is told, and taken there.
The measurement screen has its own component and does not include this: being
already on the measurement is the one place there is nothing to announce.
#}
<script src="{{ url_for('static', filename='js/production-clock.js') }}"></script>
<div x-data="productionClock({
currentUrl: '{{ url_for('measure.api_current_production') }}',
executeUrl: '{{ url_for('measure.task_execute', task_id=0) }}'
})"
x-init="init()"
x-cloak>
{# ---- Countdown bar: sticky, so scrolling never hides it ---- #}
<div x-show="visible"
x-transition
class="sticky top-0 z-30 -mx-4 sm:-mx-6 lg:-mx-8 mb-4 px-4 py-2 border-b shadow-sm"
:class="isDue
? 'bg-red-50 dark:bg-red-900/30 border-red-400 dark:border-red-700'
: paused
? 'bg-amber-100 dark:bg-amber-900/40 border-amber-500'
: 'bg-amber-50 dark:bg-amber-900/20 border-amber-300 dark:border-amber-700'">
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
<svg class="w-5 h-5 shrink-0"
:class="isDue ? 'text-red-600 animate-pulse' : 'text-amber-600'"
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{# In time #}
<template x-if="!isDue && !paused">
<span class="text-sm font-medium text-amber-800 dark:text-amber-200">
{{ _('Prossima misurazione tra') }}
</span>
</template>
{# Over the interval: the delay is the headline, not a footnote #}
<template x-if="isDue">
<span class="text-sm font-bold text-red-800 dark:text-red-200">
{{ _('Misurazione in ritardo di') }}
</span>
</template>
{# Line stopped: the figure is frozen, and says so. Frozen past the interval
it is a delay held in place, not a wait - the wording has to say which. #}
<template x-if="paused && seconds > 0">
<span class="text-sm font-semibold text-amber-900 dark:text-amber-100">
{{ _('Linea ferma — conto alla rovescia congelato a') }}
</span>
</template>
<template x-if="paused && seconds <= 0">
<span class="text-sm font-semibold text-amber-900 dark:text-amber-100">
{{ _('Linea ferma') }} — {{ _('Misurazione in ritardo di') }}
</span>
</template>
<span class="text-lg font-bold font-mono px-2 py-0.5 rounded"
:class="isDue
? 'text-red-900 dark:text-red-100 bg-red-100 dark:bg-red-900/40'
: 'text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900/40'"
x-text="display"></span>
<span class="text-xs"
:class="isDue ? 'text-red-600 dark:text-red-300' : 'text-amber-600 dark:text-amber-400'">
({{ _('Ciclo') }} #<span x-text="run ? run.cycle_count : 0"></span>)
</span>
</div>
</div>
{# ---- Time to measure: announced, then taken there ---- #}
<div x-show="announced && redirectIn > 0"
x-transition
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-8 max-w-sm mx-4 text-center border-2 border-red-500">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<svg class="w-8 h-8 text-red-600 animate-pulse" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-bold text-[var(--text-primary)] mb-1">
{{ _('È ora di misurare') }}
</h3>
<p class="text-sm text-[var(--text-secondary)] mb-5">
{{ _('Ritorno alla misurazione tra') }}
<span class="font-bold font-mono" x-text="redirectIn"></span> s
</p>
<button @click="goToMeasurement()"
class="btn w-full justify-center gap-2 bg-red-600 hover:bg-red-700 text-white font-bold py-2.5">
{{ _('Vai alla misura') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
</div>
@@ -59,6 +59,10 @@
}
}">
{# A production open at this station keeps its clock on screen even here: the
interval runs whether or not the operator is looking at the measurement. #}
{% include "components/production_clock.html" %}
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center justify-between flex-wrap gap-4">
@@ -4,6 +4,9 @@
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{# The summary is where an operator lingers: the clock has to be here as well. #}
{% include "components/production_clock.html" %}
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
@@ -420,20 +420,34 @@
{# ================================================================
MEASUREMENT TIMER BANNER
================================================================ #}
{# Past zero the counter turns round: it stops saying how long is left and starts
saying how late the line is, in red. A delay has to be read, not deduced. #}
<div x-show="timerActive"
x-transition
x-cloak
class="shrink-0 bg-amber-50 dark:bg-amber-900/20 border-t border-amber-300 dark:border-amber-700 px-4 py-2">
class="shrink-0 border-t px-4 py-2"
:class="isOverdue
? 'bg-red-50 dark:bg-red-900/30 border-red-400 dark:border-red-700'
: 'bg-amber-50 dark:bg-amber-900/20 border-amber-300 dark:border-amber-700'">
<div class="flex items-center justify-center gap-3">
<svg class="w-5 h-5 text-amber-600 animate-pulse" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<svg class="w-5 h-5 animate-pulse"
:class="isOverdue ? 'text-red-600' : 'text-amber-600'"
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="text-sm font-medium text-amber-800 dark:text-amber-200">
<span x-show="!isOverdue" class="text-sm font-medium text-amber-800 dark:text-amber-200">
{{ _('Prossima misurazione tra') }}
</span>
<span class="text-lg font-bold font-mono text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900/40 px-2 py-0.5 rounded"
<span x-show="isOverdue" class="text-sm font-bold text-red-800 dark:text-red-200">
{{ _('Misurazione in ritardo di') }}
</span>
<span class="text-lg font-bold font-mono px-2 py-0.5 rounded"
:class="isOverdue
? 'text-red-900 dark:text-red-100 bg-red-100 dark:bg-red-900/40'
: 'text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900/40'"
x-text="timerDisplay"></span>
<span class="text-xs text-amber-600 dark:text-amber-400">
<span class="text-xs"
:class="isOverdue ? 'text-red-600 dark:text-red-300' : 'text-amber-600 dark:text-amber-400'">
({{ _('Ciclo') }} #<span x-text="cycleCount"></span>)
</span>
</div>
@@ -474,8 +488,11 @@
<span class="text-sm font-semibold text-amber-900 dark:text-amber-100">
{{ _('Linea ferma') }}
</span>
{# A line stopped past its interval is frozen at a delay, not at a wait. Both
read as mm:ss, so the wording is what has to change. #}
<span class="text-xs text-amber-800 dark:text-amber-200">
{{ _('Il conto alla rovescia è congelato a') }}
<span x-show="timerRemaining > 0">{{ _('Il conto alla rovescia è congelato a') }}</span>
<span x-show="timerRemaining <= 0">{{ _('Misurazione in ritardo di') }}</span>
<span class="font-mono font-bold" x-text="timerDisplay"></span>
— {{ _('serve il capoturno per riprendere') }}
</span>
@@ -579,6 +596,17 @@
</svg>
{{ _('Fine ciclo misura') }}
</button>
{# Girare il pezzo e rimisurare, senza chiudere il ciclo: a second reading
of the same part is not a new one, and must not restart the interval. #}
<button x-show="isComplete && productionStarted"
x-transition
@click="remeasure()"
class="btn btn-secondary text-xs shrink-0 gap-1 py-1.5 px-3">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992V4.356M3.977 14.652H8.97v4.992m10.058-9.348a8.25 8.25 0 00-14.02-3.34L3.977 9.348m0 5.304a8.25 8.25 0 0014.02 3.34l1.03-1.03"/>
</svg>
{{ _('Rimisura') }}
</button>
<button x-show="cycleConfirmed"
x-transition
@click="goToNextTask()"
@@ -610,6 +638,37 @@
</div>
</div>
{# ================================================================
È ORA DI MISURARE — the interval expired on a task that is not the
measurement. The operator is told why the screen is about to change, then
taken there; there is no way to dismiss it, because putting the measurement
off is exactly what the interval exists to prevent.
================================================================ #}
<div x-show="announcedRedirect"
x-transition
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl p-8 max-w-sm mx-4 text-center border-2 border-red-500">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<svg class="w-8 h-8 text-red-600 animate-pulse" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-bold text-[var(--text-primary)] mb-1">{{ _('È ora di misurare') }}</h3>
<p class="text-sm text-[var(--text-secondary)] mb-5">
{{ _('Ritorno alla misurazione tra') }}
<span class="font-bold font-mono" x-text="redirectIn"></span> s
</p>
<button @click="goToMeasurementTask()"
class="btn w-full justify-center gap-2 bg-red-600 hover:bg-red-700 text-white font-bold py-2.5">
{{ _('Vai alla misura') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
{# ================================================================
COMPLETION OVERLAY
================================================================ #}
@@ -659,9 +718,10 @@
{{ _('Riepilogo') }}
</button>
{# Production phase: confirm the cycle (starts the interval timer) #}
{# Production phase: confirm the cycle. Goes through confirmCycle so the
server records it - setting the flag here only looked like it had. #}
<button x-show="productionStarted"
@click="showCompletionOverlay = false; cycleConfirmed = true"
@click="confirmCycle()"
class="btn btn-primary flex-1 justify-center gap-2">
{{ _('Conferma ciclo') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
@@ -689,6 +749,18 @@
</svg>
</button>
</div>
{# The piece can be turned over here, where the operator actually is when the
quotes are done - not only from the bar behind the overlay. #}
<button x-show="productionStarted"
@click="remeasure()"
class="mt-3 w-full text-sm text-[var(--text-secondary)] hover:text-primary
inline-flex items-center justify-center gap-1.5 py-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992V4.356M3.977 14.652H8.97v4.992m10.058-9.348a8.25 8.25 0 00-14.02-3.34L3.977 9.348m0 5.304a8.25 8.25 0 0014.02 3.34l1.03-1.03"/>
</svg>
{{ _('Girare il pezzo e rimisurare') }}
</button>
</div>
</div>
@@ -761,6 +833,9 @@
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
<script src="{{ url_for('static', filename='js/caliper.js') }}"></script>
{# The clock's rules - how a delay reads, when the buzzer goes, where the
measurement lives - are shared with the other screens rather than restated. #}
<script src="{{ url_for('static', filename='js/production-clock.js') }}"></script>
<script>
/**
@@ -797,6 +872,13 @@ function taskExecute() {
// This is read back from the server on every load instead.
productionRun: null,
productionError: '',
// The measurement tasks of this recipe, in order: the loop the operator stays
// in once production starts, and what says which task closes a cycle.
measurementTaskIds: [],
// The interval has elapsed and it has been said out loud - buzzer, and the
// operator on their way back to the measurement. Kept so it is said once.
dueAnnounced: false,
redirectIn: 0,
// ---- Cycle & workflow state ----
cycleConfirmed: false,
@@ -917,6 +999,7 @@ function taskExecute() {
this.productionRun = run;
this.productionStarted = run.status !== 'closed';
this.cycleCount = run.cycle_count;
this.measurementTaskIds = run.measurement_task_ids || [];
const seconds = run.seconds_to_next_measurement;
if (seconds === null || seconds === undefined) {
@@ -1078,17 +1161,26 @@ function taskExecute() {
},
// ---- Confirm measurement cycle (Fine ciclo misura) ----
/* A cycle spans every measurement task of the recipe, so this one only closes
it if it is the last. The server decides that - it knows the sequence - and
the cycle count coming back tells us which of the two happened. */
async confirmCycle() {
this.cycleConfirmed = true;
this.showCompletionOverlay = false;
// Recorded server-side so the count and the next deadline outlive this page.
if (this.productionRun) {
const countBefore = this.cycleCount;
const run = await this.postProduction(
'{{ url_for("measure.api_complete_cycle", run_id=0) }}'.replace('/0/', '/' + this.productionRun.id + '/'),
{ task_id: this.task.id },
);
if (run) {
this.adoptProductionRun(run);
// Cycle still open: on to the next measurement task, interval untouched.
if (run.cycle_count === countBefore && this.nextMeasurementTaskId) {
this.goToTask(this.nextMeasurementTaskId);
}
return;
}
}
@@ -1101,6 +1193,25 @@ function taskExecute() {
}
},
/* Rimisura: turn the piece over and measure it again, inside the same cycle.
Not a new cycle - the deadline and the count stay where they are, so a second
reading of the same part does not buy another interval's worth of time. Both
readings stay in the statistics: that is what they are for. */
async remeasure() {
if (this.productionRun) {
const run = await this.postProduction(
'{{ url_for("measure.api_remeasure", run_id=0) }}'.replace('/0/', '/' + this.productionRun.id + '/'),
{ task_id: this.task.id },
);
// Refused - the line is stopped, or the production is closed. The screen
// stays as it is: clearing it would hide the reason.
if (!run) return;
this.adoptProductionRun(run);
}
this.resetForNewCycle();
},
/* POST to a production endpoint, returning the updated run or null on failure. */
async postProduction(url, body) {
this.productionError = '';
@@ -1125,18 +1236,34 @@ function taskExecute() {
// ---- Measurement timer ----
/* Ticks locally for a smooth display, but the number it starts from always comes
from the server, and every page load resynchronises it. */
from the server, and every page load resynchronises it.
It does not stop at zero: past the deadline it keeps counting the other way,
and the banner reads the delay off it. */
startCountdownFrom(seconds) {
this.stopMeasurementTimer();
this.timerRemaining = seconds;
this.timerActive = true;
// Landing on a page that is already late is the same event as going late
// while sitting on it; being back in time re-arms the announcement.
if (seconds <= 0) {
this.onMeasurementDue();
} else {
this.dueAnnounced = false;
this.redirectIn = 0;
}
var self = this;
this._timerInterval = setInterval(function () {
self.timerRemaining--;
if (self.timerRemaining <= 0) {
self.onTimerExpired();
}
}, 1000);
this._timerInterval = setInterval(function () { self.onSecond(); }, 1000);
},
onSecond() {
const wasAnnounced = this.dueAnnounced;
this.timerRemaining--;
if (this.timerRemaining <= 0) this.onMeasurementDue();
if (wasAnnounced && this.redirectIn > 0) {
this.redirectIn--;
if (this.redirectIn === 0) this.goToMeasurementTask();
}
},
stopMeasurementTimer() {
@@ -1147,32 +1274,79 @@ function taskExecute() {
this.timerActive = false;
},
onTimerExpired() {
this.stopMeasurementTimer();
this.playBuzzer();
// Reset for new measurement cycle
/* The interval has run out. Said once, then acted on.
Someone already measuring is late, not lost: the banner turns red and they
are left alone. Taking them elsewhere mid-cycle would wipe quotes they can
see on the screen and gain nothing.
Otherwise a new cycle starts, and it starts at the first measurement task of
the recipe - re-arming wherever the operator happens to be standing would
skip whatever comes before it and still count as a full cycle. */
onMeasurementDue() {
if (this.dueAnnounced) return;
this.dueAnnounced = true;
window.ProductionClock.playBuzzer();
if (this.isMeasurementTaskOfRun && !this.cycleConfirmed) return;
if (this.task.id === this.measurementTaskIds[0]) {
this.resetForNewCycle();
return;
}
this.redirectIn = this.measurementTaskUrl ? 5 : 0;
},
/* Clear the screen for another pass over the same quotes.
Local only: everything already measured is on the server, and the next
reading is saved beside it rather than replacing it. */
resetForNewCycle() {
this.cycleConfirmed = false;
this.showCompletionOverlay = false;
this.measurements = [];
this.currentIndex = 0;
this.currentValue = null;
this.errorMessage = '';
this.inputStartedAt = Date.now();
},
playBuzzer() {
try {
var ctx = new (window.AudioContext || window.webkitAudioContext)();
// 3 short beeps
[0, 0.25, 0.5].forEach(function (delay) {
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 880;
osc.type = 'square';
gain.gain.value = 0.3;
osc.start(ctx.currentTime + delay);
osc.stop(ctx.currentTime + delay + 0.15);
});
} catch (_) {}
// ---- Where the measurement lives ----
get isMeasurementTaskOfRun() {
return this.measurementTaskIds.indexOf(this.task.id) !== -1;
},
get measurementTaskUrl() {
return window.ProductionClock.measurementUrl(
this.productionRun, '{{ url_for("measure.task_execute", task_id=0) }}',
);
},
/* The next measurement task of the recipe, or null if this is the last one -
which is the one that closes the cycle and restarts the interval. */
get nextMeasurementTaskId() {
const ids = this.measurementTaskIds;
const idx = ids.indexOf(this.task.id);
if (idx === -1 || idx >= ids.length - 1) return null;
return ids[idx + 1];
},
get isOverdue() {
return this.timerActive && this.timerRemaining <= 0;
},
get announcedRedirect() {
return this.dueAnnounced && this.redirectIn > 0;
},
goToMeasurementTask() {
const url = this.measurementTaskUrl;
if (url) window.location.href = url;
},
goToTask(taskId) {
window.location.href =
'{{ url_for("measure.task_execute", task_id=0) }}'.replace('/0', '/' + taskId);
},
// ---- Avvio Produzione ----
@@ -1224,18 +1398,35 @@ function taskExecute() {
await this.confirmCycle();
},
/* mm:ss of the distance from the deadline, either side of it. The direction is
carried by the wording in the banner, not by a minus sign. */
get timerDisplay() {
var m = Math.floor(this.timerRemaining / 60);
var s = this.timerRemaining % 60;
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
return window.ProductionClock.format(this.timerRemaining);
},
// ---- Navigate to next task (Completato) ----
/* With a production open the operator stays in the measurement loop until the
capoturno closes it: after the last measurement task they go back to the
first, not out to the summary. Before production starts, and on the
documental tasks that come before the measurement, the plain sequence
applies - that is the run-up, not the loop. */
goToNextTask() {
if (this.productionStarted && this.isMeasurementTaskOfRun) {
const next = this.nextMeasurementTaskId || this.measurementTaskIds[0];
// A recipe with a single measurement task loops on the spot: reloading the
// same page to arrive at the same state would only cost a round trip.
if (next === this.task.id) {
this.resetForNewCycle();
return;
}
this.goToTask(next);
return;
}
const taskIds = window.__allTaskIds || [];
const currentIdx = taskIds.indexOf(this.task.id);
if (currentIdx >= 0 && currentIdx < taskIds.length - 1) {
window.location.href = '{{ url_for("measure.task_execute", task_id=0) }}'.replace('/0', '/' + taskIds[currentIdx + 1]);
this.goToTask(taskIds[currentIdx + 1]);
} else {
this.goToSummary();
}
@@ -4,6 +4,9 @@
{% block content %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-5xl">
{# The clock follows the operator here too, and brings them back when it expires. #}
{% include "components/production_clock.html" %}
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
@@ -53,6 +53,23 @@ class TestTaskList:
assert resp.status_code == 200
class TestTaskComplete:
"""GET /measure/complete/<recipe_id> tests."""
def test_task_complete_renders(self, logged_in_client, mock_api_client):
"""The summary carries the measurement clock, so it has to render with it."""
mock_api_client.get.side_effect = [
# The summary serialises the version number, so the recipe carries one.
{"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1},
[{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": []}],
{"items": []}, # measurements
]
resp = logged_in_client.get("/measure/complete/1?version_id=5")
assert resp.status_code == 200
assert b"productionClock(" in resp.data
class TestSaveMeasurement:
"""POST /measure/save-measurement tests."""
@@ -140,6 +140,40 @@ def test_complete_cycle_hits_the_run(logged_in_client, monkeypatch):
assert endpoint[0] == "/api/production-runs/7/cycle"
def test_complete_cycle_forwards_the_task(logged_in_client, monkeypatch):
"""Which task was finished is what tells the server whether the cycle closed."""
measure_mod = _with_station(monkeypatch)
with patch.object(measure_mod, "api_client") as mock_api:
mock_api.post.return_value = RUN
logged_in_client.post("/measure/api/production/7/cycle", json={"task_id": 11})
assert mock_api.post.call_args[1]["data"]["task_id"] == 11
def test_remeasure_hits_its_own_endpoint(logged_in_client, monkeypatch):
"""Turning the piece over must not go down the cycle path and restart the timer."""
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/7/remeasure", json={"task_id": 11},
)
assert resp.status_code == 200
endpoint, kwargs = mock_api.post.call_args
assert endpoint[0] == "/api/production-runs/7/remeasure"
assert kwargs["data"]["task_id"] == 11
def test_remeasure_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/remeasure", json={})
assert resp.status_code == 409
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:
@@ -157,6 +191,7 @@ def test_production_routes_require_login(client, monkeypatch):
("get", "/measure/api/production/current"),
("post", "/measure/api/production/start"),
("post", "/measure/api/production/7/cycle"),
("post", "/measure/api/production/7/remeasure"),
):
resp = getattr(client, method)(url)
assert resp.status_code in (302, 401), f"{method} {url} -> {resp.status_code}"
@@ -264,3 +264,29 @@ def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
_node_check(body, f"/measure/execute script[{i}]")
_check_alpine_attributes(html, "/measure/execute")
def test_task_list_inline_js_is_valid(logged_in_client, mock_measure_api):
"""The task list carries the shared measurement clock.
Its bindings are the ones that turn the countdown red and announce the return
to the measurement; a broken expression here would take the whole page's Alpine
with it, and the operator would simply never be called back.
"""
_force_italian(logged_in_client)
mock_measure_api.get.side_effect = [
{"id": 3, "code": "REC-3", "name": "Ricetta", "description": None},
[{
"id": 11, "order_index": 0, "title": "Quota d'ingresso",
"task_type": "measure", "subtasks": [], "file_path": None,
}],
]
resp = logged_in_client.get("/measure/tasks/3")
assert resp.status_code == 200
html = resp.get_data(as_text=True)
for i, body in enumerate(_INLINE_SCRIPT_RX.findall(html)):
_node_check(body, f"/measure/tasks script[{i}]")
_check_alpine_attributes(html, "/measure/tasks")
@@ -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 18:13+0000\n"
"POT-Creation-Date: 2026-07-28 19:28+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@@ -161,7 +161,7 @@ msgstr "Missing data: subtask_id, version_id and value are required"
msgid "Errore nel salvataggio"
msgstr "Error saving"
#: blueprints/measure.py:407 blueprints/measure.py:495
#: blueprints/measure.py:407 blueprints/measure.py:515
msgid "Username e password richiesti"
msgstr "Username and password required"
@@ -267,13 +267,13 @@ msgstr "New Station"
# Task Complete Page
#: templates/admin/stations.html:49 templates/admin/stations.html:173
#: templates/maker/recipe_editor.html:195
#: templates/measure/task_complete.html:49
#: templates/measure/task_complete.html:52
msgid "Codice"
msgstr "Code"
#: templates/admin/stations.html:50 templates/admin/stations.html:190
#: templates/maker/recipe_editor.html:213
#: templates/measure/task_complete.html:53
#: templates/measure/task_complete.html:56
msgid "Nome"
msgstr "Name"
@@ -286,7 +286,7 @@ msgid "Ricette collegate"
msgstr "Assigned recipes"
#: templates/admin/stations.html:53 templates/admin/users.html:52
#: templates/measure/task_complete.html:292
#: templates/measure/task_complete.html:295
msgid "Stato"
msgstr "Status"
@@ -375,8 +375,8 @@ msgstr "Optional notes"
#: templates/maker/task_editor.html:306 templates/maker/task_editor.html:415
#: templates/maker/task_editor.html:767 templates/maker/task_editor.html:886
#: templates/maker/task_editor.html:951 templates/maker/task_editor.html:1056
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:738
#: templates/measure/select_recipe.html:371
#: templates/measure/task_execute.html:810
msgid "Annulla"
msgstr "Cancel"
@@ -521,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:723
#: templates/measure/task_execute.html:795
msgid "Username"
msgstr "Username"
@@ -573,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:728
#: templates/measure/task_execute.html:800
msgid "Password"
msgstr "Password"
@@ -728,8 +728,8 @@ msgstr "Logout"
#: templates/components/barcode_scanner.html:21
#: templates/components/barcode_scanner.html:54
#: templates/measure/select_recipe.html:107
#: templates/measure/select_recipe.html:319
#: templates/measure/select_recipe.html:111
#: templates/measure/select_recipe.html:323
msgid "Scansiona Barcode"
msgstr "Scan Barcode"
@@ -791,8 +791,8 @@ msgid "NON CONFORME"
msgstr "NON-CONFORMING"
# Navbar
#: templates/components/navbar.html:26 templates/measure/task_complete.html:16
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
#: templates/components/navbar.html:26 templates/measure/task_complete.html:19
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:19
msgid "Misure"
msgstr "Activities"
@@ -803,7 +803,7 @@ msgstr "Activities"
msgid "Ricette"
msgstr "Recipes"
#: templates/components/navbar.html:32 templates/measure/task_complete.html:261
#: templates/components/navbar.html:32 templates/measure/task_complete.html:264
msgid "Statistiche"
msgstr "Statistics"
@@ -823,6 +823,47 @@ msgstr "Logout blocked during measurements"
msgid "Prossima misura"
msgstr "Next measurement"
#: templates/components/production_clock.html:42
#: templates/measure/task_execute.html:439
msgid "Prossima misurazione tra"
msgstr "Next measurement in"
#: templates/components/production_clock.html:49
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:442
#: templates/measure/task_execute.html:495
msgid "Misurazione in ritardo di"
msgstr "Measurement overdue by"
#: templates/components/production_clock.html:57
msgid "Linea ferma — conto alla rovescia congelato a"
msgstr "Line stopped — countdown frozen at"
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:489
msgid "Linea ferma"
msgstr "Line stopped"
#: templates/components/production_clock.html:74
#: templates/measure/task_execute.html:451
msgid "Ciclo"
msgstr "Cycle"
#: templates/components/production_clock.html:92
#: templates/measure/task_execute.html:657
msgid "È ora di misurare"
msgstr "Time to measure"
#: templates/components/production_clock.html:95
#: templates/measure/task_execute.html:659
msgid "Ritorno alla misurazione tra"
msgstr "Returning to the measurement in"
#: templates/components/production_clock.html:101
#: templates/measure/task_execute.html:664
msgid "Vai alla misura"
msgstr "Go to the measurement"
#: 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,9 +904,9 @@ 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:551 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
#: templates/measure/task_complete.html:171
#: templates/measure/task_execute.html:568 templates/measure/task_list.html:2
#: templates/measure/task_list.html:159
msgid "Task"
msgstr "Task"
@@ -893,7 +934,7 @@ msgstr "E.g. Coupling Assembly 256"
#: templates/maker/task_editor.html:521 templates/maker/task_editor.html:547
#: templates/maker/task_editor.html:562 templates/maker/task_editor.html:680
#: templates/maker/task_editor.html:1045
#: templates/measure/task_complete.html:169
#: templates/measure/task_complete.html:172
msgid "Descrizione"
msgstr "Description"
@@ -1013,8 +1054,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:1337
#: templates/measure/task_execute.html:1232
#: templates/measure/task_execute.html:1528
msgid "Errore di connessione"
msgstr "Connection Error"
@@ -1047,7 +1088,7 @@ msgid "ricetta trovata"
msgstr "recipe found"
#: templates/maker/recipe_list.html:144
#: templates/measure/select_recipe.html:175
#: templates/measure/select_recipe.html:179
msgid "ricette trovate"
msgstr "recipes found"
@@ -1065,7 +1106,7 @@ msgid "Versioni"
msgstr "Versions"
#: templates/maker/recipe_list.html:297
#: templates/measure/select_recipe.html:273
#: templates/measure/select_recipe.html:277
msgid "Nessuna ricetta trovata"
msgstr "No recipe found"
@@ -1099,7 +1140,7 @@ msgid "Anteprima Ricetta"
msgstr "Recipe Preview"
#: templates/maker/recipe_preview.html:114
#: templates/measure/task_complete.html:57
#: templates/measure/task_complete.html:60
msgid "Versione"
msgstr "Version"
@@ -1154,7 +1195,7 @@ msgstr "Measurement Points"
#: templates/maker/recipe_preview.html:299 templates/maker/task_editor.html:564
#: templates/maker/task_editor.html:706
#: templates/measure/task_complete.html:170
#: templates/measure/task_complete.html:173
#: templates/measure/task_execute.html:300
msgid "Nominale"
msgstr "Nominal"
@@ -1209,7 +1250,7 @@ msgid "Torna ai Task"
msgstr "Back to Tasks"
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
#: templates/measure/select_recipe.html:253
#: templates/measure/select_recipe.html:257
msgid "Seleziona"
msgstr "Select"
@@ -1325,18 +1366,18 @@ msgid "Tipo"
msgstr "Type"
#: templates/maker/task_editor.html:277 templates/maker/task_editor.html:534
#: templates/measure/task_list.html:174
#: templates/measure/task_list.html:177
msgid "Nota"
msgstr "Note"
#: templates/maker/task_editor.html:278 templates/maker/task_editor.html:535
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:172
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:175
msgid "Misura"
msgstr "Measure"
# Maker - Task Editor
#: templates/maker/task_editor.html:279 templates/maker/task_editor.html:536
#: templates/measure/task_list.html:173
#: templates/measure/task_list.html:176
msgid "Disegno"
msgstr "Drawing"
@@ -1367,7 +1408,7 @@ msgstr "Drag to reorder"
#: templates/maker/task_editor.html:384
#: templates/maker/version_history.html:188
#: templates/measure/task_list.html:189
#: templates/measure/task_list.html:192
msgid "misurazioni"
msgstr "measurements"
@@ -1416,7 +1457,7 @@ msgid "LTL"
msgstr "LTL"
#: templates/maker/task_editor.html:569 templates/maker/task_editor.html:714
#: templates/measure/task_complete.html:275
#: templates/measure/task_complete.html:278
msgid "Unita"
msgstr "Unit"
@@ -1633,7 +1674,7 @@ msgid "Misurazione aggiunta"
msgstr "Measurement added"
#: templates/maker/task_editor.html:1668
#: templates/measure/task_execute.html:995
#: templates/measure/task_execute.html:1078
msgid "Errore nel salvataggio della misurazione"
msgstr "Error saving measurement"
@@ -1696,289 +1737,289 @@ msgstr ""
# Measure - Recipe Selection
#: templates/measure/select_recipe.html:2
#: templates/measure/select_recipe.html:73
#: templates/measure/select_recipe.html:77
msgid "Seleziona Ricetta"
msgstr "Select Recipe"
#: templates/measure/select_recipe.html:76
#: templates/measure/select_recipe.html:80
msgid "Scegli la ricetta di misura da eseguire"
msgstr "Choose the measurement recipe to execute"
#: templates/measure/select_recipe.html:79
#: templates/measure/select_recipe.html:83
msgid "Stazione"
msgstr "Station"
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "Stazione forzata per collaudo"
msgstr "Station overridden for commissioning"
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "configurata"
msgstr "configured"
#: templates/measure/select_recipe.html:94
#: templates/measure/select_recipe.html:98
msgid "Ripristina"
msgstr "Restore"
#: templates/measure/select_recipe.html:119
#: templates/measure/select_recipe.html:123
msgid "Cerca ricetta"
msgstr "Search recipe"
#: templates/measure/select_recipe.html:123
#: templates/measure/select_recipe.html:127
msgid "Nome, codice o descrizione..."
msgstr "Name, code or description..."
#: templates/measure/select_recipe.html:136
#: templates/measure/select_recipe.html:140
msgid "Tracciabilità"
msgstr "Traceability"
#: templates/measure/select_recipe.html:137
#: templates/measure/select_recipe.html:141
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
msgstr "Data of the part to measure — fill in before selecting the recipe"
#: templates/measure/select_recipe.html:147
#: templates/measure/task_complete.html:281
#: templates/measure/select_recipe.html:151
#: templates/measure/task_complete.html:284
msgid "Numero Lotto"
msgstr "Lot Number"
#: templates/measure/select_recipe.html:151
#: templates/measure/select_recipe.html:155
msgid "Es. LOT-2026-001 (opzionale)"
msgstr "E.g. LOT-2026-001 (optional)"
#: templates/measure/select_recipe.html:161
#: templates/measure/task_complete.html:282
#: templates/measure/select_recipe.html:165
#: templates/measure/task_complete.html:285
msgid "Numero Seriale"
msgstr "Serial Number"
#: templates/measure/select_recipe.html:165
#: templates/measure/select_recipe.html:169
msgid "Es. SN-000123 (opzionale)"
msgstr "E.g. SN-000123 (optional)"
#: templates/measure/select_recipe.html:218
#: templates/measure/select_recipe.html:222
msgid "Nessuna descrizione disponibile"
msgstr "No description available"
#: templates/measure/select_recipe.html:277
#: templates/measure/select_recipe.html:281
msgid "Nessun risultato per"
msgstr "No results for"
#: templates/measure/select_recipe.html:278
#: templates/measure/select_recipe.html:282
msgid "Prova con un termine diverso."
msgstr "Try a different search term."
#: templates/measure/select_recipe.html:281
#: templates/measure/select_recipe.html:285
msgid "Non ci sono ricette disponibili al momento."
msgstr "No recipes available at the moment."
#: templates/measure/select_recipe.html:334
#: templates/measure/select_recipe.html:338
msgid ""
"Inserisci o scansiona il codice della ricetta per selezionarla "
"automaticamente."
msgstr "Enter or scan the recipe code to select it automatically."
#: templates/measure/select_recipe.html:339
#: templates/measure/select_recipe.html:343
msgid "Codice Ricetta"
msgstr "Recipe Code"
#: templates/measure/select_recipe.html:345
#: templates/measure/select_recipe.html:349
msgid "Es. REC-001"
msgstr "E.g. REC-001"
#: templates/measure/select_recipe.html:381
#: templates/measure/select_recipe.html:385
msgid "Cerca"
msgstr "Search"
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_complete.html:39
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:659
#: templates/measure/task_execute.html:718
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr "Summary"
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:636
#: templates/measure/task_complete.html:47
#: templates/measure/task_execute.html:695
msgid "Misurazioni Complete"
msgstr "Measurements Complete"
# Measure - Task List
#: templates/measure/task_complete.html:61
#: templates/measure/task_complete.html:293 templates/measure/task_list.html:94
#: templates/measure/task_complete.html:64
#: templates/measure/task_complete.html:296 templates/measure/task_list.html:97
msgid "Lotto"
msgstr "Lot"
#: templates/measure/task_complete.html:67
#: templates/measure/task_complete.html:294
#: templates/measure/task_list.html:106
#: templates/measure/task_complete.html:70
#: templates/measure/task_complete.html:297
#: templates/measure/task_list.html:109
msgid "Seriale"
msgstr "Serial"
#: templates/measure/task_complete.html:86
#: templates/measure/task_complete.html:89
msgid "Totale"
msgstr "Total"
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:644
#: templates/measure/task_complete.html:106
#: templates/measure/task_execute.html:703
msgid "Conformi"
msgstr "Pass"
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:648
#: templates/measure/task_complete.html:123
#: templates/measure/task_execute.html:707
msgid "Attenzione"
msgstr "Warning"
#: templates/measure/task_complete.html:137
#: templates/measure/task_complete.html:140
msgid "Non Conformi"
msgstr "Non-Conforming"
#: templates/measure/task_complete.html:153
#: templates/measure/task_complete.html:156
msgid "Dettaglio Misurazioni"
msgstr "Measurement Details"
#: templates/measure/task_complete.html:160
#: templates/measure/task_complete.html:163
msgid "Esporta CSV"
msgstr "Export CSV"
#: templates/measure/task_complete.html:167
#: templates/measure/task_complete.html:170
msgid "Data/Ora"
msgstr "Date/Time"
#: templates/measure/task_complete.html:171
#: templates/measure/task_complete.html:174
#: templates/statistics/dashboard.html:247
msgid "Valore"
msgstr "Value"
#: templates/measure/task_complete.html:172
#: templates/measure/task_complete.html:175
msgid "Deviazione"
msgstr "Deviation"
#: templates/measure/task_complete.html:173
#: templates/measure/task_complete.html:280
#: templates/measure/task_complete.html:176
#: templates/measure/task_complete.html:283
msgid "Esito"
msgstr "Result"
#: templates/measure/task_complete.html:174
#: templates/measure/task_complete.html:177
msgid "Metodo"
msgstr "Method"
#: templates/measure/task_complete.html:202
#: templates/measure/task_complete.html:205
#: templates/statistics/dashboard.html:147
msgid "Pass"
msgstr "Pass"
#: templates/measure/task_complete.html:204
#: templates/measure/task_complete.html:207
#: templates/statistics/dashboard.html:153
msgid "Warning"
msgstr "Warning"
#: templates/measure/task_complete.html:206
#: templates/measure/task_complete.html:209
#: templates/statistics/dashboard.html:159
msgid "Fail"
msgstr "Fail"
#: templates/measure/task_complete.html:217
#: templates/measure/task_complete.html:220
msgid "Manuale"
msgstr "Manual"
#: templates/measure/task_complete.html:224
#: templates/measure/task_complete.html:227
msgid "Calibro"
msgstr "Caliper"
#: templates/measure/task_complete.html:245 templates/measure/task_list.html:37
#: templates/measure/task_complete.html:248 templates/measure/task_list.html:40
msgid "Seleziona altra ricetta"
msgstr "Select another recipe"
#: templates/measure/task_complete.html:252
#: templates/measure/task_complete.html:255
msgid "Ripeti misurazioni"
msgstr "Repeat Measurements"
# CSV Export i18n
#: templates/measure/task_complete.html:272
#: templates/measure/task_complete.html:275
msgid "Subtask ID"
msgstr "Subtask ID"
#: templates/measure/task_complete.html:273
#: templates/measure/task_complete.html:276
msgid "Nome Sottotask"
msgstr "Subtask Name"
#: templates/measure/task_complete.html:274
#: templates/measure/task_complete.html:277
msgid "Valore Misurato"
msgstr "Measured Value"
#: templates/measure/task_complete.html:276
#: templates/measure/task_complete.html:279
msgid "Valore Nominale"
msgstr "Nominal Value"
#: templates/measure/task_complete.html:277
#: templates/measure/task_complete.html:280
msgid "Tolleranza +"
msgstr "Tolerance +"
#: templates/measure/task_complete.html:278
#: templates/measure/task_complete.html:281
msgid "Tolleranza -"
msgstr "Tolerance -"
#: templates/measure/task_complete.html:279
#: templates/measure/task_complete.html:282
msgid "Scarto"
msgstr "Deviation"
#: templates/measure/task_complete.html:283
#: templates/measure/task_complete.html:286
msgid "Metodo Input"
msgstr "Input Method"
#: templates/measure/task_complete.html:284
#: templates/measure/task_complete.html:287
msgid "Data Misurazione"
msgstr "Measurement Date"
#: templates/measure/task_complete.html:285
#: templates/measure/task_complete.html:288
msgid "Operatore"
msgstr "Operator"
#: templates/measure/task_complete.html:286
#: templates/measure/task_complete.html:289
msgid "RIEPILOGO ESECUZIONE TASK"
msgstr "TASK EXECUTION SUMMARY"
#: templates/measure/task_complete.html:287
#: templates/measure/task_complete.html:290
msgid "Task ID"
msgstr "Task ID"
#: templates/measure/task_complete.html:288
#: templates/measure/task_complete.html:291
msgid "Nome Task"
msgstr "Task Name"
#: templates/measure/task_complete.html:289
#: templates/measure/task_complete.html:292
#: templates/statistics/dashboard.html:27
msgid "Ricetta"
msgstr "Recipe"
#: templates/measure/task_complete.html:290
#: templates/measure/task_complete.html:293
msgid "Data Inizio"
msgstr "Start Date"
#: templates/measure/task_complete.html:291
#: templates/measure/task_complete.html:294
msgid "Data Fine"
msgstr "End Date"
#: templates/measure/task_complete.html:295
#: templates/measure/task_complete.html:298
msgid "STATISTICHE"
msgstr "STATISTICS"
#: templates/measure/task_complete.html:296
#: templates/measure/task_complete.html:299
msgid "Totale Misure"
msgstr "Total Measurements"
#: templates/measure/task_complete.html:297
#: templates/measure/task_complete.html:300
msgid "Passate"
msgstr "Passed"
#: templates/measure/task_complete.html:298
#: templates/measure/task_complete.html:301
msgid "Fallite"
msgstr "Failed"
#: templates/measure/task_complete.html:299
#: templates/measure/task_complete.html:302
msgid "Percentuale Successo"
msgstr "Pass Rate"
#: templates/measure/task_complete.html:300
#: templates/measure/task_complete.html:303
msgid "DETTAGLIO MISURE"
msgstr "MEASUREMENT DETAILS"
@@ -2015,157 +2056,153 @@ msgstr "Measurement"
msgid "Registrata"
msgstr "Recorded"
#: templates/measure/task_execute.html:432
msgid "Prossima misurazione tra"
msgstr "Next measurement in"
#: templates/measure/task_execute.html:437
msgid "Ciclo"
msgstr "Cycle"
#: templates/measure/task_execute.html:456
#: templates/measure/task_execute.html:470
msgid "Task di misura senza quote configurate: la ricetta è incompleta"
msgstr "Measurement task with no quotes configured: the recipe is incomplete"
#: templates/measure/task_execute.html:475
msgid "Linea ferma"
msgstr "Line stopped"
#: templates/measure/task_execute.html:478
#: templates/measure/task_execute.html:494
msgid "Il conto alla rovescia è congelato a"
msgstr "The countdown is frozen at"
#: templates/measure/task_execute.html:480
#: templates/measure/task_execute.html:497
msgid "serve il capoturno per riprendere"
msgstr "the supervisor must authorise the restart"
#: templates/measure/task_execute.html:499
#: templates/measure/task_execute.html:516
msgid "Produzione non registrata sul server"
msgstr "Production not recorded on the server"
#: templates/measure/task_execute.html:520
#: templates/measure/task_execute.html:686
#: templates/measure/task_execute.html:537
#: templates/measure/task_execute.html:746
msgid "Avvio Produzione"
msgstr "Production Start"
#: templates/measure/task_execute.html:524
#: templates/measure/task_execute.html:541
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:535
#: templates/measure/task_execute.html:552
msgid "Produzione avviata"
msgstr "Production started"
#: templates/measure/task_execute.html:580
#: templates/measure/task_execute.html:597
msgid "Fine ciclo misura"
msgstr "Measurement cycle complete"
#: templates/measure/task_execute.html:590
#: templates/measure/task_execute.html:604
#: templates/measure/task_execute.html:608
msgid "Rimisura"
msgstr "Measure again"
#: templates/measure/task_execute.html:618
#: templates/measure/task_execute.html:632
msgid "Completato"
msgstr "Completed"
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "Tutte le"
msgstr "All"
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "misurazioni sono state registrate."
msgstr "measurements have been recorded."
#: templates/measure/task_execute.html:652
#: templates/measure/task_execute.html:711
msgid "Non Conf."
msgstr "Fail"
#: templates/measure/task_execute.html:666
#: templates/measure/task_execute.html:726
msgid "Conferma ciclo"
msgstr "Confirm cycle"
#: templates/measure/task_execute.html:676
#: templates/measure/task_execute.html:736
msgid "Task successivo"
msgstr "Next task"
#: templates/measure/task_execute.html:716
#: templates/measure/task_execute.html:762
msgid "Girare il pezzo e rimisurare"
msgstr "Turn the piece over and measure again"
#: templates/measure/task_execute.html:788
msgid "Autorizzazione capoturno"
msgstr "Shift supervisor authorization"
#: templates/measure/task_execute.html:725
#: templates/measure/task_execute.html:797
msgid "Username capoturno"
msgstr "Supervisor username"
#: templates/measure/task_execute.html:747
#: templates/measure/task_execute.html:819
msgid "Autorizza"
msgstr "Authorize"
#: templates/measure/task_execute.html:1039
#: templates/measure/task_execute.html:1122
msgid "Errore di rete. Riprovare."
msgstr "Network error. Please retry."
#: templates/measure/task_execute.html:1116
#: templates/measure/task_execute.html:1227
msgid "Errore di comunicazione con il server"
msgstr "Error communicating with the server"
#: templates/measure/task_execute.html:1284
#: templates/measure/task_execute.html:1475
msgid "Misurazione fuori tolleranza"
msgstr "Measurement out of tolerance"
#: templates/measure/task_execute.html:1285
#: templates/measure/task_execute.html:1476
msgid "Fermo linea richiesto"
msgstr "Line stop requested"
#: templates/measure/task_execute.html:1286
#: templates/measure/task_execute.html:1477
msgid "Ripresa della produzione"
msgstr "Resuming production"
#: templates/measure/task_execute.html:1287
#: templates/measure/task_execute.html:1478
msgid "Fine produzione richiesta"
msgstr "End of production requested"
#: templates/measure/task_execute.html:1316
#: templates/measure/task_execute.html:1507
msgid "Credenziali non valide o utente non autorizzato"
msgstr "Invalid credentials or unauthorized user"
#: templates/measure/task_execute.html:1348
#: templates/measure/task_execute.html:1539
msgid "Nessuna produzione aperta su questa stazione"
msgstr "No production open at this station"
#: templates/measure/task_execute.html:1365
#: templates/measure/task_execute.html:1556
msgid "Azione non riuscita"
msgstr "Action failed"
#: templates/measure/task_list.html:84
#: templates/measure/task_list.html:87
msgid "AVVIA"
msgstr "START"
#: templates/measure/task_list.html:121
#: templates/measure/task_list.html:124
msgid "Task da eseguire"
msgstr "Tasks to execute"
#: templates/measure/task_list.html:133
#: templates/measure/task_list.html:136
msgid "misurazioni totali"
msgstr "total measurements"
#: templates/measure/task_list.html:175
#: templates/measure/task_list.html:178
msgid "Confronto profilo"
msgstr "Profile comparison"
#: templates/measure/task_list.html:176
#: templates/measure/task_list.html:179
msgid "Misura camera"
msgstr "Camera measurement"
#: templates/measure/task_list.html:198
#: templates/measure/task_list.html:201
msgid "Allegato"
msgstr "Attachment"
#: templates/measure/task_list.html:222
#: templates/measure/task_list.html:225
msgid "Visualizza Task"
msgstr "View Tasks"
#: templates/measure/task_list.html:245
#: templates/measure/task_list.html:248
msgid "Nessun task disponibile"
msgstr "No tasks available"
#: templates/measure/task_list.html:248
#: templates/measure/task_list.html:251
msgid "Questa ricetta non ha ancora task definiti."
msgstr "This recipe has no tasks defined yet."
@@ -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 18:13+0000\n"
"POT-Creation-Date: 2026-07-28 19:28+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: it\n"
@@ -161,7 +161,7 @@ msgstr "Dati mancanti: subtask_id, version_id e value sono obbligatori"
msgid "Errore nel salvataggio"
msgstr "Errore nel salvataggio"
#: blueprints/measure.py:407 blueprints/measure.py:495
#: blueprints/measure.py:407 blueprints/measure.py:515
msgid "Username e password richiesti"
msgstr "Username e password richiesti"
@@ -269,13 +269,13 @@ msgstr "Nuova Stazione"
# Task Complete Page
#: templates/admin/stations.html:49 templates/admin/stations.html:173
#: templates/maker/recipe_editor.html:195
#: templates/measure/task_complete.html:49
#: templates/measure/task_complete.html:52
msgid "Codice"
msgstr "Codice"
#: templates/admin/stations.html:50 templates/admin/stations.html:190
#: templates/maker/recipe_editor.html:213
#: templates/measure/task_complete.html:53
#: templates/measure/task_complete.html:56
msgid "Nome"
msgstr "Nome"
@@ -288,7 +288,7 @@ msgid "Ricette collegate"
msgstr "Ricette collegate"
#: templates/admin/stations.html:53 templates/admin/users.html:52
#: templates/measure/task_complete.html:292
#: templates/measure/task_complete.html:295
msgid "Stato"
msgstr "Stato"
@@ -377,8 +377,8 @@ msgstr "Note opzionali"
#: templates/maker/task_editor.html:306 templates/maker/task_editor.html:415
#: templates/maker/task_editor.html:767 templates/maker/task_editor.html:886
#: templates/maker/task_editor.html:951 templates/maker/task_editor.html:1056
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:738
#: templates/measure/select_recipe.html:371
#: templates/measure/task_execute.html:810
msgid "Annulla"
msgstr "Annulla"
@@ -523,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:723
#: templates/measure/task_execute.html:795
msgid "Username"
msgstr "Username"
@@ -575,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:728
#: templates/measure/task_execute.html:800
msgid "Password"
msgstr "Password"
@@ -730,8 +730,8 @@ msgstr "Logout"
#: templates/components/barcode_scanner.html:21
#: templates/components/barcode_scanner.html:54
#: templates/measure/select_recipe.html:107
#: templates/measure/select_recipe.html:319
#: templates/measure/select_recipe.html:111
#: templates/measure/select_recipe.html:323
msgid "Scansiona Barcode"
msgstr "Scansiona Barcode"
@@ -793,8 +793,8 @@ msgid "NON CONFORME"
msgstr "NON CONFORME"
# Navbar
#: templates/components/navbar.html:26 templates/measure/task_complete.html:16
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
#: templates/components/navbar.html:26 templates/measure/task_complete.html:19
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:19
msgid "Misure"
msgstr "Attività"
@@ -805,7 +805,7 @@ msgstr "Attività"
msgid "Ricette"
msgstr "Ricette"
#: templates/components/navbar.html:32 templates/measure/task_complete.html:261
#: templates/components/navbar.html:32 templates/measure/task_complete.html:264
msgid "Statistiche"
msgstr "Statistiche"
@@ -825,6 +825,47 @@ msgstr "Logout bloccato durante le misurazioni"
msgid "Prossima misura"
msgstr "Prossima misura"
#: templates/components/production_clock.html:42
#: templates/measure/task_execute.html:439
msgid "Prossima misurazione tra"
msgstr "Prossima misurazione tra"
#: templates/components/production_clock.html:49
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:442
#: templates/measure/task_execute.html:495
msgid "Misurazione in ritardo di"
msgstr "Misurazione in ritardo di"
#: templates/components/production_clock.html:57
msgid "Linea ferma — conto alla rovescia congelato a"
msgstr "Linea ferma — conto alla rovescia congelato a"
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:489
msgid "Linea ferma"
msgstr "Linea ferma"
#: templates/components/production_clock.html:74
#: templates/measure/task_execute.html:451
msgid "Ciclo"
msgstr "Ciclo"
#: templates/components/production_clock.html:92
#: templates/measure/task_execute.html:657
msgid "È ora di misurare"
msgstr "È ora di misurare"
#: templates/components/production_clock.html:95
#: templates/measure/task_execute.html:659
msgid "Ritorno alla misurazione tra"
msgstr "Ritorno alla misurazione tra"
#: templates/components/production_clock.html:101
#: templates/measure/task_execute.html:664
msgid "Vai alla misura"
msgstr "Vai alla misura"
#: 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,9 +906,9 @@ 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:551 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
#: templates/measure/task_complete.html:171
#: templates/measure/task_execute.html:568 templates/measure/task_list.html:2
#: templates/measure/task_list.html:159
msgid "Task"
msgstr "Task"
@@ -895,7 +936,7 @@ msgstr "Es. Coupling Assembly 256"
#: templates/maker/task_editor.html:521 templates/maker/task_editor.html:547
#: templates/maker/task_editor.html:562 templates/maker/task_editor.html:680
#: templates/maker/task_editor.html:1045
#: templates/measure/task_complete.html:169
#: templates/measure/task_complete.html:172
msgid "Descrizione"
msgstr "Descrizione"
@@ -1015,8 +1056,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:1337
#: templates/measure/task_execute.html:1232
#: templates/measure/task_execute.html:1528
msgid "Errore di connessione"
msgstr "Errore di connessione"
@@ -1049,7 +1090,7 @@ msgid "ricetta trovata"
msgstr "ricetta trovata"
#: templates/maker/recipe_list.html:144
#: templates/measure/select_recipe.html:175
#: templates/measure/select_recipe.html:179
msgid "ricette trovate"
msgstr "ricette trovate"
@@ -1067,7 +1108,7 @@ msgid "Versioni"
msgstr "Versioni"
#: templates/maker/recipe_list.html:297
#: templates/measure/select_recipe.html:273
#: templates/measure/select_recipe.html:277
msgid "Nessuna ricetta trovata"
msgstr "Nessuna ricetta trovata"
@@ -1101,7 +1142,7 @@ msgid "Anteprima Ricetta"
msgstr "Anteprima Ricetta"
#: templates/maker/recipe_preview.html:114
#: templates/measure/task_complete.html:57
#: templates/measure/task_complete.html:60
msgid "Versione"
msgstr "Versione"
@@ -1156,7 +1197,7 @@ msgstr "Punti di Misura"
#: templates/maker/recipe_preview.html:299 templates/maker/task_editor.html:564
#: templates/maker/task_editor.html:706
#: templates/measure/task_complete.html:170
#: templates/measure/task_complete.html:173
#: templates/measure/task_execute.html:300
msgid "Nominale"
msgstr "Nominale"
@@ -1211,7 +1252,7 @@ msgid "Torna ai Task"
msgstr "Torna ai Task"
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
#: templates/measure/select_recipe.html:253
#: templates/measure/select_recipe.html:257
msgid "Seleziona"
msgstr "Seleziona"
@@ -1327,18 +1368,18 @@ msgid "Tipo"
msgstr "Tipo"
#: templates/maker/task_editor.html:277 templates/maker/task_editor.html:534
#: templates/measure/task_list.html:174
#: templates/measure/task_list.html:177
msgid "Nota"
msgstr "Nota"
#: templates/maker/task_editor.html:278 templates/maker/task_editor.html:535
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:172
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:175
msgid "Misura"
msgstr "Misura"
# Maker - Task Editor
#: templates/maker/task_editor.html:279 templates/maker/task_editor.html:536
#: templates/measure/task_list.html:173
#: templates/measure/task_list.html:176
msgid "Disegno"
msgstr "Disegno"
@@ -1369,7 +1410,7 @@ msgstr "Trascina per riordinare"
#: templates/maker/task_editor.html:384
#: templates/maker/version_history.html:188
#: templates/measure/task_list.html:189
#: templates/measure/task_list.html:192
msgid "misurazioni"
msgstr "misurazioni"
@@ -1418,7 +1459,7 @@ msgid "LTL"
msgstr "LTL"
#: templates/maker/task_editor.html:569 templates/maker/task_editor.html:714
#: templates/measure/task_complete.html:275
#: templates/measure/task_complete.html:278
msgid "Unita"
msgstr "Unita"
@@ -1635,7 +1676,7 @@ msgid "Misurazione aggiunta"
msgstr "Misurazione aggiunta"
#: templates/maker/task_editor.html:1668
#: templates/measure/task_execute.html:995
#: templates/measure/task_execute.html:1078
msgid "Errore nel salvataggio della misurazione"
msgstr "Errore nel salvataggio della misurazione"
@@ -1698,81 +1739,81 @@ msgstr ""
# Measure - Recipe Selection
#: templates/measure/select_recipe.html:2
#: templates/measure/select_recipe.html:73
#: templates/measure/select_recipe.html:77
msgid "Seleziona Ricetta"
msgstr "Seleziona Ricetta"
#: templates/measure/select_recipe.html:76
#: templates/measure/select_recipe.html:80
msgid "Scegli la ricetta di misura da eseguire"
msgstr "Scegli la ricetta di misura da eseguire"
#: templates/measure/select_recipe.html:79
#: templates/measure/select_recipe.html:83
msgid "Stazione"
msgstr "Stazione"
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "Stazione forzata per collaudo"
msgstr "Stazione forzata per collaudo"
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "configurata"
msgstr "configurata"
#: templates/measure/select_recipe.html:94
#: templates/measure/select_recipe.html:98
msgid "Ripristina"
msgstr "Ripristina"
#: templates/measure/select_recipe.html:119
#: templates/measure/select_recipe.html:123
msgid "Cerca ricetta"
msgstr "Cerca ricetta"
#: templates/measure/select_recipe.html:123
#: templates/measure/select_recipe.html:127
msgid "Nome, codice o descrizione..."
msgstr "Nome, codice o descrizione..."
#: templates/measure/select_recipe.html:136
#: templates/measure/select_recipe.html:140
msgid "Tracciabilità"
msgstr "Tracciabilità"
#: templates/measure/select_recipe.html:137
#: templates/measure/select_recipe.html:141
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
msgstr "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
#: templates/measure/select_recipe.html:147
#: templates/measure/task_complete.html:281
#: templates/measure/select_recipe.html:151
#: templates/measure/task_complete.html:284
msgid "Numero Lotto"
msgstr "Numero Lotto"
#: templates/measure/select_recipe.html:151
#: templates/measure/select_recipe.html:155
msgid "Es. LOT-2026-001 (opzionale)"
msgstr "Es. LOT-2026-001 (opzionale)"
#: templates/measure/select_recipe.html:161
#: templates/measure/task_complete.html:282
#: templates/measure/select_recipe.html:165
#: templates/measure/task_complete.html:285
msgid "Numero Seriale"
msgstr "Numero Seriale"
#: templates/measure/select_recipe.html:165
#: templates/measure/select_recipe.html:169
msgid "Es. SN-000123 (opzionale)"
msgstr "Es. SN-000123 (opzionale)"
#: templates/measure/select_recipe.html:218
#: templates/measure/select_recipe.html:222
msgid "Nessuna descrizione disponibile"
msgstr "Nessuna descrizione disponibile"
#: templates/measure/select_recipe.html:277
#: templates/measure/select_recipe.html:281
msgid "Nessun risultato per"
msgstr "Nessun risultato per"
#: templates/measure/select_recipe.html:278
#: templates/measure/select_recipe.html:282
msgid "Prova con un termine diverso."
msgstr "Prova con un termine diverso."
#: templates/measure/select_recipe.html:281
#: templates/measure/select_recipe.html:285
msgid "Non ci sono ricette disponibili al momento."
msgstr "Non ci sono ricette disponibili al momento."
#: templates/measure/select_recipe.html:334
#: templates/measure/select_recipe.html:338
msgid ""
"Inserisci o scansiona il codice della ricetta per selezionarla "
"automaticamente."
@@ -1780,209 +1821,209 @@ msgstr ""
"Inserisci o scansiona il codice della ricetta per selezionarla "
"automaticamente."
#: templates/measure/select_recipe.html:339
#: templates/measure/select_recipe.html:343
msgid "Codice Ricetta"
msgstr "Codice Ricetta"
#: templates/measure/select_recipe.html:345
#: templates/measure/select_recipe.html:349
msgid "Es. REC-001"
msgstr "Es. REC-001"
#: templates/measure/select_recipe.html:381
#: templates/measure/select_recipe.html:385
msgid "Cerca"
msgstr "Cerca"
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_complete.html:39
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:659
#: templates/measure/task_execute.html:718
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr "Riepilogo"
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:636
#: templates/measure/task_complete.html:47
#: templates/measure/task_execute.html:695
msgid "Misurazioni Complete"
msgstr "Misurazioni Complete"
# Measure - Task List
#: templates/measure/task_complete.html:61
#: templates/measure/task_complete.html:293 templates/measure/task_list.html:94
#: templates/measure/task_complete.html:64
#: templates/measure/task_complete.html:296 templates/measure/task_list.html:97
msgid "Lotto"
msgstr "Lotto"
#: templates/measure/task_complete.html:67
#: templates/measure/task_complete.html:294
#: templates/measure/task_list.html:106
#: templates/measure/task_complete.html:70
#: templates/measure/task_complete.html:297
#: templates/measure/task_list.html:109
msgid "Seriale"
msgstr "Seriale"
#: templates/measure/task_complete.html:86
#: templates/measure/task_complete.html:89
msgid "Totale"
msgstr "Totale"
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:644
#: templates/measure/task_complete.html:106
#: templates/measure/task_execute.html:703
msgid "Conformi"
msgstr "Conformi"
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:648
#: templates/measure/task_complete.html:123
#: templates/measure/task_execute.html:707
msgid "Attenzione"
msgstr "Attenzione"
#: templates/measure/task_complete.html:137
#: templates/measure/task_complete.html:140
msgid "Non Conformi"
msgstr "Non Conformi"
#: templates/measure/task_complete.html:153
#: templates/measure/task_complete.html:156
msgid "Dettaglio Misurazioni"
msgstr "Dettaglio Misurazioni"
#: templates/measure/task_complete.html:160
#: templates/measure/task_complete.html:163
msgid "Esporta CSV"
msgstr "Esporta CSV"
#: templates/measure/task_complete.html:167
#: templates/measure/task_complete.html:170
msgid "Data/Ora"
msgstr "Data/Ora"
#: templates/measure/task_complete.html:171
#: templates/measure/task_complete.html:174
#: templates/statistics/dashboard.html:247
msgid "Valore"
msgstr "Valore"
#: templates/measure/task_complete.html:172
#: templates/measure/task_complete.html:175
msgid "Deviazione"
msgstr "Deviazione"
#: templates/measure/task_complete.html:173
#: templates/measure/task_complete.html:280
#: templates/measure/task_complete.html:176
#: templates/measure/task_complete.html:283
msgid "Esito"
msgstr "Esito"
#: templates/measure/task_complete.html:174
#: templates/measure/task_complete.html:177
msgid "Metodo"
msgstr "Metodo"
#: templates/measure/task_complete.html:202
#: templates/measure/task_complete.html:205
#: templates/statistics/dashboard.html:147
msgid "Pass"
msgstr "Pass"
#: templates/measure/task_complete.html:204
#: templates/measure/task_complete.html:207
#: templates/statistics/dashboard.html:153
msgid "Warning"
msgstr "Warning"
#: templates/measure/task_complete.html:206
#: templates/measure/task_complete.html:209
#: templates/statistics/dashboard.html:159
msgid "Fail"
msgstr "Fail"
#: templates/measure/task_complete.html:217
#: templates/measure/task_complete.html:220
msgid "Manuale"
msgstr "Manuale"
#: templates/measure/task_complete.html:224
#: templates/measure/task_complete.html:227
msgid "Calibro"
msgstr "Calibro"
#: templates/measure/task_complete.html:245 templates/measure/task_list.html:37
#: templates/measure/task_complete.html:248 templates/measure/task_list.html:40
msgid "Seleziona altra ricetta"
msgstr "Seleziona altra ricetta"
#: templates/measure/task_complete.html:252
#: templates/measure/task_complete.html:255
msgid "Ripeti misurazioni"
msgstr "Ripeti misurazioni"
# CSV Export i18n
#: templates/measure/task_complete.html:272
#: templates/measure/task_complete.html:275
msgid "Subtask ID"
msgstr "Subtask ID"
#: templates/measure/task_complete.html:273
#: templates/measure/task_complete.html:276
msgid "Nome Sottotask"
msgstr "Nome Sottotask"
#: templates/measure/task_complete.html:274
#: templates/measure/task_complete.html:277
msgid "Valore Misurato"
msgstr "Valore Misurato"
#: templates/measure/task_complete.html:276
#: templates/measure/task_complete.html:279
msgid "Valore Nominale"
msgstr "Valore Nominale"
#: templates/measure/task_complete.html:277
#: templates/measure/task_complete.html:280
msgid "Tolleranza +"
msgstr "Tolleranza +"
#: templates/measure/task_complete.html:278
#: templates/measure/task_complete.html:281
msgid "Tolleranza -"
msgstr "Tolleranza -"
#: templates/measure/task_complete.html:279
#: templates/measure/task_complete.html:282
msgid "Scarto"
msgstr "Scarto"
#: templates/measure/task_complete.html:283
#: templates/measure/task_complete.html:286
msgid "Metodo Input"
msgstr "Metodo Input"
#: templates/measure/task_complete.html:284
#: templates/measure/task_complete.html:287
msgid "Data Misurazione"
msgstr "Data Misurazione"
#: templates/measure/task_complete.html:285
#: templates/measure/task_complete.html:288
msgid "Operatore"
msgstr "Operatore"
#: templates/measure/task_complete.html:286
#: templates/measure/task_complete.html:289
msgid "RIEPILOGO ESECUZIONE TASK"
msgstr "RIEPILOGO ESECUZIONE TASK"
#: templates/measure/task_complete.html:287
#: templates/measure/task_complete.html:290
msgid "Task ID"
msgstr "Task ID"
#: templates/measure/task_complete.html:288
#: templates/measure/task_complete.html:291
msgid "Nome Task"
msgstr "Nome Task"
#: templates/measure/task_complete.html:289
#: templates/measure/task_complete.html:292
#: templates/statistics/dashboard.html:27
msgid "Ricetta"
msgstr "Ricetta"
#: templates/measure/task_complete.html:290
#: templates/measure/task_complete.html:293
msgid "Data Inizio"
msgstr "Data Inizio"
#: templates/measure/task_complete.html:291
#: templates/measure/task_complete.html:294
msgid "Data Fine"
msgstr "Data Fine"
#: templates/measure/task_complete.html:295
#: templates/measure/task_complete.html:298
msgid "STATISTICHE"
msgstr "STATISTICHE"
#: templates/measure/task_complete.html:296
#: templates/measure/task_complete.html:299
msgid "Totale Misure"
msgstr "Totale Misure"
#: templates/measure/task_complete.html:297
#: templates/measure/task_complete.html:300
msgid "Passate"
msgstr "Passate"
#: templates/measure/task_complete.html:298
#: templates/measure/task_complete.html:301
msgid "Fallite"
msgstr "Fallite"
#: templates/measure/task_complete.html:299
#: templates/measure/task_complete.html:302
msgid "Percentuale Successo"
msgstr "Percentuale Successo"
#: templates/measure/task_complete.html:300
#: templates/measure/task_complete.html:303
msgid "DETTAGLIO MISURE"
msgstr "DETTAGLIO MISURE"
@@ -2019,157 +2060,153 @@ msgstr "Misurazione"
msgid "Registrata"
msgstr "Registrata"
#: templates/measure/task_execute.html:432
msgid "Prossima misurazione tra"
msgstr "Prossima misurazione tra"
#: templates/measure/task_execute.html:437
msgid "Ciclo"
msgstr "Ciclo"
#: templates/measure/task_execute.html:456
#: templates/measure/task_execute.html:470
msgid "Task di misura senza quote configurate: la ricetta è incompleta"
msgstr "Task di misura senza quote configurate: la ricetta è incompleta"
#: templates/measure/task_execute.html:475
msgid "Linea ferma"
msgstr "Linea ferma"
#: templates/measure/task_execute.html:478
#: templates/measure/task_execute.html:494
msgid "Il conto alla rovescia è congelato a"
msgstr "Il conto alla rovescia è congelato a"
#: templates/measure/task_execute.html:480
#: templates/measure/task_execute.html:497
msgid "serve il capoturno per riprendere"
msgstr "serve il capoturno per riprendere"
#: templates/measure/task_execute.html:499
#: templates/measure/task_execute.html:516
msgid "Produzione non registrata sul server"
msgstr "Produzione non registrata sul server"
#: templates/measure/task_execute.html:520
#: templates/measure/task_execute.html:686
#: templates/measure/task_execute.html:537
#: templates/measure/task_execute.html:746
msgid "Avvio Produzione"
msgstr "Avvio Produzione"
#: templates/measure/task_execute.html:524
#: templates/measure/task_execute.html:541
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:535
#: templates/measure/task_execute.html:552
msgid "Produzione avviata"
msgstr "Produzione avviata"
#: templates/measure/task_execute.html:580
#: templates/measure/task_execute.html:597
msgid "Fine ciclo misura"
msgstr "Fine ciclo misura"
#: templates/measure/task_execute.html:590
#: templates/measure/task_execute.html:604
#: templates/measure/task_execute.html:608
msgid "Rimisura"
msgstr "Rimisura"
#: templates/measure/task_execute.html:618
#: templates/measure/task_execute.html:632
msgid "Completato"
msgstr "Completato"
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "Tutte le"
msgstr "Tutte le"
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "misurazioni sono state registrate."
msgstr "misurazioni sono state registrate."
#: templates/measure/task_execute.html:652
#: templates/measure/task_execute.html:711
msgid "Non Conf."
msgstr "Non Conf."
#: templates/measure/task_execute.html:666
#: templates/measure/task_execute.html:726
msgid "Conferma ciclo"
msgstr "Conferma ciclo"
#: templates/measure/task_execute.html:676
#: templates/measure/task_execute.html:736
msgid "Task successivo"
msgstr "Task successivo"
#: templates/measure/task_execute.html:716
#: templates/measure/task_execute.html:762
msgid "Girare il pezzo e rimisurare"
msgstr "Girare il pezzo e rimisurare"
#: templates/measure/task_execute.html:788
msgid "Autorizzazione capoturno"
msgstr "Autorizzazione capoturno"
#: templates/measure/task_execute.html:725
#: templates/measure/task_execute.html:797
msgid "Username capoturno"
msgstr "Username capoturno"
#: templates/measure/task_execute.html:747
#: templates/measure/task_execute.html:819
msgid "Autorizza"
msgstr "Autorizza"
#: templates/measure/task_execute.html:1039
#: templates/measure/task_execute.html:1122
msgid "Errore di rete. Riprovare."
msgstr "Errore di rete. Riprovare."
#: templates/measure/task_execute.html:1116
#: templates/measure/task_execute.html:1227
msgid "Errore di comunicazione con il server"
msgstr "Errore di comunicazione con il server"
#: templates/measure/task_execute.html:1284
#: templates/measure/task_execute.html:1475
msgid "Misurazione fuori tolleranza"
msgstr "Misurazione fuori tolleranza"
#: templates/measure/task_execute.html:1285
#: templates/measure/task_execute.html:1476
msgid "Fermo linea richiesto"
msgstr "Fermo linea richiesto"
#: templates/measure/task_execute.html:1286
#: templates/measure/task_execute.html:1477
msgid "Ripresa della produzione"
msgstr "Ripresa della produzione"
#: templates/measure/task_execute.html:1287
#: templates/measure/task_execute.html:1478
msgid "Fine produzione richiesta"
msgstr "Fine produzione richiesta"
#: templates/measure/task_execute.html:1316
#: templates/measure/task_execute.html:1507
msgid "Credenziali non valide o utente non autorizzato"
msgstr "Credenziali non valide o utente non autorizzato"
#: templates/measure/task_execute.html:1348
#: templates/measure/task_execute.html:1539
msgid "Nessuna produzione aperta su questa stazione"
msgstr "Nessuna produzione aperta su questa stazione"
#: templates/measure/task_execute.html:1365
#: templates/measure/task_execute.html:1556
msgid "Azione non riuscita"
msgstr "Azione non riuscita"
#: templates/measure/task_list.html:84
#: templates/measure/task_list.html:87
msgid "AVVIA"
msgstr "AVVIA"
#: templates/measure/task_list.html:121
#: templates/measure/task_list.html:124
msgid "Task da eseguire"
msgstr "Task da eseguire"
#: templates/measure/task_list.html:133
#: templates/measure/task_list.html:136
msgid "misurazioni totali"
msgstr "misurazioni totali"
#: templates/measure/task_list.html:175
#: templates/measure/task_list.html:178
msgid "Confronto profilo"
msgstr "Confronto profilo"
#: templates/measure/task_list.html:176
#: templates/measure/task_list.html:179
msgid "Misura camera"
msgstr "Misura camera"
#: templates/measure/task_list.html:198
#: templates/measure/task_list.html:201
msgid "Allegato"
msgstr "Allegato"
#: templates/measure/task_list.html:222
#: templates/measure/task_list.html:225
msgid "Visualizza Task"
msgstr "Visualizza Task"
#: templates/measure/task_list.html:245
#: templates/measure/task_list.html:248
msgid "Nessun task disponibile"
msgstr "Nessun task disponibile"
#: templates/measure/task_list.html:248
#: templates/measure/task_list.html:251
msgid "Questa ricetta non ha ancora task definiti."
msgstr "Questa ricetta non ha ancora task definiti."
+193 -156
View File
@@ -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 18:13+0000\n"
"POT-Creation-Date: 2026-07-28 19:28+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"
@@ -158,7 +158,7 @@ msgstr ""
msgid "Errore nel salvataggio"
msgstr ""
#: blueprints/measure.py:407 blueprints/measure.py:495
#: blueprints/measure.py:407 blueprints/measure.py:515
msgid "Username e password richiesti"
msgstr ""
@@ -261,13 +261,13 @@ msgstr ""
#: templates/admin/stations.html:49 templates/admin/stations.html:173
#: templates/maker/recipe_editor.html:195
#: templates/measure/task_complete.html:49
#: templates/measure/task_complete.html:52
msgid "Codice"
msgstr ""
#: templates/admin/stations.html:50 templates/admin/stations.html:190
#: templates/maker/recipe_editor.html:213
#: templates/measure/task_complete.html:53
#: templates/measure/task_complete.html:56
msgid "Nome"
msgstr ""
@@ -280,7 +280,7 @@ msgid "Ricette collegate"
msgstr ""
#: templates/admin/stations.html:53 templates/admin/users.html:52
#: templates/measure/task_complete.html:292
#: templates/measure/task_complete.html:295
msgid "Stato"
msgstr ""
@@ -368,8 +368,8 @@ msgstr ""
#: templates/maker/task_editor.html:306 templates/maker/task_editor.html:415
#: templates/maker/task_editor.html:767 templates/maker/task_editor.html:886
#: templates/maker/task_editor.html:951 templates/maker/task_editor.html:1056
#: templates/measure/select_recipe.html:367
#: templates/measure/task_execute.html:738
#: templates/measure/select_recipe.html:371
#: templates/measure/task_execute.html:810
msgid "Annulla"
msgstr ""
@@ -513,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:723
#: templates/measure/task_execute.html:795
msgid "Username"
msgstr ""
@@ -565,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:728
#: templates/measure/task_execute.html:800
msgid "Password"
msgstr ""
@@ -716,8 +716,8 @@ msgstr ""
#: templates/components/barcode_scanner.html:21
#: templates/components/barcode_scanner.html:54
#: templates/measure/select_recipe.html:107
#: templates/measure/select_recipe.html:319
#: templates/measure/select_recipe.html:111
#: templates/measure/select_recipe.html:323
msgid "Scansiona Barcode"
msgstr ""
@@ -774,8 +774,8 @@ msgstr ""
msgid "NON CONFORME"
msgstr ""
#: templates/components/navbar.html:26 templates/measure/task_complete.html:16
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
#: templates/components/navbar.html:26 templates/measure/task_complete.html:19
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:19
msgid "Misure"
msgstr ""
@@ -786,7 +786,7 @@ msgstr ""
msgid "Ricette"
msgstr ""
#: templates/components/navbar.html:32 templates/measure/task_complete.html:261
#: templates/components/navbar.html:32 templates/measure/task_complete.html:264
msgid "Statistiche"
msgstr ""
@@ -806,6 +806,47 @@ msgstr ""
msgid "Prossima misura"
msgstr ""
#: templates/components/production_clock.html:42
#: templates/measure/task_execute.html:439
msgid "Prossima misurazione tra"
msgstr ""
#: templates/components/production_clock.html:49
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:442
#: templates/measure/task_execute.html:495
msgid "Misurazione in ritardo di"
msgstr ""
#: templates/components/production_clock.html:57
msgid "Linea ferma — conto alla rovescia congelato a"
msgstr ""
#: templates/components/production_clock.html:62
#: templates/measure/task_execute.html:489
msgid "Linea ferma"
msgstr ""
#: templates/components/production_clock.html:74
#: templates/measure/task_execute.html:451
msgid "Ciclo"
msgstr ""
#: templates/components/production_clock.html:92
#: templates/measure/task_execute.html:657
msgid "È ora di misurare"
msgstr ""
#: templates/components/production_clock.html:95
#: templates/measure/task_execute.html:659
msgid "Ritorno alla misurazione tra"
msgstr ""
#: templates/components/production_clock.html:101
#: templates/measure/task_execute.html:664
msgid "Vai alla misura"
msgstr ""
#: templates/errors/station_not_configured.html:20
msgid "Questo client non ha impostato la variabile di ambiente STATION_CODE."
msgstr ""
@@ -842,9 +883,9 @@ 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:551 templates/measure/task_list.html:2
#: templates/measure/task_list.html:156
#: templates/measure/task_complete.html:171
#: templates/measure/task_execute.html:568 templates/measure/task_list.html:2
#: templates/measure/task_list.html:159
msgid "Task"
msgstr ""
@@ -872,7 +913,7 @@ msgstr ""
#: templates/maker/task_editor.html:521 templates/maker/task_editor.html:547
#: templates/maker/task_editor.html:562 templates/maker/task_editor.html:680
#: templates/maker/task_editor.html:1045
#: templates/measure/task_complete.html:169
#: templates/measure/task_complete.html:172
msgid "Descrizione"
msgstr ""
@@ -990,8 +1031,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:1337
#: templates/measure/task_execute.html:1232
#: templates/measure/task_execute.html:1528
msgid "Errore di connessione"
msgstr ""
@@ -1024,7 +1065,7 @@ msgid "ricetta trovata"
msgstr ""
#: templates/maker/recipe_list.html:144
#: templates/measure/select_recipe.html:175
#: templates/measure/select_recipe.html:179
msgid "ricette trovate"
msgstr ""
@@ -1042,7 +1083,7 @@ msgid "Versioni"
msgstr ""
#: templates/maker/recipe_list.html:297
#: templates/measure/select_recipe.html:273
#: templates/measure/select_recipe.html:277
msgid "Nessuna ricetta trovata"
msgstr ""
@@ -1075,7 +1116,7 @@ msgid "Anteprima Ricetta"
msgstr ""
#: templates/maker/recipe_preview.html:114
#: templates/measure/task_complete.html:57
#: templates/measure/task_complete.html:60
msgid "Versione"
msgstr ""
@@ -1128,7 +1169,7 @@ msgstr ""
#: templates/maker/recipe_preview.html:299 templates/maker/task_editor.html:564
#: templates/maker/task_editor.html:706
#: templates/measure/task_complete.html:170
#: templates/measure/task_complete.html:173
#: templates/measure/task_execute.html:300
msgid "Nominale"
msgstr ""
@@ -1181,7 +1222,7 @@ msgid "Torna ai Task"
msgstr ""
#: templates/maker/task_drawing.html:205 templates/maker/task_drawing.html:209
#: templates/measure/select_recipe.html:253
#: templates/measure/select_recipe.html:257
msgid "Seleziona"
msgstr ""
@@ -1295,17 +1336,17 @@ msgid "Tipo"
msgstr ""
#: templates/maker/task_editor.html:277 templates/maker/task_editor.html:534
#: templates/measure/task_list.html:174
#: templates/measure/task_list.html:177
msgid "Nota"
msgstr ""
#: templates/maker/task_editor.html:278 templates/maker/task_editor.html:535
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:172
#: templates/measure/task_execute.html:288 templates/measure/task_list.html:175
msgid "Misura"
msgstr ""
#: templates/maker/task_editor.html:279 templates/maker/task_editor.html:536
#: templates/measure/task_list.html:173
#: templates/measure/task_list.html:176
msgid "Disegno"
msgstr ""
@@ -1336,7 +1377,7 @@ msgstr ""
#: templates/maker/task_editor.html:384
#: templates/maker/version_history.html:188
#: templates/measure/task_list.html:189
#: templates/measure/task_list.html:192
msgid "misurazioni"
msgstr ""
@@ -1385,7 +1426,7 @@ msgid "LTL"
msgstr ""
#: templates/maker/task_editor.html:569 templates/maker/task_editor.html:714
#: templates/measure/task_complete.html:275
#: templates/measure/task_complete.html:278
msgid "Unita"
msgstr ""
@@ -1602,7 +1643,7 @@ msgid "Misurazione aggiunta"
msgstr ""
#: templates/maker/task_editor.html:1668
#: templates/measure/task_execute.html:995
#: templates/measure/task_execute.html:1078
msgid "Errore nel salvataggio della misurazione"
msgstr ""
@@ -1661,287 +1702,287 @@ msgid ""
msgstr ""
#: templates/measure/select_recipe.html:2
#: templates/measure/select_recipe.html:73
#: templates/measure/select_recipe.html:77
msgid "Seleziona Ricetta"
msgstr ""
#: templates/measure/select_recipe.html:76
#: templates/measure/select_recipe.html:80
msgid "Scegli la ricetta di misura da eseguire"
msgstr ""
#: templates/measure/select_recipe.html:79
#: templates/measure/select_recipe.html:83
msgid "Stazione"
msgstr ""
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "Stazione forzata per collaudo"
msgstr ""
#: templates/measure/select_recipe.html:89
#: templates/measure/select_recipe.html:93
msgid "configurata"
msgstr ""
#: templates/measure/select_recipe.html:94
#: templates/measure/select_recipe.html:98
msgid "Ripristina"
msgstr ""
#: templates/measure/select_recipe.html:119
#: templates/measure/select_recipe.html:123
msgid "Cerca ricetta"
msgstr ""
#: templates/measure/select_recipe.html:123
#: templates/measure/select_recipe.html:127
msgid "Nome, codice o descrizione..."
msgstr ""
#: templates/measure/select_recipe.html:136
#: templates/measure/select_recipe.html:140
msgid "Tracciabilità"
msgstr ""
#: templates/measure/select_recipe.html:137
#: templates/measure/select_recipe.html:141
msgid "Dati del pezzo da misurare — compila prima di selezionare la ricetta"
msgstr ""
#: templates/measure/select_recipe.html:147
#: templates/measure/task_complete.html:281
#: templates/measure/select_recipe.html:151
#: templates/measure/task_complete.html:284
msgid "Numero Lotto"
msgstr ""
#: templates/measure/select_recipe.html:151
#: templates/measure/select_recipe.html:155
msgid "Es. LOT-2026-001 (opzionale)"
msgstr ""
#: templates/measure/select_recipe.html:161
#: templates/measure/task_complete.html:282
#: templates/measure/select_recipe.html:165
#: templates/measure/task_complete.html:285
msgid "Numero Seriale"
msgstr ""
#: templates/measure/select_recipe.html:165
#: templates/measure/select_recipe.html:169
msgid "Es. SN-000123 (opzionale)"
msgstr ""
#: templates/measure/select_recipe.html:218
#: templates/measure/select_recipe.html:222
msgid "Nessuna descrizione disponibile"
msgstr ""
#: templates/measure/select_recipe.html:277
#: templates/measure/select_recipe.html:281
msgid "Nessun risultato per"
msgstr ""
#: templates/measure/select_recipe.html:278
#: templates/measure/select_recipe.html:282
msgid "Prova con un termine diverso."
msgstr ""
#: templates/measure/select_recipe.html:281
#: templates/measure/select_recipe.html:285
msgid "Non ci sono ricette disponibili al momento."
msgstr ""
#: templates/measure/select_recipe.html:334
#: templates/measure/select_recipe.html:338
msgid ""
"Inserisci o scansiona il codice della ricetta per selezionarla "
"automaticamente."
msgstr ""
#: templates/measure/select_recipe.html:339
#: templates/measure/select_recipe.html:343
msgid "Codice Ricetta"
msgstr ""
#: templates/measure/select_recipe.html:345
#: templates/measure/select_recipe.html:349
msgid "Es. REC-001"
msgstr ""
#: templates/measure/select_recipe.html:381
#: templates/measure/select_recipe.html:385
msgid "Cerca"
msgstr ""
#: templates/measure/task_complete.html:3
#: templates/measure/task_complete.html:36
#: templates/measure/task_complete.html:39
#: templates/measure/task_execute.html:112
#: templates/measure/task_execute.html:659
#: templates/measure/task_execute.html:718
#: templates/statistics/dashboard.html:139
msgid "Riepilogo"
msgstr ""
#: templates/measure/task_complete.html:44
#: templates/measure/task_execute.html:636
#: templates/measure/task_complete.html:47
#: templates/measure/task_execute.html:695
msgid "Misurazioni Complete"
msgstr ""
#: templates/measure/task_complete.html:61
#: templates/measure/task_complete.html:293 templates/measure/task_list.html:94
#: templates/measure/task_complete.html:64
#: templates/measure/task_complete.html:296 templates/measure/task_list.html:97
msgid "Lotto"
msgstr ""
#: templates/measure/task_complete.html:67
#: templates/measure/task_complete.html:294
#: templates/measure/task_list.html:106
#: templates/measure/task_complete.html:70
#: templates/measure/task_complete.html:297
#: templates/measure/task_list.html:109
msgid "Seriale"
msgstr ""
#: templates/measure/task_complete.html:86
#: templates/measure/task_complete.html:89
msgid "Totale"
msgstr ""
#: templates/measure/task_complete.html:103
#: templates/measure/task_execute.html:644
#: templates/measure/task_complete.html:106
#: templates/measure/task_execute.html:703
msgid "Conformi"
msgstr ""
#: templates/measure/task_complete.html:120
#: templates/measure/task_execute.html:648
#: templates/measure/task_complete.html:123
#: templates/measure/task_execute.html:707
msgid "Attenzione"
msgstr ""
#: templates/measure/task_complete.html:137
#: templates/measure/task_complete.html:140
msgid "Non Conformi"
msgstr ""
#: templates/measure/task_complete.html:153
#: templates/measure/task_complete.html:156
msgid "Dettaglio Misurazioni"
msgstr ""
#: templates/measure/task_complete.html:160
#: templates/measure/task_complete.html:163
msgid "Esporta CSV"
msgstr ""
#: templates/measure/task_complete.html:167
#: templates/measure/task_complete.html:170
msgid "Data/Ora"
msgstr ""
#: templates/measure/task_complete.html:171
#: templates/measure/task_complete.html:174
#: templates/statistics/dashboard.html:247
msgid "Valore"
msgstr ""
#: templates/measure/task_complete.html:172
#: templates/measure/task_complete.html:175
msgid "Deviazione"
msgstr ""
#: templates/measure/task_complete.html:173
#: templates/measure/task_complete.html:280
#: templates/measure/task_complete.html:176
#: templates/measure/task_complete.html:283
msgid "Esito"
msgstr ""
#: templates/measure/task_complete.html:174
#: templates/measure/task_complete.html:177
msgid "Metodo"
msgstr ""
#: templates/measure/task_complete.html:202
#: templates/measure/task_complete.html:205
#: templates/statistics/dashboard.html:147
msgid "Pass"
msgstr ""
#: templates/measure/task_complete.html:204
#: templates/measure/task_complete.html:207
#: templates/statistics/dashboard.html:153
msgid "Warning"
msgstr ""
#: templates/measure/task_complete.html:206
#: templates/measure/task_complete.html:209
#: templates/statistics/dashboard.html:159
msgid "Fail"
msgstr ""
#: templates/measure/task_complete.html:217
#: templates/measure/task_complete.html:220
msgid "Manuale"
msgstr ""
#: templates/measure/task_complete.html:224
#: templates/measure/task_complete.html:227
msgid "Calibro"
msgstr ""
#: templates/measure/task_complete.html:245 templates/measure/task_list.html:37
#: templates/measure/task_complete.html:248 templates/measure/task_list.html:40
msgid "Seleziona altra ricetta"
msgstr ""
#: templates/measure/task_complete.html:252
#: templates/measure/task_complete.html:255
msgid "Ripeti misurazioni"
msgstr ""
#: templates/measure/task_complete.html:272
#: templates/measure/task_complete.html:275
msgid "Subtask ID"
msgstr ""
#: templates/measure/task_complete.html:273
#: templates/measure/task_complete.html:276
msgid "Nome Sottotask"
msgstr ""
#: templates/measure/task_complete.html:274
#: templates/measure/task_complete.html:277
msgid "Valore Misurato"
msgstr ""
#: templates/measure/task_complete.html:276
#: templates/measure/task_complete.html:279
msgid "Valore Nominale"
msgstr ""
#: templates/measure/task_complete.html:277
#: templates/measure/task_complete.html:280
msgid "Tolleranza +"
msgstr ""
#: templates/measure/task_complete.html:278
#: templates/measure/task_complete.html:281
msgid "Tolleranza -"
msgstr ""
#: templates/measure/task_complete.html:279
#: templates/measure/task_complete.html:282
msgid "Scarto"
msgstr ""
#: templates/measure/task_complete.html:283
#: templates/measure/task_complete.html:286
msgid "Metodo Input"
msgstr ""
#: templates/measure/task_complete.html:284
#: templates/measure/task_complete.html:287
msgid "Data Misurazione"
msgstr ""
#: templates/measure/task_complete.html:285
#: templates/measure/task_complete.html:288
msgid "Operatore"
msgstr ""
#: templates/measure/task_complete.html:286
#: templates/measure/task_complete.html:289
msgid "RIEPILOGO ESECUZIONE TASK"
msgstr ""
#: templates/measure/task_complete.html:287
#: templates/measure/task_complete.html:290
msgid "Task ID"
msgstr ""
#: templates/measure/task_complete.html:288
#: templates/measure/task_complete.html:291
msgid "Nome Task"
msgstr ""
#: templates/measure/task_complete.html:289
#: templates/measure/task_complete.html:292
#: templates/statistics/dashboard.html:27
msgid "Ricetta"
msgstr ""
#: templates/measure/task_complete.html:290
#: templates/measure/task_complete.html:293
msgid "Data Inizio"
msgstr ""
#: templates/measure/task_complete.html:291
#: templates/measure/task_complete.html:294
msgid "Data Fine"
msgstr ""
#: templates/measure/task_complete.html:295
#: templates/measure/task_complete.html:298
msgid "STATISTICHE"
msgstr ""
#: templates/measure/task_complete.html:296
#: templates/measure/task_complete.html:299
msgid "Totale Misure"
msgstr ""
#: templates/measure/task_complete.html:297
#: templates/measure/task_complete.html:300
msgid "Passate"
msgstr ""
#: templates/measure/task_complete.html:298
#: templates/measure/task_complete.html:301
msgid "Fallite"
msgstr ""
#: templates/measure/task_complete.html:299
#: templates/measure/task_complete.html:302
msgid "Percentuale Successo"
msgstr ""
#: templates/measure/task_complete.html:300
#: templates/measure/task_complete.html:303
msgid "DETTAGLIO MISURE"
msgstr ""
@@ -1977,157 +2018,153 @@ msgstr ""
msgid "Registrata"
msgstr ""
#: templates/measure/task_execute.html:432
msgid "Prossima misurazione tra"
msgstr ""
#: templates/measure/task_execute.html:437
msgid "Ciclo"
msgstr ""
#: templates/measure/task_execute.html:456
#: templates/measure/task_execute.html:470
msgid "Task di misura senza quote configurate: la ricetta è incompleta"
msgstr ""
#: templates/measure/task_execute.html:475
msgid "Linea ferma"
msgstr ""
#: templates/measure/task_execute.html:478
#: templates/measure/task_execute.html:494
msgid "Il conto alla rovescia è congelato a"
msgstr ""
#: templates/measure/task_execute.html:480
#: templates/measure/task_execute.html:497
msgid "serve il capoturno per riprendere"
msgstr ""
#: templates/measure/task_execute.html:499
#: templates/measure/task_execute.html:516
msgid "Produzione non registrata sul server"
msgstr ""
#: templates/measure/task_execute.html:520
#: templates/measure/task_execute.html:686
#: templates/measure/task_execute.html:537
#: templates/measure/task_execute.html:746
msgid "Avvio Produzione"
msgstr ""
#: templates/measure/task_execute.html:524
#: templates/measure/task_execute.html:541
msgid "Invia segnale al gestionale per avviare il timer della linea"
msgstr ""
#: templates/measure/task_execute.html:535
#: templates/measure/task_execute.html:552
msgid "Produzione avviata"
msgstr ""
#: templates/measure/task_execute.html:580
#: templates/measure/task_execute.html:597
msgid "Fine ciclo misura"
msgstr ""
#: templates/measure/task_execute.html:590
#: templates/measure/task_execute.html:604
#: templates/measure/task_execute.html:608
msgid "Rimisura"
msgstr ""
#: templates/measure/task_execute.html:618
#: templates/measure/task_execute.html:632
msgid "Completato"
msgstr ""
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "Tutte le"
msgstr ""
#: templates/measure/task_execute.html:638
#: templates/measure/task_execute.html:697
msgid "misurazioni sono state registrate."
msgstr ""
#: templates/measure/task_execute.html:652
#: templates/measure/task_execute.html:711
msgid "Non Conf."
msgstr ""
#: templates/measure/task_execute.html:666
#: templates/measure/task_execute.html:726
msgid "Conferma ciclo"
msgstr ""
#: templates/measure/task_execute.html:676
#: templates/measure/task_execute.html:736
msgid "Task successivo"
msgstr ""
#: templates/measure/task_execute.html:716
#: templates/measure/task_execute.html:762
msgid "Girare il pezzo e rimisurare"
msgstr ""
#: templates/measure/task_execute.html:788
msgid "Autorizzazione capoturno"
msgstr ""
#: templates/measure/task_execute.html:725
#: templates/measure/task_execute.html:797
msgid "Username capoturno"
msgstr ""
#: templates/measure/task_execute.html:747
#: templates/measure/task_execute.html:819
msgid "Autorizza"
msgstr ""
#: templates/measure/task_execute.html:1039
#: templates/measure/task_execute.html:1122
msgid "Errore di rete. Riprovare."
msgstr ""
#: templates/measure/task_execute.html:1116
#: templates/measure/task_execute.html:1227
msgid "Errore di comunicazione con il server"
msgstr ""
#: templates/measure/task_execute.html:1284
#: templates/measure/task_execute.html:1475
msgid "Misurazione fuori tolleranza"
msgstr ""
#: templates/measure/task_execute.html:1285
#: templates/measure/task_execute.html:1476
msgid "Fermo linea richiesto"
msgstr ""
#: templates/measure/task_execute.html:1286
#: templates/measure/task_execute.html:1477
msgid "Ripresa della produzione"
msgstr ""
#: templates/measure/task_execute.html:1287
#: templates/measure/task_execute.html:1478
msgid "Fine produzione richiesta"
msgstr ""
#: templates/measure/task_execute.html:1316
#: templates/measure/task_execute.html:1507
msgid "Credenziali non valide o utente non autorizzato"
msgstr ""
#: templates/measure/task_execute.html:1348
#: templates/measure/task_execute.html:1539
msgid "Nessuna produzione aperta su questa stazione"
msgstr ""
#: templates/measure/task_execute.html:1365
#: templates/measure/task_execute.html:1556
msgid "Azione non riuscita"
msgstr ""
#: templates/measure/task_list.html:84
#: templates/measure/task_list.html:87
msgid "AVVIA"
msgstr ""
#: templates/measure/task_list.html:121
#: templates/measure/task_list.html:124
msgid "Task da eseguire"
msgstr ""
#: templates/measure/task_list.html:133
#: templates/measure/task_list.html:136
msgid "misurazioni totali"
msgstr ""
#: templates/measure/task_list.html:175
#: templates/measure/task_list.html:178
msgid "Confronto profilo"
msgstr ""
#: templates/measure/task_list.html:176
#: templates/measure/task_list.html:179
msgid "Misura camera"
msgstr ""
#: templates/measure/task_list.html:198
#: templates/measure/task_list.html:201
msgid "Allegato"
msgstr ""
#: templates/measure/task_list.html:222
#: templates/measure/task_list.html:225
msgid "Visualizza Task"
msgstr ""
#: templates/measure/task_list.html:245
#: templates/measure/task_list.html:248
msgid "Nessun task disponibile"
msgstr ""
#: templates/measure/task_list.html:248
#: templates/measure/task_list.html:251
msgid "Questa ricetta non ha ancora task definiti."
msgstr ""