[UX+SEC] Eccellenza pre-audit: idle timeout, loading states, i18n, UX polish
- common.js: idle session timeout 30min con avviso countdown 5min prima del logout - common.js: checkAuth() attiva automaticamente il monitor di inattività - api.js: messaggi errore connessione usano i18n (IT/EN) tramite I18n.t() - risks.html: saveRisk() e aiSuggest() con setButtonLoading durante salvataggio - risks.html: deleteRisk() ricarica la matrice se si è in matrix view - incidents.html: createIncident() con setButtonLoading durante registrazione - policies.html: savePolicy() e saveAIGeneratedPolicy() con setButtonLoading - policies.html: banner AI-draft con pulsante X per dismissione Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a56401f5a9
commit
59fad22c0e
@ -607,6 +607,8 @@
|
|||||||
if (!data.description) { showNotification('La descrizione e\' obbligatoria', 'warning'); return; }
|
if (!data.description) { showNotification('La descrizione e\' obbligatoria', 'warning'); return; }
|
||||||
if (!data.detected_at) { showNotification('La data di rilevamento 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 {
|
try {
|
||||||
const result = await api.createIncident(data);
|
const result = await api.createIncident(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -617,9 +619,11 @@
|
|||||||
showNotification(msg, 'success');
|
showNotification(msg, 'success');
|
||||||
loadIncidents();
|
loadIncidents();
|
||||||
} else {
|
} else {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification(result.message || 'Errore nella creazione', 'error');
|
showNotification(result.message || 'Errore nella creazione', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification('Errore di connessione', 'error');
|
showNotification('Errore di connessione', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,8 @@ class NIS2API {
|
|||||||
return json;
|
return json;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[API] Network error: ${method} ${endpoint}`, 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();
|
return response.json();
|
||||||
} catch (error) {
|
} 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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -440,6 +440,7 @@ function _toggleSidebar(forceState) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica che l'utente sia autenticato. Se non lo e', redirige al login.
|
* Verifica che l'utente sia autenticato. Se non lo e', redirige al login.
|
||||||
|
* Attiva automaticamente il timeout di sessione per inattivita'.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function checkAuth() {
|
function checkAuth() {
|
||||||
@ -447,10 +448,110 @@ function checkAuth() {
|
|||||||
window.location.href = 'login.html';
|
window.location.href = 'login.html';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!_idleInitialized) {
|
||||||
|
_idleInitialized = true;
|
||||||
|
initIdleTimeout();
|
||||||
|
}
|
||||||
return true;
|
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 = `
|
||||||
|
<div style="background:var(--card-bg,#fff);border-radius:12px;padding:32px;max-width:400px;width:90%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||||
|
<div style="width:56px;height:56px;background:#fff7ed;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 20 20" fill="#c2410c"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 style="font-size:1.1rem;font-weight:700;color:var(--gray-800,#1f2937);margin-bottom:8px;">Sessione in scadenza</h3>
|
||||||
|
<p style="font-size:0.875rem;color:var(--gray-500,#6b7280);margin-bottom:20px;">
|
||||||
|
Per motivi di sicurezza, verrai disconnesso tra<br>
|
||||||
|
<strong id="idle-countdown" style="font-size:1.5rem;color:#c2410c;display:block;margin:8px 0;">5:00</strong>
|
||||||
|
a causa di inattivita'.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:12px;justify-content:center;">
|
||||||
|
<button id="idle-stay-btn"
|
||||||
|
style="padding:10px 24px;background:var(--primary,#2563eb);color:#fff;border:none;border-radius:8px;font-weight:600;cursor:pointer;font-size:0.875rem;">
|
||||||
|
Rimani connesso
|
||||||
|
</button>
|
||||||
|
<button onclick="api.logout();"
|
||||||
|
style="padding:10px 24px;background:var(--gray-100,#f3f4f6);color:var(--gray-700,#374151);border:none;border-radius:8px;font-weight:600;cursor:pointer;font-size:0.875rem;">
|
||||||
|
Disconnetti
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
// Utilities
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -520,12 +520,16 @@
|
|||||||
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
|
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
|
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
|
||||||
<div style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start;">
|
<div id="ai-draft-warning-${policy.id}" style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start; position:relative;">
|
||||||
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||||
<div>
|
<div style="flex:1;">
|
||||||
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
|
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
|
||||||
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">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.</p>
|
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">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.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button onclick="document.getElementById('ai-draft-warning-${policy.id}').remove();" title="Chiudi" aria-label="Chiudi avviso"
|
||||||
|
style="flex-shrink:0;background:none;border:none;cursor:pointer;color:#c2410c;padding:2px;line-height:1;opacity:0.7;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||||
|
</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-header-actions">
|
<div class="detail-header-actions">
|
||||||
@ -699,6 +703,8 @@
|
|||||||
next_review_date: document.getElementById('form-review-date').value || null,
|
next_review_date: document.getElementById('form-review-date').value || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
||||||
|
setButtonLoading(btn, true);
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -715,9 +721,11 @@
|
|||||||
viewPolicy(id);
|
viewPolicy(id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification('Errore di connessione.', 'error');
|
showNotification('Errore di connessione.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -868,6 +876,8 @@
|
|||||||
ai_generated: 1,
|
ai_generated: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
||||||
|
setButtonLoading(btn, true);
|
||||||
try {
|
try {
|
||||||
const result = await api.createPolicy(data);
|
const result = await api.createPolicy(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -876,9 +886,11 @@
|
|||||||
aiGeneratedData = null;
|
aiGeneratedData = null;
|
||||||
loadPolicies();
|
loadPolicies();
|
||||||
} else {
|
} else {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification('Errore di connessione.', 'error');
|
showNotification('Errore di connessione.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -307,7 +307,7 @@
|
|||||||
<button class="active" onclick="switchView('table')">Tabella</button>
|
<button class="active" onclick="switchView('table')">Tabella</button>
|
||||||
<button onclick="switchView('matrix')">Matrice</button>
|
<button onclick="switchView('matrix')">Matrice</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" onclick="aiSuggest()">
|
<button class="btn btn-secondary" id="btn-ai-suggest" onclick="aiSuggest()">
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
||||||
AI Suggerisci
|
AI Suggerisci
|
||||||
</button>
|
</button>
|
||||||
@ -955,6 +955,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveRisk(riskId) {
|
async function saveRisk(riskId) {
|
||||||
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
||||||
const data = {
|
const data = {
|
||||||
title: document.getElementById('risk-title').value.trim(),
|
title: document.getElementById('risk-title').value.trim(),
|
||||||
description: document.getElementById('risk-description').value.trim(),
|
description: document.getElementById('risk-description').value.trim(),
|
||||||
@ -974,6 +975,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setButtonLoading(btn, true);
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (riskId) {
|
if (riskId) {
|
||||||
@ -991,9 +993,11 @@
|
|||||||
viewRiskDetail(riskId || result.data.id);
|
viewRiskDetail(riskId || result.data.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification(result.message || 'Errore nel salvataggio', 'error');
|
showNotification(result.message || 'Errore nel salvataggio', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
showNotification('Errore di connessione', 'error');
|
showNotification('Errore di connessione', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1018,8 +1022,10 @@
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
closeModal();
|
closeModal();
|
||||||
showNotification('Rischio eliminato', 'success');
|
showNotification('Rischio eliminato', 'success');
|
||||||
|
const wasMatrix = currentView === 'matrix';
|
||||||
backToList();
|
backToList();
|
||||||
loadRisks();
|
loadRisks();
|
||||||
|
if (wasMatrix) loadMatrix();
|
||||||
} else {
|
} else {
|
||||||
showNotification(result.message || 'Errore nella eliminazione', 'error');
|
showNotification(result.message || 'Errore nella eliminazione', 'error');
|
||||||
}
|
}
|
||||||
@ -1097,6 +1103,8 @@
|
|||||||
|
|
||||||
// ── AI Suggest ───────────────────────────────────────────────
|
// ── AI Suggest ───────────────────────────────────────────────
|
||||||
async function aiSuggest() {
|
async function aiSuggest() {
|
||||||
|
const btn = document.getElementById('btn-ai-suggest');
|
||||||
|
setButtonLoading(btn, true);
|
||||||
showNotification('Generazione suggerimenti AI in corso...', 'info');
|
showNotification('Generazione suggerimenti AI in corso...', 'info');
|
||||||
try {
|
try {
|
||||||
const result = await api.aiSuggestRisks();
|
const result = await api.aiSuggestRisks();
|
||||||
@ -1131,6 +1139,8 @@
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showNotification('Errore di connessione al servizio AI', 'error');
|
showNotification('Errore di connessione al servizio AI', 'error');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user