Files
nis2-agile/public/reports.html
T
DevEnv nis2-agileandClaude Opus 4.8 7c711b727a [FEAT] Rischi: matrice sempre visibile, rimosse viste FAIR e KRI (ticket #500 fase 1)
Ticket #500 (Simon Fattori) - semplificazione pagina Gestione Rischi:
- matrice 5x5 ora sempre visibile sotto la tabella (rimosso bottone 'Matrice' e il view-toggle)
- rimosse le viste 'Quantitativo (FAIR)' e 'KRI' con relativi bottoni, blocchi HTML e JS
- help.js allineato (rimosse sezioni FAIR/KRI), cache-buster help.js -> 20260727a
- version.json -> 1.25.2
Rimandato a fase 2 (richiede modello dati): rischi derivati dai requisiti LEX&ISO
+ codifica RSK-<misura>Com.N + rimozione bottoni 'Nuovo Rischio'/'AI Suggerisci'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:15:22 +02:00

831 lines
50 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>Audit e Report · NIS2 Agile</title>
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style>
/* ── Compat V3: token + widget emessi dal JS (ex style.css, niente CDN) ───── */
:root{
--primary:#1F5FA8;--primary-bg:#EAF1FA;
--secondary:#16794A;--secondary-bg:#E6F4EC;
--warning:#B45309;--danger:#B42318;--info:#1F5FA8;--info-bg:#EAF1FA;
--gray-50:#F8FAFC;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;
--gray-400:#94A3B8;--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#1E293B;
--card-bg:#fff;--border-radius:10px;--border-radius-sm:7px;
--card-shadow:0 1px 2px rgba(16,40,80,.06),0 1px 3px rgba(16,40,80,.05);
--transition-fast:.12s ease;--font-mono:ui-monospace,SFMono-Regular,Menlo,monospace;
}
/* Bottoni emessi dal JS (modali) — coerenti con v3.css */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 14px;font:inherit;font-weight:600;font-size:13px;cursor:pointer;line-height:1.2;text-decoration:none}
.btn:disabled{opacity:.55;cursor:not-allowed}
.btn-primary{background:var(--c-blue,#1F5FA8);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
.btn-secondary{background:#fff;color:var(--ink,#1E293B);border-color:var(--line,#E3E9F0)}.btn-secondary:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
/* ── Tab Navigation ─────────────────────────────────────── */
.tab-nav { display:flex; gap:0; border-bottom:2px solid var(--gray-200); margin-bottom:24px; flex-wrap:wrap; }
.tab-nav button {
padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px;
color:var(--gray-500); font:inherit; font-size:.9rem; font-weight:600; cursor:pointer; transition:all var(--transition-fast);
}
.tab-nav button:hover { color:var(--gray-700); background:var(--gray-50); }
.tab-nav button.active { color:var(--primary); border-bottom-color:var(--primary); }
.tab-nav button:focus-visible { outline:2px solid var(--primary); outline-offset:-2px; border-radius:4px 4px 0 0; }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
/* ── Data Table ─────────────────────────────────────────── */
.data-table { width:100%; border-collapse:collapse; background:var(--card-bg); border-radius:var(--border-radius); overflow:hidden; box-shadow:var(--card-shadow); }
.data-table thead { background:var(--gray-50); }
.data-table th { text-align:left; padding:11px 16px; font-size:.72rem; font-weight:700; color:var(--gray-500); text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid var(--gray-200); }
.data-table td { padding:11px 16px; font-size:.875rem; color:var(--gray-700); border-bottom:1px solid var(--gray-100); }
.data-table tr:last-child td { border-bottom:none; }
.data-table tbody tr:hover td { background:var(--gray-50); }
.data-table tbody tr.clickable-row { cursor:pointer; }
/* ── Status Badges ──────────────────────────────────────── */
.status-badge { display:inline-flex; align-items:center; padding:3px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
.status-badge.not_started { background:var(--gray-100); color:var(--gray-600); }
.status-badge.in_progress { background:var(--info-bg); color:var(--info); }
.status-badge.implemented { background:var(--secondary-bg); color:var(--secondary); }
.status-badge.verified { background:var(--primary-bg); color:var(--primary); }
/* ── Progress Bar ───────────────────────────────────────── */
.progress-bar-container { display:flex; align-items:center; gap:8px; }
.progress-bar { flex-grow:1; height:6px; background:var(--gray-200); border-radius:3px; overflow:hidden; min-width:80px; }
.progress-bar-fill { height:100%; border-radius:3px; transition:width .3s ease; }
.progress-pct { font-size:.8rem; font-weight:700; min-width:36px; text-align:right; color:var(--gray-600); }
/* ── stat-box (monitoraggio) ────────────────────────────── */
.stat-box { padding:10px 14px; background:var(--gray-50); border-radius:8px; }
.stat-box .stat-value { font-size:1.4rem; font-weight:800; }
.stat-box .stat-label { font-size:.72rem; color:var(--gray-500); }
/* ── Report Section ─────────────────────────────────────── */
.report-container { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); overflow:hidden; }
.report-header { background:var(--navy-nav,#142B4D); color:#fff; padding:30px; text-align:center; }
.report-header h3 { font-size:1.3rem; margin:0 0 4px; }
.report-header p { color:#cbd5e1; font-size:.85rem; margin:0; }
.report-body { padding:24px 30px; }
.report-section { margin-bottom:24px; }
.report-section-title { font-size:1rem; font-weight:700; color:var(--gray-800); margin-bottom:12px; padding-bottom:8px; border-bottom:2px solid var(--primary-bg); }
.report-summary-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:16px; }
.report-summary-item { text-align:center; padding:16px; background:var(--gray-50); border-radius:var(--border-radius); }
.report-summary-value { font-size:1.8rem; font-weight:800; color:var(--gray-800); line-height:1.2; }
.report-summary-label { font-size:.8rem; color:var(--gray-500); margin-top:4px; }
.report-compliance-bar { height:20px; background:var(--gray-200); border-radius:10px; overflow:hidden; margin:12px 0; }
.report-compliance-fill { height:100%; border-radius:10px; transition:width .5s ease; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.75rem; font-weight:700; }
/* ── ISO Mapping ────────────────────────────────────────── */
.iso-mapping-row { display:flex; align-items:center; gap:16px; padding:12px 0; border-bottom:1px solid var(--gray-100); }
.iso-mapping-row:last-child { border-bottom:none; }
.iso-nis2-badge { background:var(--primary-bg); color:var(--primary); padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:700; white-space:nowrap; min-width:90px; text-align:center; }
.iso-arrow { color:var(--gray-300); flex-shrink:0; }
.iso-arrow svg { width:20px; height:20px; }
.iso-controls { display:flex; flex-wrap:wrap; gap:6px; flex-grow:1; }
.iso-control-badge { background:var(--secondary-bg); color:var(--secondary); padding:4px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
.iso-title { font-size:.85rem; color:var(--gray-500); min-width:200px; }
/* ── Pagination ─────────────────────────────────────────── */
.pagination { display:flex; justify-content:center; align-items:center; gap:12px; margin-top:20px; padding:12px; }
.pagination button { padding:8px 16px; border:1px solid var(--gray-200); background:var(--card-bg); border-radius:var(--border-radius-sm); color:var(--gray-600); cursor:pointer; font:inherit; font-size:.85rem; transition:all var(--transition-fast); }
.pagination button:hover:not(:disabled) { background:var(--primary-bg); border-color:var(--primary); color:var(--primary); }
.pagination button:disabled { opacity:.4; cursor:not-allowed; }
.pagination .page-info { font-size:.85rem; color:var(--gray-500); }
/* ── Audit Log ──────────────────────────────────────────── */
.log-action { display:inline-flex; align-items:center; gap:4px; font-size:.82rem; }
.log-entity { background:var(--gray-100); padding:2px 8px; border-radius:var(--border-radius-sm); font-size:.8rem; color:var(--gray-600); font-family:var(--font-mono); }
.log-details { display:inline-block; max-width:300px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:.8rem; color:var(--gray-400); cursor:help; vertical-align:bottom; }
/* ── Loading / Empty ────────────────────────────────────── */
.loading-state { text-align:center; padding:48px 20px; color:var(--gray-400); }
.loading-state .spinner { width:32px; height:32px; border:3px solid var(--gray-200); border-top-color:var(--primary); border-radius:50%; animation:rpt-spin .8s linear infinite; margin:0 auto 12px; }
@keyframes rpt-spin { to { transform:rotate(360deg); } }
.empty-state-box { text-align:center; padding:48px 20px; color:var(--gray-400); }
.empty-state-box svg { width:48px; height:48px; margin-bottom:12px; opacity:.5; }
.empty-state-box h4 { color:var(--gray-600); margin:0 0 4px; }
.empty-state-box p { margin:0; }
/* ── Print ──────────────────────────────────────────────── */
@media print {
#topnav, .crumb, .page-head .btn-primary, .tab-nav, .btn { display:none !important; }
.wrap { max-width:100% !important; padding:0 !important; }
.report-container { box-shadow:none; border:1px solid #ddd; }
body { background:#fff; }
}
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width:768px) {
.tab-nav button { padding:10px 12px; font-size:.8rem; }
.iso-mapping-row { flex-direction:column; align-items:flex-start; }
.iso-title { min-width:auto; }
.report-summary-grid { grid-template-columns:1fr 1fr; }
}
</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 in questo host -->
<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>Audit e Report</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="audit.title">Audit &amp; Report</h1>
<p class="sub">Report di compliance, controlli, audit log e mapping ISO 27001</p>
</div>
<button class="btn-primary" onclick="generateReport()"><i data-lucide="file-text" class="ic"></i>Genera Report</button>
</div>
<section class="card card-pad">
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="active" onclick="switchTab('report', this)">Report Compliance</button>
<button onclick="switchTab('controls', this)">Controlli</button>
<button onclick="switchTab('audit', this)">Audit Log</button>
<button onclick="switchTab('iso', this)">ISO 27001</button>
<button onclick="switchTab('monitoring', this)" data-i18n="audit.monitoring_tab">Monitoraggio Continuo</button>
<button onclick="switchTab('acn', this)">Requisiti ACN</button>
</div>
<!-- Tab: Report Compliance -->
<div class="tab-panel active" id="tab-report">
<div id="report-container">
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>
<h4>Nessun report generato</h4>
<p>Clicca "Genera Report" per creare un report di compliance aggiornato.</p>
</div>
</div>
</div>
<!-- Tab: Controlli -->
<div class="tab-panel" id="tab-controls">
<div id="controls-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento controlli...</p>
</div>
</div>
</div>
<!-- Tab: Audit Log -->
<div class="tab-panel" id="tab-audit">
<div id="audit-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento log...</p>
</div>
</div>
</div>
<!-- Tab: ISO 27001 -->
<div class="tab-panel" id="tab-iso">
<div id="iso-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento mapping...</p>
</div>
</div>
</div>
<!-- Tab: Continuous Control Monitoring -->
<div class="tab-panel" id="tab-monitoring">
<div id="monitoring-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento monitoraggio...</p>
</div>
</div>
</div>
<div class="tab-panel" id="tab-acn">
<div id="acn-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento requisiti ACN...</p>
</div>
</div>
</div>
</section>
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<script src="/js/api.js?v=20260627u"></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=20260627p"></script>
<script src="/js/help.js?v=20260727a"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ──────────────────────────────────────────────
const controlStatusLabels = {
not_started: 'Non Iniziato',
in_progress: 'In Corso',
implemented: 'Implementato',
verified: 'Verificato'
};
const actionLabels = {
login: 'Accesso effettuato',
logout: 'Disconnessione',
user_created: 'Utente creato',
org_created: 'Organizzazione creata',
assessment_created: 'Assessment creato',
assessment_completed: 'Assessment completato',
risk_created: 'Rischio creato',
risk_updated: 'Rischio aggiornato',
risk_deleted: 'Rischio eliminato',
incident_created: 'Incidente registrato',
incident_updated: 'Incidente aggiornato',
early_warning_sent: 'Pre-allarme inviato',
notification_sent: 'Notifica inviata',
policy_created: 'Policy creata',
policy_updated: 'Policy aggiornata',
policy_approved: 'Policy approvata',
policy_ai_generated: 'Policy generata con AI',
supplier_created: 'Fornitore creato',
supplier_assessed: 'Fornitore valutato',
course_created: 'Corso creato',
training_assigned: 'Formazione assegnata',
asset_created: 'Asset registrato',
asset_updated: 'Asset aggiornato',
asset_deleted: 'Asset eliminato',
control_updated: 'Controllo aggiornato',
evidence_uploaded: 'Evidenza caricata'
};
// ── State ───────────────────────────────────────────────
let auditPage = 1;
let auditTotalPages = 1;
// ── Tab Switch ──────────────────────────────────────────
function switchTab(tabId, btn) {
document.querySelectorAll('.tab-nav button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
if (tabId === 'controls') loadControls();
if (tabId === 'audit') { auditPage = 1; loadAuditLogs(); }
if (tabId === 'iso') loadIsoMapping();
if (tabId === 'monitoring') loadMonitoring();
if (tabId === 'acn') loadAcnRequirements();
}
// ── Requisiti ACN (specifiche di base, Determina 164179/2025) ──────────
const ACN_ST = {
not_started: ['Da iniziare', '#9ca3af'],
in_progress: ['In corso', '#eab308'],
implemented: ['Implementato', '#22c55e'],
not_applicable:['Non applicabile', '#64748b']
};
async function loadAcnRequirements() {
const c = document.getElementById('acn-container');
if (!c) return;
c.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento...</p></div>';
let res;
try { res = await api.getAcnRequirements(); }
catch (e) { c.innerHTML = '<p style="color:#ef4444;padding:16px;">Errore di connessione</p>'; return; }
if (!res.success) { c.innerHTML = '<p style="color:#ef4444;padding:16px;">' + escapeHtml(res.message || 'Errore') + '</p>'; return; }
const d = res.data;
const s = d.summary || {};
const cards = ['implemented','in_progress','not_started','not_applicable'].map(k =>
`<div style="border-left:4px solid ${ACN_ST[k][1]};padding:8px 14px;background:var(--gray-50,#f8fafc);border-radius:6px;min-width:110px;">
<div style="font-size:1.4rem;font-weight:700;">${s[k]||0}</div><div style="font-size:.75rem;color:#64748b;">${ACN_ST[k][0]}</div></div>`).join('');
const groups = {};
(d.requirements||[]).forEach(r => { (groups[r.function_name] = groups[r.function_name] || []).push(r); });
let body = '';
Object.keys(groups).forEach(fn => {
const reqs = groups[fn];
body += `<h3 style="margin:18px 0 8px;">${escapeHtml(fn)} <span style="font-weight:400;color:#64748b;font-size:.85rem;">(${reqs.length})</span></h3>`;
body += '<table class="data-table" style="width:100%;"><thead><tr><th style="width:90px;">Sottocat.</th><th>Requisito</th><th style="width:175px;">Stato</th></tr></thead><tbody>';
reqs.forEach(r => {
const st = ACN_ST[r.status] || ACN_ST.not_started;
const opts = Object.keys(ACN_ST).map(k => `<option value="${k}"${k===r.status?' selected':''}>${ACN_ST[k][0]}</option>`).join('');
body += `<tr>
<td><code title="${escapeHtml(r.subcategory_text||'')}">${escapeHtml(r.subcategory)}</code></td>
<td>${escapeHtml(r.requirement)}</td>
<td><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${st[1]};margin-right:6px;"></span>
<select onchange="updateAcn(${r.id}, this.value)" style="padding:4px;border:1px solid #cbd5e1;border-radius:5px;">${opts}</select></td></tr>`;
});
body += '</tbody></table>';
});
c.innerHTML = `
<p style="font-size:.85rem;color:#475569;margin-bottom:12px;">
Requisiti operativi <strong>specifiche di base ACN</strong> (Determina 164179/2025, Framework Nazionale 2025)
per questa organizzazione (<strong>${escapeHtml(d.entity)}</strong>: ${d.total} requisiti).
Compliance: <strong>${d.compliance_percent}%</strong>. <em>${escapeHtml(d.source||'')}</em></p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;">${cards}</div>
${body}`;
}
async function updateAcn(id, status) {
try {
const res = await api.updateAcnRequirement(id, status, null);
if (res.success) { if (typeof showNotification==='function') showNotification('Stato aggiornato', 'success'); loadAcnRequirements(); }
else if (typeof showNotification==='function') showNotification(res.message || 'Errore', 'error');
} catch (e) { if (typeof showNotification==='function') showNotification('Errore di connessione', 'error'); }
}
// ── Continuous Control Monitoring (P1) ───────────────────────
const MON_STATUS = {
healthy: ['#22c55e', 'Sano'],
warning: ['#eab308', 'Attenzione'],
stale: ['#f97316', 'Evidenza scaduta'],
failing: ['#ef4444', 'Non conforme'],
not_monitored: ['#9ca3af', 'Non monitorato']
};
async function loadMonitoring() {
const c = document.getElementById('monitoring-container');
try {
const res = await api.getControlsMonitoring();
if (!res.success) { c.innerHTML = '<div class="empty-state-box"><p>' + escapeHtml(res.message || 'Errore') + '</p></div>'; return; }
const d = res.data, s = d.summary || {};
const cards = ['healthy','warning','stale','failing','not_monitored'].map(k =>
`<div class="stat-box" style="border-left:4px solid ${MON_STATUS[k][0]};min-width:120px;">
<div class="stat-value">${s[k] || 0}</div><div class="stat-label">${MON_STATUS[k][1]}</div></div>`).join('');
const rows = (d.controls || []).map(ct => {
const st = MON_STATUS[ct.monitoring_status] || MON_STATUS.not_monitored;
return `<tr>
<td><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:${st[0]};vertical-align:middle;" title="${st[1]}"></span> ${st[1]}</td>
<td><code>${escapeHtml(ct.control_code)}</code></td>
<td>${escapeHtml(ct.title || '')}</td>
<td>${ct.last_checked_at || '<span style="color:var(--gray-400);">mai</span>'}</td>
<td>${ct.freshness_days || 30} gg</td></tr>`;
}).join('');
c.innerHTML = `
<p style="font-size:.85rem;color:var(--gray-600);margin-bottom:12px;">
Stato di freschezza dei controlli alimentato dalle <strong>evidenze automatiche</strong> dai connettori (Evidence Automation).
Copertura: <strong>${d.coverage_percent}%</strong> (${d.monitored}/${d.total_controls} controlli monitorati).</p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;">${cards}</div>
<table class="data-table"><thead><tr>
<th>Stato</th><th>Controllo</th><th>Titolo</th><th>Ultima evidenza</th><th>Freschezza</th>
</tr></thead><tbody>${rows || '<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--gray-500);">Nessun controllo. Le evidenze arrivano via POST /api/services/evidence-ingest.</td></tr>'}</tbody></table>`;
} catch (e) {
c.innerHTML = '<div class="empty-state-box"><p>Errore di connessione</p></div>';
}
}
// ══════════════════════════════════════════════════════════
// TAB 1: Compliance Report
// ══════════════════════════════════════════════════════════
async function generateReport() {
const container = document.getElementById('report-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Generazione report in corso...</p></div>';
// Switch to report tab
document.querySelectorAll('.tab-nav button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelector('.tab-nav button').classList.add('active');
document.getElementById('tab-report').classList.add('active');
try {
const result = await api.generateComplianceReport();
if (result.success && result.data) {
renderReport(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nella generazione</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderReport(data) {
const container = document.getElementById('report-container');
const org = data.organization || {};
const summary = data.compliance_summary || {};
const controls = data.controls || [];
const pct = summary.compliance_percentage || 0;
const pctColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)';
let controlsTableHtml = '';
controls.forEach(c => {
const st = c.status || 'not_started';
const implPct = c.implementation_percentage || 0;
const implColor = implPct >= 80 ? 'var(--secondary)' : implPct >= 50 ? 'var(--warning)' : 'var(--danger)';
controlsTableHtml += `
<tr>
<td style="font-family:var(--font-mono); font-size:0.8rem;">${escapeHtml(c.control_code || '-')}</td>
<td>${escapeHtml(c.title || '-')}</td>
<td><span class="status-badge ${st}">${controlStatusLabels[st] || st}</span></td>
<td>
<div class="progress-bar-container">
<div class="progress-bar"><div class="progress-bar-fill" style="width:${implPct}%; background:${implColor};"></div></div>
<span class="progress-pct">${implPct}%</span>
</div>
</td>
</tr>`;
});
container.innerHTML = `
<div class="report-container">
<div class="report-header">
<h3>Report di Compliance NIS2</h3>
<p>${escapeHtml(org.name || 'Organizzazione')} - ${formatDateTime(data.report_date)}</p>
</div>
<div class="report-body">
<!-- Organization Info -->
<div class="report-section">
<div class="report-section-title">Informazioni Organizzazione</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-size:0.9rem;">
<div><strong>Nome:</strong> ${escapeHtml(org.name || '-')}</div>
<div><strong>Settore:</strong> ${escapeHtml(org.sector || '-')}</div>
<div><strong>Tipo Entita':</strong> ${escapeHtml(org.entity_type || '-')}</div>
<div><strong>Paese:</strong> ${escapeHtml(org.country || '-')}</div>
</div>
</div>
<!-- Compliance Summary -->
<div class="report-section">
<div class="report-section-title">Riepilogo Compliance</div>
<div class="report-compliance-bar">
<div class="report-compliance-fill" style="width:${pct}%; background:${pctColor};">${pct}%</div>
</div>
<div class="report-summary-grid">
<div class="report-summary-item">
<div class="report-summary-value">${summary.total_controls || 0}</div>
<div class="report-summary-label">Controlli Totali</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value">${summary.implemented_controls || 0}</div>
<div class="report-summary-label">Implementati</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--danger);">${data.risk_count || 0}</div>
<div class="report-summary-label">Rischi Aperti</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--warning);">${data.incident_count || 0}</div>
<div class="report-summary-label">Incidenti</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--secondary);">${data.policy_count || 0}</div>
<div class="report-summary-label">Policy Approvate</div>
</div>
</div>
</div>
<!-- Controls Status -->
${controls.length > 0 ? `
<div class="report-section">
<div class="report-section-title">Stato Controlli</div>
<table class="data-table" style="box-shadow:none; border:1px solid var(--gray-200);">
<thead>
<tr>
<th>Codice</th>
<th>Titolo</th>
<th>Stato</th>
<th>Implementazione</th>
</tr>
</thead>
<tbody>${controlsTableHtml}</tbody>
</table>
</div>
` : ''}
<!-- Print Button -->
<div style="text-align:center; margin-top:24px;">
<button class="btn btn-secondary" onclick="window.print()" style="gap:6px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>
Stampa Report
</button>
</div>
</div>
</div>
`;
}
// ══════════════════════════════════════════════════════════
// TAB 2: Controls
// ══════════════════════════════════════════════════════════
async function loadControls() {
const container = document.getElementById('controls-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento controlli...</p></div>';
try {
const result = await api.listControls();
if (result.success && result.data) {
renderControls(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderControls(controls) {
const container = document.getElementById('controls-container');
if (!controls || controls.length === 0) {
container.innerHTML = `
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
<h4>Nessun controllo configurato</h4>
<p>I controlli di compliance appariranno qui dopo il primo assessment.</p>
</div>`;
return;
}
let html = `
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Framework</th>
<th>Titolo</th>
<th>Stato</th>
<th>% Implementazione</th>
<th>Responsabile</th>
<th>Prossima Verifica</th>
</tr>
</thead>
<tbody>`;
controls.forEach(c => {
const st = c.status || 'not_started';
const implPct = c.implementation_percentage || 0;
const implColor = implPct >= 80 ? 'var(--secondary)' : implPct >= 50 ? 'var(--warning)' : 'var(--danger)';
html += `
<tr class="clickable-row" onclick="showEditControlModal(${c.id}, this)" title="Clicca per modificare">
<td style="font-family:var(--font-mono); font-size:0.82rem; font-weight:600;">${escapeHtml(c.control_code || '-')}</td>
<td>${escapeHtml(c.framework || 'NIS2')}</td>
<td style="font-weight:500;">${escapeHtml(c.title || '-')}</td>
<td><span class="status-badge ${st}">${controlStatusLabels[st] || st}</span></td>
<td>
<div class="progress-bar-container">
<div class="progress-bar"><div class="progress-bar-fill" style="width:${implPct}%; background:${implColor};"></div></div>
<span class="progress-pct">${implPct}%</span>
</div>
</td>
<td>${escapeHtml(c.responsible_name || '-')}</td>
<td>${formatDate(c.next_review_date)}</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function showEditControlModal(id) {
// First, find the control from the table data
api.listControls().then(result => {
if (!result.success || !result.data) return;
const control = result.data.find(c => c.id === id);
if (!control) return;
const st = control.status || 'not_started';
const implPct = control.implementation_percentage || 0;
const statusOptions = Object.entries(controlStatusLabels).map(([val, label]) =>
`<option value="${val}" ${st === val ? 'selected' : ''}>${label}</option>`
).join('');
showModal('Modifica Controllo: ' + escapeHtml(control.control_code || ''), `
<div class="form-group">
<label class="form-label">Titolo</label>
<p style="font-size:0.9rem; color:var(--gray-700); padding:8px 0;">${escapeHtml(control.title || '-')}</p>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="form-group">
<label class="form-label">Stato</label>
<select class="form-select" id="ctrl-status">${statusOptions}</select>
</div>
<div class="form-group">
<label class="form-label">% Implementazione</label>
<input type="range" id="ctrl-impl-pct" min="0" max="100" value="${implPct}" oninput="document.getElementById('ctrl-impl-pct-val').textContent = this.value + '%'" style="width:100%; margin-top:8px;">
<span id="ctrl-impl-pct-val" style="font-size:0.85rem; font-weight:600; color:var(--gray-700);">${implPct}%</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Descrizione Evidenza</label>
<textarea class="form-input" id="ctrl-evidence" rows="3" placeholder="Descrivi le evidenze di implementazione...">${escapeHtml(control.evidence_description || '')}</textarea>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="ctrl-responsible" value="${escapeHtml(control.responsible_user_id || '')}">
</div>
<div class="form-group">
<label class="form-label">Prossima Verifica</label>
<input type="date" class="form-input" id="ctrl-next-review" value="${control.next_review_date || ''}">
</div>
</div>
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveControl(${id})">Salva</button>
`
});
});
}
async function saveControl(id) {
const data = {
status: document.getElementById('ctrl-status').value,
implementation_percentage: parseInt(document.getElementById('ctrl-impl-pct').value),
evidence_description: document.getElementById('ctrl-evidence').value.trim() || null,
responsible_user_id: document.getElementById('ctrl-responsible').value.trim() || null,
next_review_date: document.getElementById('ctrl-next-review').value || null,
};
closeModal();
try {
const result = await api.updateControl(id, data);
if (result.success) {
showNotification('Controllo aggiornato con successo!', 'success');
loadControls();
} else {
showNotification(result.message || 'Errore nell\'aggiornamento.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ══════════════════════════════════════════════════════════
// TAB 3: Audit Logs
// ══════════════════════════════════════════════════════════
async function loadAuditLogs() {
const container = document.getElementById('audit-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento log...</p></div>';
try {
const result = await api.getAuditLogs({ page: auditPage, per_page: 25 });
if (result.success) {
// /audit/logs risponde paginato: data = {items, total, page, per_page, pages}.
const d = result.data || {};
const logs = Array.isArray(d) ? d : (d.items || []);
const pagination = Array.isArray(d) ? (result.pagination || {}) : d;
auditTotalPages = pagination.pages || pagination.total_pages || 1;
renderAuditLogs(logs, pagination);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderAuditLogs(logs, pagination) {
const container = document.getElementById('audit-container');
if (!logs || logs.length === 0) {
container.innerHTML = `
<div class="empty-state-box">
<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>
<h4>Nessun log disponibile</h4>
<p>Le attivita' di audit appariranno qui.</p>
</div>`;
return;
}
let html = `
<table class="data-table">
<thead>
<tr>
<th>Data/Ora</th>
<th>Utente</th>
<th>Azione</th>
<th>Entita'</th>
<th>Dettagli</th>
</tr>
</thead>
<tbody>`;
logs.forEach(log => {
const actionText = actionLabels[log.action] || log.action || '-';
let detailsText = '-';
if (log.details) {
try {
const d = typeof log.details === 'string' ? JSON.parse(log.details) : log.details;
detailsText = Object.entries(d).map(([k, v]) => `${k}: ${v}`).join(', ');
} catch (e) {
detailsText = String(log.details);
}
}
html += `
<tr>
<td style="white-space:nowrap;">${formatDateTime(log.created_at)}</td>
<td>${escapeHtml(log.full_name || '-')}</td>
<td><span class="log-action">${escapeHtml(actionText)}</span></td>
<td>
${log.entity_type ? `<span class="log-entity">${escapeHtml(log.entity_type)}</span>` : '-'}
${log.entity_id ? `<span style="font-size:0.8rem; color:var(--gray-400);"> #${log.entity_id}</span>` : ''}
</td>
<td><span class="log-details" title="${escapeHtml(detailsText)}">${escapeHtml(detailsText)}</span></td>
</tr>`;
});
html += '</tbody></table>';
// Pagination (data paginato: 'pages' = numero pagine totali)
const totalPages = pagination.pages || pagination.total_pages || 1;
const currentPage = pagination.page || auditPage;
html += `
<div class="pagination">
<button onclick="goAuditPage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>Precedente</button>
<span class="page-info">Pagina ${currentPage} di ${totalPages}</span>
<button onclick="goAuditPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>Successiva</button>
</div>`;
container.innerHTML = html;
}
function goAuditPage(page) {
if (page < 1 || page > auditTotalPages) return;
auditPage = page;
loadAuditLogs();
}
// ══════════════════════════════════════════════════════════
// TAB 4: ISO 27001 Mapping
// ══════════════════════════════════════════════════════════
async function loadIsoMapping() {
const container = document.getElementById('iso-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento mapping ISO 27001...</p></div>';
try {
const result = await api.getIsoMapping();
if (result.success && result.data) {
renderIsoMapping(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderIsoMapping(mapping) {
const container = document.getElementById('iso-container');
if (!mapping || mapping.length === 0) {
container.innerHTML = '<div class="empty-state-box"><h4>Nessun mapping disponibile</h4></div>';
return;
}
let html = `
<div>
<h3 style="margin:0 0 4px;font-size:1.05rem;">Mapping NIS2 - ISO 27001:2022</h3>
<p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;">
Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022.
</p>`;
mapping.forEach(m => {
const isoControls = (m.iso27001 || '').split(',').map(c => c.trim()).filter(c => c);
const controlBadges = isoControls.map(c => `<span class="iso-control-badge">${escapeHtml(c)}</span>`).join('');
html += `
<div class="iso-mapping-row">
<span class="iso-nis2-badge">Art. ${escapeHtml(m.nis2)}</span>
<span class="iso-arrow">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</span>
<div class="iso-controls">${controlBadges}</div>
<span class="iso-title">${escapeHtml(m.title || '')}</span>
</div>`;
});
html += '</div>';
container.innerHTML = html;
}
// ── Initial: no auto-load, user triggers report ─────────
</script>
</body>
</html>