[TEST] Test Runner NIS2 Agile — pattern lg231
Single-file PHP runner con token auth (Nis2Test2026), SSE streaming, dark terminal UI. Comandi: health, smoke, sim01-05, simulate, chain-verify, reset, all. Tab: Test / Simulazioni / Credenziali demo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d4a028e71f
commit
4d8e7c74f4
687
test-runner.php
Normal file
687
test-runner.php
Normal file
@ -0,0 +1,687 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile — Test Runner
|
||||
* Accesso: https://nis2.agile.software/test-runner.php?t=Nis2Test2026
|
||||
*
|
||||
* Pattern ispirato a lg231.agile/test-runner.php
|
||||
*/
|
||||
|
||||
define('ACCESS_TOKEN', 'Nis2Test2026');
|
||||
define('PROJECT_ROOT', '/var/www/nis2-agile');
|
||||
define('API_BASE', 'https://nis2.agile.software/api');
|
||||
define('SIMULATE_PHP', PROJECT_ROOT . '/simulate-nis2.php');
|
||||
define('RESET_SQL', PROJECT_ROOT . '/docs/sql/reset-demo.sql');
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
session_start();
|
||||
|
||||
$token = $_GET['t'] ?? $_POST['t'] ?? '';
|
||||
if ($token === ACCESS_TOKEN) {
|
||||
$_SESSION['auth'] = true;
|
||||
}
|
||||
|
||||
if (empty($_SESSION['auth'])) {
|
||||
http_response_code(401);
|
||||
echo '<!doctype html><html><body style="font-family:monospace;background:#0f172a;color:#f87171;padding:2rem">'
|
||||
. '<h2>401 — Token richiesto</h2><p>Accedi con <code>?t=Nis2Test2026</code></p></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Router ────────────────────────────────────────────────────────────────────
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
|
||||
$slug = ltrim(substr($path, strlen($base)), '/');
|
||||
|
||||
if ($slug === 'events') {
|
||||
serveEvents();
|
||||
exit;
|
||||
}
|
||||
if ($slug === 'status') {
|
||||
serveStatus();
|
||||
exit;
|
||||
}
|
||||
|
||||
serveUI();
|
||||
exit;
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function getCommands(): array
|
||||
{
|
||||
$php = PHP_BINARY ?: 'php';
|
||||
$root = PROJECT_ROOT;
|
||||
$api = API_BASE;
|
||||
|
||||
return [
|
||||
'health' => [
|
||||
'label' => 'Health Check',
|
||||
'bash' => "curl -sf {$api}/../api-status.php | python3 -m json.tool",
|
||||
'cwd' => $root,
|
||||
'timeout' => 15,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'smoke' => [
|
||||
'label' => 'Smoke Tests (curl)',
|
||||
'bash' => implode(' && ', [
|
||||
"echo '=== Login demo ===' && curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore.demo\",\"password\":\"Demo2026!\"}' | python3 -m json.tool || echo '[SKIP] utente non ancora creato'",
|
||||
"echo '=== /api/auth/me (no token) ===' && curl -sf {$api}/auth/me | python3 -m json.tool",
|
||||
"echo '=== /api/dashboard/overview (no token → 401) ===' && curl -sf -w '\\nHTTP %{http_code}\\n' {$api}/dashboard/overview || true",
|
||||
"echo '=== API status ===' && curl -sf {$api}/../api-status.php | python3 -m json.tool",
|
||||
]),
|
||||
'cwd' => $root,
|
||||
'timeout' => 30,
|
||||
'continue_on_fail' => true,
|
||||
],
|
||||
'sim01' => [
|
||||
'label' => 'SIM-01 Onboarding + Assessment',
|
||||
'bash' => "NIS2_SIM=SIM01 {$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 180,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'sim02' => [
|
||||
'label' => 'SIM-02 Ransomware Art.23',
|
||||
'bash' => "NIS2_SIM=SIM02 {$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 120,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'sim03' => [
|
||||
'label' => 'SIM-03 Data Breach Supply Chain',
|
||||
'bash' => "NIS2_SIM=SIM03 {$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 120,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'sim04' => [
|
||||
'label' => 'SIM-04 Whistleblowing SCADA',
|
||||
'bash' => "NIS2_SIM=SIM04 {$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 90,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'sim05' => [
|
||||
'label' => 'SIM-05 Audit Chain Verify',
|
||||
'bash' => "NIS2_SIM=SIM05 {$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 60,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'simulate' => [
|
||||
'label' => 'Tutte le Simulazioni (SIM-01→05)',
|
||||
'bash' => "{$php} {$root}/simulate-nis2.php",
|
||||
'cwd' => $root,
|
||||
'timeout' => 600,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'chain-verify' => [
|
||||
'label' => 'Verifica Hash Chain Audit',
|
||||
'bash' => implode(' && ', [
|
||||
"echo '=== Verifica chain org demo ==='",
|
||||
"curl -sf -H 'Authorization: Bearer \$(cat /tmp/nis2_chain_token 2>/dev/null || echo \"\")' {$api}/audit/chain-verify | python3 -m json.tool || echo '[INFO] Autenticarsi prima con simulate'",
|
||||
]),
|
||||
'cwd' => $root,
|
||||
'timeout' => 30,
|
||||
'continue_on_fail' => true,
|
||||
],
|
||||
'reset' => [
|
||||
'label' => 'Reset Dati Demo',
|
||||
'bash' => "mysql -u nis2_agile_user -p\$(grep DB_PASSWORD {$root}/.env | cut -d= -f2) nis2_agile_db < {$root}/docs/sql/reset-demo.sql",
|
||||
'cwd' => $root,
|
||||
'timeout' => 30,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
'all' => [
|
||||
'label' => 'Full Suite (health + simulate + chain)',
|
||||
'bash' => implode(' && ', [
|
||||
"curl -sf " . API_BASE . "/../api-status.php | python3 -m json.tool",
|
||||
PHP_BINARY . " {$root}/simulate-nis2.php",
|
||||
]),
|
||||
'cwd' => $root,
|
||||
'timeout' => 660,
|
||||
'continue_on_fail' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// ── SSE Events handler ────────────────────────────────────────────────────────
|
||||
|
||||
function serveEvents(): void
|
||||
{
|
||||
$cmd = $_GET['cmd'] ?? '';
|
||||
$commands = getCommands();
|
||||
|
||||
if (!isset($commands[$cmd])) {
|
||||
http_response_code(400);
|
||||
echo "Comando sconosciuto: {$cmd}";
|
||||
return;
|
||||
}
|
||||
|
||||
$def = $commands[$cmd];
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
|
||||
sseWrite('start', json_encode(['cmd' => $cmd, 'label' => $def['label'], 'ts' => date('c')]));
|
||||
|
||||
$t0 = microtime(true);
|
||||
runCommand($def, function (string $type, string $line) {
|
||||
sseWrite($type, $line);
|
||||
});
|
||||
|
||||
$elapsed = round(microtime(true) - $t0, 2);
|
||||
sseWrite('done', json_encode(['cmd' => $cmd, 'elapsed' => $elapsed]));
|
||||
}
|
||||
|
||||
function runCommand(array $def, callable $emit): void
|
||||
{
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$env = array_merge($_ENV, getenv() ?: [], ['NIS2_API_BASE' => API_BASE]);
|
||||
|
||||
$proc = proc_open(
|
||||
$def['bash'],
|
||||
$descriptors,
|
||||
$pipes,
|
||||
$def['cwd'] ?? PROJECT_ROOT,
|
||||
$env
|
||||
);
|
||||
|
||||
if (!is_resource($proc)) {
|
||||
$emit('stderr', '[ERRORE] Impossibile avviare il processo');
|
||||
return;
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
$timeout = $def['timeout'] ?? 120;
|
||||
$deadline = microtime(true) + $timeout;
|
||||
$buf1 = '';
|
||||
$buf2 = '';
|
||||
|
||||
while (true) {
|
||||
if (microtime(true) > $deadline) {
|
||||
$emit('stderr', "[TIMEOUT] Processo terminato dopo {$timeout}s");
|
||||
proc_terminate($proc, 9);
|
||||
break;
|
||||
}
|
||||
|
||||
$r = [$pipes[1], $pipes[2]];
|
||||
$w = null; $e = null;
|
||||
|
||||
$changed = @stream_select($r, $w, $e, 0, 50000);
|
||||
|
||||
if ($changed === false) break;
|
||||
|
||||
foreach ($r as $s) {
|
||||
$chunk = fread($s, 4096);
|
||||
if ($chunk === false || $chunk === '') continue;
|
||||
|
||||
if ($s === $pipes[1]) {
|
||||
$buf1 .= $chunk;
|
||||
while (($nl = strpos($buf1, "\n")) !== false) {
|
||||
$emit('stdout', rtrim(substr($buf1, 0, $nl)));
|
||||
$buf1 = substr($buf1, $nl + 1);
|
||||
}
|
||||
} else {
|
||||
$buf2 .= $chunk;
|
||||
while (($nl = strpos($buf2, "\n")) !== false) {
|
||||
$emit('stderr', rtrim(substr($buf2, 0, $nl)));
|
||||
$buf2 = substr($buf2, $nl + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (feof($pipes[1]) && feof($pipes[2])) break;
|
||||
if (connection_aborted()) { proc_terminate($proc, 9); break; }
|
||||
|
||||
// flush heartbeat
|
||||
if (ob_get_level()) ob_flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
if ($buf1 !== '') $emit('stdout', rtrim($buf1));
|
||||
if ($buf2 !== '') $emit('stderr', rtrim($buf2));
|
||||
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($proc);
|
||||
|
||||
if (ob_get_level()) ob_flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
function sseWrite(string $event, string $data): void
|
||||
{
|
||||
// JSON-encode non-JSON data for transport safety
|
||||
$out = "event: {$event}\ndata: " . $data . "\n\n";
|
||||
echo $out;
|
||||
if (ob_get_level()) ob_flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
// ── Status JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
function serveStatus(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$api = API_BASE;
|
||||
$up = false;
|
||||
$info = [];
|
||||
|
||||
$ch = curl_init("{$api}/../api-status.php");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code === 200 && $resp) {
|
||||
$up = true;
|
||||
$info = json_decode($resp, true) ?: [];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'api_up' => $up,
|
||||
'api_url' => $api,
|
||||
'api_info' => $info,
|
||||
'php_version'=> PHP_VERSION,
|
||||
'ts' => date('c'),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function serveUI(): void
|
||||
{
|
||||
$commands = getCommands();
|
||||
$cmdsJson = json_encode(array_map(fn($k, $v) => [
|
||||
'id' => $k,
|
||||
'label' => $v['label'],
|
||||
], array_keys($commands), $commands), JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$token = ACCESS_TOKEN;
|
||||
$api = API_BASE;
|
||||
$url = "https://nis2.agile.software/test-runner.php?t={$token}";
|
||||
|
||||
$demoCredentials = [
|
||||
['role' => 'Admin (DataCore IT)', 'email' => 'admin@datacore.demo', 'password' => 'Demo2026!', 'org' => 'DataCore S.r.l.'],
|
||||
['role' => 'Compliance (MedClinic)', 'email' => 'compliance@medclinic.demo', 'password' => 'Demo2026!', 'org' => 'MedClinic Italia S.p.A.'],
|
||||
['role' => 'CISO (EnerNet)', 'email' => 'ciso@enernet.demo', 'password' => 'Demo2026!', 'org' => 'EnerNet Distribuzione S.r.l.'],
|
||||
];
|
||||
$credsRows = '';
|
||||
foreach ($demoCredentials as $c) {
|
||||
$credsRows .= "<tr><td>{$c['role']}</td><td><code>{$c['email']}</code></td><td><code>{$c['password']}</code></td><td>{$c['org']}</td></tr>";
|
||||
}
|
||||
|
||||
$simCards = '';
|
||||
$simDefs = [
|
||||
'sim01' => ['SIM-01', 'Onboarding + Assessment', '3 aziende, gap analysis 80 domande, score iniziale', 'cyan'],
|
||||
'sim02' => ['SIM-02', 'Ransomware Art.23', 'Incidente critico, timeline 24h/72h, CSIRT notifica', 'orange'],
|
||||
'sim03' => ['SIM-03', 'Data Breach Supply Chain','Fornitore compromesso, Art.23 + GDPR Art.33 parallelo', 'red'],
|
||||
'sim04' => ['SIM-04', 'Whistleblowing SCADA', 'Segnalazione anonima, assegnazione, chiusura tracciata', 'purple'],
|
||||
'sim05' => ['SIM-05', 'Audit Chain Verify', 'Verifica integrità SHA-256, export certificato', 'green'],
|
||||
];
|
||||
foreach ($simDefs as $id => [$code, $name, $desc, $col]) {
|
||||
$simCards .= <<<HTML
|
||||
<div class="sim-card" data-cmd="{$id}" onclick="runCmd('{$id}')">
|
||||
<span class="sim-badge badge-{$col}">{$code}</span>
|
||||
<div class="sim-info">
|
||||
<strong>{$name}</strong>
|
||||
<small>{$desc}</small>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
$testBtns = '';
|
||||
$testCmds = ['health', 'smoke', 'simulate', 'chain-verify', 'reset', 'all'];
|
||||
foreach ($testCmds as $id) {
|
||||
$label = $commands[$id]['label'] ?? $id;
|
||||
$extra = in_array($id, ['reset', 'all']) ? ' btn-danger' : '';
|
||||
$testBtns .= "<button class='btn{$extra}' onclick=\"runCmd('{$id}')\">{$label}</button>\n";
|
||||
}
|
||||
|
||||
echo <<<HTML
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NIS2 Agile — Test Runner</title>
|
||||
<style>
|
||||
:root {
|
||||
--navy: #0F172A;
|
||||
--slate: #1E293B;
|
||||
--border: #334155;
|
||||
--cyan: #06B6D4;
|
||||
--green: #22C55E;
|
||||
--red: #EF4444;
|
||||
--orange: #F97316;
|
||||
--yellow: #EAB308;
|
||||
--purple: #A855F7;
|
||||
--muted: #64748B;
|
||||
--text: #E2E8F0;
|
||||
--term-bg: #0F172A;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { display: flex; height: 100vh; background: var(--navy); color: var(--text); font-family: 'Inter', system-ui, sans-serif; overflow: hidden; }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 340px; min-width: 340px;
|
||||
background: var(--slate);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar-header {
|
||||
padding: 1.25rem 1rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-header h1 { font-size: 1rem; font-weight: 700; color: var(--cyan); }
|
||||
.sidebar-header p { font-size: .75rem; color: var(--muted); margin-top: .2rem; }
|
||||
|
||||
.badge-status {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
font-size: .7rem; font-weight: 600;
|
||||
padding: .2rem .55rem; border-radius: 999px;
|
||||
margin-top: .6rem;
|
||||
}
|
||||
.badge-status.up { background: rgba(34,197,94,.15); color: var(--green); }
|
||||
.badge-status.down { background: rgba(239,68,68,.15); color: var(--red); }
|
||||
.badge-status .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
||||
.tab { flex: 1; padding: .65rem .5rem; font-size: .75rem; font-weight: 600;
|
||||
text-align: center; cursor: pointer; color: var(--muted);
|
||||
border-bottom: 2px solid transparent; transition: .15s; }
|
||||
.tab.active { color: var(--cyan); border-color: var(--cyan); }
|
||||
|
||||
.tab-panel { display: none; padding: .75rem; flex-direction: column; gap: .5rem; }
|
||||
.tab-panel.active { display: flex; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
width: 100%; padding: .55rem .75rem; border-radius: 6px; border: none;
|
||||
background: rgba(6,182,212,.12); color: var(--cyan); font-size: .8rem;
|
||||
font-weight: 600; cursor: pointer; text-align: left; transition: .15s;
|
||||
}
|
||||
.btn:hover { background: rgba(6,182,212,.22); }
|
||||
.btn:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.btn-danger { background: rgba(239,68,68,.12); color: var(--red); }
|
||||
.btn-danger:hover { background: rgba(239,68,68,.22); }
|
||||
|
||||
/* Sim cards */
|
||||
.sim-card {
|
||||
display: flex; align-items: flex-start; gap: .65rem;
|
||||
padding: .65rem .75rem; border-radius: 8px;
|
||||
background: rgba(255,255,255,.03); border: 1px solid var(--border);
|
||||
cursor: pointer; transition: .15s;
|
||||
}
|
||||
.sim-card:hover { background: rgba(6,182,212,.06); border-color: var(--cyan); }
|
||||
.sim-badge {
|
||||
font-size: .65rem; font-weight: 700; padding: .2rem .45rem;
|
||||
border-radius: 4px; white-space: nowrap; margin-top: .1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.badge-cyan { background: rgba(6,182,212,.15); color: var(--cyan); }
|
||||
.badge-orange { background: rgba(249,115,22,.15); color: var(--orange); }
|
||||
.badge-red { background: rgba(239,68,68,.15); color: var(--red); }
|
||||
.badge-purple { background: rgba(168,85,247,.15); color: var(--purple); }
|
||||
.badge-green { background: rgba(34,197,94,.15); color: var(--green); }
|
||||
.sim-info { display: flex; flex-direction: column; gap: .15rem; }
|
||||
.sim-info strong { font-size: .82rem; color: var(--text); }
|
||||
.sim-info small { font-size: .72rem; color: var(--muted); }
|
||||
|
||||
/* Credentials table */
|
||||
.cred-table { width: 100%; border-collapse: collapse; font-size: .72rem; }
|
||||
.cred-table th { background: rgba(255,255,255,.05); color: var(--muted);
|
||||
padding: .4rem .5rem; text-align: left; font-weight: 600; border-bottom: 1px solid var(--border); }
|
||||
.cred-table td { padding: .4rem .5rem; border-bottom: 1px solid rgba(255,255,255,.04); }
|
||||
.cred-table code { background: rgba(255,255,255,.06); padding: .1rem .3rem; border-radius: 3px;
|
||||
font-size: .7rem; font-family: monospace; }
|
||||
|
||||
/* Running indicator */
|
||||
.running-bar {
|
||||
height: 3px; background: linear-gradient(90deg, var(--cyan), var(--purple));
|
||||
background-size: 200% 100%;
|
||||
animation: slide 1.2s linear infinite;
|
||||
display: none;
|
||||
}
|
||||
.running-bar.active { display: block; }
|
||||
@keyframes slide { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||
|
||||
.elapsed { font-size: .7rem; color: var(--muted); padding: .4rem .75rem; min-height: 1.6rem; }
|
||||
|
||||
/* Terminal */
|
||||
.terminal-wrap {
|
||||
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.term-header {
|
||||
padding: .6rem 1rem; background: var(--slate);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
}
|
||||
.term-dots span {
|
||||
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
|
||||
}
|
||||
.term-dots .d1 { background: #EF4444; }
|
||||
.term-dots .d2 { background: #EAB308; }
|
||||
.term-dots .d3 { background: #22C55E; }
|
||||
.term-title { font-size: .78rem; color: var(--muted); flex: 1; }
|
||||
.btn-clear { background: none; border: 1px solid var(--border); color: var(--muted);
|
||||
font-size: .72rem; padding: .2rem .6rem; border-radius: 4px; cursor: pointer; }
|
||||
.btn-clear:hover { color: var(--text); border-color: var(--text); }
|
||||
.btn-stop { background: rgba(239,68,68,.15); border: 1px solid rgba(239,68,68,.3);
|
||||
color: var(--red); font-size: .72rem; padding: .2rem .6rem; border-radius: 4px;
|
||||
cursor: pointer; display: none; }
|
||||
.btn-stop:hover { background: rgba(239,68,68,.3); }
|
||||
.btn-stop.visible { display: inline-block; }
|
||||
|
||||
#terminal {
|
||||
flex: 1; overflow-y: auto; overflow-x: hidden;
|
||||
padding: .75rem 1rem; background: var(--term-bg);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: .78rem; line-height: 1.55;
|
||||
white-space: pre-wrap; word-break: break-all;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
#terminal .ln { display: block; }
|
||||
#terminal .ln-out { color: #CBD5E1; }
|
||||
#terminal .ln-err { color: #FCA5A5; }
|
||||
#terminal .ln-info { color: var(--cyan); }
|
||||
#terminal .ln-ok { color: var(--green); }
|
||||
#terminal .ln-warn { color: var(--yellow); }
|
||||
#terminal .ln-sep { color: var(--border); user-select: none; }
|
||||
#terminal .ln-ts { color: var(--muted); font-size: .7rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>NIS2 Agile — Test Runner</h1>
|
||||
<p>Ambiente: <strong>Production</strong> · {$api}</p>
|
||||
<div id="api-status" class="badge-status down"><span class="dot"></span><span id="api-label">Controllo API...</span></div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="showTab('test')">Test</div>
|
||||
<div class="tab" onclick="showTab('sim')">Simulazioni</div>
|
||||
<div class="tab" onclick="showTab('cred')">Credenziali</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Test -->
|
||||
<div id="tab-test" class="tab-panel active">
|
||||
{$testBtns}
|
||||
</div>
|
||||
|
||||
<!-- Tab Simulazioni -->
|
||||
<div id="tab-sim" class="tab-panel">
|
||||
{$simCards}
|
||||
</div>
|
||||
|
||||
<!-- Tab Credenziali -->
|
||||
<div id="tab-cred" class="tab-panel">
|
||||
<p style="font-size:.72rem;color:var(--muted);margin-bottom:.5rem">Utenti creati dalla simulazione demo:</p>
|
||||
<table class="cred-table">
|
||||
<thead><tr><th>Ruolo</th><th>Email</th><th>Password</th><th>Azienda</th></tr></thead>
|
||||
<tbody>{$credsRows}</tbody>
|
||||
</table>
|
||||
<p style="font-size:.68rem;color:var(--muted);margin-top:.75rem">
|
||||
URL: <code style="font-size:.68rem">{$url}</code><br>
|
||||
App: <a href="https://nis2.agile.software/" target="_blank" style="color:var(--cyan)">nis2.agile.software</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:auto">
|
||||
<div class="running-bar" id="running-bar"></div>
|
||||
<div class="elapsed" id="elapsed"></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Terminal -->
|
||||
<div class="terminal-wrap">
|
||||
<div class="term-header">
|
||||
<div class="term-dots"><span class="d1"></span><span class="d2"></span><span class="d3"></span></div>
|
||||
<span class="term-title" id="term-title">Pronto · seleziona un test o una simulazione</span>
|
||||
<button class="btn-stop" id="btn-stop" onclick="stopCmd()">Interrompi</button>
|
||||
<button class="btn-clear" onclick="clearTerm()">Pulisci</button>
|
||||
</div>
|
||||
<div id="terminal">
|
||||
<span class="ln ln-info">NIS2 Agile Test Runner — pronto.</span>
|
||||
<span class="ln ln-sep">────────────────────────────────────────────────────────</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const COMMANDS = {$cmdsJson};
|
||||
let es = null;
|
||||
let timerInterval = null;
|
||||
let t0 = null;
|
||||
|
||||
// ── API status check ─────────────────────────────────────────────────────────
|
||||
async function checkApiStatus() {
|
||||
try {
|
||||
const r = await fetch('status', { signal: AbortSignal.timeout(6000) });
|
||||
const d = await r.json();
|
||||
const el = document.getElementById('api-status');
|
||||
const lbl = document.getElementById('api-label');
|
||||
if (d.api_up) {
|
||||
el.className = 'badge-status up';
|
||||
lbl.textContent = 'API Online · v' + (d.api_info?.version || '1.x');
|
||||
} else {
|
||||
el.className = 'badge-status down';
|
||||
lbl.textContent = 'API Offline';
|
||||
}
|
||||
} catch {
|
||||
document.getElementById('api-label').textContent = 'API irraggiungibile';
|
||||
}
|
||||
}
|
||||
checkApiStatus();
|
||||
setInterval(checkApiStatus, 30000);
|
||||
|
||||
// ── Tab switch ───────────────────────────────────────────────────────────────
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab').forEach((t, i) => {
|
||||
const ids = ['test', 'sim', 'cred'];
|
||||
t.classList.toggle('active', ids[i] === id);
|
||||
});
|
||||
document.querySelectorAll('.tab-panel').forEach(p => {
|
||||
p.classList.toggle('active', p.id === 'tab-' + id);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Terminal ─────────────────────────────────────────────────────────────────
|
||||
function appendLine(cls, text) {
|
||||
const term = document.getElementById('terminal');
|
||||
const span = document.createElement('span');
|
||||
span.className = 'ln ln-' + cls;
|
||||
span.textContent = text;
|
||||
term.appendChild(span);
|
||||
term.scrollTop = term.scrollHeight;
|
||||
}
|
||||
function clearTerm() {
|
||||
document.getElementById('terminal').innerHTML =
|
||||
'<span class="ln ln-info">Terminal pulita.</span>';
|
||||
}
|
||||
|
||||
// ── Run command ───────────────────────────────────────────────────────────────
|
||||
function runCmd(id) {
|
||||
if (es) { es.close(); es = null; }
|
||||
clearInterval(timerInterval);
|
||||
|
||||
const cmd = COMMANDS.find(c => c.id === id);
|
||||
const label = cmd ? cmd.label : id;
|
||||
|
||||
document.getElementById('term-title').textContent = '▶ ' + label;
|
||||
document.getElementById('running-bar').classList.add('active');
|
||||
document.getElementById('btn-stop').classList.add('visible');
|
||||
document.querySelectorAll('.btn').forEach(b => b.disabled = true);
|
||||
|
||||
appendLine('sep', '────────────────────────────────────────────────────────');
|
||||
appendLine('ts', new Date().toLocaleTimeString('it-IT') + ' Avvio: ' + label);
|
||||
appendLine('sep', '────────────────────────────────────────────────────────');
|
||||
|
||||
t0 = Date.now();
|
||||
timerInterval = setInterval(() => {
|
||||
const s = ((Date.now() - t0) / 1000).toFixed(1);
|
||||
document.getElementById('elapsed').textContent = 'Elapsed: ' + s + 's';
|
||||
}, 300);
|
||||
|
||||
es = new EventSource('events?cmd=' + encodeURIComponent(id));
|
||||
|
||||
es.addEventListener('stdout', e => appendLine('out', e.data));
|
||||
es.addEventListener('stderr', e => {
|
||||
const d = e.data;
|
||||
if (d.includes('[OK]') || d.includes('✓')) appendLine('ok', d);
|
||||
else if (d.includes('[WARN]')) appendLine('warn', d);
|
||||
else appendLine('err', d);
|
||||
});
|
||||
es.addEventListener('done', e => {
|
||||
try {
|
||||
const d = JSON.parse(e.data);
|
||||
appendLine('ok', '✓ Completato in ' + d.elapsed + 's');
|
||||
} catch { appendLine('ok', '✓ Completato'); }
|
||||
finish();
|
||||
});
|
||||
es.onerror = () => { appendLine('err', '[ERRORE] Connessione SSE interrotta'); finish(); };
|
||||
}
|
||||
|
||||
function stopCmd() {
|
||||
if (es) { es.close(); es = null; }
|
||||
appendLine('warn', '[STOP] Interrotto dall\'utente');
|
||||
finish();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
if (es) { es.close(); es = null; }
|
||||
clearInterval(timerInterval);
|
||||
document.getElementById('running-bar').classList.remove('active');
|
||||
document.getElementById('btn-stop').classList.remove('visible');
|
||||
document.getElementById('term-title').textContent = 'Completato · ' + new Date().toLocaleTimeString('it-IT');
|
||||
document.querySelectorAll('.btn').forEach(b => b.disabled = false);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user