fd3384f08c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019A9CSrh9rfM5WwqKtg8BSE
106 lines
5.0 KiB
HTML
106 lines
5.0 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ _("Monitor stazioni") }} — TieMeasureFlow{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="tmf-page" x-data="stationMonitor()" x-init="init()">
|
|
<div class="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-[var(--text-primary)]">{{ _("Monitor stazioni") }}</h1>
|
|
<p class="text-sm text-steel mt-1">{{ _("Produzioni aperte, aggiornate ogni 10 secondi") }}</p>
|
|
</div>
|
|
<div class="text-xs text-steel" x-text="updatedLabel"></div>
|
|
</div>
|
|
|
|
<div x-show="error" class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm" x-text="error"></div>
|
|
|
|
<div x-show="loaded && runs.length === 0 && !error"
|
|
class="p-8 text-center text-steel bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)]">
|
|
{{ _("Nessuna stazione in produzione") }}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
<template x-for="r in runs" :key="r.id">
|
|
<div class="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] border-l-8 shadow-sm p-4"
|
|
:class="borderClass(r)">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div>
|
|
<div class="text-lg font-bold text-[var(--text-primary)]" x-text="r.station_code + ' — ' + r.station_name"></div>
|
|
<div class="text-sm text-steel" x-text="r.recipe_code + ' ' + r.recipe_name + ' (v' + r.version_number + ')'"></div>
|
|
</div>
|
|
<span class="text-xs font-semibold px-2 py-1 rounded"
|
|
:class="r.overdue ? 'bg-red-600 text-white animate-pulse' : 'bg-gray-200 text-gray-700'"
|
|
x-text="statusLabel(r)"></span>
|
|
</div>
|
|
|
|
<dl class="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
|
<dt class="text-steel">{{ _("Operatore") }}</dt><dd x-text="r.operator"></dd>
|
|
<dt class="text-steel">{{ _("Avviata alle") }}</dt><dd x-text="startedAt(r)"></dd>
|
|
<dt class="text-steel">{{ _("Durata") }}</dt><dd x-text="fmt(r.duration_seconds)"></dd>
|
|
<dt class="text-steel">{{ _("Cicli") }}</dt><dd x-text="r.cycle_count"></dd>
|
|
<dt class="text-steel">{{ _("Prossima misura") }}</dt>
|
|
<dd :class="r.overdue ? 'text-red-600 font-bold' : ''" x-text="timerLabel(r)"></dd>
|
|
<dt class="text-steel">{{ _("Ultimo evento") }}</dt>
|
|
<dd x-text="r.last_event ? r.last_event.event_type : '-'"></dd>
|
|
</dl>
|
|
|
|
<div class="mt-3 flex gap-2 text-xs font-semibold">
|
|
<span class="px-2 py-1 rounded bg-green-100 text-green-800" x-text="'OK ' + r.pass_count"></span>
|
|
<span class="px-2 py-1 rounded bg-yellow-100 text-yellow-800" x-text="'{{ _("Avviso") }} ' + r.warning_count"></span>
|
|
<span class="px-2 py-1 rounded bg-red-100 text-red-800" x-text="'NOK ' + r.fail_count"></span>
|
|
</div>
|
|
|
|
<ul class="mt-3 space-y-1 text-sm">
|
|
<template x-for="s in r.subtasks" :key="s.subtask_id">
|
|
<li class="flex justify-between gap-2">
|
|
<span class="truncate" x-text="s.description"></span>
|
|
<span class="font-mono whitespace-nowrap"
|
|
:class="s.in_tolerance ? 'text-green-700' : 'text-red-700 font-bold'"
|
|
x-text="(s.mean == null ? 'NaN' : s.mean.toFixed(3)) + ' ' + s.unit"></span>{# #118: quota con soli valori NaN: media null #}
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function stationMonitor() {
|
|
return {
|
|
runs: [], loaded: false, error: '', updatedLabel: '',
|
|
init() { this.load(); setInterval(() => this.load(), 10000); },
|
|
async load() {
|
|
try {
|
|
const resp = await fetch('/monitor/api/runs');
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) { this.error = data.detail || {{ _("Errore di caricamento")|tojson }}; return; }
|
|
this.runs = data; this.error = ''; this.loaded = true;
|
|
this.updatedLabel = new Date().toLocaleTimeString();
|
|
} catch (e) { this.error = {{ _("Errore di rete")|tojson }}; }
|
|
},
|
|
// Green in tolerance, yellow with warnings, red with fails or a mean out of tolerance.
|
|
borderClass(r) {
|
|
if (r.fail_count > 0 || r.subtasks.some(s => !s.in_tolerance)) return 'border-l-red-600';
|
|
if (r.warning_count > 0) return 'border-l-yellow-500';
|
|
return 'border-l-green-600';
|
|
},
|
|
statusLabel(r) {
|
|
if (r.overdue) return {{ _("IN RITARDO")|tojson }};
|
|
return r.status === 'paused' ? {{ _("In pausa")|tojson }} : {{ _("In corso")|tojson }};
|
|
},
|
|
startedAt(r) { return new Date(r.started_at + 'Z').toLocaleTimeString(); },
|
|
timerLabel(r) {
|
|
if (r.seconds_to_next_measurement === null) return '-';
|
|
const s = r.seconds_to_next_measurement;
|
|
return (s < 0 ? '-' : '') + this.fmt(Math.abs(s));
|
|
},
|
|
fmt(sec) {
|
|
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
|
|
return (h ? h + 'h ' : '') + m + 'm ' + String(s).padStart(2, '0') + 's';
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|