@@ -106,10 +106,11 @@
label { display : flex ; flex-direction : column ; gap : 6 px ; font-size : 12 px ; color : var ( - - muted ) }
select , input [ type = "number" ] , input [ type = "text" ] {
height : 40 px ; border-radius : 12 px ; border : 1 px 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 12 px ; outline : none ; box-shadow : inset 0 0 0 1 px 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 4 px rgba ( 167 , 139 , 250 , .15 ) }
. controls { display : grid ; grid-template-columns : 1.1 fr .7 fr .7 fr ; gap : 10 px }
@ media ( max-width : 720px ) { . controls { grid-template-columns : 1 fr } }
@@ -895,6 +896,106 @@
. server-example . se-icon { font-size : 18 px ; flex-shrink : 0 ; }
. server-example . se-text { font-size : 12.5 px ; 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 : 12 px ;
background : rgba ( 255 , 255 , 255 , .04 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .1 ) ;
border-radius : 14 px ; padding : 16 px 18 px ;
}
. tf-row { display : flex ; gap : 14 px ; flex-wrap : wrap ; }
. tf-field { display : flex ; flex-direction : column ; gap : 5 px ; flex : 1 ; min-width : 180 px ; }
. tf-label { font-size : 11.5 px ; color : var ( - - muted2 ) ; font-weight : 700 ; letter-spacing : .3 px ; text-transform : uppercase ; }
. tf-input , . tf-select {
background : rgba ( 255 , 255 , 255 , .06 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .16 ) ;
border-radius : 10 px ; padding : 9 px 12 px ; color : var ( - - text ) ; font-family : var ( - - sans ) ; font-size : 13 px ;
outline : none ; transition : border-color .2 s ;
}
. 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 12 px center ; padding-right : 32 px ; }
. tf-input : focus , . tf-select : focus { border-color : rgba ( 167 , 139 , 250 , .55 ) ; box-shadow : 0 0 0 3 px rgba ( 167 , 139 , 250 , .12 ) ; }
. tf-hint { font-size : 11.5 px ; color : var ( - - muted2 ) ; line-height : 1.45 ; }
. teacher-code-box {
background : linear-gradient ( 135 deg , rgba ( 167 , 139 , 250 , .12 ) , rgba ( 34 , 211 , 238 , .08 ) ) ;
border : 1 px solid rgba ( 167 , 139 , 250 , .35 ) ; border-radius : 14 px ; padding : 16 px 18 px ;
}
. tcb-header { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 12 px ; }
. tcb-label { font-size : 13 px ; font-weight : 700 ; color : var ( - - accent ) ; }
. tcb-code {
font-family : var ( - - mono ) ; font-size : 15 px ; letter-spacing : 2 px ; color : var ( - - text ) ;
background : rgba ( 0 , 0 , 0 , .35 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .12 ) ;
border-radius : 10 px ; padding : 12 px 16 px ; word-break : break-all ; line-height : 1.6 ;
user-select : all ; cursor : text ;
}
. tcb-meta { margin-top : 8 px ; font-size : 11.5 px ; color : var ( - - muted2 ) ; line-height : 1.5 ; }
/* section titles inside teacher form */
. tf-section-title {
font-size : 12 px ; font-weight : 800 ; letter-spacing : .6 px ; text-transform : uppercase ;
color : var ( - - accent ) ; padding : 6 px 0 4 px ; border-bottom : 1 px solid rgba ( 167 , 139 , 250 , .2 ) ;
margin-bottom : 10 px ;
}
/* array preview chips */
. tf-array-preview {
display : flex ; gap : 6 px ; flex-wrap : wrap ; margin-top : 10 px ; padding : 10 px 12 px ;
background : rgba ( 0 , 0 , 0 , .25 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .08 ) ; border-radius : 10 px ;
min-height : 38 px ; align-items : center ;
}
. tf-disk-chip {
display : inline-flex ; align-items : center ; gap : 5 px ;
padding : 4 px 10 px ; border-radius : 999 px ; font-size : 12 px ; font-weight : 700 ; font-family : var ( - - mono ) ;
background : rgba ( 74 , 222 , 128 , .15 ) ; border : 1 px solid rgba ( 74 , 222 , 128 , .3 ) ; color : var ( - - ok ) ;
transition : background .2 s , border-color .2 s , color .2 s ;
}
. 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 : 6 px ; }
. tf-fault-row {
display : flex ; align-items : center ; gap : 10 px ; flex-wrap : wrap ;
background : rgba ( 255 , 255 , 255 , .03 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , .07 ) ;
border-radius : 10 px ; padding : 8 px 12 px ;
}
. tf-fault-dev {
font-family : var ( - - mono ) ; font-size : 13 px ; font-weight : 700 ; color : var ( - - text ) ;
min-width : 52 px ; flex-shrink : 0 ;
}
. tf-fault-select {
background : #111827 ; border : 1 px solid rgba ( 255 , 255 , 255 , .18 ) ;
border-radius : 8 px ; padding : 6 px 28 px 6 px 10 px ; color : rgba ( 255 , 255 , 255 , .92 ) ; font-size : 12.5 px ;
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 9 px center ; appearance : none ;
transition : border-color .2 s ;
}
. 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.5 px ; color : var ( - - muted2 ) ; flex : 1 ; line-height : 1.35 ; }
/* Rebuild didactic banner */
. rebuild-banner {
margin-top : 10 px ; padding : 12 px 14 px ; border-radius : 16 px ;
border : 1 px solid rgba ( 251 , 191 , 36 , .40 ) ;
background : linear-gradient ( 135 deg , rgba ( 251 , 191 , 36 , .10 ) , rgba ( 251 , 146 , 60 , .07 ) ) ;
display : flex ; align-items : flex-start ; gap : 12 px ;
animation : rb-pulse 2 s 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 : 20 px ; flex-shrink : 0 ; line-height : 1 }
. rb-body { font-size : 12.5 px ; 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 > ·
< span > ETA simulato: < b id = "rbEtaSim" > —< / b > < / span > ·
< 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 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 < 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:" , " 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 >