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()"
|
x-init="init()"
|
||||||
@numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)"
|
@numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)"
|
||||||
@numpad-rejected.window="onNumpadRejected($event.detail)"
|
@numpad-rejected.window="onNumpadRejected($event.detail)"
|
||||||
|
@numpad-error.window="onNumpadError($event.detail)"
|
||||||
@marker-click.window="goToSubtaskByMarker($event.detail.marker_number)">
|
@marker-click.window="goToSubtaskByMarker($event.detail.marker_number)">
|
||||||
|
|
||||||
{# ================================================================
|
{# ================================================================
|
||||||
@@ -932,6 +933,11 @@ function taskExecute() {
|
|||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...]
|
measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...]
|
||||||
saving: false,
|
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: '',
|
errorMessage: '',
|
||||||
showCompletionOverlay: false,
|
showCompletionOverlay: false,
|
||||||
|
|
||||||
@@ -942,6 +948,7 @@ function taskExecute() {
|
|||||||
timerActive: false,
|
timerActive: false,
|
||||||
timerRemaining: 0,
|
timerRemaining: 0,
|
||||||
_timerInterval: null,
|
_timerInterval: null,
|
||||||
|
_timerDeadline: null, // wall-clock ms the countdown is aimed at (#37.7)
|
||||||
cycleCount: 0,
|
cycleCount: 0,
|
||||||
productionStarted: false,
|
productionStarted: false,
|
||||||
|
|
||||||
@@ -1094,6 +1101,13 @@ function taskExecute() {
|
|||||||
// Rejoin whatever production is already open at this station.
|
// Rejoin whatever production is already open at this station.
|
||||||
await this.loadProductionRun();
|
await this.loadProductionRun();
|
||||||
await this.loadPendingAuthorisation();
|
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?
|
/* 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 ----
|
// ---- Handle numpad confirm ----
|
||||||
async handleMeasurement(value, inputMethod) {
|
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.currentValue = value;
|
||||||
this.errorMessage = '';
|
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) {
|
if (!response.ok || result.error) {
|
||||||
this.errorMessage = result.detail || '{{ _("Errore nel salvataggio della misurazione") }}';
|
this.errorMessage = result.detail || '{{ _("Errore nel salvataggio della misurazione") }}';
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
|
this.busy = false;
|
||||||
return;
|
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
|
// Record measurement locally
|
||||||
const existingIdx = this.measurements.findIndex(m => m.subtask_id === this.currentSubtask.id);
|
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) {
|
if (existingIdx !== -1) {
|
||||||
this.measurements.splice(existingIdx, 1, mEntry);
|
this.measurements.splice(existingIdx, 1, mEntry);
|
||||||
} else {
|
} else {
|
||||||
@@ -1249,9 +1309,10 @@ function taskExecute() {
|
|||||||
|
|
||||||
// Out of tolerance: nothing moves until the capoturno says so, or until
|
// Out of tolerance: nothing moves until the capoturno says so, or until
|
||||||
// this same quote is measured again and comes out inside tolerance.
|
// 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.pendingFail = { id: result.id, subtask_id: this.currentSubtask.id };
|
||||||
this.openSupervisorModal('out_of_tolerance');
|
this.openSupervisorModal('out_of_tolerance');
|
||||||
|
this.busy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1270,18 +1331,22 @@ function taskExecute() {
|
|||||||
// Produzione") or once production is running ("Conferma ciclo").
|
// Produzione") or once production is running ("Conferma ciclo").
|
||||||
if (!this.productionStarted && !this.isLastTask) {
|
if (!this.productionStarted && !this.isLastTask) {
|
||||||
this.goToNextTask();
|
this.goToNextTask();
|
||||||
|
this.busy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.showCompletionOverlay = true;
|
this.showCompletionOverlay = true;
|
||||||
|
this.busy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.advanceToNext();
|
this.advanceToNext();
|
||||||
|
this.busy = false;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Save measurement error:', err);
|
console.error('Save measurement error:', err);
|
||||||
this.errorMessage = '{{ _("Errore di rete. Riprovare.") }}';
|
this.errorMessage = '{{ _("Errore di rete. Riprovare.") }}';
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1374,6 +1439,13 @@ function taskExecute() {
|
|||||||
}
|
}
|
||||||
return;
|
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
|
// No production open (recipe run outside a production): keep the old local
|
||||||
@@ -1414,6 +1486,13 @@ function taskExecute() {
|
|||||||
body: JSON.stringify(body || {}),
|
body: JSON.stringify(body || {}),
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => null);
|
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) {
|
if (!resp.ok) {
|
||||||
this.productionError = (data && data.detail) || '{{ _("Errore di comunicazione con il server") }}';
|
this.productionError = (data && data.detail) || '{{ _("Errore di comunicazione con il server") }}';
|
||||||
return null;
|
return null;
|
||||||
@@ -1434,6 +1513,12 @@ function taskExecute() {
|
|||||||
this.stopMeasurementTimer();
|
this.stopMeasurementTimer();
|
||||||
this.timerRemaining = seconds;
|
this.timerRemaining = seconds;
|
||||||
this.timerActive = true;
|
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
|
// 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.
|
// while sitting on it; being back in time re-arms the announcement.
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
@@ -1448,7 +1533,7 @@ function taskExecute() {
|
|||||||
|
|
||||||
onSecond() {
|
onSecond() {
|
||||||
const wasAnnounced = this.dueAnnounced;
|
const wasAnnounced = this.dueAnnounced;
|
||||||
this.timerRemaining--;
|
this.timerRemaining = Math.ceil((this._timerDeadline - Date.now()) / 1000);
|
||||||
if (this.timerRemaining <= 0) this.onMeasurementDue();
|
if (this.timerRemaining <= 0) this.onMeasurementDue();
|
||||||
|
|
||||||
if (wasAnnounced && this.redirectIn > 0) {
|
if (wasAnnounced && this.redirectIn > 0) {
|
||||||
|
|||||||
@@ -2748,3 +2748,15 @@ msgstr "Error generating report"
|
|||||||
#~ msgid "Errore nel caricamento dei task: %(detail)s"
|
#~ msgid "Errore nel caricamento dei task: %(detail)s"
|
||||||
#~ msgstr "Error loading tasks: %(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"
|
#~ msgid "Errore nel caricamento dei task: %(detail)s"
|
||||||
#~ msgstr "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