From 20f4531ad357c2941aa358018dbaa6f805bfe7a2 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 08:53:43 +0100 Subject: [PATCH 1/8] Merge sorgente "di Campi" versione 2.7 --- simulatore.html | 599 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 589 insertions(+), 10 deletions(-) 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 @@
+ + +

📈 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. +
+
+ + +
+
+
+
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. +
+ +
+ + +
① 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).
@@ -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); -- 2.49.1 From 7f083f187d273605640a02b04043c9183bbbdf67 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 09:05:24 +0100 Subject: [PATCH 2/8] Aggiunta pill contatore rebuild --- simulatore.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/simulatore.html b/simulatore.html index 494dc61..ec94ddc 100644 --- a/simulatore.html +++ b/simulatore.html @@ -1156,7 +1156,10 @@

💻 Terminale Linux (simulato)

-
Mode LAB
+
+
Rebuild effettuati 0
+
Mode LAB
+
-- 2.49.1 From 53460bd70ee4d42fac9c2d2095fa88e04e6b1d13 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 09:19:44 +0100 Subject: [PATCH 3/8] Implementato contatore rebuild completati --- simulatore.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/simulatore.html b/simulatore.html index ec94ddc..f91b6a4 100644 --- a/simulatore.html +++ b/simulatore.html @@ -1157,7 +1157,7 @@

💻 Terminale Linux (simulato)

-
Rebuild effettuati 0
+
Rebuild effettuati 727
Mode LAB
@@ -3277,6 +3277,7 @@ $ rm /mnt/verifica.txt hints: 0, teacher: false, mode: "LAB", + rebuildCount: 0, exerciseOn: false, timer: { on:false, endTs:0, interval:null }, actions: [], @@ -3322,6 +3323,7 @@ $ rm /mnt/verifica.txt const termBody = $("termBody"); const termInput = $("termInput"); const btnRun = $("btnRun"); + const pillRebuildCountEl = $("pillRebuildCount"); let history = []; let histIdx = -1; @@ -3430,6 +3432,7 @@ $ rm /mnt/verifica.txt 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"); + 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"); @@ -3619,6 +3622,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)); -- 2.49.1 From 98e15d2e6cc8003f2432e7d40bf757af81b758c5 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 09:26:18 +0100 Subject: [PATCH 4/8] Implementato comando `echo` senza redirect su file --- simulatore.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulatore.html b/simulatore.html index f91b6a4..5abfd48 100644 --- a/simulatore.html +++ b/simulatore.html @@ -3757,7 +3757,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"); -- 2.49.1 From e0fe2a53e9d303e930b94ccdb3ed6c45c0dba61b Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 09:39:48 +0100 Subject: [PATCH 5/8] Implementazione base uptime --- simulatore.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulatore.html b/simulatore.html index 5abfd48..bd5eeda 100644 --- a/simulatore.html +++ b/simulatore.html @@ -3266,6 +3266,7 @@ $ rm /mnt/verifica.txt let state = { arrayName: "/dev/md0", + startupTime: Date.now(), raidLevel: 5, diskSizeGB: 1000, disks: [], @@ -3713,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: "+(uptime.getDate()-1)+":"+uptime.getHours()+":"+uptime.getMinutes()+":"+uptime.getSeconds(),"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" -- 2.49.1 From b9675d03b04161c1e39ec41e40612debf7c578dc Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 09:43:28 +0100 Subject: [PATCH 6/8] fix: ora mostrata incorrettamente nel comando `uptime` --- simulatore.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulatore.html b/simulatore.html index bd5eeda..4d6fd9c 100644 --- a/simulatore.html +++ b/simulatore.html @@ -3714,7 +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: "+(uptime.getDate()-1)+":"+uptime.getHours()+":"+uptime.getMinutes()+":"+uptime.getSeconds(),"dim");return;} + if(cmd==="uptime"){let uptime=new Date(Date.now()-state.startupTime);termPrint("Uptime: "+(uptime.getUTCDate()-1)+":"+uptime.getUTCHours()+":"+uptime.getUTCMinutes()+":"+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" -- 2.49.1 From 0a4cb932c659f39318cd07fa6b0f7155711aa3f9 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 12:03:44 +0100 Subject: [PATCH 7/8] fix: mostra numeri a una cifra con 2 nel comando `uptime` --- simulatore.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simulatore.html b/simulatore.html index 4d6fd9c..fa30058 100644 --- a/simulatore.html +++ b/simulatore.html @@ -3714,7 +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: "+(uptime.getUTCDate()-1)+":"+uptime.getUTCHours()+":"+uptime.getUTCMinutes()+":"+uptime.getUTCSeconds(),"dim");return;} + 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" @@ -4088,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");} -- 2.49.1 From 078998668e300c2a2c4939211d27b3ea94ce19b6 Mon Sep 17 00:00:00 2001 From: Andrea Fiorencis Date: Thu, 26 Mar 2026 12:08:52 +0100 Subject: [PATCH 8/8] Aggiunta homepage --- index.html | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..ac5b6f8 --- /dev/null +++ b/index.html @@ -0,0 +1,48 @@ + + + + + + Simulatore RAID Web + + + +

Simulatore RAID Web

+

by Marco di Campi, forked and tweaked by Andrea Fiorencis

+

+ + -- 2.49.1