[FEAT] Simulatore v2: auto-reset, UX migliorata, 6 scenari (lg231-inspired)

- simulate-nis2.php: autoResetDemo() via PDO pulisce dati demo prima di ogni
  run (SIM-01→05), skip per SIM-06 indipendente. Rimuove tutte le tabelle
  org_id>4 eccetto audit_logs (trigger immutabile).
- simulate.html v2.0: rimosso pulsante "Reset Dati Demo" (chiamava endpoint
  inesistente /api/admin/reset-demo). Aggiunti: confirm dialog con lista
  aziende + durata, spinner sul bottone, nota auto-reset visibile, run history
  localStorage (ultimi 5), card SIM-06 B2B License Provisioning, console
  phase-banner stile lg231.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-03-10 10:15:41 +01:00
parent ca8f077a7a
commit e8b74a7cb7
2 changed files with 364 additions and 103 deletions

View File

@ -13,28 +13,29 @@
}
body { background: var(--dark); color: #e2e8f0; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; min-height: 100vh; }
.header { background: linear-gradient(135deg,#0c4a6e,#0e7490); padding: 32px 48px; border-bottom: 1px solid #0e7490; }
.header { background: linear-gradient(135deg,#0c4a6e,#0e7490); padding: 28px 48px; border-bottom: 1px solid #0e7490; }
.header h1 { font-size: 1.5rem; font-weight: 800; color: #fff; margin-bottom: 4px; }
.header p { color: #7dd3fc; font-size: 0.875rem; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 700; margin-right: 8px; }
.badge-cyan { background: rgba(6,182,212,.2); color: #67e8f9; border: 1px solid rgba(6,182,212,.3); }
.badge-green { background: rgba(34,197,94,.2); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
.badge-yellow { background: rgba(245,158,11,.2); color: #fcd34d; border: 1px solid rgba(245,158,11,.3); }
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; display: grid; grid-template-columns: 320px 1fr; gap: 24px; }
.container { max-width: 1100px; margin: 0 auto; padding: 28px 24px; display: grid; grid-template-columns: 330px 1fr; gap: 24px; }
/* Left panel */
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.panel h3 { font-size: 0.875rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 16px; }
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
.panel h3 { font-size: 0.8rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
.sim-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 10px; cursor: pointer; transition: border-color .2s; }
.sim-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: border-color .2s; }
.sim-card:hover { border-color: var(--primary); }
.sim-card.active { border-color: var(--primary); background: #0c2230; }
.sim-card h4 { font-size: 0.8125rem; font-weight: 700; color: #e2e8f0; margin-bottom: 4px; }
.sim-card p { font-size: 0.75rem; color: #64748b; line-height: 1.5; }
.sim-card .sim-badge { font-size: 0.65rem; padding: 1px 7px; border-radius: 10px; font-weight: 700; margin-bottom: 6px; display: inline-block; }
.sim-card h4 { font-size: 0.8125rem; font-weight: 700; color: #e2e8f0; margin-bottom: 3px; }
.sim-card p { font-size: 0.7375rem; color: #64748b; line-height: 1.5; }
.sim-card .sim-badge { font-size: 0.65rem; padding: 1px 7px; border-radius: 10px; font-weight: 700; margin-bottom: 5px; display: inline-block; }
.company-list { margin-bottom: 20px; }
.company-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 8px; }
.company-list { margin-bottom: 16px; }
.company-item { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 6px; }
.company-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-cyan { background: #06b6d4; }
.dot-purple { background: #8b5cf6; }
@ -43,8 +44,8 @@
.company-item small { font-size: 0.7rem; color: #64748b; display: block; }
/* Controls */
.controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all .2s; }
.controls { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all .2s; display: flex; align-items: center; justify-content: center; gap: 8px; }
.btn-primary { background: var(--primary); color: #0f172a; }
.btn-primary:hover:not(:disabled) { background: #0891b2; }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
@ -53,7 +54,16 @@
.btn-gray { background: #1e293b; color: #94a3b8; border: 1px solid var(--border); }
.btn-gray:hover { background: #334155; }
.status-bar { padding: 10px 12px; border-radius: 8px; font-size: 0.8rem; background: #0f172a; border: 1px solid var(--border); }
/* Spinner */
.spinner { display: none; width: 13px; height: 13px; border: 2px solid rgba(15,23,42,.3); border-top-color: #0f172a; border-radius: 50%; animation: spin .7s linear infinite; flex-shrink: 0; }
.spinner.active { display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Auto-reset note */
.auto-reset-note { font-size: 0.7rem; color: #475569; text-align: center; padding: 6px; border: 1px dashed #334155; border-radius: 6px; background: rgba(6,182,212,.03); }
.auto-reset-note span { color: #67e8f9; }
.status-bar { padding: 9px 12px; border-radius: 8px; font-size: 0.8rem; background: #0f172a; border: 1px solid var(--border); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.dot-idle { background: #475569; }
.dot-running { background: #22c55e; animation: pulse 1s infinite; }
@ -61,59 +71,88 @@
.dot-error { background: #ef4444; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 12px; }
.stat-box { background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; }
.stat-box .num { font-size: 1.25rem; font-weight: 800; }
.stat-box .lbl { font-size: 0.65rem; color: #64748b; text-transform: uppercase; margin-top: 2px; }
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; margin-top: 10px; }
.stat-box { background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 8px; text-align: center; }
.stat-box .num { font-size: 1.2rem; font-weight: 800; }
.stat-box .lbl { font-size: 0.6rem; color: #64748b; text-transform: uppercase; margin-top: 2px; }
.num-pass { color: var(--green); }
.num-skip { color: #94a3b8; }
.num-warn { color: var(--yellow); }
.num-fail { color: var(--red); }
/* Run History */
.history-section { margin-top: 14px; }
.history-title { font-size: 0.75rem; font-weight: 700; color: #475569; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }
.history-item { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 6px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 5px; cursor: pointer; transition: border-color .15s; font-size: 0.73rem; }
.history-item:hover { border-color: #475569; }
.hi-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.hi-dot.ok { background: #22c55e; }
.hi-dot.fail { background: #ef4444; }
.hi-label { color: #94a3b8; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hi-time { color: #475569; font-size: 0.65rem; white-space: nowrap; }
.hi-elapsed { color: #67e8f9; font-size: 0.65rem; margin-left: 4px; white-space: nowrap; }
.history-empty { font-size: 0.73rem; color: #475569; text-align: center; padding: 8px; }
/* Right panel — Console */
.console-panel { display: flex; flex-direction: column; gap: 0; }
.console-header { background: #1e293b; border: 1px solid var(--border); border-radius: 12px 12px 0 0; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; }
.console-header { background: #1e293b; border: 1px solid var(--border); border-radius: 12px 12px 0 0; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; }
.console-header span { font-size: 0.8rem; color: #94a3b8; }
.console-dots { display: flex; gap: 6px; }
.console-dot { width: 10px; height: 10px; border-radius: 50%; }
.console-dot-r { background: #ef4444; }
.console-dot-y { background: #f59e0b; }
.console-dot-g { background: #22c55e; }
#console { background: #0a0f1a; border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; padding: 16px; height: 520px; overflow-y: auto; font-family: 'Cascadia Code','Consolas',monospace; font-size: 0.78rem; line-height: 1.7; }
.log-phase { color: #38bdf8; font-weight: 700; border-top: 1px solid #1e3a5f; padding-top: 8px; margin-top: 4px; }
#console {
background: #0a0f1a;
border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px;
padding: 16px; height: 560px; overflow-y: auto;
font-family: 'Cascadia Code','Consolas','Fira Code',monospace;
font-size: 0.775rem; line-height: 1.75;
}
/* log line colors */
.log-phase { color: #38bdf8; font-weight: 700; border-top: 1px solid #1e3a5f; padding-top: 8px; margin-top: 6px; }
.log-ok { color: #86efac; }
.log-skip { color: #64748b; }
.log-warn { color: #fcd34d; }
.log-error { color: #fca5a5; }
.log-email { color: #67e8f9; }
.log-info { color: #94a3b8; }
.log-done { color: #a78bfa; font-weight: 700; border-top: 1px solid #3730a3; padding-top: 8px; margin-top: 4px; }
.log-done { color: #a78bfa; font-weight: 700; border-top: 1px solid #3730a3; padding-top: 8px; margin-top: 6px; }
/* Phase step banner */
.log-phase-banner {
background: rgba(56,189,248,.07);
border-left: 3px solid #38bdf8;
padding: 4px 10px; margin: 6px 0 4px;
color: #7dd3fc; font-weight: 700;
}
/* Progress bar */
.progress-wrap { height: 4px; background: #1e293b; border-radius: 2px; overflow: hidden; margin-bottom: 16px; }
.progress-bar { height: 100%; background: var(--primary); width: 0%; transition: width .3s; }
.progress-wrap { height: 4px; background: #1e293b; border-radius: 2px; overflow: hidden; margin-bottom: 14px; }
.progress-bar { height: 100%; background: var(--primary); width: 0%; transition: width .4s; }
@media (max-width: 768px) {
.container { grid-template-columns: 1fr; }
}
@media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="header">
<div style="margin-bottom:10px;">
<div style="margin-bottom:8px;">
<span class="badge badge-cyan">NIS2 Agile</span>
<span class="badge badge-green">Demo Simulator v1.0</span>
<span class="badge badge-green">Demo Simulator v2.0</span>
<span class="badge badge-yellow">Auto-Reset</span>
</div>
<h1>Simulazione Demo Realistica</h1>
<p>Genera dati demo NIS2 realistici tramite API reali — 3 aziende, 5 scenari, audit trail certificato SHA-256</p>
<p>3 aziende · 6 scenari · reset automatico · audit trail SHA-256 · dati creati via API reali</p>
</div>
<div class="container">
<!-- Left Panel -->
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;flex-direction:column;gap:14px;">
<!-- Aziende -->
<div class="panel">
<h3>3 Aziende Demo</h3>
<div class="company-list">
@ -121,14 +160,14 @@
<div class="company-dot dot-cyan"></div>
<div>
<span>DataCore S.r.l.</span>
<small>IT/Cloud · Essential Entity · 320 dip · Milano</small>
<small>IT/Cloud · Essential · 320 dip · Milano</small>
</div>
</div>
<div class="company-item">
<div class="company-dot dot-purple"></div>
<div>
<span>MedClinic Italia S.p.A.</span>
<small>Sanità · Important Entity · 750 dip · Roma</small>
<small>Sanità · Important · 750 dip · Roma</small>
</div>
</div>
<div class="company-item">
@ -141,50 +180,60 @@
</div>
</div>
<!-- Scenari -->
<div class="panel">
<h3>5 Scenari Reali</h3>
<h3>6 Scenari Reali</h3>
<div class="sim-card active" onclick="selectSim(this, 'all')">
<div class="sim-badge" style="background:rgba(6,182,212,.15);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">COMPLETA</div>
<h4>Tutti gli scenari</h4>
<p>Esegue tutti e 5 gli scenari in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain.</p>
<p>SIM-01→06 in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain, B2B.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim01')">
<div class="sim-badge" style="background:rgba(34,197,94,.1);color:#86efac;border:1px solid rgba(34,197,94,.3);">SIM-01</div>
<h4>Onboarding + Gap Assessment</h4>
<p>Registra le 3 aziende, classifica NIS2 (Essential/Important), esegue assessment 80 domande Art.21.</p>
<p>Registra 3 aziende, classifica NIS2, esegue assessment 80 domande Art.21.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim02')">
<div class="sim-badge" style="background:rgba(239,68,68,.1);color:#fca5a5;border:1px solid rgba(239,68,68,.3);">SIM-02</div>
<h4>Ransomware Art.23 DataCore</h4>
<p>Attacco ransomware su infrastruttura cloud. Timeline 24h/72h/30d con notifiche ACN e CSIRT.</p>
<p>Attacco critico su cloud. Timeline 24h/72h/30d con notifiche CSIRT.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim03')">
<div class="sim-badge" style="background:rgba(139,92,246,.1);color:#ddd6fe;border:1px solid rgba(139,92,246,.3);">SIM-03</div>
<h4>Data Breach Supply Chain</h4>
<p>Fornitore LIS compromesso, esfiltrazione 2.340 pazienti. GDPR Art.33 + NIS2 Art.23 in parallelo.</p>
<p>Fornitore LIS compromesso. GDPR Art.33 + NIS2 Art.23 in parallelo.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim04')">
<div class="sim-badge" style="background:rgba(245,158,11,.1);color:#fcd34d;border:1px solid rgba(245,158,11,.3);">SIM-04</div>
<h4>Whistleblowing SCADA EnerNet</h4>
<p>Segnalazione anonima accesso non autorizzato SCADA. Token tracking, investigazione, chiusura.</p>
<p>Segnalazione anonima accesso non autorizzato SCADA. Token tracking.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim05')">
<div class="sim-badge" style="background:rgba(6,182,212,.1);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">SIM-05</div>
<h4>Audit Trail Hash Chain</h4>
<p>Verifica integrità SHA-256 hash chain per le 3 org. Export certificato NIS2 + ISO 27001.</p>
<p>Verifica integrità SHA-256 hash chain per le 3 org. Export certificato.</p>
</div>
<div class="sim-card" onclick="selectSim(this, 'sim06')">
<div class="sim-badge" style="background:rgba(245,158,11,.12);color:#fbbf24;border:1px solid rgba(245,158,11,.3);">SIM-06</div>
<h4>B2B License Provisioning</h4>
<p>Invite token → org + user + API key creati atomicamente. SSO JWT.</p>
</div>
</div>
<!-- Controlli -->
<div class="panel">
<h3>Controlli</h3>
<div class="controls">
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">▶ Avvia Simulazione</button>
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">
<span class="spinner" id="spinner"></span>
<span id="btnRunLabel">▶ Avvia Simulazione</span>
</button>
<button id="btnStop" class="btn btn-danger" onclick="stopSimulation()" disabled>■ Interrompi</button>
<button class="btn btn-gray" onclick="clearConsole()">⌫ Pulisci Console</button>
<button class="btn btn-gray" onclick="resetDemo()">↺ Reset Dati Demo</button>
</div>
<div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div>
<div class="auto-reset-note"><span>Reset automatico</span> dati demo precedenti all'avvio</div>
<div class="progress-wrap" style="margin-top:12px;"><div class="progress-bar" id="progressBar"></div></div>
<div class="status-bar">
<span class="status-dot dot-idle" id="statusDot"></span>
<span id="statusText">In attesa</span>
@ -195,6 +244,12 @@
<div class="stat-box"><div class="num num-warn" id="sWarn">0</div><div class="lbl">Warn</div></div>
<div class="stat-box"><div class="num num-fail" id="sFail">0</div><div class="lbl">Fail</div></div>
</div>
<!-- Run History -->
<div class="history-section">
<div class="history-title">Ultimi run</div>
<div id="historyList"></div>
</div>
</div>
</div>
@ -207,55 +262,91 @@
<div class="console-dot console-dot-y"></div>
<div class="console-dot console-dot-g"></div>
</div>
<span id="consoleTitle">NIS2 Agile Simulator — Console</span>
<span id="elapsedTime"></span>
<span id="consoleTitle">NIS2 Agile Simulator v2.0 — Console</span>
<span id="elapsedTime" style="font-variant-numeric:tabular-nums;"></span>
</div>
<div id="console">
<div class="log-info">NIS2 Agile Demo Simulator v1.0 — Pronto</div>
<div class="log-info">NIS2 Agile Demo Simulator v2.0 — Pronto</div>
<div class="log-info">Seleziona uno scenario e premi "Avvia Simulazione".</div>
<div class="log-info" style="margin-top:8px;">Scenari disponibili:</div>
<div class="log-info" style="margin-top:8px;color:#475569;">Scenari disponibili:</div>
<div class="log-ok"> SIM-01 Onboarding + Gap Assessment 80 domande</div>
<div class="log-ok"> SIM-02 Incidente Ransomware Art.23 (24h/72h/30d)</div>
<div class="log-ok"> SIM-03 Data Breach Supply Chain + GDPR</div>
<div class="log-ok"> SIM-04 Whistleblowing anonimo SCADA</div>
<div class="log-ok"> SIM-05 Audit Trail Hash Chain Verification</div>
<div class="log-info" style="margin-top:8px;">Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).</div>
<div class="log-ok"> SIM-06 B2B License Provisioning (invite token)</div>
<div class="log-info" style="margin-top:8px;color:#38bdf8;">↺ Ogni run pulisce automaticamente i dati demo precedenti.</div>
<div class="log-info" style="color:#475569;">Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).</div>
</div>
</div>
</div>
<script>
let evtSource = null;
const HISTORY_KEY = 'nis2_sim_history';
let evtSource = null;
let selectedSim = 'all';
let startTime = null;
let startTime = null;
let timerInterval = null;
const SIM_LABELS = {
all: 'Tutti (SIM-01→06)',
sim01: 'SIM-01 Onboarding',
sim02: 'SIM-02 Ransomware',
sim03: 'SIM-03 Data Breach',
sim04: 'SIM-04 Whistleblowing',
sim05: 'SIM-05 Audit Chain',
sim06: 'SIM-06 B2B License',
};
// ── Selezione scenario ────────────────────────────────────────────────────
function selectSim(el, sim) {
document.querySelectorAll('.sim-card').forEach(c => c.classList.remove('active'));
el.classList.add('active');
selectedSim = sim;
}
// ── Stato UI ─────────────────────────────────────────────────────────────
function setStatus(text, dotClass) {
document.getElementById('statusText').textContent = text;
const dot = document.getElementById('statusDot');
dot.className = 'status-dot ' + dotClass;
document.getElementById('statusDot').className = 'status-dot ' + dotClass;
}
function setRunning(running) {
const runBtn = document.getElementById('btnRun');
const stopBtn = document.getElementById('btnStop');
const spinner = document.getElementById('spinner');
const label = document.getElementById('btnRunLabel');
runBtn.disabled = running;
stopBtn.disabled = !running;
spinner.classList.toggle('active', running);
label.textContent = running ? 'Simulazione in corso...' : '▶ Avvia Simulazione';
}
// ── Console ───────────────────────────────────────────────────────────────
function appendLog(msg, type) {
const console = document.getElementById('console');
const con = document.getElementById('console');
const line = document.createElement('div');
line.className = 'log-' + (type || 'info');
if (type === 'phase') {
line.className = 'log-phase-banner';
} else {
line.className = 'log-' + (type || 'info');
}
line.textContent = msg;
console.appendChild(line);
console.scrollTop = console.scrollHeight;
con.appendChild(line);
con.scrollTop = con.scrollHeight;
}
function clearConsole() {
document.getElementById('console').innerHTML = '<div class="log-info">Console pulita.</div>';
}
// ── Progress ──────────────────────────────────────────────────────────────
function updateProgress(pct) {
document.getElementById('progressBar').style.width = pct + '%';
}
@ -269,106 +360,178 @@ function updateStats(stats) {
document.getElementById('sFail').textContent = stats.fail || 0;
}
// ── Timer ─────────────────────────────────────────────────────────────────
function startTimer() {
startTime = Date.now();
timerInterval = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('elapsedTime').textContent = elapsed + 's';
const s = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('elapsedTime').textContent = s + 's';
}, 500);
}
function stopTimer() {
if (timerInterval) clearInterval(timerInterval);
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
const s = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : null;
return s;
}
// ── Run History (localStorage) ────────────────────────────────────────────
function getHistory() {
try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); }
catch(e) { return []; }
}
function addHistory(sim, stats, elapsed) {
const ok = (stats.fail || 0) === 0;
const h = getHistory();
h.unshift({
sim,
label: SIM_LABELS[sim] || sim,
ok,
pass: stats.pass || 0,
fail: stats.fail || 0,
skip: stats.skip || 0,
elapsed: parseFloat(elapsed) || 0,
ts: new Date().toLocaleString('it-IT', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' }),
});
localStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, 5)));
renderHistory();
}
function renderHistory() {
const list = document.getElementById('historyList');
const h = getHistory();
if (!h.length) {
list.innerHTML = '<div class="history-empty">Nessun run ancora</div>';
return;
}
list.innerHTML = h.map(e => `
<div class="history-item" onclick="replayRun('${e.sim}')" title="Riesegui ${e.label}">
<span class="hi-dot ${e.ok ? 'ok' : 'fail'}"></span>
<span class="hi-label">${e.label}</span>
<span class="hi-time">${e.ts}</span>
<span class="hi-elapsed">${e.elapsed}s</span>
</div>
`).join('');
}
function replayRun(sim) {
// Seleziona lo scenario e avvia
document.querySelectorAll('.sim-card').forEach(c => {
const badge = c.querySelector('.sim-badge');
const isSim06 = badge && badge.textContent.trim() === 'SIM-06';
const isAll = c.querySelector('h4') && c.querySelector('h4').textContent.includes('Tutti');
if (
(sim === 'all' && isAll) ||
(sim === 'sim06' && isSim06) ||
(sim !== 'all' && sim !== 'sim06' && badge && badge.textContent.trim().toLowerCase().replace('-','') === sim.replace('sim','sim-'))
) {
selectSim(c, sim);
}
});
selectedSim = sim;
runSimulation();
}
// ── Avvio simulazione ─────────────────────────────────────────────────────
function runSimulation() {
// Confirm dialog (pattern lg231)
const labels = {
all: 'Tutti gli scenari (SIM-01→06)',
sim01: 'SIM-01 — Onboarding + Gap Assessment',
sim02: 'SIM-02 — Ransomware Art.23',
sim03: 'SIM-03 — Data Breach Supply Chain',
sim04: 'SIM-04 — Whistleblowing SCADA',
sim05: 'SIM-05 — Audit Trail Hash Chain',
sim06: 'SIM-06 — B2B License Provisioning',
};
const scenarioName = labels[selectedSim] || selectedSim;
const resetNote = selectedSim !== 'sim06'
? '\n↺ I dati demo precedenti verranno ripuliti automaticamente.'
: '\n(SIM-06 è indipendente — nessun reset automatico)';
if (!confirm(
'Avvia Simulazione NIS2 Agile?\n\n' +
'▸ ' + scenarioName + '\n\n' +
'• DataCore S.r.l. (IT/Cloud - Essential)\n' +
'• MedClinic Italia S.p.A. (Sanità - Important)\n' +
'• EnerNet Distribuzione S.r.l. (Energia - Critical)\n' +
resetNote + '\n' +
'\nDurata stimata: ~8-12 minuti'
)) return;
if (evtSource) { evtSource.close(); evtSource = null; }
const runBtn = document.getElementById('btnRun');
const stopBtn = document.getElementById('btnStop');
runBtn.disabled = true;
stopBtn.disabled = false;
setRunning(true);
clearConsole();
setStatus('Simulazione in corso...', 'dot-running');
updateProgress(5);
startTimer();
document.getElementById('statsGrid').style.display = 'none';
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator v2.0 — ' + (SIM_LABELS[selectedSim] || selectedSim);
const url = `../simulate-nis2.php?sim=${selectedSim}&t=${Date.now()}`;
evtSource = new EventSource(url);
evtSource = new EventSource(url);
const phaseKeywords = ['FASE', '══'];
let phaseCount = 0;
evtSource.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.t === 'done') {
evtSource.close();
runBtn.disabled = false;
stopBtn.disabled = true;
setStatus('Completata', 'dot-done');
stopTimer();
evtSource.close(); evtSource = null;
const elapsed = stopTimer();
setRunning(false);
setStatus('Completata (' + elapsed + 's)', 'dot-done');
updateProgress(100);
updateStats(data.stats);
appendLog('', 'info');
appendLog('Simulazione completata.', 'done');
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator — Completata';
appendLog('Simulazione completata in ' + elapsed + 's', 'done');
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator v2.0 — Completata ✓';
if (data.stats) addHistory(selectedSim, data.stats, elapsed);
return;
}
const type = data.t || 'info';
const msg = data.m || '';
const msg = data.m || '';
appendLog(msg, type);
// Stima progress
if (type === 'phase' || msg.includes('FASE')) {
if (type === 'phase') {
phaseCount++;
updateProgress(Math.min(5 + phaseCount * 8, 95));
updateProgress(Math.min(5 + phaseCount * 7, 93));
}
};
evtSource.onerror = () => {
evtSource.close();
runBtn.disabled = false;
stopBtn.disabled = true;
evtSource.close(); evtSource = null;
const elapsed = stopTimer();
setRunning(false);
setStatus('Errore connessione', 'dot-error');
stopTimer();
appendLog('Errore SSE — connessione interrotta.', 'error');
if (elapsed) {
addHistory(selectedSim, { pass: 0, fail: 1, skip: 0, warn: 0 }, elapsed);
}
};
}
function stopSimulation() {
if (evtSource) { evtSource.close(); evtSource = null; }
document.getElementById('btnRun').disabled = false;
document.getElementById('btnStop').disabled = true;
const elapsed = stopTimer();
setRunning(false);
setStatus('Interrotta', 'dot-error');
stopTimer();
appendLog('Simulazione interrotta manualmente.', 'warn');
if (elapsed) {
addHistory(selectedSim, { pass: 0, fail: 0, skip: 1, warn: 1 }, elapsed);
}
}
function resetDemo() {
if (!confirm('Reset dati demo? Verranno eliminati tutti i dati con organization_id > 4.')) return;
fetch('../api/admin/reset-demo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('nis2_access_token') || ''),
},
})
.then(r => r.json())
.then(d => {
if (d.success) {
appendLog('Reset demo completato.', 'ok');
} else {
appendLog('Reset demo fallito: ' + (d.error || 'errore'), 'error');
appendLog('Tip: eseguire manualmente docs/sql/reset-demo.sql su Hetzner.', 'warn');
}
})
.catch(() => appendLog('Reset demo: errore di rete — eseguire manualmente su server.', 'warn'));
}
// ── Init ──────────────────────────────────────────────────────────────────
renderHistory();
</script>
</body>
</html>

View File

@ -606,6 +606,95 @@ function getAssessmentResponses(string $sector): array
return array_merge($base, $overrides);
}
// ────────────────────────────────────────────────────────────────────────────
// Auto-Reset Demo Data — eseguito prima di ogni run SIM-01→05
// Usa PDO con nis2_user (NO SUPER privilege → non tocca audit_logs/trigger)
// ────────────────────────────────────────────────────────────────────────────
function autoResetDemo(): void
{
$host = readEnvValue('DB_HOST', 'localhost');
$port = readEnvValue('DB_PORT', '3306');
$name = readEnvValue('DB_NAME', 'nis2_agile_db');
$user = readEnvValue('DB_USER', 'nis2_user');
$pass = readEnvValue('DB_PASS', '');
try {
$pdo = new PDO(
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
$user,
$pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 10]
);
} catch (\Throwable $e) {
simLog('Auto-reset: connessione DB fallita — ' . $e->getMessage(), 'warn');
return;
}
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
// Tabelle con organization_id (ordine: dipendenti prima dei parent)
$orgTables = [
'incident_timeline' => 'incident_id IN (SELECT id FROM incidents WHERE organization_id > 4)',
'incidents' => 'organization_id > 4',
'risk_treatments' => 'risk_id IN (SELECT id FROM risks WHERE organization_id > 4)',
'risks' => 'organization_id > 4',
'assessment_responses' => 'assessment_id IN (SELECT id FROM assessments WHERE organization_id > 4)',
'assessments' => 'organization_id > 4',
'policies' => 'organization_id > 4',
'suppliers' => 'organization_id > 4',
'training_assignments' => 'course_id IN (SELECT id FROM training_courses WHERE organization_id > 4)',
'training_courses' => 'organization_id > 4',
'assets' => 'organization_id > 4',
'compliance_controls' => 'organization_id > 4',
'evidence_files' => 'organization_id > 4',
'capa_actions' => 'ncr_id IN (SELECT id FROM non_conformities WHERE organization_id > 4)',
'non_conformities' => 'organization_id > 4',
'whistleblowing_timeline' => 'report_id IN (SELECT id FROM whistleblowing_reports WHERE organization_id > 4)',
'whistleblowing_reports' => 'organization_id > 4',
'normative_ack' => 'organization_id > 4',
'webhook_deliveries' => 'subscription_id IN (SELECT id FROM webhook_subscriptions WHERE organization_id > 4)',
'webhook_subscriptions' => 'organization_id > 4',
'api_keys' => 'organization_id > 4',
'feedback_reports' => 'organization_id > 4',
'ai_interactions' => 'organization_id > 4',
// audit_logs: trigger immutabile blocca DELETE — skip (dati storici accettabili)
];
$deleted = 0;
foreach ($orgTables as $table => $where) {
try {
$n = $pdo->exec("DELETE FROM `{$table}` WHERE {$where}");
$deleted += ($n ?: 0);
} catch (\Throwable $e) {
simLog(" skip {$table}: " . $e->getMessage(), 'skip');
}
}
// Email log (no organization_id)
try { $pdo->exec("DELETE FROM email_log WHERE recipient LIKE '%.demo%'"); } catch (\Throwable) {}
// User organizations + refresh tokens demo
try { $pdo->exec('DELETE FROM user_organizations WHERE organization_id > 4'); } catch (\Throwable) {}
try {
$pdo->exec("DELETE rt FROM refresh_tokens rt
JOIN users u ON rt.user_id = u.id
WHERE u.email LIKE '%.demo%'");
} catch (\Throwable) {}
// Utenti demo
try { $n = $pdo->exec("DELETE FROM users WHERE email LIKE '%.demo%'"); $deleted += ($n ?: 0); } catch (\Throwable) {}
// Organizzazioni demo
try { $n = $pdo->exec('DELETE FROM organizations WHERE id > 4'); $deleted += ($n ?: 0); } catch (\Throwable) {}
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
// Verifica org rimaste
$orgCount = $pdo->query('SELECT COUNT(*) FROM organizations')->fetchColumn();
simLog("Auto-reset completato — {$deleted} record rimossi, {$orgCount} org base mantenute", 'ok');
}
// ────────────────────────────────────────────────────────────────────────────
// FASE 0 — Health Check
// ────────────────────────────────────────────────────────────────────────────
@ -623,6 +712,15 @@ if ($code === 200 && !empty($hRes['status'])) {
warn("API health check degradato (HTTP $code) — continuo comunque");
}
// ────────────────────────────────────────────────────────────────────────────
// FASE 0b — Auto-Reset Dati Demo Precedenti
// (skip se si lancia solo SIM-06 che è indipendente)
// ────────────────────────────────────────────────────────────────────────────
if ($SIM_FILTER !== 'SIM06') {
simPhase(0, 'Auto-Reset Dati Demo Precedenti');
autoResetDemo();
}
// ────────────────────────────────────────────────────────────────────────────
// SIM-01→05 — skip se si esegue solo SIM-06
// ────────────────────────────────────────────────────────────────────────────