diff --git a/public/incidents.html b/public/incidents.html index 0f64fa2..4af96a0 100644 --- a/public/incidents.html +++ b/public/incidents.html @@ -607,6 +607,8 @@ if (!data.description) { showNotification('La descrizione e\' obbligatoria', 'warning'); return; } if (!data.detected_at) { showNotification('La data di rilevamento e\' obbligatoria', 'warning'); return; } + const btn = document.querySelector('#modal-overlay .btn-primary'); + setButtonLoading(btn, true); try { const result = await api.createIncident(data); if (result.success) { @@ -617,9 +619,11 @@ showNotification(msg, 'success'); loadIncidents(); } else { + setButtonLoading(btn, false); showNotification(result.message || 'Errore nella creazione', 'error'); } } catch (e) { + setButtonLoading(btn, false); showNotification('Errore di connessione', 'error'); } } diff --git a/public/js/api.js b/public/js/api.js index 5b40712..ad41e2d 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -55,7 +55,8 @@ class NIS2API { return json; } catch (error) { console.error(`[API] Network error: ${method} ${endpoint}`, error); - return { success: false, message: 'Errore di connessione al server' }; + const msg = typeof I18n !== 'undefined' ? I18n.t('msg.error_connection') : 'Errore di connessione al server'; + return { success: false, message: msg }; } } @@ -274,7 +275,8 @@ class NIS2API { }); return response.json(); } catch (error) { - return { success: false, message: 'Errore di connessione al server' }; + const msg = typeof I18n !== 'undefined' ? I18n.t('msg.error_connection') : 'Errore di connessione al server'; + return { success: false, message: msg }; } } diff --git a/public/js/common.js b/public/js/common.js index 2b66f72..bc51b7a 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -440,6 +440,7 @@ function _toggleSidebar(forceState) { /** * Verifica che l'utente sia autenticato. Se non lo e', redirige al login. + * Attiva automaticamente il timeout di sessione per inattivita'. * @returns {boolean} */ function checkAuth() { @@ -447,10 +448,110 @@ function checkAuth() { window.location.href = 'login.html'; return false; } + if (!_idleInitialized) { + _idleInitialized = true; + initIdleTimeout(); + } return true; } +// ═══════════════════════════════════════════════════════════════════ +// Idle Session Timeout +// ═══════════════════════════════════════════════════════════════════ + +let _idleTimer = null; +let _idleWarningTimer = null; +let _idleCountdownInterval = null; +let _idleInitialized = false; + +const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minuti totali +const IDLE_WARNING_MS = 5 * 60 * 1000; // avviso 5 minuti prima della scadenza + +/** + * Avvia il monitoraggio di inattivita' della sessione. + * Mostra un avviso 5 minuti prima del logout automatico. + */ +function initIdleTimeout() { + _resetIdleTimer(); + ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(evt => { + document.addEventListener(evt, _resetIdleTimer, { passive: true }); + }); +} + +function _resetIdleTimer() { + clearTimeout(_idleTimer); + clearTimeout(_idleWarningTimer); + clearInterval(_idleCountdownInterval); + + // Chiudi avviso se gia' aperto + const existingWarning = document.getElementById('idle-warning-overlay'); + if (existingWarning) existingWarning.remove(); + + // Timer per mostrare avviso + _idleWarningTimer = setTimeout(_showIdleWarning, IDLE_TIMEOUT_MS - IDLE_WARNING_MS); + + // Timer per logout automatico + _idleTimer = setTimeout(() => { + if (typeof api !== 'undefined') api.logout(); + }, IDLE_TIMEOUT_MS); +} + +function _showIdleWarning() { + const existing = document.getElementById('idle-warning-overlay'); + if (existing) existing.remove(); + + let remaining = Math.floor(IDLE_WARNING_MS / 1000); + + const overlay = document.createElement('div'); + overlay.id = 'idle-warning-overlay'; + overlay.style.cssText = [ + 'position:fixed', 'top:0', 'left:0', 'right:0', 'bottom:0', 'z-index:9999', + 'background:rgba(0,0,0,0.55)', 'display:flex', 'align-items:center', 'justify-content:center' + ].join(';'); + + overlay.innerHTML = ` +
+
+ +
+

Sessione in scadenza

+

+ Per motivi di sicurezza, verrai disconnesso tra
+ 5:00 + a causa di inattivita'. +

+
+ + +
+
+ `; + + document.body.appendChild(overlay); + + document.getElementById('idle-stay-btn').addEventListener('click', () => { + overlay.remove(); + _resetIdleTimer(); + }); + + _idleCountdownInterval = setInterval(() => { + remaining--; + const min = Math.floor(remaining / 60); + const sec = remaining % 60; + const el = document.getElementById('idle-countdown'); + if (el) el.textContent = `${min}:${sec.toString().padStart(2, '0')}`; + if (remaining <= 0) clearInterval(_idleCountdownInterval); + }, 1000); +} + + // ═══════════════════════════════════════════════════════════════════ // Utilities // ═══════════════════════════════════════════════════════════════════ diff --git a/public/policies.html b/public/policies.html index 1ba15d6..8e7eff8 100644 --- a/public/policies.html +++ b/public/policies.html @@ -520,12 +520,16 @@ ${policy.ai_generated ? 'AI Generata' : ''} ${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? ` -
+
-
+
Bozza AI — Revisione obbligatoria prima dell'approvazione

Questo documento e' stato generato automaticamente da un sistema AI. Prima di approvarlo, verificare che sia conforme alle procedure interne, al quadro normativo applicabile e alla realta' specifica dell'organizzazione. L'AI puo' produrre contenuti imprecisi o incompleti.

+
` : ''}
@@ -699,6 +703,8 @@ next_review_date: document.getElementById('form-review-date').value || null, }; + const btn = document.querySelector('#modal-overlay .btn-primary'); + setButtonLoading(btn, true); try { let result; if (id) { @@ -715,9 +721,11 @@ viewPolicy(id); } } else { + setButtonLoading(btn, false); showNotification(result.message || 'Errore nel salvataggio.', 'error'); } } catch (e) { + setButtonLoading(btn, false); showNotification('Errore di connessione.', 'error'); } } @@ -868,6 +876,8 @@ ai_generated: 1, }; + const btn = document.querySelector('#modal-overlay .btn-primary'); + setButtonLoading(btn, true); try { const result = await api.createPolicy(data); if (result.success) { @@ -876,9 +886,11 @@ aiGeneratedData = null; loadPolicies(); } else { + setButtonLoading(btn, false); showNotification(result.message || 'Errore nel salvataggio.', 'error'); } } catch (e) { + setButtonLoading(btn, false); showNotification('Errore di connessione.', 'error'); } } diff --git a/public/risks.html b/public/risks.html index c5bb035..2f5eddb 100644 --- a/public/risks.html +++ b/public/risks.html @@ -307,7 +307,7 @@
- @@ -955,6 +955,7 @@ } async function saveRisk(riskId) { + const btn = document.querySelector('#modal-overlay .btn-primary'); const data = { title: document.getElementById('risk-title').value.trim(), description: document.getElementById('risk-description').value.trim(), @@ -974,6 +975,7 @@ return; } + setButtonLoading(btn, true); try { let result; if (riskId) { @@ -991,9 +993,11 @@ viewRiskDetail(riskId || result.data.id); } } else { + setButtonLoading(btn, false); showNotification(result.message || 'Errore nel salvataggio', 'error'); } } catch (e) { + setButtonLoading(btn, false); showNotification('Errore di connessione', 'error'); } } @@ -1018,8 +1022,10 @@ if (result.success) { closeModal(); showNotification('Rischio eliminato', 'success'); + const wasMatrix = currentView === 'matrix'; backToList(); loadRisks(); + if (wasMatrix) loadMatrix(); } else { showNotification(result.message || 'Errore nella eliminazione', 'error'); } @@ -1097,6 +1103,8 @@ // ── AI Suggest ─────────────────────────────────────────────── async function aiSuggest() { + const btn = document.getElementById('btn-ai-suggest'); + setButtonLoading(btn, true); showNotification('Generazione suggerimenti AI in corso...', 'info'); try { const result = await api.aiSuggestRisks(); @@ -1131,6 +1139,8 @@ } } catch (e) { showNotification('Errore di connessione al servizio AI', 'error'); + } finally { + setButtonLoading(btn, false); } }