feat: timer intervallo misura su ricetta + auto-logout per inattività

- Recipe: nuovo campo measurement_interval_minutes (migration 003) con UI nel recipe editor
- Auto-logout: impostazione di sistema configurabile da admin/settings, timer inattività JS
  con warning modale 60s prima del logout, tracking eventi utente throttled
- Navbar: aggiunto link "Impostazioni" per admin (desktop + mobile)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-23 14:36:02 +00:00
parent b9c767fb0c
commit 0748ce9b1e
13 changed files with 387 additions and 4 deletions
@@ -115,6 +115,74 @@
{% block extra_js %}{% endblock %}
{% if current_user and auto_logout_minutes %}
<script>
function inactivityTimer() {
var timeoutMs = {{ auto_logout_minutes }} * 60 * 1000;
var warningMs = 60 * 1000;
return {
lastActivity: Date.now(),
showWarning: false,
remainingSeconds: 60,
_interval: null,
init() {
var self = this;
this._interval = setInterval(function() { self._check(); }, 5000);
var lastUpdate = 0;
var handler = function() {
var now = Date.now();
if (now - lastUpdate > 3000) {
lastUpdate = now;
self.lastActivity = now;
self.showWarning = false;
}
};
['mousemove','mousedown','keypress','touchstart','scroll'].forEach(function(evt) {
window.addEventListener(evt, handler, { passive: true });
});
},
resetTimer() {
this.lastActivity = Date.now();
this.showWarning = false;
},
_check() {
var elapsed = Date.now() - this.lastActivity;
var remaining = timeoutMs - elapsed;
if (remaining <= 0) {
clearInterval(this._interval);
window.location.href = '{{ url_for("auth.logout") }}';
} else if (remaining <= warningMs) {
this.showWarning = true;
this.remainingSeconds = Math.ceil(remaining / 1000);
}
}
};
}
</script>
<div x-data="inactivityTimer()" class="contents">
<template x-if="showWarning">
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50" @click.self="resetTimer()">
<div class="bg-[var(--bg-primary)] rounded-xl p-6 shadow-2xl max-w-sm mx-4 border border-[var(--border-color)]">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-[var(--text-primary)]">{{ _('Sessione in scadenza') }}</h3>
</div>
<p class="text-sm text-[var(--text-secondary)] mb-5">
{{ _('Sarai disconnesso tra') }} <span class="font-mono font-bold text-amber-600" x-text="remainingSeconds"></span> {{ _('secondi per inattività.') }}
</p>
<button @click="resetTimer()" class="btn btn-primary w-full">
{{ _('Continua a lavorare') }}
</button>
</div>
</div>
</template>
</div>
{% endif %}
<!-- Alpine.js CDN (defer) - must load AFTER extra_js so component functions are defined -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</body>