@@ -1063,17 +1196,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.
+
+
+
+
+
+
+
+
+
+
+ ① Configurazione array RAID
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ② Guasti da assegnare ai dischi
+
+
+ Lascia "Nessun guasto" per i dischi che devono restare OK. Puoi assegnare guasti multipli su dischi diversi.
+
+
+
+ ③ Dati verifica
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
🎓 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).
@@ -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 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 +3414,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 +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";
@@ -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 ","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{
+ 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); @@ -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");}
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); @@ -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");}