Compare commits

..

16 Commits

Author SHA1 Message Date
6c36f43760 Fix nome immagine in docker compose 2026-03-26 12:56:44 +01:00
4dd19b1104 Fix compose per usare nome immagine corretta 2026-03-26 12:44:41 +01:00
3e21620d1b Merge branch 'docker' 2026-03-26 12:39:04 +01:00
ecee75af4a Cambio porta host nel docker-compose di esempio 2026-03-26 12:38:36 +01:00
1233d7b441 Aggiunto docker compose di esempio 2026-03-26 12:38:12 +01:00
128df31840 Aggiunta dockerfile 2026-03-26 12:36:52 +01:00
abcb6539fc Aggiunta comando uptime in testo help 2026-03-26 12:15:41 +01:00
06a0c2dd99 Merge funzionalità aggioramento 2.7.1 (#2) (from update/2.7 into main)
Reviewed-on: #2
2026-03-26 11:11:30 +00:00
078998668e Aggiunta homepage 2026-03-26 12:08:52 +01:00
0a4cb932c6 fix: mostra numeri a una cifra con 2 nel comando uptime 2026-03-26 12:03:44 +01:00
b9675d03b0 fix: ora mostrata incorrettamente nel comando uptime 2026-03-26 09:43:28 +01:00
e0fe2a53e9 Implementazione base uptime 2026-03-26 09:39:48 +01:00
98e15d2e6c Implementato comando echo senza redirect su file 2026-03-26 09:26:18 +01:00
53460bd70e Implementato contatore rebuild completati 2026-03-26 09:19:44 +01:00
7f083f187d Aggiunta pill contatore rebuild 2026-03-26 09:05:24 +01:00
20f4531ad3 Merge sorgente "di Campi" versione 2.7 2026-03-26 08:53:43 +01:00
4 changed files with 666 additions and 13 deletions

8
Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM nginx
# Copia pagine HTML
COPY index.html /usr/share/nginx/html/index.html
COPY simulatore.html /usr/share/nginx/html/simulatore.html
COPY guida.html /usr/share/nginx/html/guida.html
EXPOSE 80

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
simulator:
image: git.fiorencis.eu/eduprojects/dicampi-raid-simulator:latest
ports:
- 8080:80
restart: unless-stopped
network_mode: bridge

48
index.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulatore RAID Web</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
}
h1 {
color: #333;
margin-bottom: 40px;
}
.buttons {
display: flex;
gap: 20px;
}
a {
text-decoration: none;
background-color: #007bff;
color: white;
padding: 10px 20px;
border-radius: 5px;
transition: background-color 0.3s;
}
a:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1 style="margin-bottom: 0;">Simulatore RAID Web</h1>
<p style="margin-bottom: 0;">by Marco di Campi, forked and tweaked by Andrea Fiorencis<p>
<div class="buttons">
<a href="simulatore.html" target="_blank">Simulatore</a>
<a href="guida.html" target="_blank">Guida</a>
</div>
</body>
</html>

View File

