nis2-agile/public/dashboard.html
DevEnv nis2-agile 782389849f [SEC+UX] Hardening sicurezza + miglioramenti UX pre-audit
SICUREZZA:
- index.php: rimosso CORS wildcard in debug mode (solo origini autorizzate)
- AuthController: getClientIP() con X-Forwarded-For sicuro (proxy-aware)
- AuthController: refresh token con SELECT FOR UPDATE in transazione atomica
- AIService: anonimizzazione dati org nei prompt Anthropic API (no nome/fatturato)

UX AUDIT-READY:
- dashboard.html: gauge rinominato 'Avanzamento implementazione misure Art.21'
- incidents.html: decision tree Art.23 con 5 criteri per 'Is Significant?'
- policies.html: banner warning obbligatorio su bozze generate da AI
- risks.html: tooltip dettagliati scala Likelihood/Impact (ISO 27005)
- assessment.html: progress bar % completamento risposta domande

DB:
- migration 006: indici performance + audit_log immutabile (trigger) + soft delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:01:33 +01:00

332 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="app-layout">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar"></aside>
<!-- Main Content -->
<main class="main-content">
<header class="content-header">
<h2 data-i18n="dashboard.title">Dashboard</h2>
<div class="content-header-actions">
<span class="text-muted" id="header-date"></span>
</div>
</header>
<div class="content-body">
<!-- Compliance Score + Stats -->
<div class="grid-3 mb-24" style="grid-template-columns: 1fr 2fr;">
<!-- Gauge -->
<div class="card">
<div class="card-body text-center">
<div id="compliance-gauge">
<div class="spinner-lg" style="margin:40px auto;"></div>
</div>
<p class="text-muted mt-8" style="font-size:0.8rem;">Avanzamento implementazione misure Art.21 NIS2</p>
<p class="text-muted" style="font-size:0.7rem; margin-top:2px;">Misura l'implementazione tecnica, non la conformita' legale</p>
</div>
</div>
<!-- Stats Cards -->
<div>
<div class="stats-grid" id="stats-grid">
<div class="stat-card">
<div class="stat-card-icon danger">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
</div>
<div class="stat-card-content">
<h4>Rischi Aperti</h4>
<div class="stat-card-value" id="stat-risks">--</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon warning">
<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>
</div>
<div class="stat-card-content">
<h4>Incidenti Attivi</h4>
<div class="stat-card-value" id="stat-incidents">--</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon success">
<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>
</div>
<div class="stat-card-content">
<h4>Policy Approvate</h4>
<div class="stat-card-value" id="stat-policies">--</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon primary">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0z"/></svg>
</div>
<div class="stat-card-content">
<h4>Formazione</h4>
<div class="stat-card-value" id="stat-training">--%</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card mb-24">
<div class="card-header">
<h3>Azioni Rapide</h3>
</div>
<div class="card-body">
<div class="quick-actions">
<button class="quick-action-btn" onclick="window.location.href='assessment.html'">
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuovo Assessment
</button>
<button class="quick-action-btn" onclick="window.location.href='incidents.html'">
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><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-6z"/></svg>
Registra Incidente
</button>
<button class="quick-action-btn" onclick="generateAIPolicy()">
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
Genera Policy AI
</button>
</div>
</div>
</div>
<!-- Deadlines + Activity -->
<div class="grid-2">
<!-- Scadenze -->
<div class="card">
<div class="card-header">
<h3>Prossime Scadenze</h3>
</div>
<div class="card-body" id="deadlines-list">
<div class="spinner" style="margin:20px auto;"></div>
</div>
</div>
<!-- Attivita' Recenti -->
<div class="card">
<div class="card-header">
<h3>Attivita' Recenti</h3>
</div>
<div class="card-body" id="activity-list">
<div class="spinner" style="margin:20px auto;"></div>
</div>
</div>
</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 check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
// ── Init ─────────────────────────────────────────────────
loadSidebar();
I18n.init();
HelpSystem.init();
// Data corrente nell'header
document.getElementById('header-date').textContent = new Date().toLocaleDateString('it-IT', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
});
// ── Caricamento dati ─────────────────────────────────────
loadDashboard();
async function loadDashboard() {
try {
const result = await api.getDashboardOverview();
if (result.success && result.data) {
const data = result.data;
// Compliance gauge
const score = data.compliance_score != null ? Math.round(data.compliance_score) : 0;
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
// Stats - map backend response structure to UI
const openRisks = data.risks ? (parseInt(data.risks.total) || 0) : 0;
const activeIncidents = data.active_incidents || 0;
const approvedPolicies = Array.isArray(data.policies)
? data.policies.reduce((sum, p) => p.status === 'approved' || p.status === 'published' ? sum + parseInt(p.count) : sum, 0)
: 0;
const trainingTotal = data.training ? (parseInt(data.training.total) || 0) : 0;
const trainingCompleted = data.training ? (parseInt(data.training.completed) || 0) : 0;
const trainingPct = trainingTotal > 0 ? Math.round((trainingCompleted / trainingTotal) * 100) : 0;
document.getElementById('stat-risks').textContent = openRisks;
document.getElementById('stat-incidents').textContent = activeIncidents;
document.getElementById('stat-policies').textContent = approvedPolicies;
document.getElementById('stat-training').textContent = trainingPct + '%';
}
} catch (err) {
console.error('Dashboard load error:', err);
}
// Always load deadlines and activity from dedicated endpoints
loadIndividualData();
}
async function loadIndividualData() {
// Compliance score
try {
const scoreRes = await api.getComplianceScore();
if (scoreRes.success && scoreRes.data) {
const score = scoreRes.data.avg_implementation || scoreRes.data.score || 0;
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
} else {
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
}
} catch (e) {
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
}
// Deadlines
try {
const dlRes = await api.getUpcomingDeadlines();
renderDeadlines(dlRes.success ? (dlRes.data || []) : []);
} catch (e) {
renderDeadlines([]);
}
// Activity
try {
const actRes = await api.getRecentActivity();
renderActivity(actRes.success ? (actRes.data || []) : []);
} catch (e) {
renderActivity([]);
}
}
function renderDeadlines(deadlines) {
const container = document.getElementById('deadlines-list');
if (!deadlines || deadlines.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>
<h4>Nessuna scadenza imminente</h4>
<p>Le prossime scadenze appariranno qui.</p>
</div>`;
return;
}
const months = ['GEN','FEB','MAR','APR','MAG','GIU','LUG','AGO','SET','OTT','NOV','DIC'];
let html = '';
deadlines.slice(0, 5).forEach(dl => {
const d = new Date(dl.due_date || dl.date);
const now = new Date();
const diffDays = Math.ceil((d - now) / 86400000);
let urgency = 'normal';
if (diffDays <= 3) urgency = 'urgent';
else if (diffDays <= 7) urgency = 'soon';
html += `
<div class="deadline-item">
<div class="deadline-date ${urgency}">
<span class="day">${d.getDate()}</span>
<span class="month">${months[d.getMonth()]}</span>
</div>
<div class="deadline-info">
<div class="deadline-title">${escapeHtml(dl.title || dl.description || 'Scadenza')}</div>
<div class="deadline-desc">${escapeHtml(dl.category || dl.type || '')}</div>
</div>
${diffDays <= 3 ? '<span class="badge badge-danger">Urgente</span>' : diffDays <= 7 ? '<span class="badge badge-warning">Prossima</span>' : ''}
</div>`;
});
container.innerHTML = html;
}
function renderActivity(activities) {
const container = document.getElementById('activity-list');
if (!activities || activities.length === 0) {
container.innerHTML = `
<div class="empty-state">
<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>Nessuna attivita' recente</h4>
<p>Le attivita' appariranno qui quando inizierai ad usare la piattaforma.</p>
</div>`;
return;
}
let html = '<ul class="activity-list">';
activities.slice(0, 8).forEach(act => {
html += `
<li class="activity-item">
<div class="activity-icon">
<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>
</div>
<div class="activity-content">
<div class="activity-text">${escapeHtml(act.description || act.message || act.action || '')}</div>
<div class="activity-time">${timeAgo(act.created_at || act.timestamp || act.date)}</div>
</div>
</li>`;
});
html += '</ul>';
container.innerHTML = html;
}
async function generateAIPolicy() {
showModal('Genera Policy con AI', `
<p style="margin-bottom:16px;">Seleziona la categoria per cui generare una policy automatica tramite intelligenza artificiale.</p>
<div class="form-group">
<label class="form-label">Categoria Policy</label>
<select class="form-select" id="ai-policy-category">
<option value="risk_management">Gestione del Rischio</option>
<option value="incident_response">Risposta agli Incidenti</option>
<option value="business_continuity">Continuita' Operativa</option>
<option value="supply_chain">Sicurezza Supply Chain</option>
<option value="access_control">Controllo degli Accessi</option>
<option value="encryption">Crittografia</option>
<option value="hr_security">Sicurezza del Personale</option>
<option value="asset_management">Gestione degli Asset</option>
</select>
</div>
`, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doGeneratePolicy()">Genera</button>
`
});
}
async function doGeneratePolicy() {
const category = document.getElementById('ai-policy-category').value;
closeModal();
showNotification('Generazione policy in corso...', 'info');
try {
const result = await api.aiGeneratePolicy(category);
if (result.success) {
showNotification('Policy generata con successo!', 'success');
setTimeout(() => window.location.href = 'policies.html', 1500);
} else {
showNotification(result.message || 'Errore nella generazione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
</script>
</body>
</html>