feat: measurement workflow improvements and recipe update-in-place

- Auto-advance to next task after completing all subtask measurements
- 1s pause between measurements to show pass/fail/warning result
- Colored marker strip (green/red/amber) based on measurement status
- Replace duplicate measurements instead of appending (fixes progress bar)
- Add Task column and Date/Time column to measurement summary table
- Enrich summary with task_info for each measurement
- Update-in-place for recipe versions without measurements (no copy-on-write)
- Dark theme improvements and navbar cleanup
- Server config: ignore extra env vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-09 19:36:42 +01:00
parent 4854966bf7
commit e8b88d48c1
9 changed files with 232 additions and 118 deletions
+1 -73
View File
@@ -1,6 +1,6 @@
<!-- TieMeasureFlow Navbar -->
<nav class="sticky top-0 z-40 bg-[var(--bg-card)] border-b border-[var(--border-color)] shadow-sm transition-colors duration-300"
x-data="{ mobileOpen: false, userDropdown: false, adminDropdown: false }">
x-data="{ mobileOpen: false, userDropdown: false }">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14">
@@ -15,9 +15,6 @@
alt="TieMeasureFlow"
class="h-8 w-auto">
{% endif %}
<span class="text-lg font-bold text-[var(--text-primary)] tracking-tight hidden sm:inline">
TieMeasureFlow
</span>
</a>
</div>
@@ -76,53 +73,6 @@
</a>
{% endif %}
{# Admin: dropdown #}
{% if current_user.get('is_admin') %}
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
<button @click="open = !open"
class="nav-link group flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
transition-colors duration-200"
:class="{ 'text-primary bg-primary-50 dark:bg-primary-900/20': open }">
<!-- Cog Icon -->
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>{{ _('Admin') }}</span>
<svg class="w-3.5 h-3.5 transition-transform duration-200" :class="{ 'rotate-180': open }"
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- Admin Dropdown -->
<div x-show="open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95 -translate-y-1"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute left-0 mt-1 w-48 rounded-lg bg-[var(--bg-card)] border border-[var(--border-color)]
shadow-lg py-1 z-50">
<a href="#" class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-[var(--text-secondary)]
hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
{{ _('Utenti') }}
</a>
<a href="#" class="flex items-center gap-2.5 px-4 py-2.5 text-sm text-[var(--text-secondary)]
hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
{{ _('Impostazioni') }}
</a>
</div>
</div>
{% endif %}
</div>
{% endif %}
@@ -298,28 +248,6 @@
</a>
{% endif %}
{% if current_user.get('is_admin') %}
<div class="border-t border-[var(--border-color)] my-2"></div>
<p class="px-3 py-1 text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">{{ _('Admin') }}</p>
<a href="#"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
{{ _('Utenti') }}
</a>
<a href="#"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
text-[var(--text-secondary)] hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/20
transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
{{ _('Impostazioni') }}
</a>
{% endif %}
</div>
</div>
+10 -6
View File
@@ -164,7 +164,8 @@
<table class="tmf-table">
<thead>
<tr>
<th class="text-center">#</th>
<th class="text-center">{{ _('Data/Ora') }}</th>
<th>{{ _('Task') }}</th>
<th>{{ _('Descrizione') }}</th>
<th class="text-right">{{ _('Nominale') }}</th>
<th class="text-right">{{ _('Valore') }}</th>
@@ -174,10 +175,13 @@
</tr>
</thead>
<tbody>
{% for m in measurements|sort(attribute='subtask.marker_number') %}
{% for m in measurements|sort(attribute='task_info.order_index,subtask.marker_number') %}
<tr>
<td class="text-center font-mono font-medium" style="color: var(--text-primary);">
{{ m.subtask.marker_number }}
<td class="text-center font-mono text-xs" style="color: var(--text-secondary);">
{{ m.measured_at[:16]|replace('T', ' ') if m.measured_at else '-' }}
</td>
<td style="color: var(--text-secondary);" class="text-sm">
{{ m.task_info.title if m.task_info else '-' }}
</td>
<td style="color: var(--text-primary);">
{{ m.subtask.description }}
@@ -203,14 +207,14 @@
{% endif %}
</td>
<td class="text-center">
{% if m.method == 'manual' %}
{% if m.input_method == 'manual' %}
<div class="inline-flex items-center gap-1" style="color: var(--text-secondary);">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
</svg>
<span class="text-sm">{{ _('Manuale') }}</span>
</div>
{% elif m.method == 'caliper' %}
{% elif m.input_method == 'usb_caliper' %}
<div class="inline-flex items-center gap-1" style="color: var(--text-secondary);">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
+56 -20
View File
@@ -54,6 +54,7 @@
{% block content %}
<script>
window.__taskAnnotations = {{ task.annotations_json|default('null')|tojson }};
window.__allTaskIds = {{ all_task_ids|tojson }};
</script>
<div class="min-h-[calc(100vh-3.5rem)] flex flex-col"
x-data="taskExecute()"
@@ -210,23 +211,43 @@
class="shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-medium transition-all duration-200"
:class="idx === currentIndex
? 'bg-primary text-white border-primary shadow-md marker-active-pulse'
: isMeasured(st.id)
: getMeasurementStatus(st.id) === 'pass'
? 'bg-measure-pass/10 text-measure-pass border-measure-pass/30 hover:bg-measure-pass/20'
: 'bg-[var(--bg-card)] text-[var(--text-secondary)] border-[var(--border-color)] hover:border-primary/50 hover:text-primary'
: getMeasurementStatus(st.id) === 'fail'
? 'bg-red-50 dark:bg-red-900/20 text-measure-fail border-measure-fail/30 hover:bg-red-100 dark:hover:bg-red-900/30'
: getMeasurementStatus(st.id) === 'warning'
? 'bg-amber-50 dark:bg-amber-900/20 text-measure-warning border-measure-warning/30 hover:bg-amber-100 dark:hover:bg-amber-900/30'
: 'bg-[var(--bg-card)] text-[var(--text-secondary)] border-[var(--border-color)] hover:border-primary/50 hover:text-primary'
">
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-[10px] font-bold"
:class="idx === currentIndex
? 'bg-white/20'
: isMeasured(st.id)
: getMeasurementStatus(st.id) === 'pass'
? 'bg-measure-pass/20'
: 'bg-[var(--bg-secondary)]'
: getMeasurementStatus(st.id) === 'fail'
? 'bg-measure-fail/20'
: getMeasurementStatus(st.id) === 'warning'
? 'bg-measure-warning/20'
: 'bg-[var(--bg-secondary)]'
"
x-text="st.marker_number"></span>
<span x-text="st.description" class="truncate max-w-[100px]"></span>
{# Check icon if measured #}
<svg x-show="isMeasured(st.id)" class="w-3.5 h-3.5 text-measure-pass shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
{# Status icon: pass=check, fail=X, warning=! #}
<template x-if="getMeasurementStatus(st.id) === 'pass'">
<svg class="w-3.5 h-3.5 text-measure-pass shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</template>
<template x-if="getMeasurementStatus(st.id) === 'fail'">
<svg class="w-3.5 h-3.5 text-measure-fail shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</template>
<template x-if="getMeasurementStatus(st.id) === 'warning'">
<svg class="w-3.5 h-3.5 text-measure-warning shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
</svg>
</template>
</button>
</template>
</div>
@@ -628,6 +649,11 @@ function taskExecute() {
return m ? m.value : null;
},
getMeasurementStatus(subtaskId) {
const m = this.measurements.find(m => m.subtask_id === subtaskId);
return m ? m.pass_fail : null;
},
// ---- Handle numpad confirm ----
async handleMeasurement(value, inputMethod) {
if (!this.currentSubtask || this.saving) return;
@@ -652,10 +678,8 @@ function taskExecute() {
},
body: JSON.stringify({
subtask_id: this.currentSubtask.id,
task_id: this.task.id,
version_id: this.task.version_id,
value: value,
pass_fail: pf,
deviation: dev,
lot_number: this.lotNumber,
serial_number: this.serialNumber,
input_method: inputMethod || 'manual',
@@ -670,20 +694,32 @@ function taskExecute() {
return;
}
// Record measurement locally
this.measurements.push({
subtask_id: this.currentSubtask.id,
value: value,
pass_fail: pf,
deviation: dev,
});
// Record measurement locally (replace if already measured)
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 };
if (existingIdx !== -1) {
this.measurements.splice(existingIdx, 1, mEntry);
} else {
this.measurements.push(mEntry);
}
this.saving = false;
// Pause 1s to let user see the result (pass/fail/warning)
await new Promise(r => setTimeout(r, 1000));
// Check if all done
if (this.completedCount >= this.totalSubtasks) {
// Show completion overlay
this.showCompletionOverlay = true;
// Auto-advance to next task, or show overlay on last task
const taskIds = window.__allTaskIds || [];
const currentIdx = taskIds.indexOf(this.task.id);
if (currentIdx >= 0 && currentIdx < taskIds.length - 1) {
// Navigate to next task
window.location.href = '{{ url_for("measure.task_execute", task_id=0) }}'.replace('/0', '/' + taskIds[currentIdx + 1]);
} else {
// Last task (or unknown): show completion overlay
this.showCompletionOverlay = true;
}
return;
}