Ogni rischio derivato da un requisito puo' ora essere valutato con Probabilita' e Impatto (1-5) e compare nella matrice 5x5. - DB: mig 066 (org_requisito_state += likelihood/impact) + runner idempotente. - Backend: FrameworkController::setState accetta likelihood/impact; RiskController ::derivedList ritorna likelihood/impact/score. Applicata su prod + reload. - Frontend risks.html: colonne Probabilita'/Impatto/Rischio(P×I) + tasto "Valuta" (modale, riservato a super_admin/org_admin/compliance_manager); matrice calcolata client-side dai rischi derivati valutati (esclusi i non_applicabile, caveat nis2-expert) + rischi custom. Framing normativo invariato. - help.js/i18n aggiornati; version 1.25.7; buster help/i18n ?v=20260727b. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
306 lines
20 KiB
HTML
306 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>Aggiornamenti Normativi · NIS2 Agile</title>
|
|
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
|
<style>
|
|
/* V3 — specifico pagina Normative (non coperto da v3.css) */
|
|
.nrm-bars{display:grid;gap:18px}
|
|
.nrm-progress-track{height:8px;background:#EAF0F6;border-radius:99px;overflow:hidden}
|
|
.nrm-progress-fill{height:100%;background:var(--c-blue);border-radius:99px;transition:width .5s ease}
|
|
.nrm-filters{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
|
|
.nrm-select{padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;font-size:13px;color:var(--ink)}
|
|
.nrm-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;color:var(--ink)}
|
|
.nrm-check input{accent-color:var(--c-blue)}
|
|
.update-card{border:1px solid var(--line);border-radius:11px;padding:18px;margin-bottom:14px;transition:border-color .12s,box-shadow .12s;background:#fff}
|
|
.update-card:hover{border-color:#9FBEDD;box-shadow:0 4px 12px rgba(16,40,80,.08)}
|
|
.update-card.action-required{border-left:3px solid var(--c-amber)}
|
|
.update-card.acked{opacity:.72}
|
|
.source-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:10.5px;font-weight:700;background:#EEF2F7;color:#5b6b80;font-family:'Inter',monospace}
|
|
.impact-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase}
|
|
.impact-critical{background:#FBE9E9;color:#B23636}
|
|
.impact-high{background:#FBF1E2;color:#C2410C}
|
|
.impact-medium{background:#FBF1E2;color:#9A6418}
|
|
.impact-low{background:#EEF2F7;color:#5b6b80}
|
|
.impact-informational{background:#E7F0FB;color:#1F5FA8}
|
|
.ack-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:6px;font-size:10.5px;font-weight:700}
|
|
.ack-done{background:#E6F4EC;color:#1E7A4E}
|
|
.ack-pending{background:#FBF1E2;color:#9A6418}
|
|
.nrm-uupd-h3{font-size:15px;font-weight:700;color:var(--ink);margin:0 0 6px}
|
|
.nrm-ref{font-size:11.5px;color:var(--muted);margin:0 0 8px}
|
|
.nrm-ref code{background:#EEF2F7;border-radius:5px;padding:1px 6px}
|
|
.nrm-summary{font-size:13px;color:var(--muted);line-height:1.6;margin:0 0 10px}
|
|
.nrm-card-grid{display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
|
|
.nrm-card-side{display:flex;flex-direction:column;gap:8px;align-items:flex-end;flex-shrink:0}
|
|
.nrm-eff{font-size:11.5px;color:var(--muted)}
|
|
.nrm-action-tag{font-size:10px;font-weight:700;color:#9A6418;background:#FBF1E2;padding:2px 8px;border-radius:6px;text-transform:uppercase;letter-spacing:.03em}
|
|
/* Modal + toast prodotti da common.js (style.css rimosso): stile minimo coerente V3 */
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(15,27,46,.5);display:flex;align-items:flex-start;justify-content:center;z-index:1050;opacity:0;transition:opacity .2s;padding:48px 16px;overflow:auto}
|
|
.modal-overlay.active{opacity:1}
|
|
.modal-overlay .modal{background:#fff;border-radius:12px;width:100%;max-width:560px;box-shadow:0 20px 50px rgba(11,40,80,.25);transform:translateY(-8px);transition:transform .2s}
|
|
.modal-overlay.active .modal{transform:none}
|
|
.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--line)}
|
|
.modal-header h3{margin:0;font-size:16px;font-weight:700}
|
|
.modal-close{background:none;border:0;font-size:1.5rem;line-height:1;cursor:pointer;color:var(--muted)}
|
|
.modal-body{padding:18px 20px}
|
|
.modal-footer{padding:14px 20px;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}
|
|
.form-group{margin-bottom:14px}
|
|
.form-label{display:block;font-weight:600;font-size:13px;margin-bottom:5px}
|
|
.form-control{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:14px}
|
|
textarea.form-control{min-height:64px;resize:vertical}
|
|
.notification-container{position:fixed;top:18px;right:18px;z-index:1100;display:flex;flex-direction:column;gap:10px}
|
|
.notification{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--c-blue);border-radius:10px;box-shadow:0 8px 24px rgba(16,40,80,.14);padding:12px 14px;min-width:240px;max-width:360px;font-size:13px}
|
|
.notification.success{border-left-color:var(--c-green)}.notification.error{border-left-color:var(--c-red)}.notification.warning{border-left-color:var(--c-amber)}
|
|
.notification-icon{width:18px;height:18px;flex:0 0 auto}.notification-icon svg{width:18px;height:18px}
|
|
.notification.success .notification-icon{color:var(--c-green)}.notification.error .notification-icon{color:var(--c-red)}.notification.warning .notification-icon{color:var(--c-amber)}.notification.info .notification-icon{color:var(--c-blue)}
|
|
.notification-message{flex:1;min-width:0}
|
|
.notification-close{background:none;border:0;font-size:1.2rem;line-height:1;cursor:pointer;color:var(--muted)}
|
|
.notification.fade-out{opacity:0;transition:opacity .3s}
|
|
</style>
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
</head>
|
|
<body>
|
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
|
<div id="topnav"></div>
|
|
|
|
<main class="wrap">
|
|
<nav class="crumb" aria-label="breadcrumb">
|
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>Lex e ISO</span>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>Normative</span>
|
|
</nav>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1 data-i18n="normative.title">Aggiornamenti Normativi</h1>
|
|
<p class="sub">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
|
|
</div>
|
|
<button class="btn-ghost" onclick="loadPending()"><i data-lucide="clock" class="ic"></i>Solo pendenti</button>
|
|
</div>
|
|
|
|
<div class="stack">
|
|
<!-- Stats (dati reali da /normative/stats) -->
|
|
<div class="grid3" id="stats-grid">
|
|
<section class="card card-pad stat blue"><div style="flex:1;min-width:0"><div class="lbl">Caricamento…</div><div class="val">…</div></div></section>
|
|
</div>
|
|
|
|
<!-- Avanzamento presa visione -->
|
|
<section class="card card-pad">
|
|
<div class="sec-head"><h3>Avanzamento presa visione</h3><span class="hint" id="ack-pct">—</span></div>
|
|
<div class="nrm-progress-track"><div class="nrm-progress-fill" id="ack-bar" style="width:0%"></div></div>
|
|
<p class="sub" style="margin:8px 0 0;font-size:12px;color:var(--muted)">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art. 21.</p>
|
|
</section>
|
|
|
|
<!-- Filtri -->
|
|
<section class="card card-pad">
|
|
<div class="nrm-filters">
|
|
<label for="filter-source" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per fonte normativa</label>
|
|
<select id="filter-source" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
|
|
<option value="">Tutte le fonti</option>
|
|
<option value="dlgs_138_2024">D.Lgs. 138/2024</option>
|
|
<option value="nis2_directive">Direttiva NIS2</option>
|
|
<option value="acn_guideline">ACN Guideline</option>
|
|
<option value="dora">DORA</option>
|
|
<option value="enisa">ENISA</option>
|
|
<option value="iso27001">ISO 27001</option>
|
|
</select>
|
|
<label for="filter-impact" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per livello di impatto</label>
|
|
<select id="filter-impact" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
|
|
<option value="">Tutti i livelli</option>
|
|
<option value="critical">Critico</option>
|
|
<option value="high">Alto</option>
|
|
<option value="medium">Medio</option>
|
|
<option value="low">Basso</option>
|
|
<option value="informational">Informativo</option>
|
|
</select>
|
|
<label class="nrm-check"><input type="checkbox" id="filter-action" onchange="loadUpdates()"> Solo azione richiesta</label>
|
|
</div>
|
|
</section>
|
|
|
|
<div id="updates-container"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
|
|
<script src="/js/api.js?v=20260727r"></script>
|
|
<script src="/js/common.js?v=20260627t"></script>
|
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
|
<script src="/js/topnav-v3.js?v=20260627t"></script>
|
|
<script src="/js/i18n.js?v=20260727b"></script>
|
|
<script src="/js/help.js?v=20260727b"></script>
|
|
<script>
|
|
// ── Auth + chrome + i18n + help ──────────────────────────
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar(); // = loadTopnavV3()
|
|
if (typeof I18n !== 'undefined') I18n.init();
|
|
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
|
|
|
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
|
|
|
const sourceLabels = {
|
|
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
|
|
acn_guideline: 'ACN', dora: 'DORA', enisa: 'ENISA', iso27001: 'ISO 27001', other: 'Altro'
|
|
};
|
|
const impactLabels = {
|
|
critical: 'Critico', high: 'Alto', medium: 'Medio', low: 'Basso', informational: 'Informativo'
|
|
};
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const result = await api.request('GET', '/normative/stats');
|
|
if (result.success) renderStats(result.data);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderStats(d) {
|
|
document.getElementById('stats-grid').innerHTML = `
|
|
<a class="card card-pad stat blue" style="cursor:default">
|
|
<div class="ibox"><i data-lucide="layers" class="ic"></i></div>
|
|
<div style="flex:1;min-width:0"><div class="lbl">Aggiornamenti totali</div><div class="val blue">${d.total_updates || 0}</div></div>
|
|
</a>
|
|
<a class="card card-pad stat green" style="cursor:default">
|
|
<div class="ibox"><i data-lucide="check-check" class="ic"></i></div>
|
|
<div style="flex:1;min-width:0"><div class="lbl">Prese visione</div><div class="val green">${d.acknowledged || 0}</div></div>
|
|
</a>
|
|
<a class="card card-pad stat amber" style="cursor:default">
|
|
<div class="ibox"><i data-lucide="clock" class="ic"></i></div>
|
|
<div style="flex:1;min-width:0"><div class="lbl">Pendenti</div><div class="val amber">${d.pending || 0}</div><div class="sub">Compliance ACK ${d.ack_rate || 0}%</div></div>
|
|
</a>`;
|
|
|
|
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
|
|
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
|
|
refreshIcons();
|
|
}
|
|
|
|
async function loadUpdates() {
|
|
const container = document.getElementById('updates-container');
|
|
container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
|
|
try {
|
|
const params = new URLSearchParams();
|
|
const source = document.getElementById('filter-source').value;
|
|
const impact = document.getElementById('filter-impact').value;
|
|
const action = document.getElementById('filter-action').checked;
|
|
if (source) params.append('source', source);
|
|
if (impact) params.append('impact', impact);
|
|
if (action) params.append('action_required', '1');
|
|
|
|
const result = await api.request('GET', '/normative/list?' + params);
|
|
if (result.success) renderUpdates(result.data.updates || []);
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
|
|
refreshIcons();
|
|
}
|
|
}
|
|
|
|
async function loadPending() {
|
|
const container = document.getElementById('updates-container');
|
|
container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
|
|
try {
|
|
const result = await api.request('GET', '/normative/pending');
|
|
if (result.success) renderUpdates(result.data.pending || [], true);
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
|
|
refreshIcons();
|
|
}
|
|
}
|
|
|
|
function renderUpdates(updates, pendingOnly) {
|
|
const container = document.getElementById('updates-container');
|
|
if (!updates.length) {
|
|
container.innerHTML = `<section class="card card-pad"><div class="v3-empty">
|
|
<i data-lucide="file-text" class="ic"></i>
|
|
<b>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</b>
|
|
<span>I prossimi aggiornamenti normativi appariranno qui.</span>
|
|
</div></section>`;
|
|
refreshIcons();
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="stack">';
|
|
updates.forEach(u => {
|
|
const acked = u.is_acknowledged || !!(u.acknowledged_at);
|
|
const actionClass = u.action_required ? 'action-required' : '';
|
|
const ackedClass = acked ? 'acked' : '';
|
|
const domains = (u.affected_domains || []).map(d =>
|
|
`<span class="source-chip">${escapeHtml(d)}</span>`
|
|
).join(' ');
|
|
|
|
html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}">
|
|
<div class="nrm-card-grid">
|
|
<div style="flex:1;min-width:0;">
|
|
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px;">
|
|
<span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span>
|
|
<span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span>
|
|
${u.action_required ? '<span class="nrm-action-tag">Azione richiesta</span>' : ''}
|
|
${acked
|
|
? `<span class="ack-badge ack-done"><i data-lucide="check" style="width:12px;height:12px"></i>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
|
|
: '<span class="ack-badge ack-pending">Da prendere in visione</span>'
|
|
}
|
|
</div>
|
|
<h3 class="nrm-uupd-h3">${escapeHtml(u.title)}</h3>
|
|
${u.reference ? `<p class="nrm-ref"><code>${escapeHtml(u.reference)}</code></p>` : ''}
|
|
<p class="nrm-summary">${escapeHtml(u.summary)}</p>
|
|
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
|
|
</div>
|
|
<div class="nrm-card-side">
|
|
${u.effective_date ? `<p class="nrm-eff">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
|
|
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn-ghost"><i data-lucide="external-link" class="ic"></i>Fonte</a>` : ''}
|
|
${!acked ? `<button class="btn-primary" onclick="acknowledgeUpdate(${u.id})"><i data-lucide="check" class="ic"></i>Prendi in visione</button>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
refreshIcons();
|
|
}
|
|
|
|
async function acknowledgeUpdate(id) {
|
|
showModal('Presa Visione', `
|
|
<p style="font-size:13px; color:var(--muted); margin-bottom:16px;">
|
|
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
|
|
</p>
|
|
<div class="form-group">
|
|
<label for="ack-notes" class="form-label">Note (opzionale)</label>
|
|
<textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea>
|
|
</div>
|
|
`, { footer: `<button class="btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
|
|
<button class="btn-ghost" onclick="closeModal()">Annulla</button>` });
|
|
}
|
|
|
|
async function confirmAck(id) {
|
|
const notes = document.getElementById('ack-notes').value;
|
|
try {
|
|
const result = await api.request('POST', `/normative/${id}/ack`, { notes });
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Presa visione registrata e documentata.', 'success');
|
|
loadUpdates();
|
|
loadStats();
|
|
} else showNotification(result.message || 'Errore.', 'error');
|
|
} catch (e) { showNotification('Errore di connessione.', 'error'); }
|
|
}
|
|
|
|
// ── Init dati reali ──────────────────────────────────────
|
|
loadStats();
|
|
loadUpdates();
|
|
</script>
|
|
</body>
|
|
</html>
|