@@ -106,10 +106,11 @@
label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)}
select,input[type="number"],input[type="text"]{
height:40px;border-radius:12px;border:1px solid rgba(255,255,255,.14);
background:rgba(10,14,28,.55);color:rgba(255,255,255,.92);
background:#0d1220;color:rgba(255,255,255,.92);
padding:0 12px;outline:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.04);
font-family:var(--sans)
}
select option{background:#0d1220;color:rgba(255,255,255,.92);}
select:focus,input:focus{border-color:rgba(167,139,250,.55);box-shadow:0 0 0 4px rgba(167,139,250,.15)}
.controls{display:grid;grid-template-columns:1.1fr .7fr .7fr;gap:10px}
@media (max-width: 720px){.controls{grid-template-columns:1fr}}
@@ -895,6 +896,106 @@
.server-example .se-icon{ font-size: 18px; flex-shrink: 0; }
.server-example .se-text{ font-size: 12.5px; color: rgba(255,255,255,.75); line-height: 1.55; }
.server-example .se-text b{ color: rgba(255,255,255,.92); }
/* ── PANNELLO DOCENTE ── */
.teacher-form{
display:flex;flex-direction:column;gap:12px;
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
border-radius:14px;padding:16px 18px;
}
.tf-row{ display:flex;gap:14px;flex-wrap:wrap; }
.tf-field{ display:flex;flex-direction:column;gap:5px;flex:1;min-width:180px; }
.tf-label{ font-size:11.5px;color:var(--muted2);font-weight:700;letter-spacing:.3px;text-transform:uppercase; }
.tf-input,.tf-select{
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);
border-radius:10px;padding:9px 12px;color:var(--text);font-family:var(--sans);font-size:13px;
outline:none;transition:border-color .2s;
}
.tf-select{ appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='rgba(255,255,255,.5)' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px; }
.tf-input:focus,.tf-select:focus{ border-color:rgba(167,139,250,.55);box-shadow:0 0 0 3px rgba(167,139,250,.12); }
.tf-hint{ font-size:11.5px;color:var(--muted2);line-height:1.45; }
.teacher-code-box{
background:linear-gradient(135deg,rgba(167,139,250,.12),rgba(34,211,238,.08));
border:1px solid rgba(167,139,250,.35);border-radius:14px;padding:16px 18px;
}
.tcb-header{ display:flex;justify-content:space-between;align-items:center;margin-bottom:12px; }
.tcb-label{ font-size:13px;font-weight:700;color:var(--accent); }
.tcb-code{
font-family:var(--mono);font-size:15px;letter-spacing:2px;color:var(--text);
background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.12);
border-radius:10px;padding:12px 16px;word-break:break-all;line-height:1.6;
user-select:all;cursor:text;
}
.tcb-meta{ margin-top:8px;font-size:11.5px;color:var(--muted2);line-height:1.5; }
/* section titles inside teacher form */
.tf-section-title{
font-size:12px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;
color:var(--accent);padding:6px 0 4px;border-bottom:1px solid rgba(167,139,250,.2);
margin-bottom:10px;
}
/* array preview chips */
.tf-array-preview{
display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;padding:10px 12px;
background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.08);border-radius:10px;
min-height:38px;align-items:center;
}
.tf-disk-chip{
display:inline-flex;align-items:center;gap:5px;
padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;font-family:var(--mono);
background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.3);color:var(--ok);
transition:background .2s,border-color .2s,color .2s;
}
.tf-disk-chip.fault-FAILED{background:rgba(251,113,133,.15);border-color:rgba(251,113,133,.4);color:var(--bad);}
.tf-disk-chip.fault-CRC{background:rgba(251,191,36,.12);border-color:rgba(251,191,36,.35);color:var(--warn);}
.tf-disk-chip.fault-OVERHEAT{background:rgba(251,146,60,.12);border-color:rgba(251,146,60,.35);color:#fb923c;}
.tf-disk-chip.fault-SLOW{background:rgba(96,165,250,.12);border-color:rgba(96,165,250,.3);color:var(--info);}
/* fault configuration grid */
.tf-fault-grid{display:flex;flex-direction:column;gap:6px;}
.tf-fault-row{
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
border-radius:10px;padding:8px 12px;
}
.tf-fault-dev{
font-family:var(--mono);font-size:13px;font-weight:700;color:var(--text);
min-width:52px;flex-shrink:0;
}
.tf-fault-select{
background:#111827;border:1px solid rgba(255,255,255,.18);
border-radius:8px;padding:6px 28px 6px 10px;color:rgba(255,255,255,.92);font-size:12.5px;
font-family:var(--sans);outline:none;cursor:pointer;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='rgba(255,255,255,.55)' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 9px center;appearance:none;
transition:border-color .2s;
}
.tf-fault-select option{background:#111827;color:rgba(255,255,255,.92);}
.tf-fault-select:focus{border-color:rgba(167,139,250,.6);}
.tf-fault-select.sel-FAILED{border-color:rgba(251,113,133,.6);color:var(--bad);}
.tf-fault-select.sel-FAILED option{color:rgba(255,255,255,.92);}
.tf-fault-select.sel-CRC{border-color:rgba(251,191,36,.6);color:var(--warn);}
.tf-fault-select.sel-CRC option{color:rgba(255,255,255,.92);}
.tf-fault-select.sel-OVERHEAT{border-color:rgba(251,146,60,.6);color:#fb923c;}
.tf-fault-select.sel-OVERHEAT option{color:rgba(255,255,255,.92);}
.tf-fault-select.sel-SLOW{border-color:rgba(96,165,250,.55);color:var(--info);}
.tf-fault-select.sel-SLOW option{color:rgba(255,255,255,.92);}
.tf-fault-desc{font-size:11.5px;color:var(--muted2);flex:1;line-height:1.35;}
/* Rebuild didactic banner */
.rebuild-banner{
margin-top:10px;padding:12px 14px;border-radius:16px;
border:1px solid rgba(251,191,36,.40);
background:linear-gradient(135deg, rgba(251,191,36,.10), rgba(251,146,60,.07));
display:flex;align-items:flex-start;gap:12px;
animation: rb-pulse 2s ease-in-out infinite;
}
@keyframes rb-pulse{0%,100%{border-color:rgba(251,191,36,.35)}50%{border-color:rgba(251,191,36,.70)}}
.rb-icon{font-size:20px;flex-shrink:0;line-height:1}
.rb-body{font-size:12.5px;color:rgba(255,255,255,.88);line-height:1.6}
.rb-body b{color:#FBBF24}
</style>
</head>
<body>
@@ -972,6 +1073,17 @@
<div class="mini" id="miniStatus"></div>
</div>
<!-- Banner didattico rebuild -->
<div id="rebuildBanner" style="display:none" class="rebuild-banner">
<div class="rb-icon"></div>
<div class="rb-body">
<b>MODALITÀ DIDATTICA — Rebuild accelerato</b><br>
<span id="rbSpeedText">Velocità aumentata di <b id="rbFactor"></b>×</span> &nbsp;·&nbsp;
<span>ETA simulato: <b id="rbEtaSim"></b></span> &nbsp;·&nbsp;
<span>Tempo reale stimato: <b id="rbEtaReal"></b></span>
</div>
</div>
<div class="row2">
<div class="miniCard">
<h3>📈 Telemetria (sim)</h3>
@@ -1014,6 +1126,24 @@
</div>
</div>
<!-- CARICA CODICE PROVA (studente o docente in test) -->
<div class="scenarioBox" style="border-color:rgba(167,139,250,.35);background:rgba(167,139,250,.07)">
<div class="scenarioTitle" style="color:var(--accent)">🔑 Carica codice prova</div>
<div class="p" style="margin-bottom:10px;font-size:12.5px">
Hai ricevuto un codice dal docente? Incollalo qui e clicca <b>Avvia prova</b> per caricare lo scenario pianificato.
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input id="labCodeInput" type="text" placeholder="Incolla il codice prova..." style="
flex:1;min-width:200px;
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.18);
border-radius:10px;padding:9px 12px;color:var(--text);font-family:var(--mono);
font-size:13px;outline:none;letter-spacing:.5px;
" />
<button class="primary" id="btnLoadCodeInLab" style="padding:9px 18px;white-space:nowrap">▶ Avvia prova</button>
</div>
<div id="labCodeFeedback" style="margin-top:8px;font-size:12px;color:var(--muted2);min-height:18px"></div>
</div>
<div class="note">
<b>Regola didattica:</b> lo studente deve lavorare col <b>terminale</b>. Il click sui dischi è disabilitato, salvo <b>Teacher ON</b>.
<br><br>
@@ -1026,8 +1156,11 @@
<div class="card">
<div class="head">
<h2>💻 Terminale Linux (simulato)</h2>
<div class="pills">
<div class="pill"><strong>Rebuild effettuati</strong> <span id="pillRebuildCount">727</span></div>
<div class="pill"><strong>Mode</strong> <span id="pillMode">LAB</span></div>
</div>
</div>
<div class="content">
<div class="term">
<div class="termTop">
@@ -1063,17 +1196,148 @@
<!-- PAGE: EXERCISES -->
<div class="page" id="page-ex">
<!-- ── PANNELLO DOCENTE ─────────────────────────────────────────── -->
<div class="card" style="margin-bottom:16px">
<div class="head">
<h2>👨‍🏫 Pannello Docente — Programmazione verifiche</h2>
<div class="pill" style="background:linear-gradient(135deg,rgba(167,139,250,.25),rgba(34,211,238,.18));border-color:rgba(167,139,250,.35)">
<span style="color:var(--accent)"></span> <strong>Configurazione personalizzata</strong>
</div>
</div>
<div class="content">
<div class="note" style="margin-bottom:16px;border-color:rgba(167,139,250,.3);background:rgba(167,139,250,.07)">
<b>Come funziona:</b> configura l'array RAID esattamente come vuoi → assegna i guasti ai dischi → imposta classe/data/timer →
clicca <b>Genera codice prova</b>. Lo studente incolla il codice nel LAB e la prova parte identica per tutti.
</div>
<div class="teacher-form" id="teacherForm">
<!-- ── SEZIONE 1: CONFIGURAZIONE ARRAY ── -->
<div class="tf-section-title">① Configurazione array RAID</div>
<div class="tf-row">
<div class="tf-field">
<label class="tf-label">Livello RAID</label>
<select id="tfRaidLevel" class="tf-select" onchange="tfUpdateDiskFaultRows()">
<option value="0">RAID 0 — striping (no fault tolerance)</option>
<option value="1">RAID 1 — mirroring (1 disco di tolleranza)</option>
<option value="5" selected>RAID 5 — parità distribuita (1 disco di tolleranza)</option>
<option value="6">RAID 6 — doppia parità (2 dischi di tolleranza)</option>
<option value="10">RAID 10 — stripe + mirror (50% ridondanza)</option>
</select>
</div>
<div class="tf-field">
<label class="tf-label">Numero dischi</label>
<input id="tfDiskCount" class="tf-input" type="number" min="2" max="12" value="4"
oninput="tfUpdateDiskFaultRows()" />
</div>
<div class="tf-field">
<label class="tf-label">Size disco (GB)</label>
<input id="tfDiskSize" class="tf-input" type="number" min="10" max="200000" step="10" value="1000" />
</div>
</div>
<!-- anteprima array -->
<div id="tfArrayPreview" class="tf-array-preview"></div>
<!-- ── SEZIONE 2: GUASTI ── -->
<div class="tf-section-title" style="margin-top:14px">② Guasti da assegnare ai dischi</div>
<div id="tfDiskFaultRows" class="tf-fault-grid"></div>
<div class="tf-hint" style="margin-top:6px">
Lascia "Nessun guasto" per i dischi che devono restare OK. Puoi assegnare guasti multipli su dischi diversi.
</div>
<!-- ── SEZIONE 3: DATI VERIFICA ── -->
<div class="tf-section-title" style="margin-top:14px">③ Dati verifica</div>
<div class="tf-row">
<div class="tf-field">
<label class="tf-label">Classe</label>
<input id="tfClasse" class="tf-input" type="text" placeholder="es. 4A-INF" maxlength="20" />
</div>
<div class="tf-field">
<label class="tf-label">Data verifica</label>
<input id="tfData" class="tf-input" type="date" />
</div>
<div class="tf-field">
<label class="tf-label">Durata (minuti)</label>
<select id="tfDurata" class="tf-select">
<option value="0">Nessun timer</option>
<option value="5">5 minuti</option>
<option value="10" selected>10 minuti</option>
<option value="15">15 minuti</option>
<option value="20">20 minuti</option>
<option value="30">30 minuti</option>
</select>
</div>
</div>
<div class="tf-row">
<div class="tf-field" style="flex:1">
<label class="tf-label">Note per lo studente (opzionale)</label>
<input id="tfNote" class="tf-input" type="text" placeholder="es. Usa solo i comandi visti in laboratorio" maxlength="120" />
</div>
</div>
<!-- ── GENERA ── -->
<div class="tf-row" style="align-items:center;gap:14px;flex-wrap:wrap;margin-top:6px">
<button class="primary" id="btnGenTeacherCode" style="padding:11px 24px;font-size:14px">🔐 Genera codice prova</button>
<div id="tfValidationMsg" class="tf-hint" style="color:var(--bad)"></div>
</div>
</div>
<!-- OUTPUT CODICE DOCENTE -->
<div id="teacherCodeOutput" style="display:none;margin-top:20px">
<div class="teacher-code-box">
<div class="tcb-header">
<span class="tcb-label">📋 Codice prova generato</span>
<button class="mutebtn" id="btnCopyTeacherCode" style="font-size:12px;padding:5px 12px">⎘ Copia</button>
</div>
<div class="tcb-code" id="teacherCodeText"></div>
<div class="tcb-meta" id="teacherCodeMeta"></div>
</div>
<div class="row2" style="margin-top:12px">
<div class="miniCard">
<h3>Riepilogo prova</h3>
<div class="kv"><span>Array</span><b id="tcArray"></b></div>
<div class="kv"><span>Guasti</span><b id="tcFaults"></b></div>
<div class="kv"><span>Classe</span><b id="tcClasse"></b></div>
<div class="kv"><span>Data</span><b id="tcData"></b></div>
<div class="kv"><span>Durata</span><b id="tcDurata"></b></div>
<div class="kv"><span>Obiettivo atteso</span><b id="tcGoal"></b></div>
</div>
<div class="miniCard">
<div class="scenarioBox" style="margin:0">
<div class="scenarioTitle">📌 Istruzioni per lo studente</div>
<div class="scenarioGoal" style="font-size:12.5px">
1) Aprire il simulatore RAID<br>
2) Tab <b>LAB</b> → sezione "Carica codice prova"<br>
3) Incollare il codice e cliccare <b>Avvia prova</b><br>
4) Svolgere la prova entro il tempo indicato<br>
5) Consegnare: codice prova + <span class="kbd">export</span> del report
</div>
</div>
<div class="btnrow" style="margin-top:10px">
<button class="mutebtn" id="btnLoadTeacherInLab">▶ Carica nel LAB (test docente)</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── PROVA RAPIDA (ex sezione originale) ──────────────────────── -->
<div class="card">
<div class="head">
<h2>🎓 Esercizi per studenti</h2>
<h2>🎓 Prova rapida (casuale)</h2>
<div class="pill"><strong>Codice</strong> <span id="exerciseCode"></span></div>
</div>
<div class="content">
<div class="row2">
<div class="miniCard">
<h3>Genera prova (unica)</h3>
<h3>Genera prova casuale</h3>
<div class="p">
Clicca "Genera prova": otterrai un <b>codice prova</b> e un <b>scenario</b>.
Clicca "Genera prova": otterrai un <b>codice prova</b> e uno <b>scenario random</b>.
Lo studente deve consegnare il codice + il report (comando <span class="kbd">export</span> o tasto Export report).
</div>
<div class="btnrow" style="margin-top:10px">
@@ -3002,17 +3266,19 @@ $ rm /mnt/verifica.txt
let state = {
arrayName: "/dev/md0",
startupTime: Date.now(),
raidLevel: 5,
diskSizeGB: 1000,
disks: [],
spares: [],
rebuild: { active:false, start:0, etaSec:0, progress:0 },
rebuild: { active:false, start:0, etaSec:0, progress:0, realSec:0, speedFactor:1 },
fs: { created:false, mountedAt:null, uuid:null, label:null, files: new Map() },
dmesg: [],
score: 0,
hints: 0,
teacher: false,
mode: "LAB",
rebuildCount: 0,
exerciseOn: false,
timer: { on:false, endTs:0, interval:null },
actions: [],
@@ -3027,6 +3293,10 @@ $ rm /mnt/verifica.txt
const pillModeEl = $("pillMode");
const lampEl = $("lamp");
const bigStatusEl = $("bigStatus");
const rebuildBannerEl = $("rebuildBanner");
const rbFactorEl = $("rbFactor");
const rbEtaSimEl = $("rbEtaSim");
const rbEtaRealEl = $("rbEtaReal");
const miniStatusEl = $("miniStatus");
const capValueEl = $("capValue");
const capNoteEl = $("capNote");
@@ -3054,6 +3324,7 @@ $ rm /mnt/verifica.txt
const termBody = $("termBody");
const termInput = $("termInput");
const btnRun = $("btnRun");
const pillRebuildCountEl = $("pillRebuildCount");
let history = [];
let histIdx = -1;
@@ -3114,7 +3385,24 @@ $ rm /mnt/verifica.txt
function resetFS(){ state.fs.created=false;state.fs.mountedAt=null;state.fs.uuid=null;state.fs.label=null;state.fs.files=new Map(); }
let rebuildTimer=null;
function rebuildEstimateSec(){ const sizeGB=state.diskSizeGB,n=state.disks.length; let speed=1.6; if(state.raidLevel===5)speed=1.1; if(state.raidLevel===6)speed=0.85; if(state.raidLevel===10)speed=1.25; if(state.raidLevel===1)speed=1.35; if(state.raidLevel===0)speed=2.0; if(state.disks.some(d=>d.state===DiskState.SLOW))speed*=0.65; if(state.disks.some(d=>d.state===DiskState.OVERHEAT))speed*=0.75; speed*=(1+Math.log2(Math.max(2,n))*0.12); return Math.max(18,Math.round(sizeGB/speed)); }
function rebuildEstimateSec(){
const sizeGB=state.diskSizeGB,n=state.disks.length;
let speed=1.6;
if(state.raidLevel===5)speed=1.1;
if(state.raidLevel===6)speed=0.85;
if(state.raidLevel===10)speed=1.25;
if(state.raidLevel===1)speed=1.35;
if(state.raidLevel===0)speed=2.0;
if(state.disks.some(d=>d.state===DiskState.SLOW))speed*=0.65;
if(state.disks.some(d=>d.state===DiskState.OVERHEAT))speed*=0.75;
speed*=(1+Math.log2(Math.max(2,n))*0.12);
const realSec=Math.max(60,Math.round(sizeGB/speed));
// Modalità didattica: cappato a 1015s, calcola fattore di accelerazione
const didacticSec=Math.round(10+Math.random()*5); // 1015s
state.rebuild.realSec=realSec;
state.rebuild.speedFactor=Math.round(realSec/didacticSec);
return didacticSec;
}
function stopRebuild(silent=false){ state.rebuild.active=false;state.rebuild.progress=0; state.disks.forEach(d=>{ if(d.state===DiskState.REBUILDING){d.state=DiskState.FAILED;d.progress=0;} }); if(rebuildTimer){clearInterval(rebuildTimer);rebuildTimer=null;} if(!silent)pushDmesg("warn","md0: rebuild interrupted"); render(); }
function startRebuild(){
if(state.rebuild.active)return termPrint("Rebuild già in corso.","warn");
@@ -3126,8 +3414,13 @@ $ rm /mnt/verifica.txt
if(spare.sizeGB<state.diskSizeGB){ pushDmesg("err",`${spare.dev}: spare too small (${spare.sizeGB}GB < ${state.diskSizeGB}GB)`); return termPrint(`mdadm: spare troppo piccolo (${spare.sizeGB}GB).`,"err"); }
state.disks[failedIdx].state=DiskState.REBUILDING; state.disks[failedIdx].progress=0; state.spares.shift();
state.rebuild.active=true; state.rebuild.start=Date.now(); state.rebuild.etaSec=rebuildEstimateSec(); state.rebuild.progress=0;
pushDmesg("info",`md0: rebuild started (ETA ~ ${state.rebuild.etaSec}s)`);
termPrint(`mdadm: rebuild avviato. ETA ~ ${state.rebuild.etaSec}s (didattica).`,"ok");
const realMin=Math.round(state.rebuild.realSec/60);
const realHr=(state.rebuild.realSec>=3600)?(` = ${(state.rebuild.realSec/3600).toFixed(1)}h`):"";
pushDmesg("info",`md0: rebuild started (ETA ~ ${state.rebuild.etaSec}s — MODALITÀ DIDATTICA x${state.rebuild.speedFactor})`);
termPrint(`mdadm: rebuild avviato.`,"ok");
termPrint(` ⚡ Velocità aumentata di ${state.rebuild.speedFactor}× per uso didattico`,"warn");
termPrint(` ⏱ ETA simulato : ~${state.rebuild.etaSec}s`,"warn");
termPrint(` 🕐 Tempo reale : ~${realMin} min${realHr} (disco ${state.diskSizeGB} GB, RAID${state.raidLevel})`,"warn");
if(state.scenario.name&&!state.scenario.checkpoints.rebuild){ state.scenario.checkpoints.rebuild=true; addScore(6,"(scenario) rebuild avviato."); }
rebuildTimer=setInterval(()=>{
if(!state.rebuild.active)return;
@@ -3139,7 +3432,12 @@ $ rm /mnt/verifica.txt
state.rebuild.active=false;state.rebuild.progress=0;
state.disks.forEach(d=>{ if(d.state===DiskState.REBUILDING){d.state=DiskState.OK;d.progress=0;d.smart.realloc=0;d.smart.pending=0;d.smart.crc=0;d.smart.temp=33;} });
pushDmesg("info","md0: rebuild completed");
termPrint("mdadm: rebuild completato.","ok");
termPrint("mdadm: rebuild completato.","ok");
state.rebuildCount+=1;
const rf=state.rebuild.speedFactor||1, rs=state.rebuild.realSec||0;
const rm=Math.round(rs/60), rh=(rs>=3600)?` = ${(rs/3600).toFixed(1)}h`:"";
termPrint(` Ricorda: nella realtà questa operazione avrebbe richiesto ~${rm} min${rh}`,"warn");
termPrint(` (accelerato di ${rf}× per uso didattico)`,"warn");
if(state.scenario.name&&!state.scenario.checkpoints.ok&&volumeStatus()===VolState.OK){ state.scenario.checkpoints.ok=true; addScore(8,"(scenario) array OK."); }
clearInterval(rebuildTimer);rebuildTimer=null;
}
@@ -3179,7 +3477,7 @@ $ rm /mnt/verifica.txt
const n=normalizeDiskCount(diskCountEl.value); diskCountEl.value=String(n);
state.disks=Array.from({length:n},(_,i)=>makeDisk(i,state.diskSizeGB)); state.spares=[];
resetFS(); if(rebuildTimer){clearInterval(rebuildTimer);rebuildTimer=null;}
state.rebuild={active:false,start:0,etaSec:0,progress:0}; state.mode="LAB"; state.exerciseOn=false;
state.rebuild={active:false,start:0,etaSec:0,progress:0,realSec:0,speedFactor:1}; state.mode="LAB"; state.exerciseOn=false;
state.scenario={name:null,goalHtml:null,solution:[],checkpoints:{},done:false,startedAt:null,seed:null};
state.score=0;state.hints=0;state.actions=[];
scoreEl.textContent="0";hintsEl.textContent="0";
@@ -3298,7 +3596,7 @@ $ rm /mnt/verifica.txt
function mdadmRemove(dev){ if(state.rebuild.active)return{ok:false,msg:"mdadm: rebuild in corso, remove bloccato."}; const d=state.disks.find(x=>x.dev===dev); if(!d)return{ok:false,msg:`mdadm: ${dev}: non è membro dell'array.`}; if(!(d.state===DiskState.FAILED||d.state===DiskState.REMOVED))return{ok:false,msg:`mdadm: ${dev}: rimozione solo se FAILED/REMOVED (didattica).`}; d.state=DiskState.REMOVED;pushDmesg("warn",`${dev}: removed from array`); if(state.scenario.name&&!state.scenario.checkpoints.removed){if(["raid5_1fail","raid1_onefail","rebuild_interrupted","wrong_size_spare","raid6_2fail"].includes(state.scenario.name)){state.scenario.checkpoints.removed=true;addScore(5,"(scenario) remove fatto.");}} return{ok:true,msg:`mdadm: ${dev} rimosso (REMOVED).`}; }
function mdadmAdd(dev,sizeOverrideGB=null){ if(state.disks.some(x=>x.dev===dev)||state.spares.some(x=>x.dev===dev))return{ok:false,msg:`mdadm: ${dev}: già presente.`}; let size=sizeOverrideGB??state.diskSizeGB; size=clamp(parseInt(size,10),10,200000); const idx=state.spares.length; const spare=makeDisk(state.disks.length+idx,size); spare.dev=dev;spare.name=`spare${idx+1}`;spare.state=DiskState.SPARE; state.spares.push(spare);pushDmesg("info",`${dev}: added as spare (${size}GB)`); if(state.scenario.name){if(["raid5_1fail","raid1_onefail","rebuild_interrupted","raid6_2fail"].includes(state.scenario.name)&&!state.scenario.checkpoints.added){state.scenario.checkpoints.added=true;addScore(5,"(scenario) spare aggiunto.");}if(state.scenario.name==="wrong_size_spare"){if(size<state.diskSizeGB&&!state.scenario.checkpoints.triedSmall){state.scenario.checkpoints.triedSmall=true;addScore(5,"(scenario) spare piccolo provato.");}else if(size>=state.diskSizeGB&&!state.scenario.checkpoints.addedOk){state.scenario.checkpoints.addedOk=true;addScore(5,"(scenario) spare corretto aggiunto.");}}} return{ok:true,msg:`mdadm: aggiunto ${dev} come SPARE (${size}GB).`}; }
function helpText(){ return["Comandi (simulazione):"," help | clear | report | export"," scenario list | scenario load <n>"," hint | solution (teacher)"," powerfail (simula blackout: interrompe rebuild)","","RAID (entrambe le sintassi accettate):"," cat /proc/mdstat"," mdadm --detail /dev/md0"," mdadm --fail /dev/md0 /dev/sdX (oppure: mdadm /dev/md0 --fail /dev/sdX)"," mdadm --remove /dev/md0 /dev/sdX (oppure: mdadm /dev/md0 --remove /dev/sdX)"," mdadm --add /dev/md0 /dev/sdX [--size <GB>]"," mdadm --rebuild /dev/md0"," mdadm --stop-rebuild /dev/md0","","Dischi & OS:"," unplug /dev/sdX"," lsblk | fdisk -l | blkid"," dmesg | tail"," smartctl -a /dev/sdX"," df -h","","Filesystem:"," mkfs.ext4 /dev/md0"," mount /dev/md0 /mnt | umount /mnt"," ls /mnt | touch /mnt/file | echo \"testo\" > /mnt/file | cat /mnt/file | rm /mnt/file","","Conclusioni (scenari FAILED o non-RAID):"," conclude <testo>"].join("\n"); }
function helpText(){ return["Comandi (simulazione):"," help | clear | report | export"," scenario list | scenario load <n>"," hint | solution (teacher)"," powerfail (simula blackout: interrompe rebuild)","","Sistema:"," uptime (mostra il tempo di esecuzione del simulatore)","","RAID (entrambe le sintassi accettate):"," cat /proc/mdstat"," mdadm --detail /dev/md0"," mdadm --fail /dev/md0 /dev/sdX (oppure: mdadm /dev/md0 --fail /dev/sdX)"," mdadm --remove /dev/md0 /dev/sdX (oppure: mdadm /dev/md0 --remove /dev/sdX)"," mdadm --add /dev/md0 /dev/sdX [--size <GB>]"," mdadm --rebuild /dev/md0"," mdadm --stop-rebuild /dev/md0","","Dischi & OS:"," unplug /dev/sdX"," lsblk | fdisk -l | blkid"," dmesg | tail"," smartctl -a /dev/sdX"," df -h","","Filesystem:"," mkfs.ext4 /dev/md0"," mount /dev/md0 /mnt | umount /mnt"," ls /mnt | touch /mnt/file | echo \"testo\" > /mnt/file | cat /mnt/file | rm /mnt/file","","Conclusioni (scenari FAILED o non-RAID):"," conclude <testo>"].join("\n"); }
function makeReport(){
const vol=volumeStatus(),cap=capacityGB(state.raidLevel,state.disks.length,state.diskSizeGB),lines=[];
lines.push("RAID LAB REPORT");lines.push("===============");lines.push(`Timestamp: ${new Date().toISOString()}`);lines.push(`ExerciseCode: ${state.exgen.code||"(none)"}`);lines.push(`Scenario: ${state.scenario.name||"(none)"}`);lines.push(`Mode: ${state.mode} Teacher: ${state.teacher?"ON":"OFF"}`);lines.push(`RAID: ${state.raidLevel} Members: ${state.disks.length} DiskSizeGB: ${state.diskSizeGB}`);lines.push(`CapacityGB: ${cap} Status: ${vol}`);lines.push(`Score: ${state.score} Hints: ${state.hints}`);lines.push("");lines.push("Disks:");
@@ -3325,6 +3623,7 @@ $ rm /mnt/verifica.txt
}
function render(){
pillRebuildCountEl.textContent=state.rebuildCount;
pillArrayEl.textContent=state.arrayName;
const n=state.disks.length,L=state.raidLevel,sizeGB=state.diskSizeGB;
capValueEl.textContent=fmtGB(capacityGB(L,n,sizeGB));
@@ -3336,6 +3635,19 @@ $ rm /mnt/verifica.txt
lampEl.className=`lamp ${lampClass(vol)}`;
bigStatusEl.textContent=`STATUS: ${vol}`;
miniStatusEl.textContent=`RAID${L} · members=${n} · failed=${failedCount()} · removed=${removedCount()} · spares=${state.spares.length} · rebuild=${state.rebuild.active?state.rebuild.progress.toFixed(1)+"%":"no"}`;
// Banner didattico rebuild
if(state.rebuild.active){
const leftSim=Math.max(0,Math.round(state.rebuild.etaSec-(Date.now()-state.rebuild.start)/1000));
const realSec=state.rebuild.realSec||0;
const realMin=Math.round(realSec/60);
const realStr=realSec>=3600?`~${(realSec/3600).toFixed(1)} ore`:`~${realMin} min`;
rbFactorEl.textContent=state.rebuild.speedFactor||"?";
rbEtaSimEl.textContent=`${leftSim}s (${state.rebuild.progress.toFixed(0)}%)`;
rbEtaRealEl.textContent=`${realStr} (disco ${state.diskSizeGB} GB, RAID${L})`;
rebuildBannerEl.style.display="flex";
} else {
rebuildBannerEl.style.display="none";
}
pillModeEl.textContent=state.mode;
promptTextEl.textContent=`raidlab(${vol})$`;
if(!state.fs.created)fsChipEl.textContent="fs: none";
@@ -3402,6 +3714,7 @@ $ rm /mnt/verifica.txt
if(cmd==="powerfail"){if(state.rebuild.active){stopRebuild(false);termPrint("⚡ POWER FAIL: rebuild interrotto.","warn");if(state.scenario.name==="rebuild_interrupted"&&!state.scenario.checkpoints.stopped){state.scenario.checkpoints.stopped=true;addScore(6,"(scenario) rebuild interrotto.");}}else termPrint("POWER FAIL: nessun rebuild attivo.","dim");render();scenarioCheck();return;}
if(cmd==="dmesg | tail"){termPrint(dmesgTail(),"dim");scenarioActionDiag();render();scenarioCheck();return;}
const t=tokenize(cmd),c0=t[0];
if(cmd==="uptime"){let uptime=new Date(Date.now()-state.startupTime);termPrint("Uptime: "+twoDigits((uptime.getUTCDate()-1))+":"+twoDigits(uptime.getUTCHours())+":"+twoDigits(uptime.getUTCMinutes())+":"+twoDigits(uptime.getUTCSeconds()),"dim");return;}
if(c0==="cat"&&t[1]==="/proc/mdstat"){termPrint(mdstat(),"dim");scenarioActionDiag();render();scenarioCheck();return;}
if(c0==="mdadm"){
// Supporta entrambe le sintassi: "mdadm --op /dev/md0 /dev/sdX" e "mdadm /dev/md0 --op /dev/sdX"
@@ -3446,7 +3759,7 @@ $ rm /mnt/verifica.txt
if(c0==="touch"&&t[1]){const r=fileTouch(t[1]);if(r.msg)termPrint(r.msg,r.ok?"dim":"err");render();scenarioCheck();return;}
if(c0==="rm"&&t[1]){const r=fileRm(t[1]);if(r.msg)termPrint(r.msg,r.ok?"dim":"err");render();scenarioCheck();return;}
if(c0==="cat"&&t[1]&&t[1]!=="/proc/mdstat"){const r=fileCat(t[1]);termPrint(r.msg,r.ok?"dim":"err");render();scenarioCheck();return;}
if(c0==="echo"){const gt=t.indexOf(">");if(gt<0)return termPrint('echo: usa: echo "testo" > /mnt/file',"err");const text=t.slice(1,gt).join(" ");const path=t[gt+1];if(!path)return termPrint("echo: manca destinazione.","err");const r=fileEchoTo(text,path);if(r.msg)termPrint(r.msg,r.ok?"dim":"err");render();scenarioCheck();return;}
if(c0==="echo"){const gt=t.indexOf(">");if(gt<0)return termPrint(t[1],"dim");const text=t.slice(1,gt).join(" ");const path=t[gt+1];if(!path)return termPrint("echo: manca destinazione.","err");const r=fileEchoTo(text,path);if(r.msg)termPrint(r.msg,r.ok?"dim":"err");render();scenarioCheck();return;}
if(c0==="conclude"){
const text=t.slice(1).join(" ");if(!text)return termPrint("Uso: conclude <testo spiegazione>","err");
termPrint("Annotazione registrata (didattica).","ok");
@@ -3458,6 +3771,281 @@ $ rm /mnt/verifica.txt
termPrint(`${c0}: comando non riconosciuto. Digita 'help'.`,"err");
}
// ── CODICE DOCENTE: encode / decode ────────────────────────────────
function teacherEncode(payload){
const json=JSON.stringify(payload);
const b64=btoa(unescape(encodeURIComponent(json))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"");
const alpha="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let cs=0; for(let i=0;i<b64.length;i++)cs=(cs+b64.charCodeAt(i))%1296;
const c1=alpha[Math.floor(cs/36)], c2=alpha[cs%36];
return `TC-${b64}-${c1}${c2}`;
}
function teacherDecode(raw){
try{
const s=raw.trim().toUpperCase().replace(/\s/g,"");
if(!s.startsWith("TC-"))return null;
const inner=s.slice(3);
const lastDash=inner.lastIndexOf("-");
if(lastDash<0)return null;
const b64=inner.slice(0,lastDash);
const cs2=inner.slice(lastDash+1);
const alpha="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let cs=0; for(let i=0;i<b64.length;i++)cs=(cs+b64.charCodeAt(i))%1296;
const c1=alpha[Math.floor(cs/36)], c2=alpha[cs%36];
if(cs2!==c1+c2)return null;
const json=decodeURIComponent(escape(atob(b64.replace(/-/g,"+").replace(/_/g,"/"))));
return JSON.parse(json);
}catch(e){return null;}
}
// ── TEACHER FORM: UI dinamica ───────────────────────────────────────
const devNames=["sda","sdb","sdc","sdd","sde","sdf","sdg","sdh","sdi","sdj","sdk","sdl"];
const faultLabels={
"":"Nessun guasto (OK)",
"FAILED":"FAILED — disco guasto (I/O error)",
"CRC":"CRC — errori di comunicazione (cavo/porta)",
"OVERHEAT":"OVERHEAT — temperatura critica",
"SLOW":"SLOW — risposte lente (pre-guasto)"
};
const faultDescriptions={
"":"Disco funzionante, nessun problema.",
"FAILED":"Disco non risponde. Se la tolleranza del RAID è superata l'array diventa FAILED.",
"CRC":"Errori sul cavo/connettore SATA. Diagnosticabili con dmesg e smartctl.",
"OVERHEAT":"Temperatura sopra soglia critica. Rilevabile con smartctl -a.",
"SLOW":"Disco funziona ma lento. Segnale di guasto imminente, rilevabile da dmesg."
};
function tfRaidMinDisks(level){
const L=Number(level);
if(L===0||L===1)return 2;
if(L===5)return 3;
if(L===6||L===10)return 4;
return 2;
}
function tfNormDiskCount(n,level){
const L=Number(level);
n=Math.max(n,tfRaidMinDisks(L));
if(L===10&&n%2===1)n+=1;
return Math.min(n,12);
}
function tfCapacityText(level,n,size){
const L=Number(level); let cap=0;
if(L===0)cap=n*size;
else if(L===1)cap=size;
else if(L===5)cap=(n-1)*size;
else if(L===6)cap=(n-2)*size;
else if(L===10)cap=Math.floor(n/2)*size;
const ft={0:"nessuna",1:`${n-1} disco/i`,5:"1 disco",6:"2 dischi",10:"1 disco per coppia"}[L]||"—";
return `Capacità utile: ${cap} GB · Tolleranza: ${ft}`;
}
function tfUpdateDiskFaultRows(){
const level=parseInt($("tfRaidLevel").value)||5;
let n=parseInt($("tfDiskCount").value)||4;
n=tfNormDiskCount(n,level);
$("tfDiskCount").value=String(n);
// aggiorna anteprima
const preview=$("tfArrayPreview");
preview.innerHTML="";
const capSpan=document.createElement("span");
capSpan.style.cssText="font-size:11.5px;color:var(--muted2);margin-right:6px;white-space:nowrap";
capSpan.textContent=tfCapacityText(level,n,parseInt($("tfDiskSize").value)||1000)+" —";
preview.appendChild(capSpan);
// salva selezioni precedenti
const existing={};
$("tfDiskFaultRows").querySelectorAll(".tf-fault-select").forEach(sel=>{
existing[sel.dataset.dev]=sel.value;
});
// genera chip preview e righe fault
const faultGrid=$("tfDiskFaultRows");
faultGrid.innerHTML="";
for(let i=0;i<n;i++){
const dev="/dev/"+devNames[i];
const chip=document.createElement("span");
chip.className="tf-disk-chip";
chip.id=`tf-chip-${i}`;
chip.textContent=`/dev/${devNames[i]}`;
preview.appendChild(chip);
// riga fault
const row=document.createElement("div");
row.className="tf-fault-row";
const devLabel=document.createElement("span");
devLabel.className="tf-fault-dev";
devLabel.textContent=dev;
const sel=document.createElement("select");
sel.className="tf-fault-select";
sel.dataset.dev=dev;
sel.dataset.idx=String(i);
Object.entries(faultLabels).forEach(([val,label])=>{
const opt=document.createElement("option");
opt.value=val; opt.textContent=label;
sel.appendChild(opt);
});
// ripristina selezione precedente se stesso disco
if(existing[dev])sel.value=existing[dev];
const desc=document.createElement("span");
desc.className="tf-fault-desc";
desc.textContent=faultDescriptions[sel.value]||"";
sel.addEventListener("change",()=>{
desc.textContent=faultDescriptions[sel.value]||"";
// aggiorna colore select
sel.className="tf-fault-select"+(sel.value?` sel-${sel.value}`:"");
// aggiorna chip preview
const chipEl=$(`tf-chip-${i}`);
if(chipEl)chipEl.className="tf-disk-chip"+(sel.value?` fault-${sel.value}`:"");
});
row.appendChild(devLabel);
row.appendChild(sel);
row.appendChild(desc);
faultGrid.appendChild(row);
}
}
// ── GENERA CODICE CUSTOM ────────────────────────────────────────────
function generateTeacherCode(){
const level=parseInt($("tfRaidLevel").value)||5;
let n=parseInt($("tfDiskCount").value)||4;
n=tfNormDiskCount(n,level);
const size=Math.max(10,parseInt($("tfDiskSize").value)||1000);
const classe=$("tfClasse").value.trim()||"—";
const data=$("tfData").value||"—";
const durata=parseInt($("tfDurata").value)||0;
const note=$("tfNote").value.trim()||"";
// raccogli guasti
const faults=[];
$("tfDiskFaultRows").querySelectorAll(".tf-fault-select").forEach(sel=>{
if(sel.value) faults.push({idx:parseInt(sel.dataset.idx),dev:sel.dataset.dev,type:sel.value});
});
// validazione
const vmsg=$("tfValidationMsg");
const L=Number(level);
const failedCount=faults.filter(f=>f.type==="FAILED").length;
let warning="";
if(L===0&&failedCount>=1) warning="⚠ RAID0: anche 1 FAILED porta l'array a FAILED — scenario didatticamente corretto.";
if(L===5&&failedCount>=2) warning="⚠ RAID5 con 2+ FAILED → array FAILED. L'obiettivo sarà restore da backup.";
if(L===1&&failedCount>=n) warning="⚠ Tutti i dischi guasti: nessuna tolleranza disponibile.";
if(L===6&&failedCount>=3) warning="⚠ RAID6 con 3+ FAILED → array FAILED.";
vmsg.textContent=warning;
const ts=Date.now();
// payload custom (v=2 = custom)
const payload={v:2,level,n,size,faults,classe,data,durata,note,ts};
const code=teacherEncode(payload);
$("teacherCodeText").textContent=code;
$("teacherCodeMeta").innerHTML=`Generato il ${new Date(ts).toLocaleString("it-IT")} · RAID${level}/${n} dischi/${size}GB`;
$("tcArray").textContent=`RAID${level} · ${n} dischi · ${size} GB/disco`;
$("tcFaults").textContent=faults.length?faults.map(f=>`${f.dev}${f.type}`).join(", "):"nessun guasto preimpostato";
$("tcClasse").textContent=classe;
$("tcData").textContent=data;
$("tcDurata").textContent=durata?`${durata} minuti`:"Nessun timer";
// obiettivo automatico in base ai guasti
let goal="Analisi array RAID e diagnosi stato.";
if(faults.some(f=>f.type==="FAILED")){
const canRebuild=(L===5&&failedCount===1)||(L===6&&failedCount<=2)||(L===1&&failedCount<n)||(L===10&&failedCount<n/2);
goal=canRebuild?"Rimuovere disco/i guasti e completare rebuild fino a stato OK.":"Diagnosticare array FAILED e concludere con restore da backup.";
} else if(faults.some(f=>f.type==="CRC")) goal="Diagnosticare errori CRC, identificare il disco, concludere con sostituzione cavo SATA.";
else if(faults.some(f=>f.type==="OVERHEAT")) goal="Diagnosticare temperatura critica con smartctl e dmesg, concludere con intervento di raffreddamento.";
else if(faults.some(f=>f.type==="SLOW")) goal="Identificare disco lento, valutare sostituzione preventiva prima del guasto.";
$("tcGoal").textContent=goal;
$("teacherCodeOutput").style.display="block";
state._teacherPayload=payload;
}
// ── CARICA PAYLOAD CUSTOM NEL LAB ──────────────────────────────────
function loadTeacherPayloadInLab(payload){
if(!payload)return;
const L=Number(payload.level||5);
const n=Number(payload.n||4);
const size=Number(payload.size||1000);
const faults=payload.faults||[];
// reset array con parametri custom
raidLevelEl.value=String(L);
state.raidLevel=L;
diskCountEl.value=String(n);
diskSizeEl.value=String(size);
state.diskSizeGB=size;
state.disks=Array.from({length:n},(_,i)=>makeDisk(i,size));
state.spares=[];
if(rebuildTimer){clearInterval(rebuildTimer);rebuildTimer=null;}
state.rebuild={active:false,start:0,etaSec:0,progress:0,realSec:0,speedFactor:1};
state.score=0;state.hints=0;state.actions=[];
state.mode="EXERCISE";state.exerciseOn=true;
// applica guasti
faults.forEach(f=>{
const d=state.disks[f.idx];
if(!d)return;
if(f.type==="FAILED"){d.state=DiskState.FAILED;d.smart.realloc=130;d.smart.pending=14;pushDmesg("warn",`${d.dev}: I/O error, disk FAILED (docente)`);}
else if(f.type==="CRC"){d.state=DiskState.CRC;d.smart.crc=22;pushDmesg("warn",`${d.dev}: UDMA CRC errors (docente)`);}
else if(f.type==="OVERHEAT"){d.state=DiskState.OVERHEAT;d.smart.temp=60;pushDmesg("warn",`${d.dev}: temperature critical (docente)`);}
else if(f.type==="SLOW"){d.state=DiskState.SLOW;pushDmesg("info",`${d.dev}: device responding slow (docente)`);}
});
// scenario generico custom
const faultSummary=faults.length?faults.map(f=>`${f.dev}${f.type}`).join(", "):"nessun guasto preimpostato";
state.scenario={
name:"custom_docente",
goalHtml:`Prova docente: RAID${L} · ${n} dischi · ${size}GB/disco.<br>Guasti: <b>${faultSummary}</b>.<br>${payload.note?`<i>${payload.note}</i>`:""}`,
solution:[],checkpoints:{diag:false,ok:false},done:false,startedAt:Date.now(),seed:`custom-${payload.ts}`
};
if(payload.durata>0)setTimerMinutes(payload.durata);
termPrint(`▶ PROVA DOCENTE caricata — RAID${L} · ${n} dischi · ${size}GB`,"info");
termPrint(` Guasti : ${faultSummary}`,"dim");
termPrint(` Classe : ${payload.classe||"—"}`,"dim");
termPrint(` Data : ${payload.data||"—"}`,"dim");
termPrint(` Durata : ${payload.durata?payload.durata+" min":"nessun timer"}`,"dim");
if(payload.note)termPrint(` Note : ${payload.note}`,"dim");
pushDmesg("info",`md0: created RAID${L} with ${n} disks (docente)`);
showPage("tab-lab");
render();scenarioCheck();
}
function loadCodeFromLabInput(){
const raw=$("labCodeInput").value;
const fb=$("labCodeFeedback");
if(!raw.trim()){fb.style.color="var(--warn)";fb.textContent="⚠ Incolla un codice prima di procedere.";return;}
const payload=teacherDecode(raw);
if(!payload){fb.style.color="var(--bad)";fb.textContent="✗ Codice non valido o corrotto. Controlla di averlo copiato per intero.";return;}
const label=payload.v===2?`RAID${payload.level}·${payload.n}dischi·${payload.size}GB`:(payload.scn||"?");
fb.style.color="var(--ok)";fb.textContent=`✓ Codice valido — ${label}${payload.classe&&payload.classe!=="—"?" · "+payload.classe:""}`;
setTimeout(()=>{ $("labCodeInput").value="";fb.textContent=""; },2000);
// supporta sia payload v1 (scenario predefinito) che v2 (custom)
if(payload.v===2) loadTeacherPayloadInLab(payload);
else {
if(payload.scn) loadScenario(payload.scn);
if(payload.durata>0)setTimerMinutes(payload.durata);
showPage("tab-lab");render();
}
}
// Init form dinamico
$("btnGenTeacherCode").addEventListener("click",generateTeacherCode);
$("btnLoadTeacherInLab").addEventListener("click",()=>{
if(state._teacherPayload)loadTeacherPayloadInLab(state._teacherPayload);
else termPrint("Genera prima un codice prova.","warn");
});
$("btnCopyTeacherCode").addEventListener("click",()=>{
const t=$("teacherCodeText").textContent;
if(t&&t!=="—"){navigator.clipboard.writeText(t).then(()=>{$("btnCopyTeacherCode").textContent="✓ Copiato!";setTimeout(()=>$("btnCopyTeacherCode").textContent="⎘ Copia",2000)});}
});
$("btnLoadCodeInLab").addEventListener("click",loadCodeFromLabInput);
$("labCodeInput").addEventListener("keydown",(e)=>{if(e.key==="Enter")loadCodeFromLabInput();});
$("tfDiskSize").addEventListener("input",tfUpdateDiskFaultRows);
// pre-fill today & init rows
(()=>{ $("tfData").value=new Date().toISOString().slice(0,10); tfUpdateDiskFaultRows(); })();
$("btnApply").addEventListener("click",createArray);
$("btnResetDisks").addEventListener("click",resetDiskStates);
$("btnRandomFault").addEventListener("click",randomFault);
@@ -3500,6 +4088,8 @@ function showProc(id, btn){
if(section)section.classList.add("active");
if(btn)btn.classList.add("active");
}
function twoDigits(n){return n.toString().padStart(2,"0");}
</script>
</body>
</html>