diff --git a/simulatore.html b/simulatore.html
index 93255f7..494dc61 100644
--- a/simulatore.html
+++ b/simulatore.html
@@ -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}
@@ -972,6 +1073,17 @@
—
+
+
+
⚡
+
+ MODALITÀ DIDATTICA — Rebuild accelerato
+ Velocità aumentata di — × ·
+ ETA simulato: — ·
+ Tempo reale stimato: —
+
+
+
📈 Telemetria (sim)
@@ -1014,6 +1126,24 @@
+
+
+
🔑 Carica codice prova
+
+ Hai ricevuto un codice dal docente? Incollalo qui e clicca Avvia prova per caricare lo scenario pianificato.
+
+
+
+ ▶ Avvia prova
+
+
+
+
Regola didattica: lo studente deve lavorare col
terminale . Il click sui dischi è disabilitato, salvo
Teacher ON .
@@ -1063,17 +1193,148 @@
+
+
+
+
+
👨🏫 Pannello Docente — Programmazione verifiche
+
+ ✦ Configurazione personalizzata
+
+
+
+
+
+ Come funziona: configura l'array RAID esattamente come vuoi → assegna i guasti ai dischi → imposta classe/data/timer →
+ clicca Genera codice prova . Lo studente incolla il codice nel LAB e la prova parte identica per tutti.
+
+
+
+
+
+
+
+
+
+
Riepilogo prova
+
Array —
+
Guasti —
+
Classe —
+
Data —
+
Durata —
+
Obiettivo atteso —
+
+
+
+
📌 Istruzioni per lo studente
+
+ 1) Aprire il simulatore RAID
+ 2) Tab LAB → sezione "Carica codice prova"
+ 3) Incollare il codice e cliccare Avvia prova
+ 4) Svolgere la prova entro il tempo indicato
+ 5) Consegnare: codice prova + export del report
+
+
+
+ ▶ Carica nel LAB (test docente)
+
+
+
+
+
+
+
+
+
-
🎓 Esercizi per studenti
+
🎓 Prova rapida (casuale)
Codice —
-
Genera prova (unica)
+
Genera prova casuale
- Clicca "Genera prova": otterrai un codice prova e un scenario .
+ Clicca "Genera prova": otterrai un codice prova e uno scenario random .
Lo studente deve consegnare il codice + il report (comando export o tasto Export report).
@@ -3006,7 +3267,7 @@ $ rm /mnt/verifica.txt
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,
@@ -3027,6 +3288,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");
@@ -3114,7 +3379,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 10–15s, calcola fattore di accelerazione
+ const didacticSec=Math.round(10+Math.random()*5); // 10–15s
+ 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 +3408,13 @@ $ rm /mnt/verifica.txt
if(spare.sizeGB=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 +3426,11 @@ $ 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");
+ 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 +3470,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";
@@ -3336,6 +3627,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";
@@ -3458,6 +3762,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{
+ existing[sel.dataset.dev]=sel.value;
+ });
+
+ // genera chip preview e righe fault
+ const faultGrid=$("tfDiskFaultRows");
+ faultGrid.innerHTML="";
+ for(let i=0;i{
+ 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&&failedCountf.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. Guasti: ${faultSummary} . ${payload.note?`${payload.note} `:""}`,
+ 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);