Complete MVP implementation including: - PHP 8.4 backend with Front Controller pattern (80+ API endpoints) - Multi-tenant architecture with organization_id isolation - JWT authentication (HS256, 2h access + 7d refresh tokens) - 14 controllers: Auth, Organization, Assessment, Dashboard, Risk, Incident, Policy, SupplyChain, Training, Asset, Audit, Admin - AI Service integration (Anthropic Claude API) for gap analysis, risk suggestions, policy generation, incident classification - NIS2 gap analysis questionnaire (~80 questions, 10 categories) - MySQL schema (20 tables) with NIS2 Art. 21 compliance controls - NIS2 Art. 23 incident reporting workflow (24h/72h/30d) - Frontend: login, register, dashboard, assessment wizard, org setup - Docker configuration (PHP-FPM + Nginx + MySQL) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
17 KiB
HTML
325 lines
17 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>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;">Punteggio complessivo di conformita' NIS2</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>
|
|
// ── Auth check ───────────────────────────────────────────
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
|
|
// ── Init ─────────────────────────────────────────────────
|
|
loadSidebar();
|
|
|
|
// 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 ? data.compliance_score : 0;
|
|
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
|
|
|
// Stats
|
|
document.getElementById('stat-risks').textContent = data.open_risks != null ? data.open_risks : 0;
|
|
document.getElementById('stat-incidents').textContent = data.active_incidents != null ? data.active_incidents : 0;
|
|
document.getElementById('stat-policies').textContent = data.approved_policies != null ? data.approved_policies : 0;
|
|
document.getElementById('stat-training').textContent = (data.training_completion != null ? data.training_completion : 0) + '%';
|
|
|
|
// Scadenze
|
|
renderDeadlines(data.upcoming_deadlines || []);
|
|
|
|
// Attivita'
|
|
renderActivity(data.recent_activity || []);
|
|
} else {
|
|
// Fallback: prova endpoint singoli
|
|
loadIndividualData();
|
|
}
|
|
} catch (err) {
|
|
console.error('Dashboard load error:', err);
|
|
loadIndividualData();
|
|
}
|
|
}
|
|
|
|
async function loadIndividualData() {
|
|
// Compliance score
|
|
try {
|
|
const scoreRes = await api.getComplianceScore();
|
|
if (scoreRes.success && scoreRes.data) {
|
|
const score = scoreRes.data.score || scoreRes.data.compliance_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>
|