nis2-agile/public/incidents.html
DevEnv nis2-agile 59fad22c0e [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>
2026-02-20 12:25:52 +01:00

1220 lines
63 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Incidenti - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Filters Bar ─────────────────────────────────────── */
.filters-bar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
}
.filters-bar .form-select {
width: auto;
min-width: 160px;
padding: 8px 36px 8px 12px;
font-size: 0.8rem;
}
/* ── Severity Colors ─────────────────────────────────── */
.severity-critical { background: var(--danger-bg); color: var(--danger); }
.severity-high { background: #fff7ed; color: #c2410c; }
.severity-medium { background: var(--warning-bg); color: #a16207; }
.severity-low { background: var(--secondary-bg); color: #15803d; }
/* ── Overdue Warning Icon ─────────────────────────────── */
.overdue-icon {
color: var(--danger);
display: inline-flex;
vertical-align: middle;
animation: pulse-warn 1.5s ease-in-out infinite;
}
@keyframes pulse-warn {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── NIS2 Timeline ───────────────────────────────────── */
.nis2-timeline {
position: relative;
padding: 16px 0;
}
.nis2-timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 3px;
background: var(--gray-200);
border-radius: 2px;
}
.nis2-step {
position: relative;
padding-left: 52px;
margin-bottom: 28px;
}
.nis2-step:last-child { margin-bottom: 0; }
.nis2-step-dot {
position: absolute;
left: 10px;
top: 2px;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-200);
z-index: 1;
}
.nis2-step-dot svg {
width: 14px;
height: 14px;
fill: var(--gray-500);
}
.nis2-step-dot.sent {
background: var(--secondary-bg);
}
.nis2-step-dot.sent svg { fill: var(--secondary); }
.nis2-step-dot.overdue {
background: var(--danger-bg);
}
.nis2-step-dot.overdue svg { fill: var(--danger); }
.nis2-step-dot.approaching {
background: var(--warning-bg);
}
.nis2-step-dot.approaching svg { fill: #a16207; }
.nis2-step-content {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 16px;
}
.nis2-step-content.sent { border-color: var(--secondary); background: #f0fdf4; }
.nis2-step-content.overdue { border-color: var(--danger); background: #fef2f2; }
.nis2-step-content.approaching { border-color: var(--warning); background: #fffbeb; }
.nis2-step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.nis2-step-title {
font-weight: 700;
font-size: 0.9rem;
color: var(--gray-800);
}
.nis2-step-time {
font-size: 0.8rem;
color: var(--gray-500);
}
.nis2-step-countdown {
font-size: 0.85rem;
font-weight: 600;
margin-top: 6px;
}
.countdown-overdue { color: var(--danger); }
.countdown-approaching { color: #a16207; }
.countdown-ok { color: var(--secondary); }
/* ── Event Timeline ──────────────────────────────────── */
.event-timeline {
position: relative;
padding-left: 20px;
}
.event-timeline::before {
content: '';
position: absolute;
left: 6px;
top: 0;
bottom: 0;
width: 2px;
background: var(--gray-200);
}
.event-item {
position: relative;
padding-left: 24px;
padding-bottom: 20px;
}
.event-item:last-child { padding-bottom: 0; }
.event-item::before {
content: '';
position: absolute;
left: -17px;
top: 6px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--primary);
border: 2px solid var(--card-bg);
z-index: 1;
}
.event-item.type-notification::before { background: var(--secondary); }
.event-item.type-detection::before { background: var(--danger); }
.event-item.type-action::before { background: var(--warning); }
.event-item-time {
font-size: 0.7rem;
color: var(--gray-400);
margin-bottom: 2px;
}
.event-item-text {
font-size: 0.85rem;
color: var(--gray-700);
}
.event-item-author {
font-size: 0.75rem;
color: var(--gray-400);
margin-top: 2px;
}
/* ── Detail View ─────────────────────────────────────── */
.detail-back {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
color: var(--gray-500);
cursor: pointer;
margin-bottom: 16px;
border: none;
background: none;
font-family: inherit;
padding: 0;
}
.detail-back:hover { color: var(--primary); }
.detail-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
.detail-field {
margin-bottom: 16px;
}
.detail-field-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--gray-500);
margin-bottom: 4px;
}
.detail-field-value {
font-size: 0.9rem;
color: var(--gray-800);
}
/* ── Table ────────────────────────────────────────────── */
.table-clickable tbody tr { cursor: pointer; }
/* ── NIS2 Status Compact ──────────────────────────────── */
.nis2-compact {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.7rem;
}
.nis2-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.nis2-dot.sent { background: var(--secondary); }
.nis2-dot.overdue { background: var(--danger); }
.nis2-dot.pending { background: var(--gray-300); }
.nis2-dot.approaching { background: var(--warning); }
/* ── Checkbox inline ──────────────────────────────────── */
.form-check-inline {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
cursor: pointer;
padding: 8px 0;
}
.form-check-inline input[type="checkbox"] {
accent-color: var(--primary);
width: 18px;
height: 18px;
}
@media (max-width: 768px) {
.detail-grid { grid-template-columns: 1fr; }
.filters-bar { flex-direction: column; align-items: stretch; }
.filters-bar .form-select { min-width: 100%; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="incidents.title">Gestione Incidenti</h2>
<div class="content-header-actions">
<button class="btn btn-primary" onclick="showCreateIncidentModal()">+ Nuovo Incidente</button>
</div>
</header>
<div class="content-body">
<!-- List View -->
<div id="view-list">
<div class="filters-bar">
<select class="form-select" id="filter-classification" onchange="loadIncidents()">
<option value="">Tutte le classificazioni</option>
<option value="cyber_attack">Attacco Cyber</option>
<option value="data_breach">Data Breach</option>
<option value="system_failure">Guasto Sistema</option>
<option value="human_error">Errore Umano</option>
<option value="natural_disaster">Disastro Naturale</option>
<option value="supply_chain">Supply Chain</option>
<option value="other">Altro</option>
</select>
<select class="form-select" id="filter-severity" onchange="loadIncidents()">
<option value="">Tutte le severita'</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
</select>
<select class="form-select" id="filter-status" onchange="loadIncidents()">
<option value="">Tutti gli stati</option>
<option value="detected">Rilevato</option>
<option value="analyzing">In Analisi</option>
<option value="containing">Contenimento</option>
<option value="eradicating">Eradicazione</option>
<option value="recovering">Ripristino</option>
<option value="closed">Chiuso</option>
<option value="post_mortem">Post Mortem</option>
</select>
</div>
<div class="card">
<div class="table-container">
<table class="table-clickable">
<thead>
<tr>
<th>Codice</th>
<th>Titolo</th>
<th>Classificazione</th>
<th>Severita'</th>
<th>Stato</th>
<th>Rilevato</th>
<th>NIS2 Art. 23</th>
<th>Azioni</th>
</tr>
</thead>
<tbody id="incidents-table-body">
<tr>
<td colspan="8">
<div class="spinner" style="margin:40px auto;"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Detail View -->
<div id="view-detail" class="hidden"></div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
HelpSystem.init();
// ── Labels ───────────────────────────────────────────────────
const CLASSIFICATION_LABELS = {
cyber_attack: 'Attacco Cyber', data_breach: 'Data Breach',
system_failure: 'Guasto Sistema', human_error: 'Errore Umano',
natural_disaster: 'Disastro Naturale', supply_chain: 'Supply Chain', other: 'Altro'
};
const SEVERITY_LABELS = { low: 'Basso', medium: 'Medio', high: 'Alto', critical: 'Critico' };
const STATUS_LABELS = {
detected: 'Rilevato', analyzing: 'In Analisi', containing: 'Contenimento',
eradicating: 'Eradicazione', recovering: 'Ripristino',
closed: 'Chiuso', post_mortem: 'Post Mortem'
};
// ── Load ─────────────────────────────────────────────────────
loadIncidents();
async function loadIncidents() {
const params = {};
const cls = document.getElementById('filter-classification').value;
const sev = document.getElementById('filter-severity').value;
const sts = document.getElementById('filter-status').value;
if (cls) params.classification = cls;
if (sev) params.severity = sev;
if (sts) params.status = sts;
try {
const result = await api.listIncidents(params);
if (result.success) {
renderIncidentsTable(result.data || []);
} else {
showNotification(result.message || 'Errore nel caricamento incidenti', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Table Render ─────────────────────────────────────────────
function renderIncidentsTable(incidents) {
const tbody = document.getElementById('incidents-table-body');
if (!incidents || incidents.length === 0) {
tbody.innerHTML = `
<tr><td colspan="8">
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>
<h4>Nessun incidente registrato</h4>
<p>Clicca "Nuovo Incidente" per registrare un nuovo incidente di sicurezza.</p>
</div>
</td></tr>`;
return;
}
const now = new Date();
let html = '';
incidents.forEach(inc => {
const severityBadge = getSeverityBadge(inc.severity);
const statusBadge = getIncidentStatusBadge(inc.status);
const classLabel = CLASSIFICATION_LABELS[inc.classification] || inc.classification;
const nis2Status = renderNis2Compact(inc, now);
html += `
<tr onclick="viewIncidentDetail(${inc.id})">
<td><code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(inc.incident_code || '-')}</code></td>
<td>
<strong>${escapeHtml(inc.title)}</strong>
${inc.is_significant ? '<span class="badge badge-danger" style="margin-left:6px; font-size:0.6rem;">SIGNIFICATIVO</span>' : ''}
</td>
<td><span class="tag">${escapeHtml(classLabel)}</span></td>
<td>${severityBadge}</td>
<td>${statusBadge}</td>
<td style="font-size:0.8rem;">${formatDateTime(inc.detected_at)}</td>
<td>${nis2Status}</td>
<td>
<div class="btn-group" onclick="event.stopPropagation()">
<button class="btn btn-ghost btn-sm btn-icon" onclick="showUpdateIncidentModal(${inc.id})" title="Aggiorna">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</button>
</div>
</td>
</tr>`;
});
tbody.innerHTML = html;
}
function getSeverityBadge(severity) {
const cls = { critical: 'severity-critical', high: 'severity-high', medium: 'severity-medium', low: 'severity-low' };
return `<span class="badge ${cls[severity] || 'badge-neutral'}">${escapeHtml(SEVERITY_LABELS[severity] || severity)}</span>`;
}
function getIncidentStatusBadge(status) {
const map = {
detected: 'badge-danger', analyzing: 'badge-warning', containing: 'badge-primary',
eradicating: 'badge-primary', recovering: 'badge-info',
closed: 'badge-success', post_mortem: 'badge-neutral'
};
return `<span class="badge ${map[status] || 'badge-neutral'}">${escapeHtml(STATUS_LABELS[status] || status)}</span>`;
}
function renderNis2Compact(inc, now) {
if (!inc.is_significant) return '<span class="text-muted" style="font-size:0.75rem;">N/A</span>';
function dotClass(dueDate, sentAt) {
if (sentAt) return 'sent';
if (!dueDate) return 'pending';
const due = new Date(dueDate);
if (now > due) return 'overdue';
const hoursLeft = (due - now) / 3600000;
if (hoursLeft < 6) return 'approaching';
return 'pending';
}
const ew = dotClass(inc.early_warning_due, inc.early_warning_sent_at);
const nt = dotClass(inc.notification_due, inc.notification_sent_at);
const fr = dotClass(inc.final_report_due, inc.final_report_sent_at);
return `
<div class="nis2-compact">
<span class="nis2-dot ${ew}" title="Early Warning 24h"></span>
<span class="nis2-dot ${nt}" title="Notifica 72h"></span>
<span class="nis2-dot ${fr}" title="Report Finale 30gg"></span>
${ew === 'overdue' || nt === 'overdue' || fr === 'overdue'
? '<span class="overdue-icon"><svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><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></span>'
: ''}
</div>`;
}
// ── Create Incident Modal ────────────────────────────────────
function showCreateIncidentModal() {
const nowLocal = new Date();
const dtLocal = nowLocal.getFullYear() + '-' +
String(nowLocal.getMonth()+1).padStart(2,'0') + '-' +
String(nowLocal.getDate()).padStart(2,'0') + 'T' +
String(nowLocal.getHours()).padStart(2,'0') + ':' +
String(nowLocal.getMinutes()).padStart(2,'0');
const content = `
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="inc-title" placeholder="Titolo dell'incidente">
</div>
<div class="form-group">
<label class="form-label">Descrizione <span class="required">*</span></label>
<textarea class="form-textarea" id="inc-description" rows="3" placeholder="Descrizione dettagliata dell'incidente"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Classificazione</label>
<select class="form-select" id="inc-classification">
<option value="cyber_attack">Attacco Cyber</option>
<option value="data_breach">Data Breach</option>
<option value="system_failure">Guasto Sistema</option>
<option value="human_error">Errore Umano</option>
<option value="natural_disaster">Disastro Naturale</option>
<option value="supply_chain">Supply Chain</option>
<option value="other">Altro</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Severita'</label>
<select class="form-select" id="inc-severity">
<option value="low">Basso</option>
<option value="medium" selected>Medio</option>
<option value="high">Alto</option>
<option value="critical">Critico</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Data e ora rilevamento <span class="required">*</span></label>
<input type="datetime-local" class="form-input" id="inc-detected-at" value="${dtLocal}">
</div>
<!-- Decision tree Art.23 NIS2 - significativita' incidente -->
<div style="background:var(--gray-50); border:1px solid var(--gray-200); border-radius:var(--border-radius); padding:14px; margin-bottom:16px;">
<div style="font-size:0.8rem; font-weight:700; color:var(--gray-700); margin-bottom:10px; display:flex; align-items:center; gap:6px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16" style="color:var(--primary);"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>
Criteri Art. 23 NIS2 — Incidente Significativo
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:10px;">Un incidente e' significativo se soddisfa almeno uno dei criteri seguenti (Considerando 101 NIS2 / D.Lgs. 138/2024 art. 25):</p>
<div style="display:grid; gap:6px;">
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-users" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Ha causato o puo' causare interruzione grave dei servizi per <strong>&ge;500 utenti</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-duration" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Durata dell'interruzione superiore a <strong>4 ore</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-crossborder" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Impatto su altri Stati Membri UE (impatto transfrontaliero)</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-attack" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>E' un attacco cyber o data breach con dati personali coinvolti</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-financial" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Perdita finanziaria stimata superiore a <strong>€100.000</strong></span>
</label>
</div>
<div id="significance-result" style="margin-top:10px; padding:8px 12px; border-radius:6px; font-size:0.8rem; font-weight:600; display:none;"></div>
</div>
<div class="form-group">
<label class="form-check-inline">
<input type="checkbox" id="inc-significant">
<span><strong>Incidente significativo</strong> (richiede notifica CSIRT/ACN ai sensi dell'Art. 23 NIS2)</span>
</label>
</div>
<div class="form-group">
<label class="form-label">Servizi interessati</label>
<textarea class="form-textarea" id="inc-affected-services" rows="2" placeholder="Elenco dei servizi e sistemi interessati"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Utenti coinvolti</label>
<input type="number" class="form-input" id="inc-affected-users" min="0" placeholder="0">
</div>
</div>
<div class="form-group">
<label class="form-check-inline">
<input type="checkbox" id="inc-cross-border">
<span>Impatto transfrontaliero</span>
</label>
</div>
<div class="form-group">
<label class="form-check-inline">
<input type="checkbox" id="inc-malicious">
<span>Azione dolosa / malevola</span>
</label>
</div>
`;
showModal('Registra Nuovo Incidente', content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="createIncident()">Registra Incidente</button>
`
});
}
async function createIncident() {
const data = {
title: document.getElementById('inc-title').value.trim(),
description: document.getElementById('inc-description').value.trim(),
classification: document.getElementById('inc-classification').value,
severity: document.getElementById('inc-severity').value,
detected_at: document.getElementById('inc-detected-at').value,
is_significant: document.getElementById('inc-significant').checked ? 1 : 0,
affected_services: document.getElementById('inc-affected-services').value.trim(),
affected_users_count: parseInt(document.getElementById('inc-affected-users').value) || 0,
cross_border_impact: document.getElementById('inc-cross-border').checked ? 1 : 0,
malicious_action: document.getElementById('inc-malicious').checked ? 1 : 0,
};
if (!data.title) { showNotification('Il titolo e\' obbligatorio', '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; }
const btn = document.querySelector('#modal-overlay .btn-primary');
setButtonLoading(btn, true);
try {
const result = await api.createIncident(data);
if (result.success) {
closeModal();
const msg = data.is_significant
? 'Incidente significativo registrato. Scadenze NIS2 Art. 23 attivate.'
: 'Incidente registrato con successo.';
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');
}
}
// ── Update Incident Modal ────────────────────────────────────
async function showUpdateIncidentModal(id) {
let incident = {};
try {
const result = await api.getIncident(id);
if (result.success) incident = result.data;
} catch (e) { /* defaults */ }
const content = `
<div class="form-group">
<label class="form-label">Stato</label>
<select class="form-select" id="upd-status">
${Object.entries(STATUS_LABELS).map(([k, v]) =>
`<option value="${k}" ${incident.status === k ? 'selected' : ''}>${v}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">Causa Radice</label>
<textarea class="form-textarea" id="upd-root-cause" rows="3" placeholder="Analisi della causa radice">${escapeHtml(incident.root_cause || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Azioni di Rimedio</label>
<textarea class="form-textarea" id="upd-remediation" rows="3" placeholder="Azioni correttive intraprese">${escapeHtml(incident.remediation_actions || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Lezioni Apprese</label>
<textarea class="form-textarea" id="upd-lessons" rows="3" placeholder="Lezioni apprese dall'incidente">${escapeHtml(incident.lessons_learned || '')}</textarea>
</div>
`;
showModal('Aggiorna Incidente - ' + escapeHtml(incident.incident_code || ''), content, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="updateIncident(${id})">Salva Aggiornamento</button>
`
});
}
async function updateIncident(id) {
const data = {
status: document.getElementById('upd-status').value,
root_cause: document.getElementById('upd-root-cause').value.trim(),
remediation_actions: document.getElementById('upd-remediation').value.trim(),
lessons_learned: document.getElementById('upd-lessons').value.trim(),
};
try {
const result = await api.updateIncident(id, data);
if (result.success) {
closeModal();
showNotification('Incidente aggiornato', 'success');
loadIncidents();
if (!document.getElementById('view-detail').classList.contains('hidden')) {
viewIncidentDetail(id);
}
} else {
showNotification(result.message || 'Errore nell\'aggiornamento', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Detail View ──────────────────────────────────────────────
async function viewIncidentDetail(id) {
try {
const result = await api.getIncident(id);
if (!result.success) {
showNotification(result.message || 'Errore nel caricamento', 'error');
return;
}
renderIncidentDetail(result.data);
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
function renderIncidentDetail(inc) {
document.getElementById('view-list').classList.add('hidden');
const container = document.getElementById('view-detail');
container.classList.remove('hidden');
const now = new Date();
const classLabel = CLASSIFICATION_LABELS[inc.classification] || inc.classification;
// NIS2 Timeline
let nis2Html = '';
if (inc.is_significant) {
nis2Html = renderNis2Timeline(inc, now);
} else {
nis2Html = `
<div class="card mb-24">
<div class="card-header"><h3>NIS2 Art. 23 - Notifiche</h3></div>
<div class="card-body">
<p class="text-muted" style="font-size:0.85rem;">
Questo incidente non e' stato classificato come significativo.
Le notifiche NIS2 Art. 23 non sono richieste.
</p>
</div>
</div>`;
}
// Event Timeline
let timelineHtml = '';
if (inc.timeline && inc.timeline.length > 0) {
inc.timeline.forEach(ev => {
const typeClass = 'type-' + (ev.event_type || 'action');
timelineHtml += `
<div class="event-item ${typeClass}">
<div class="event-item-time">${formatDateTime(ev.created_at)}</div>
<div class="event-item-text">${escapeHtml(ev.description)}</div>
${ev.created_by_name ? `<div class="event-item-author">di ${escapeHtml(ev.created_by_name)}</div>` : ''}
</div>`;
});
} else {
timelineHtml = '<p class="text-muted" style="font-size:0.85rem;">Nessun evento nella timeline.</p>';
}
container.innerHTML = `
<button class="detail-back" onclick="backToList()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
Torna alla lista
</button>
<div class="detail-grid">
<div>
<!-- Info Card -->
<div class="card mb-24">
<div class="card-header">
<h3>
<code style="color:var(--primary); margin-right:8px;">${escapeHtml(inc.incident_code || '')}</code>
${escapeHtml(inc.title)}
</h3>
<div class="btn-group">
<button class="btn btn-secondary btn-sm" onclick="showUpdateIncidentModal(${inc.id})">Aggiorna</button>
</div>
</div>
<div class="card-body">
<div class="form-row mb-16">
<div class="detail-field">
<div class="detail-field-label">Classificazione</div>
<div class="detail-field-value"><span class="tag">${escapeHtml(classLabel)}</span></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Severita'</div>
<div class="detail-field-value">${getSeverityBadge(inc.severity)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Stato</div>
<div class="detail-field-value">${getIncidentStatusBadge(inc.status)}</div>
</div>
</div>
${inc.description ? `
<div class="detail-field">
<div class="detail-field-label">Descrizione</div>
<div class="detail-field-value">${escapeHtml(inc.description)}</div>
</div>` : ''}
<div class="form-row">
<div class="detail-field">
<div class="detail-field-label">Data Rilevamento</div>
<div class="detail-field-value">${formatDateTime(inc.detected_at)}</div>
</div>
${inc.reported_by_name ? `
<div class="detail-field">
<div class="detail-field-label">Segnalato da</div>
<div class="detail-field-value">${escapeHtml(inc.reported_by_name)}</div>
</div>` : ''}
${inc.assigned_to_name ? `
<div class="detail-field">
<div class="detail-field-label">Assegnato a</div>
<div class="detail-field-value">${escapeHtml(inc.assigned_to_name)}</div>
</div>` : ''}
</div>
<div class="form-row">
${inc.affected_services ? `
<div class="detail-field">
<div class="detail-field-label">Servizi Interessati</div>
<div class="detail-field-value">${escapeHtml(inc.affected_services)}</div>
</div>` : ''}
${inc.affected_users_count ? `
<div class="detail-field">
<div class="detail-field-label">Utenti Coinvolti</div>
<div class="detail-field-value">${inc.affected_users_count}</div>
</div>` : ''}
</div>
<div class="form-row">
<div class="detail-field">
<div class="detail-field-label">Incidente Significativo</div>
<div class="detail-field-value">${inc.is_significant ? '<span class="badge badge-danger">Si</span>' : '<span class="badge badge-neutral">No</span>'}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Impatto Transfrontaliero</div>
<div class="detail-field-value">${inc.cross_border_impact ? 'Si' : 'No'}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Azione Malevola</div>
<div class="detail-field-value">${inc.malicious_action ? 'Si' : 'No'}</div>
</div>
</div>
${inc.root_cause ? `
<div class="detail-field">
<div class="detail-field-label">Causa Radice</div>
<div class="detail-field-value">${escapeHtml(inc.root_cause)}</div>
</div>` : ''}
${inc.remediation_actions ? `
<div class="detail-field">
<div class="detail-field-label">Azioni di Rimedio</div>
<div class="detail-field-value">${escapeHtml(inc.remediation_actions)}</div>
</div>` : ''}
${inc.lessons_learned ? `
<div class="detail-field">
<div class="detail-field-label">Lezioni Apprese</div>
<div class="detail-field-value">${escapeHtml(inc.lessons_learned)}</div>
</div>` : ''}
</div>
</div>
<!-- NIS2 Timeline -->
${nis2Html}
<!-- Event Timeline -->
<div class="card">
<div class="card-header">
<h3>Timeline Eventi</h3>
<button class="btn btn-sm btn-primary" onclick="showAddEventModal(${inc.id})">+ Aggiungi Evento</button>
</div>
<div class="card-body">
<div class="event-timeline">
${timelineHtml}
</div>
</div>
</div>
</div>
<div>
<!-- Summary -->
<div class="card mb-24">
<div class="card-header"><h3>Riepilogo</h3></div>
<div class="card-body">
<div class="detail-field">
<div class="detail-field-label">Codice</div>
<div class="detail-field-value"><code>${escapeHtml(inc.incident_code || '-')}</code></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Creato il</div>
<div class="detail-field-value">${formatDateTime(inc.created_at)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Aggiornato il</div>
<div class="detail-field-value">${formatDateTime(inc.updated_at)}</div>
</div>
${inc.closed_at ? `
<div class="detail-field">
<div class="detail-field-label">Chiuso il</div>
<div class="detail-field-value">${formatDateTime(inc.closed_at)}</div>
</div>` : ''}
<div class="detail-field">
<div class="detail-field-label">Eventi Timeline</div>
<div class="detail-field-value">${inc.timeline ? inc.timeline.length : 0}</div>
</div>
</div>
</div>
<!-- Quick Actions -->
${inc.is_significant ? `
<div class="card">
<div class="card-header"><h3>Azioni Rapide</h3></div>
<div class="card-body" style="display:flex; flex-direction:column; gap:8px;">
${!inc.early_warning_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendEarlyWarning(${inc.id})">Invia Early Warning (24h)</button>` : `<button class="btn btn-success w-full" disabled>Early Warning Inviato</button>`}
${!inc.notification_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendNotification(${inc.id})">Invia Notifica (72h)</button>` : `<button class="btn btn-success w-full" disabled>Notifica Inviata</button>`}
${!inc.final_report_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendFinalReport(${inc.id})">Invia Report Finale (30gg)</button>` : `<button class="btn btn-success w-full" disabled>Report Finale Inviato</button>`}
</div>
</div>` : ''}
</div>
</div>
`;
}
// ── NIS2 Art. 23 Timeline Render ─────────────────────────────
function renderNis2Timeline(inc, now) {
const steps = [
{
title: 'Early Warning (24h)',
dueDate: inc.early_warning_due,
sentAt: inc.early_warning_sent_at,
description: 'Allerta iniziale al CSIRT nazionale (ACN) entro 24 ore dal rilevamento.',
action: `doSendEarlyWarning(${inc.id})`,
actionLabel: 'Invia Early Warning'
},
{
title: 'Notifica Incidente (72h)',
dueDate: inc.notification_due,
sentAt: inc.notification_sent_at,
description: 'Notifica formale al CSIRT nazionale (ACN) con dettagli dell\'incidente entro 72 ore.',
action: `doSendNotification(${inc.id})`,
actionLabel: 'Invia Notifica'
},
{
title: 'Report Finale (30 giorni)',
dueDate: inc.final_report_due,
sentAt: inc.final_report_sent_at,
description: 'Report finale completo con analisi della causa radice, impatto e lezioni apprese entro 30 giorni.',
action: `doSendFinalReport(${inc.id})`,
actionLabel: 'Invia Report Finale'
}
];
let stepsHtml = '';
steps.forEach(step => {
let statusClass = '';
let dotClass = '';
let countdownHtml = '';
let actionHtml = '';
if (step.sentAt) {
statusClass = 'sent';
dotClass = 'sent';
countdownHtml = `<div class="nis2-step-countdown countdown-ok">Inviato il ${formatDateTime(step.sentAt)}</div>`;
} else if (step.dueDate) {
const due = new Date(step.dueDate);
const diffMs = due - now;
if (diffMs < 0) {
// Overdue
statusClass = 'overdue';
dotClass = 'overdue';
const overdueMins = Math.floor(-diffMs / 60000);
const overdueHours = Math.floor(overdueMins / 60);
const overdueDays = Math.floor(overdueHours / 24);
let overdueText = '';
if (overdueDays > 0) overdueText = `Scaduto da ${overdueDays} giorn${overdueDays === 1 ? 'o' : 'i'} e ${overdueHours % 24} ore`;
else if (overdueHours > 0) overdueText = `Scaduto da ${overdueHours} or${overdueHours === 1 ? 'a' : 'e'} e ${overdueMins % 60} min`;
else overdueText = `Scaduto da ${overdueMins} minut${overdueMins === 1 ? 'o' : 'i'}`;
countdownHtml = `<div class="nis2-step-countdown countdown-overdue">${overdueText}</div>`;
actionHtml = `<button class="btn btn-danger btn-sm mt-8" onclick="${step.action}">${step.actionLabel} (URGENTE)</button>`;
} else {
// Not yet due
const minsLeft = Math.floor(diffMs / 60000);
const hoursLeft = Math.floor(minsLeft / 60);
const daysLeft = Math.floor(hoursLeft / 24);
let timeText = '';
if (daysLeft > 0) timeText = `${daysLeft} giorn${daysLeft === 1 ? 'o' : 'i'} e ${hoursLeft % 24} ore rimanenti`;
else if (hoursLeft > 0) timeText = `${hoursLeft} or${hoursLeft === 1 ? 'a' : 'e'} e ${minsLeft % 60} min rimanenti`;
else timeText = `${minsLeft} minut${minsLeft === 1 ? 'o' : 'i'} rimanenti`;
if (hoursLeft < 6) {
statusClass = 'approaching';
dotClass = 'approaching';
countdownHtml = `<div class="nis2-step-countdown countdown-approaching">${timeText}</div>`;
} else {
countdownHtml = `<div class="nis2-step-countdown" style="color:var(--gray-600);">${timeText}</div>`;
}
actionHtml = `<button class="btn btn-primary btn-sm mt-8" onclick="${step.action}">${step.actionLabel}</button>`;
}
}
const checkIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
const clockIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><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>';
const warnIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><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>';
let icon = clockIcon;
if (step.sentAt) icon = checkIcon;
else if (statusClass === 'overdue') icon = warnIcon;
stepsHtml += `
<div class="nis2-step">
<div class="nis2-step-dot ${dotClass}">${icon}</div>
<div class="nis2-step-content ${statusClass}">
<div class="nis2-step-header">
<div class="nis2-step-title">${step.title}</div>
${step.dueDate ? `<div class="nis2-step-time">Scadenza: ${formatDateTime(step.dueDate)}</div>` : ''}
</div>
<p style="font-size:0.8rem; color:var(--gray-600); margin:0;">${step.description}</p>
${countdownHtml}
${!step.sentAt ? actionHtml : ''}
</div>
</div>`;
});
return `
<div class="card mb-24">
<div class="card-header">
<h3>NIS2 Art. 23 - Obblighi di Notifica</h3>
<span class="badge badge-danger">Incidente Significativo</span>
</div>
<div class="card-body">
<div class="nis2-timeline">
${stepsHtml}
</div>
</div>
</div>`;
}
// ── NIS2 Actions ─────────────────────────────────────────────
async function doSendEarlyWarning(id) {
showModal('Conferma Early Warning', `
<p>Stai per registrare l'invio dell'<strong>Early Warning (24h)</strong> al CSIRT nazionale (ACN).</p>
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
Questa azione registra nel sistema che l'allerta iniziale e' stata inviata.
Assicurati di aver effettivamente trasmesso la comunicazione tramite i canali ufficiali.
</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="executeSendEarlyWarning(${id})">Conferma Invio</button>
`
});
}
async function executeSendEarlyWarning(id) {
try {
const result = await api.sendEarlyWarning(id);
closeModal();
if (result.success) {
showNotification('Early Warning registrato con successo', 'success');
viewIncidentDetail(id);
} else {
showNotification(result.message || 'Errore', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
async function doSendNotification(id) {
showModal('Conferma Notifica CSIRT', `
<p>Stai per registrare l'invio della <strong>Notifica Incidente (72h)</strong> al CSIRT nazionale (ACN).</p>
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
La notifica deve contenere una valutazione iniziale dell'incidente, la sua gravita' e il suo impatto,
nonche' gli indicatori di compromissione, ove disponibili.
</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="executeSendNotification(${id})">Conferma Invio</button>
`
});
}
async function executeSendNotification(id) {
try {
const result = await api.sendNotification(id);
closeModal();
if (result.success) {
showNotification('Notifica CSIRT registrata con successo', 'success');
viewIncidentDetail(id);
} else {
showNotification(result.message || 'Errore', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
async function doSendFinalReport(id) {
showModal('Conferma Report Finale', `
<p>Stai per registrare l'invio del <strong>Report Finale (30 giorni)</strong> al CSIRT nazionale (ACN).</p>
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
Il report finale deve includere la descrizione dettagliata dell'incidente, la causa radice,
le misure di mitigazione applicate e l'impatto transfrontaliero, se applicabile.
</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="executeSendFinalReport(${id})">Conferma Invio</button>
`
});
}
async function executeSendFinalReport(id) {
try {
const result = await api.sendFinalReport(id);
closeModal();
if (result.success) {
showNotification('Report finale registrato con successo', 'success');
viewIncidentDetail(id);
} else {
showNotification(result.message || 'Errore', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Add Timeline Event Modal ─────────────────────────────────
function showAddEventModal(incidentId) {
const content = `
<div class="form-group">
<label class="form-label">Tipo Evento</label>
<select class="form-select" id="event-type">
<option value="detection">Rilevamento</option>
<option value="analysis">Analisi</option>
<option value="containment">Contenimento</option>
<option value="eradication">Eradicazione</option>
<option value="recovery">Ripristino</option>
<option value="notification">Notifica</option>
<option value="action">Azione</option>
<option value="update">Aggiornamento</option>
<option value="other">Altro</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Descrizione <span class="required">*</span></label>
<textarea class="form-textarea" id="event-description" rows="3" placeholder="Descrizione dell'evento"></textarea>
</div>
`;
showModal('Aggiungi Evento alla Timeline', content, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="addTimelineEvent(${incidentId})">Aggiungi Evento</button>
`
});
}
async function addTimelineEvent(incidentId) {
const data = {
event_type: document.getElementById('event-type').value,
description: document.getElementById('event-description').value.trim(),
};
if (!data.description) {
showNotification('La descrizione e\' obbligatoria', 'warning');
return;
}
try {
const result = await api.post(`/incidents/${incidentId}/timeline`, data);
if (result.success) {
closeModal();
showNotification('Evento aggiunto alla timeline', 'success');
viewIncidentDetail(incidentId);
} else {
showNotification(result.message || 'Errore nel salvataggio', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Art.23 Significance Evaluator ────────────────────────────
function evalSignificance() {
const criteria = [
document.getElementById('crit-users'),
document.getElementById('crit-duration'),
document.getElementById('crit-crossborder'),
document.getElementById('crit-attack'),
document.getElementById('crit-financial'),
].filter(Boolean);
const checkedCount = criteria.filter(c => c.checked).length;
const resultDiv = document.getElementById('significance-result');
const sigCheckbox = document.getElementById('inc-significant');
if (!resultDiv) return;
resultDiv.style.display = 'block';
if (checkedCount >= 1) {
resultDiv.style.background = '#fef2f2';
resultDiv.style.border = '1px solid #fca5a5';
resultDiv.style.color = 'var(--danger)';
resultDiv.innerHTML = '&#9888; ' + checkedCount + ' criteri soddisfatti — l'incidente e' probabilmente <strong>SIGNIFICATIVO</strong>. Attivare le notifiche Art. 23 (24h/72h/30gg).';
if (sigCheckbox) sigCheckbox.checked = true;
} else {
resultDiv.style.background = '#f0fdf4';
resultDiv.style.border = '1px solid #bbf7d0';
resultDiv.style.color = 'var(--secondary)';
resultDiv.innerHTML = '&#10003; Nessun criterio soddisfatto — l'incidente appare <strong>NON significativo</strong> ai fini Art. 23. Verificare manualmente prima di confermare.';
if (sigCheckbox) sigCheckbox.checked = false;
}
}
// ── Navigation ───────────────────────────────────────────────
function backToList() {
document.getElementById('view-detail').classList.add('hidden');
document.getElementById('view-list').classList.remove('hidden');
}
</script>
</body>
</html>