fix(task_execute): letture doppie, sessione scaduta, ciclo rifiutato dal server (#13, #12, #29, #37)
#13 — handleMeasurement era rientrante: saving tornava false prima della pausa di 1s di feedback, currentIndex avanzava dopo. Una seconda lettura in quella finestra salvava due volte la stessa quota e saltava la successiva. Guardia unica this.busy: true all'ingresso, false su ogni uscita della funzione; l'ingresso è bloccato anche con showCompletionOverlay, showSupervisorModal, isPaused o cycleConfirmed pendenti. Una lettura scartata non sparisce in silenzio: errorMessage "Lettura ignorata: attendere". #12 — dopo la scadenza del token CSRF (config lato server, lotto B2) il POST tornava 400 in HTML: response.json() lanciava, finiva nel catch generico e l'operatore vedeva "Errore di rete" invece che "Sessione scaduta". Aggiunto try/catch dedicato sul parsing e controllo di result.csrf_expired / session_expired, sia in handleMeasurement che in postProduction(); la lettura non salvata resta sul display. #29 — confirmCycle mostrava il ciclo chiuso anche quando il server rifiutava /cycle (produzione aperta, postProduction torna null): cadeva nel ramo "nessuna produzione", incrementando cycleCount in locale. Ora, con produzione aperta, un rifiuto del server ripristina cycleConfirmed=false, riapre l'overlay di completamento e mostra l'errore (già in productionError); il ramo locale resta riservato a quando non c'è produzione aperta. #37 punto 9 — il blocco capoturno era deciso su passFailStatus (preview client, prima del salvataggio) invece che su result.pass_fail del server; ora la risposta del server decide sia il blocco che i conteggi locali. Completa anche il lato pagina di #27: aggiunto il listener per l'evento numpad-error (dispatchato da numpad.js) cosi' la lettura rifiutata dal calibro è visibile all'operatore, non solo intercettata in silenzio. Stringhe nuove in messages.po (it/en): "Sessione scaduta: ricaricare la pagina", "Lettura ignorata: attendere", "Lettura del calibro rifiutata: valore troppo grande". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
x-init="init()"
|
||||
@numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)"
|
||||
@numpad-rejected.window="onNumpadRejected($event.detail)"
|
||||
@numpad-error.window="onNumpadError($event.detail)"
|
||||
@marker-click.window="goToSubtaskByMarker($event.detail.marker_number)">
|
||||
|
||||
{# ================================================================
|
||||
@@ -932,6 +933,11 @@ function taskExecute() {
|
||||
currentIndex: 0,
|
||||
measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...]
|
||||
saving: false,
|
||||
// The single reentrancy guard for handleMeasurement (#13): true for the
|
||||
// whole time a reading is being processed, including the 1s feedback
|
||||
// pause after the save where "saving" alone used to go false too early.
|
||||
// "saving" stays as it was - it only drives the UI spinner.
|
||||
busy: false,
|
||||
errorMessage: '',
|
||||
showCompletionOverlay: false,
|
||||
|
||||
@@ -942,6 +948,7 @@ function taskExecute() {
|
||||
timerActive: false,
|
||||
timerRemaining: 0,
|
||||
_timerInterval: null,
|
||||
_timerDeadline: null, // wall-clock ms the countdown is aimed at (#37.7)
|
||||
cycleCount: 0,
|
||||
productionStarted: false,
|
||||
|
||||
@@ -1094,6 +1101,13 @@ function taskExecute() {
|
||||
// Rejoin whatever production is already open at this station.
|
||||
await this.loadProductionRun();
|
||||
await this.loadPendingAuthorisation();
|
||||
// A tablet left to sleep drifts (#37.7): catch the countdown back up to
|
||||
// the server's own clock, the same source adoptProductionRun already
|
||||
// trusts on load, as soon as the tab is visible again.
|
||||
var self = this;
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') self.loadProductionRun();
|
||||
});
|
||||
},
|
||||
|
||||
/* Is a quote already waiting for the capoturno?
|
||||
@@ -1187,10 +1201,30 @@ function taskExecute() {
|
||||
}
|
||||
},
|
||||
|
||||
/* The numpad refused the reading itself (e.g. a caliper burst whose
|
||||
integer part does not fit) rather than a rule about who may type it.
|
||||
Same error line, so there is still one place to look. */
|
||||
onNumpadError(detail) {
|
||||
this.errorMessage = '{{ _("Lettura del calibro rifiutata: valore troppo grande") }}';
|
||||
},
|
||||
|
||||
// ---- Handle numpad confirm ----
|
||||
async handleMeasurement(value, inputMethod) {
|
||||
if (!this.currentSubtask || this.saving) return;
|
||||
if (!this.currentSubtask) return;
|
||||
|
||||
// Single reentrancy guard (#13): a second reading arriving while the
|
||||
// first one is still being saved - or while the operator is stuck
|
||||
// behind the modal/overlay the first one opened - must not become its
|
||||
// own measurement (double record, one quote skipped). It is not
|
||||
// silently dropped either: the same error line other rejections use
|
||||
// says so.
|
||||
if (this.busy || this.showCompletionOverlay || this.showSupervisorModal ||
|
||||
this.isPaused || this.cycleConfirmed) {
|
||||
this.errorMessage = '{{ _("Lettura ignorata: attendere") }}';
|
||||
return;
|
||||
}
|
||||
|
||||
this.busy = true;
|
||||
this.currentValue = value;
|
||||
this.errorMessage = '';
|
||||
|
||||
@@ -1225,17 +1259,43 @@ function taskExecute() {
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
let result;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch (parseErr) {
|
||||
// The server answered but not with JSON: a CSRF-expired POST gets
|
||||
// the login page's HTML back, not a payload. Distinguish that from
|
||||
// a dead network so the operator is told to reload rather than to
|
||||
// retry blindly; the unsaved reading stays on screen either way.
|
||||
this.errorMessage = '{{ _("Sessione scaduta: ricaricare la pagina") }}';
|
||||
this.saving = false;
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && (result.csrf_expired || result.session_expired)) {
|
||||
this.errorMessage = '{{ _("Sessione scaduta: ricaricare la pagina") }}';
|
||||
this.saving = false;
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.error) {
|
||||
this.errorMessage = result.detail || '{{ _("Errore nel salvataggio della misurazione") }}';
|
||||
this.saving = false;
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// The server decides pass/fail from the recipe's official tolerances;
|
||||
// passFailStatus is only a live preview while the value is on screen,
|
||||
// not what a capoturno block or the pass/warning/fail counts should
|
||||
// be keyed on.
|
||||
const serverPassFail = result.pass_fail || pf;
|
||||
|
||||
// Record measurement locally
|
||||
const existingIdx = this.measurements.findIndex(m => m.subtask_id === this.currentSubtask.id);
|
||||
const mEntry = { subtask_id: this.currentSubtask.id, value, pass_fail: pf, deviation: dev };
|
||||
const mEntry = { subtask_id: this.currentSubtask.id, value, pass_fail: serverPassFail, deviation: dev };
|
||||
if (existingIdx !== -1) {
|
||||
this.measurements.splice(existingIdx, 1, mEntry);
|
||||
} else {
|
||||
@@ -1249,9 +1309,10 @@ function taskExecute() {
|
||||
|
||||
// Out of tolerance: nothing moves until the capoturno says so, or until
|
||||
// this same quote is measured again and comes out inside tolerance.
|
||||
if (pf === 'fail') {
|
||||
if (serverPassFail === 'fail') {
|
||||
this.pendingFail = { id: result.id, subtask_id: this.currentSubtask.id };
|
||||
this.openSupervisorModal('out_of_tolerance');
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1270,18 +1331,22 @@ function taskExecute() {
|
||||
// Produzione") or once production is running ("Conferma ciclo").
|
||||
if (!this.productionStarted && !this.isLastTask) {
|
||||
this.goToNextTask();
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
this.showCompletionOverlay = true;
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.advanceToNext();
|
||||
this.busy = false;
|
||||
|
||||
} catch (err) {
|
||||
console.error('Save measurement error:', err);
|
||||
this.errorMessage = '{{ _("Errore di rete. Riprovare.") }}';
|
||||
this.saving = false;
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1374,6 +1439,13 @@ function taskExecute() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The server refused to close the cycle (line stopped, production
|
||||
// closed, network down): the screen must not show a cycle that never
|
||||
// actually closed. productionError already carries why.
|
||||
this.cycleConfirmed = false;
|
||||
this.showCompletionOverlay = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// No production open (recipe run outside a production): keep the old local
|
||||
@@ -1414,6 +1486,13 @@ function taskExecute() {
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
const data = await resp.json().catch(() => null);
|
||||
// Not JSON at all (CSRF-expired POSTs get the login page's HTML
|
||||
// back), or the server flagged the session as gone: same "reload"
|
||||
// message handleMeasurement gives for the same failure (#12).
|
||||
if (!data || data.csrf_expired || data.session_expired) {
|
||||
this.productionError = '{{ _("Sessione scaduta: ricaricare la pagina") }}';
|
||||
return null;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
this.productionError = (data && data.detail) || '{{ _("Errore di comunicazione con il server") }}';
|
||||
return null;
|
||||
@@ -1434,6 +1513,12 @@ function taskExecute() {
|
||||
this.stopMeasurementTimer();
|
||||
this.timerRemaining = seconds;
|
||||
this.timerActive = true;
|
||||
// The wall clock, not a tick count (#37.7): a tablet asleep for a few
|
||||
// minutes has to catch the countdown up in one jump when it wakes, not
|
||||
// replay the ticks it missed while suspended - setInterval does not
|
||||
// fire while a tab is backgrounded, so a tick-based countdown drifted
|
||||
// behind by exactly however long the tab was hidden.
|
||||
this._timerDeadline = Date.now() + seconds * 1000;
|
||||
// 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) {
|
||||
@@ -1448,7 +1533,7 @@ function taskExecute() {
|
||||
|
||||
onSecond() {
|
||||
const wasAnnounced = this.dueAnnounced;
|
||||
this.timerRemaining--;
|
||||
this.timerRemaining = Math.ceil((this._timerDeadline - Date.now()) / 1000);
|
||||
if (this.timerRemaining <= 0) this.onMeasurementDue();
|
||||
|
||||
if (wasAnnounced && this.redirectIn > 0) {
|
||||
|
||||
@@ -2748,3 +2748,15 @@ msgstr "Error generating report"
|
||||
#~ msgid "Errore nel caricamento dei task: %(detail)s"
|
||||
#~ msgstr "Error loading tasks: %(detail)s"
|
||||
|
||||
#: templates/measure/task_execute.html:1268
|
||||
msgid "Sessione scaduta: ricaricare la pagina"
|
||||
msgstr "Session expired: reload the page"
|
||||
|
||||
#: templates/measure/task_execute.html:1219
|
||||
msgid "Lettura ignorata: attendere"
|
||||
msgstr "Reading ignored: please wait"
|
||||
|
||||
#: templates/measure/task_execute.html:1194
|
||||
msgid "Lettura del calibro rifiutata: valore troppo grande"
|
||||
msgstr "Caliper reading refused: value too large"
|
||||
|
||||
|
||||
@@ -2736,3 +2736,15 @@ msgstr "Errore nella generazione del report"
|
||||
#~ msgid "Errore nel caricamento dei task: %(detail)s"
|
||||
#~ msgstr "Errore nel caricamento dei task: %(detail)s"
|
||||
|
||||
#: templates/measure/task_execute.html:1268
|
||||
msgid "Sessione scaduta: ricaricare la pagina"
|
||||
msgstr "Sessione scaduta: ricaricare la pagina"
|
||||
|
||||
#: templates/measure/task_execute.html:1219
|
||||
msgid "Lettura ignorata: attendere"
|
||||
msgstr "Lettura ignorata: attendere"
|
||||
|
||||
#: templates/measure/task_execute.html:1194
|
||||
msgid "Lettura del calibro rifiutata: valore troppo grande"
|
||||
msgstr "Lettura del calibro rifiutata: valore troppo grande"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user