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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user