bde8fafd77
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>
90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
"""Pydantic schemas for production runs and their events."""
|
|
from datetime import datetime
|
|
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class ProductionRunCreate(BaseModel):
|
|
station_code: str = Field(..., min_length=1, max_length=100)
|
|
recipe_id: int = Field(..., gt=0)
|
|
version_id: Optional[int] = Field(default=None, gt=0)
|
|
lot_number: Optional[str] = Field(default=None, max_length=100)
|
|
serial_number: Optional[str] = Field(default=None, max_length=100)
|
|
|
|
|
|
class ProductionEventResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
id: int
|
|
event_type: str
|
|
user_id: int
|
|
supervisor_id: Optional[int]
|
|
task_id: Optional[int] = None
|
|
note: Optional[str]
|
|
created_at: datetime
|
|
|
|
|
|
class ProductionRunResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
station_id: int
|
|
recipe_id: int
|
|
version_id: int
|
|
operator_id: int
|
|
lot_number: Optional[str]
|
|
serial_number: Optional[str]
|
|
status: str
|
|
measurement_interval_minutes: Optional[int]
|
|
next_measurement_at: Optional[datetime]
|
|
cycle_count: int
|
|
started_at: datetime
|
|
paused_at: Optional[datetime]
|
|
closed_at: Optional[datetime]
|
|
closed_by: Optional[int]
|
|
statistics_path: Optional[str] = None
|
|
|
|
# Derived server-side so every client agrees on the countdown regardless of
|
|
# clock skew. Negative once the interval has elapsed: how long the run has been
|
|
# overdue is a fact the operator must see, not deduce.
|
|
seconds_to_next_measurement: Optional[int] = None
|
|
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)
|
|
|
|
|
|
class SupervisorAction(BaseModel):
|
|
"""Credentials of the supervisor authorising a line stop or a close."""
|
|
|
|
supervisor_username: str = Field(..., min_length=1)
|
|
supervisor_password: str = Field(..., min_length=1)
|
|
note: Optional[str] = None
|
|
|
|
|
|
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", "task_measured", "remeasure",
|
|
"line_stop", "resume", "close",
|
|
]
|