diff --git a/src/frontend/flask_app/blueprints/measure.py b/src/frontend/flask_app/blueprints/measure.py
index 1eef4d5..997ae79 100644
--- a/src/frontend/flask_app/blueprints/measure.py
+++ b/src/frontend/flask_app/blueprints/measure.py
@@ -135,11 +135,15 @@ def task_execute(task_id: int):
# Load all task IDs for this recipe (ordered) for auto-advance
recipe_id = task_resp.get("recipe_id")
all_task_ids = []
+ measurement_interval_minutes = None
if recipe_id:
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
if isinstance(tasks_resp, list):
sorted_tasks = sorted(tasks_resp, key=lambda t: t.get("order_index", 0))
all_task_ids = [t["id"] for t in sorted_tasks]
+ recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
+ if not recipe_resp.get("error"):
+ measurement_interval_minutes = recipe_resp.get("measurement_interval_minutes")
return render_template(
"measure/task_execute.html",
@@ -147,6 +151,7 @@ def task_execute(task_id: int):
lot_number=lot_number,
serial_number=serial_number,
all_task_ids=all_task_ids,
+ measurement_interval_minutes=measurement_interval_minutes,
)
diff --git a/src/frontend/flask_app/templates/measure/task_execute.html b/src/frontend/flask_app/templates/measure/task_execute.html
index 1b6010d..9d9c5be 100644
--- a/src/frontend/flask_app/templates/measure/task_execute.html
+++ b/src/frontend/flask_app/templates/measure/task_execute.html
@@ -406,6 +406,28 @@
+ {# ================================================================
+ MEASUREMENT TIMER BANNER
+ ================================================================ #}
+
+
+
+
+ {{ _('Prossima misurazione tra') }}
+
+
+
+ ({{ _('Ciclo') }} #)
+
+
+
+
{# ================================================================
FOOTER — Progress bar + navigation
================================================================ #}
@@ -629,6 +651,13 @@ function taskExecute() {
errorMessage: '',
showCompletionOverlay: false,
+ // ---- Measurement timer ----
+ measurementIntervalMinutes: {{ measurement_interval_minutes|tojson if measurement_interval_minutes else 'null' }},
+ timerActive: false,
+ timerRemaining: 0,
+ _timerInterval: null,
+ cycleCount: 0,
+
// ---- Cycle & workflow state ----
cycleConfirmed: false,
showSupervisorModal: false,
@@ -852,6 +881,68 @@ function taskExecute() {
confirmCycle() {
this.cycleConfirmed = true;
this.showCompletionOverlay = false;
+ this.cycleCount++;
+
+ // Start measurement timer if recipe has an interval
+ if (this.measurementIntervalMinutes && this.measurementIntervalMinutes > 0) {
+ this.startMeasurementTimer();
+ }
+ },
+
+ // ---- Measurement timer ----
+ startMeasurementTimer() {
+ this.stopMeasurementTimer();
+ this.timerRemaining = this.measurementIntervalMinutes * 60;
+ this.timerActive = true;
+ var self = this;
+ this._timerInterval = setInterval(function () {
+ self.timerRemaining--;
+ if (self.timerRemaining <= 0) {
+ self.onTimerExpired();
+ }
+ }, 1000);
+ },
+
+ stopMeasurementTimer() {
+ if (this._timerInterval) {
+ clearInterval(this._timerInterval);
+ this._timerInterval = null;
+ }
+ this.timerActive = false;
+ },
+
+ onTimerExpired() {
+ this.stopMeasurementTimer();
+ this.playBuzzer();
+ // Reset for new measurement cycle
+ this.cycleConfirmed = false;
+ this.measurements = [];
+ this.currentIndex = 0;
+ this.currentValue = null;
+ },
+
+ 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 (_) {}
+ },
+
+ get timerDisplay() {
+ var m = Math.floor(this.timerRemaining / 60);
+ var s = this.timerRemaining % 60;
+ return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
},
// ---- Navigate to next task (Completato) ----
diff --git a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po
index 141b85b..3b4fd97 100644
--- a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po
+++ b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po
@@ -759,6 +759,12 @@ msgstr "seconds due to inactivity."
msgid "Continua a lavorare"
msgstr "Continue working"
+msgid "Prossima misurazione tra"
+msgstr "Next measurement in"
+
+msgid "Ciclo"
+msgstr "Cycle"
+
msgid "Intervallo misura (minuti)"
msgstr "Measurement interval (minutes)"
diff --git a/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po b/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po
index b100668..f2afef3 100644
--- a/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po
+++ b/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po
@@ -789,6 +789,12 @@ msgstr "secondi per inattività ."
msgid "Continua a lavorare"
msgstr "Continua a lavorare"
+msgid "Prossima misurazione tra"
+msgstr "Prossima misurazione tra"
+
+msgid "Ciclo"
+msgstr "Ciclo"
+
msgid "Intervallo misura (minuti)"
msgstr "Intervallo misura (minuti)"