Upstream con 2.8 di Campi

This commit is contained in:
2026-04-09 09:23:03 +02:00
parent 391a566581
commit f354595954
2 changed files with 688 additions and 124 deletions
+455 -114
View File
@@ -106,11 +106,10 @@
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:#0d1220;color:rgba(255,255,255,.92);
background:rgba(10,14,28,.55);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}}
@@ -959,43 +958,59 @@
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;
color:var(--text);
}
.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;}
/* ── Custom dropdown (sostituisce <select> nativo) ── */
.tf-fault-select{ display:none; } /* nascosto, usato solo per leggere il valore */
/* 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;
.cdd-wrap{
position:relative; display:inline-block; min-width:220px; flex-shrink:0;
}
@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}
.cdd-btn{
display:flex; align-items:center; justify-content:space-between; gap:8px;
background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.18);
border-radius:8px; padding:7px 10px 7px 12px;
color:rgba(255,255,255,.92); font-size:12.5px; font-family:var(--sans);
cursor:pointer; user-select:none; white-space:nowrap;
transition:border-color .2s, background .2s;
}
.cdd-btn:hover{ background:rgba(255,255,255,.10); border-color:rgba(255,255,255,.28); }
.cdd-btn.open{ border-color:rgba(167,139,250,.55); background:rgba(167,139,250,.10); }
.cdd-btn .cdd-arrow{ flex-shrink:0; opacity:.55; transition:transform .15s; }
.cdd-btn.open .cdd-arrow{ transform:rotate(180deg); }
/* colori stato selezionato */
.cdd-btn.val-FAILED{ border-color:rgba(251,113,133,.5); color:var(--bad); }
.cdd-btn.val-CRC{ border-color:rgba(251,191,36,.5); color:var(--warn); }
.cdd-btn.val-OVERHEAT{ border-color:rgba(251,146,60,.5); color:#fb923c; }
.cdd-btn.val-SLOW{ border-color:rgba(96,165,250,.45); color:var(--info); }
.cdd-menu{
display:none; position:absolute; top:calc(100% + 4px); left:0; z-index:9999;
min-width:100%; background:rgba(15,18,35,.97);
border:1px solid rgba(255,255,255,.18); border-radius:10px;
box-shadow:0 12px 40px rgba(0,0,0,.6); overflow:hidden;
backdrop-filter:blur(12px);
}
.cdd-menu.open{ display:block; }
.cdd-opt{
padding:9px 14px; font-size:12.5px; font-family:var(--sans);
color:rgba(255,255,255,.88); cursor:pointer; white-space:nowrap;
transition:background .12s;
}
.cdd-opt:hover{ background:rgba(167,139,250,.18); }
.cdd-opt.selected{ background:rgba(167,139,250,.12); color:#fff; font-weight:700; }
.cdd-opt[data-val="FAILED"]{ color:var(--bad); }
.cdd-opt[data-val="CRC"]{ color:var(--warn); }
.cdd-opt[data-val="OVERHEAT"]{ color:#fb923c; }
.cdd-opt[data-val="SLOW"]{ color:var(--info); }
.cdd-opt[data-val=""]:hover,.cdd-opt[data-val=""].selected{ color:rgba(255,255,255,.88); }
.tf-fault-desc{font-size:11.5px;color:rgba(255,255,255,.75);flex:1;line-height:1.35;}
</style>
</head>
<body>
@@ -1073,17 +1088,6 @@
<div class="mini" id="miniStatus"></div>
</div>
<!-- Banner didattico rebuild -->
<div id="rebuildBanner" style="display:none" class="rebuild-banner">
<div class="rb-icon"></div>
<div class="rb-body">
<b>MODALITÀ DIDATTICA — Rebuild accelerato</b><br>
<span id="rbSpeedText">Velocità aumentata di <b id="rbFactor"></b>×</span> &nbsp;·&nbsp;
<span>ETA simulato: <b id="rbEtaSim"></b></span> &nbsp;·&nbsp;
<span>Tempo reale stimato: <b id="rbEtaReal"></b></span>
</div>
</div>
<div class="row2">
<div class="miniCard">
<h3>📈 Telemetria (sim)</h3>
@@ -2918,6 +2922,243 @@ IOPS con rebuild attivo: ~<span style="color:var(--warn)">550</span> (
</div>
</div>
<hr class="doc-divider">
<!-- GRIGLIA DECISIONALE RAPIDA -->
<div class="cmd-card" style="border-color:rgba(34,211,238,.25)">
<div class="cmd-card-header" style="border-bottom-color:rgba(34,211,238,.15)">
<div class="cmd-card-title">
<span class="cmd-number" style="background:linear-gradient(135deg,rgba(34,211,238,.5),rgba(167,139,250,.3))">📋</span>
<span style="font-size:14px;font-weight:900;color:rgba(34,211,238,.95)">Griglia decisionale — problema → risoluzione</span>
</div>
<span class="cmd-badge" style="background:rgba(34,211,238,.1);border-color:rgba(34,211,238,.3);color:rgba(34,211,238,.9)">Riepilogo esame</span>
</div>
<div class="cmd-card-body">
<p class="cmd-desc" style="margin-bottom:12px">Ogni sessione di esercitazione <b>deve concludersi</b> con una risoluzione verificabile. Non basta diagnosticare: bisogna chiudere il problema.</p>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:12.5px;font-family:var(--mono)">
<thead>
<tr style="background:rgba(255,255,255,.06);color:rgba(255,255,255,.55);font-size:11px;text-transform:uppercase;letter-spacing:.06em">
<th style="padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.1)">Problema</th>
<th style="padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.1)">Sintomo nel sim</th>
<th style="padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.1)">Prima azione</th>
<th style="padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.1)">Decisione logica</th>
<th style="padding:8px 10px;text-align:left;border-bottom:1px solid rgba(255,255,255,.1)">Chiusura corretta</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid rgba(255,255,255,.06)">
<td style="padding:9px 10px;color:rgba(167,139,250,.95);font-weight:700">CRC errors</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">stato <code>.crc</code>, CRC counter &gt;0</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)"><code>dmesg | tail</code><br><code>smartctl -a /dev/sdX</code></td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">Realloc=0? → cavo<br>Realloc&gt;50? → disco</td>
<td style="padding:9px 10px"><code style="color:rgba(167,139,250,.9)">conclude sostituire cavo SATA</code><br><span style="color:rgba(255,255,255,.4);font-size:11px">oppure: conclude sostituire disco</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.02)">
<td style="padding:9px 10px;color:rgba(251,191,36,.95);font-weight:700">Overheat</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">stato <code>.overheat</code>, temp &gt;55°C</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)"><code>smartctl -a /dev/sdX</code><br><code>dmesg | tail</code></td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">Non è guasto ora, ma imminente → non aspettare</td>
<td style="padding:9px 10px"><code style="color:rgba(251,191,36,.9)">conclude migliorare raffreddamento</code><br><span style="color:rgba(255,255,255,.4);font-size:11px">opz: conclude pianificare sostituzione</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.06)">
<td style="padding:9px 10px;color:rgba(96,165,250,.95);font-weight:700">Slow disk</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">stato <code>.slow</code>, IOPS ridotti</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)"><code>smartctl -a /dev/sdX</code><br>verifica Pending_Sector</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">Pending&lt;20? → monitora<br>Pending&gt;20? → sostituisci</td>
<td style="padding:9px 10px"><code style="color:rgba(96,165,250,.9)">conclude monitorare e pianificare</code><br><span style="color:rgba(255,255,255,.4);font-size:11px">oppure: sostituzione preventiva</span></td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.02)">
<td style="padding:9px 10px;color:var(--err);font-weight:700">FAILED (1 disco)</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">stato <code>.failed</code>, array degradato</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)"><code>cat /proc/mdstat</code><br><code>mdadm --detail /dev/md0</code></td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">RAID1/5/6/10 tollerano → rebuild</td>
<td style="padding:9px 10px"><code style="color:var(--ok)">mdadm --remove ... --add ... --rebuild</code><br><span style="color:rgba(255,255,255,.4);font-size:11px">poi: verifica array OK</span></td>
</tr>
<tr>
<td style="padding:9px 10px;color:var(--err);font-weight:700">FAILED (array)</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">array in stato FAILED</td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)"><code>cat /proc/mdstat</code><br><code>mdadm --detail /dev/md0</code></td>
<td style="padding:9px 10px;color:rgba(255,255,255,.7)">RAID0 / RAID5 2 guasti → no recovery</td>
<td style="padding:9px 10px"><code style="color:var(--err)">conclude restore da backup</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<hr class="doc-divider">
<!-- CHIUSURA COMPLETA CRC -->
<div class="cmd-card" style="border-color:rgba(167,139,250,.22)">
<div class="cmd-card-header" style="border-bottom-color:rgba(167,139,250,.12)">
<div class="cmd-card-title">
<span class="cmd-number" style="background:linear-gradient(135deg,rgba(167,139,250,.5),rgba(167,139,250,.3))">🔌</span>
<span style="font-size:14px;font-weight:900;color:rgba(167,139,250,.95)">Chiusura completa — CRC errors (cavo SATA)</span>
</div>
<span class="cmd-badge" style="background:rgba(167,139,250,.1);border-color:rgba(167,139,250,.3);color:rgba(167,139,250,.9)">Procedura completa</span>
</div>
<div class="cmd-card-body">
<div class="cmd-two-col">
<div>
<div class="cmd-section-label">Sequenza completa nel simulatore</div>
<ol style="margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:13px;color:rgba(255,255,255,.78)">
<li><b>Rilevo il problema:</b> il disco mostra stato CRC, il contatore cresce</li>
<li><b>Leggo il log kernel:</b> <code>dmesg | tail</code> → vedo <code>UDMA CRC error count increased</code></li>
<li><b>Analizzo SMART:</b> <code>smartctl -a /dev/sdX</code> → Reallocated=0, CRC alto</li>
<li><b>Decido:</b> Reallocated=0 → causa probabile è il cavo, non il disco</li>
<li><b>NON faccio fail/remove immediato</b> → non è un guasto disco</li>
<li><b>Intervengo:</b> <code>replace cable /dev/sdX</code> — simula la sostituzione fisica del cavo SATA</li>
<li><b>Oppure concludo:</b> <code>conclude sostituire cavo SATA</code> — per registrare solo la decisione</li>
</ol>
<div class="tip-callout" style="margin-top:14px;border-left-color:rgba(167,139,250,.7);background:rgba(167,139,250,.06)">
<div class="tip-icon">⚠️</div>
<div class="tip-body"><b>Se CRC continua a crescere dopo cambio cavo:</b> a quel punto il disco è inaffidabile → <code>mdadm --fail</code> + <code>--remove</code> + <code>--add</code> nuovo disco → rebuild.</div>
</div>
</div>
<div>
<div class="cmd-section-label">Cosa valuta il simulatore</div>
<div class="cmd-output" style="font-size:12px">
<span style="color:var(--ok)">checkpoint diag</span>: hai eseguito dmesg + smartctl
<span style="color:var(--ok)">checkpoint explained</span>: hai scritto conclude ...
<span style="color:var(--ok)">punteggio</span>: +16 pt (scenario completato)
<span style="color:var(--err)">Non valida</span>: mdadm --fail immediato senza diagnosi
<span style="color:var(--err)">Non valida</span>: nessuna conclusione esplicita
</div>
<div class="cmd-section-label" style="margin-top:10px">Stato finale atteso</div>
<div class="cmd-output" style="font-size:12px">
/dev/md0: <span style="color:var(--ok)">active</span> raid5 sda sdb sdc <span style="color:rgba(167,139,250,.9)">sdd[C]</span>
→ sdd rimane in array (non è guasto)
→ si sostituisce il cavo fisico
→ si monitora: se CRC si stabilizza → OK
→ se CRC cresce → allora si esclude il disco
</div>
</div>
</div>
</div>
</div>
<hr class="doc-divider">
<!-- CHIUSURA COMPLETA OVERHEAT -->
<div class="cmd-card" style="border-color:rgba(251,191,36,.22)">
<div class="cmd-card-header" style="border-bottom-color:rgba(251,191,36,.12)">
<div class="cmd-card-title">
<span class="cmd-number" style="background:linear-gradient(135deg,rgba(251,191,36,.5),rgba(251,113,133,.3))">🌡</span>
<span style="font-size:14px;font-weight:900;color:rgba(251,191,36,.95)">Chiusura completa — Overheat (temperatura critica)</span>
</div>
<span class="cmd-badge" style="background:rgba(251,191,36,.1);border-color:rgba(251,191,36,.3);color:var(--warn)">Procedura completa</span>
</div>
<div class="cmd-card-body">
<div class="cmd-two-col">
<div>
<div class="cmd-section-label">Sequenza completa nel simulatore</div>
<ol style="margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:13px;color:rgba(255,255,255,.78)">
<li><b>Rilevo il problema:</b> disco in stato OVERHEAT, temperatura alta in telemetria</li>
<li><b>Analizzo SMART:</b> <code>smartctl -a /dev/sdX</code><code>Temperature_Celsius: 60+</code></li>
<li><b>Leggo il log kernel:</b> <code>dmesg | tail</code> → vedo <code>temperature critical</code></li>
<li><b>Decido:</b> overheat ≠ guasto immediato, ma è un pre-failure → non aspetto</li>
<li><b>Non avvio rebuild</b> con disco in overheat → peggiorerebbe</li>
<li><b>Intervengo:</b> <code>cool down /dev/sdX</code> — simula pulizia ventole e miglioramento airflow</li>
<li><b>Oppure concludo:</b> <code>conclude migliorare raffreddamento</code> — se si vuole solo registrare la decisione</li>
</ol>
<div class="tip-callout" style="margin-top:14px;border-left-color:rgba(251,191,36,.7);background:rgba(251,191,36,.05)">
<div class="tip-icon">🔧</div>
<div class="tip-body"><b>Se vuoi essere "pro" (livello esame):</b> dopo aver raffreddato il sistema, pianifichi la sostituzione preventiva: <code>mdadm --fail</code> + <code>--remove</code> + <code>--add</code> nuovo disco → rebuild controllato. Così eviti il guasto improvviso.</div>
</div>
</div>
<div>
<div class="cmd-section-label">Cosa valuta il simulatore</div>
<div class="cmd-output" style="font-size:12px">
<span style="color:var(--ok)">checkpoint diag</span>: hai eseguito smartctl + dmesg
<span style="color:var(--ok)">checkpoint explained</span>: hai scritto conclude ...
<span style="color:var(--ok)">punteggio</span>: +16 pt (scenario completato)
<span style="color:var(--err)">Non valida</span>: avviare rebuild con disco in overheat
<span style="color:var(--err)">Non valida</span>: non fare diagnosi e concludere subito
</div>
<div class="cmd-section-label" style="margin-top:10px">Differenza fondamentale</div>
<div class="cmd-output" style="font-size:12px">
OVERHEAT → <span style="color:rgba(251,191,36,.9)">rischio imminente</span>
→ NON aspetti → pianifichi sostituzione
CRC → <span style="color:rgba(167,139,250,.9)">problema di comunicazione</span>
→ NON panico → diagnosi → cavo prima
FAILED → <span style="color:var(--err)">guasto reale</span>
→ sostituzione immediata → rebuild
</div>
</div>
</div>
</div>
</div>
<hr class="doc-divider">
<!-- CHIUSURA COMPLETA SLOW DISK -->
<div class="cmd-card" style="border-color:rgba(96,165,250,.22)">
<div class="cmd-card-header" style="border-bottom-color:rgba(96,165,250,.12)">
<div class="cmd-card-title">
<span class="cmd-number" style="background:linear-gradient(135deg,rgba(96,165,250,.5),rgba(34,211,238,.3))">🐢</span>
<span style="font-size:14px;font-weight:900;color:rgba(96,165,250,.95)">Chiusura completa — Slow disk (disco lento)</span>
</div>
<span class="cmd-badge" style="background:rgba(96,165,250,.1);border-color:rgba(96,165,250,.3);color:var(--info)">Procedura completa</span>
</div>
<div class="cmd-card-body">
<div class="cmd-two-col">
<div>
<div class="cmd-section-label">Sequenza completa nel simulatore</div>
<ol style="margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:13px;color:rgba(255,255,255,.78)">
<li><b>Rilevo il problema:</b> IOPS calati, disco in stato SLOW</li>
<li><b>Analizzo SMART:</b> <code>smartctl -a /dev/sdX</code><code>Current_Pending_Sector &gt; 0</code></li>
<li><b>Valuto entità:</b> Pending &lt;20 → monitoro / Pending &gt;20 → sostituisco</li>
<li><b>Se monitoro:</b> <code>conclude monitorare e pianificare sostituzione</code></li>
<li><b>Se sostituisco:</b> <code>mdadm --fail</code> + <code>--remove</code> + <code>--add</code> + <code>--rebuild</code></li>
<li><b>Verifico:</b> <code>cat /proc/mdstat</code> → array torna OK</li>
</ol>
</div>
<div>
<div class="cmd-section-label">Cosa osservi in telemetria</div>
<div class="cmd-output" style="font-size:12px">
IOPS normali: 1200 (RAID5 4 dischi)
Con 1 SLOW: <span style="color:var(--info)">~720</span> (-40%)
Con rebuild: <span style="color:var(--warn)">~550</span> (-55%)
→ il rebuild su disco lento è doppiamente penalizzante
→ preferibile farlo in orario di bassa attività
</div>
<div class="tip-callout" style="margin-top:10px;border-left-color:rgba(96,165,250,.7);background:rgba(96,165,250,.05)">
<div class="tip-icon">📊</div>
<div class="tip-body"><b>Regola pratica:</b> un disco lento è come un campanello d'allarme. Non è ancora guasto, ma sta dicendo che lo diventerà. Agire in anticipo significa fare il rebuild in sicurezza invece di subirlo d'emergenza.</div>
</div>
</div>
</div>
</div>
</div>
<hr class="doc-divider">
<!-- PRINCIPIO FONDAMENTALE: OGNI PROVA DEVE CONCLUDERSI -->
<div class="tip-callout" style="border-left-color:rgba(34,211,238,.7);background:rgba(34,211,238,.06);padding:14px 16px">
<div class="tip-icon">🎯</div>
<div class="tip-body" style="font-size:13px">
<b style="color:rgba(34,211,238,.95);font-size:14px">Principio fondamentale — ogni prova deve concludersi</b><br><br>
Un buon amministratore di sistema non si ferma alla diagnosi. Ogni sessione deve avere un <b>risultato verificabile</b>:
<ul style="margin:8px 0 0 0;padding-left:16px;display:flex;flex-direction:column;gap:4px;color:rgba(255,255,255,.78)">
<li>CRC → <b>monitorato e stabilizzato</b> dopo cambio cavo (o disco sostituito)</li>
<li>Overheat → <b>raffreddamento migliorato</b>, sostituzione pianificata o eseguita</li>
<li>Slow disk → <b>monitoraggio attivo</b> o rebuild preventivo completato</li>
<li>FAILED → <b>rebuild completato</b> e array tornato OK (o restore da backup documentato)</li>
</ul>
<div style="margin-top:10px;padding:8px 12px;background:rgba(0,0,0,.25);border-radius:6px;font-family:var(--mono);font-size:12px;color:rgba(255,255,255,.7)">
Nel simulatore: ogni scenario si chiude con <code>conclude &lt;testo&gt;</code> oppure con l'array che torna in stato <span style="color:var(--ok)">OK</span>. Senza una chiusura esplicita, il punteggio non è completo.
</div>
</div>
</div>
</div><!-- end proc-errori -->
<!-- ─────────────────────────────────────────────────────── -->
@@ -3180,6 +3421,8 @@ $ rm /mnt/verifica.txt
<tr><td>mdadm /dev/md0 --rebuild</td><td class="ct-warn">Recovery</td><td>Avvia il rebuild dell'array</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>mdadm --stop-rebuild /dev/md0</td><td class="ct-warn">Recovery</td><td>Interrompe il rebuild</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>powerfail</td><td class="ct-warn">Simulazione</td><td>Simula blackout, interrompe rebuild</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>replace cable /dev/sdX</td><td class="ct-warn">Simulazione</td><td>Sostituisce cavo SATA su disco in stato CRC → azzera errori, ripristina OK</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>cool down /dev/sdX</td><td class="ct-warn">Simulazione</td><td>Intervento raffreddamento su disco in OVERHEAT → normalizza temperatura, ripristina OK</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>mkfs.ext4 /dev/md0</td><td class="ct-ok">Filesystem</td><td>Crea filesystem ext4 sull'array</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>mount /dev/md0 /mnt</td><td class="ct-ok">Filesystem</td><td>Monta il filesystem</td><td class="ct-ok">✅ Sì</td></tr>
<tr><td>umount /mnt</td><td class="ct-ok">Filesystem</td><td>Smonta il filesystem</td><td class="ct-ok">✅ Sì</td></tr>
@@ -3224,7 +3467,7 @@ $ rm /mnt/verifica.txt
window.scrollTo({top:0, behavior:"smooth"});
}
$("tab-lab").addEventListener("click", ()=>showPage("tab-lab"));
$("tab-ex").addEventListener("click", ()=>showPage("tab-ex"));
$("tab-ex").addEventListener("click", ()=>{ showPage("tab-ex"); tfUpdateDiskFaultRows(); });
$("tab-doc").addEventListener("click", ()=>showPage("tab-doc"));
document.querySelectorAll(".doc-tab").forEach(btn => {
@@ -3271,7 +3514,7 @@ $ rm /mnt/verifica.txt
diskSizeGB: 1000,
disks: [],
spares: [],
rebuild: { active:false, start:0, etaSec:0, progress:0, realSec:0, speedFactor:1 },
rebuild: { active:false, start:0, etaSec:0, progress:0 },
fs: { created:false, mountedAt:null, uuid:null, label:null, files: new Map() },
dmesg: [],
score: 0,
@@ -3386,7 +3629,16 @@ $ rm /mnt/verifica.txt
let rebuildTimer=null;
function rebuildEstimateSec(){
const sizeGB=state.diskSizeGB,n=state.disks.length;
// Durata simulata fissa: 8-10 secondi reali (indipendente dalla dimensione disco)
let simSec=9;
if(state.raidLevel===6)simSec=10;
if(state.raidLevel===1)simSec=8;
if(state.disks.some(d=>d.state===DiskState.SLOW))simSec=Math.min(simSec+1,10);
if(state.disks.some(d=>d.state===DiskState.OVERHEAT))simSec=Math.min(simSec+1,10);
return simSec;
}
function rebuildRealTimeMsg(){
const sizeGB=state.diskSizeGB;
let speed=1.6;
if(state.raidLevel===5)speed=1.1;
if(state.raidLevel===6)speed=0.85;
@@ -3395,13 +3647,11 @@ $ rm /mnt/verifica.txt
if(state.raidLevel===0)speed=2.0;
if(state.disks.some(d=>d.state===DiskState.SLOW))speed*=0.65;
if(state.disks.some(d=>d.state===DiskState.OVERHEAT))speed*=0.75;
speed*=(1+Math.log2(Math.max(2,n))*0.12);
const realSec=Math.max(60,Math.round(sizeGB/speed));
// Modalità didattica: cappato a 1015s, calcola fattore di accelerazione
const didacticSec=Math.round(10+Math.random()*5); // 1015s
state.rebuild.realSec=realSec;
state.rebuild.speedFactor=Math.round(realSec/didacticSec);
return didacticSec;
const realSec=Math.round(sizeGB/speed);
if(realSec<120)return realSec+" secondi";
if(realSec<3600){const m=Math.round(realSec/60);return"~"+m+" minuti";}
const h=Math.floor(realSec/3600),m=Math.round((realSec%3600)/60);
return m>0?"~"+h+"h "+m+"min":"~"+h+" ore";
}
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(){
@@ -3414,13 +3664,10 @@ $ 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;
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");
const realMsg=rebuildRealTimeMsg();
pushDmesg("info",`md0: rebuild started (simulazione ~${state.rebuild.etaSec}s)`);
termPrint(`mdadm: rebuild avviato — completamento in ~${state.rebuild.etaSec}s (simulazione).`,"ok");
termPrint(`⚠ Tempo reale equivalente su hardware reale con ${state.diskSizeGB}GB: ${realMsg}.`,"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;
@@ -3432,12 +3679,7 @@ $ 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");
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");
termPrint("mdadm: rebuild completato.","ok");
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;
}
@@ -3477,7 +3719,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,realSec:0,speedFactor:1}; state.mode="LAB"; state.exerciseOn=false;
state.rebuild={active:false,start:0,etaSec:0,progress:0}; 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";
@@ -3510,8 +3752,8 @@ $ rm /mnt/verifica.txt
raid10_pairfail:()=>{ createArray();raidLevelEl.value="10";state.raidLevel=10;diskCountEl.value="6";state.disks=Array.from({length:6},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[0].state=DiskState.FAILED;state.disks[1].state=DiskState.FAILED;pushDmesg("err","RAID10: mirror pair lost => array FAILED (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"raid10_pairfail",goalHtml:`RAID10: perso un mirror pair → array <b>FAILED</b>. Obiettivo: diagnostica + conclusione corretta: <code>conclude restore da backup</code>.`,solution:["cat /proc/mdstat","mdadm --detail /dev/md0","conclude restore da backup"],checkpoints:{diag:false,explained:false},done:false,startedAt:Date.now(),seed:"raid10_pairfail"};termPrint("Scenario caricato: RAID10 pair fail (atteso FAILED)","info");render(); },
rebuild_interrupted:()=>{ createArray();raidLevelEl.value="5";state.raidLevel=5;diskCountEl.value="4";state.disks=Array.from({length:4},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[2].state=DiskState.FAILED;pushDmesg("warn","/dev/sdc: disk FAILED (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"rebuild_interrupted",goalHtml:`RAID5 degradato. Obiettivo: sostituire disco, avviare rebuild e interromperlo (blackout) con <code>powerfail</code>, poi riavviarlo.`,solution:["mdadm --remove /dev/md0 /dev/sdc","mdadm --add /dev/md0 /dev/sde","mdadm --rebuild /dev/md0","powerfail","mdadm --rebuild /dev/md0","cat /proc/mdstat"],checkpoints:{started:false,stopped:false,restarted:false,ok:false},done:false,startedAt:Date.now(),seed:"rebuild_interrupted"};termPrint("Scenario caricato: rebuild_interrupted","info");render(); },
wrong_size_spare:()=>{ createArray();raidLevelEl.value="5";state.raidLevel=5;diskCountEl.value="4";state.disks=Array.from({length:4},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[1].state=DiskState.FAILED;pushDmesg("warn","/dev/sdb: disk FAILED (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"wrong_size_spare",goalHtml:`RAID5 degradato. Obiettivo: provare spare più piccolo (errore), poi spare corretto e rebuild.<br>Prova: <code>mdadm --add /dev/md0 /dev/sdz --size 500</code> → rebuild deve fallire.`,solution:["mdadm --add /dev/md0 /dev/sdz --size 500","mdadm --rebuild /dev/md0","mdadm --add /dev/md0 /dev/sde","mdadm --rebuild /dev/md0","cat /proc/mdstat"],checkpoints:{triedSmall:false,addedOk:false,ok:false},done:false,startedAt:Date.now(),seed:"wrong_size_spare"};termPrint("Scenario caricato: wrong_size_spare","info");render(); },
crc_errors:()=>{ createArray();raidLevelEl.value="5";state.raidLevel=5;diskCountEl.value="4";state.disks=Array.from({length:4},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[3].state=DiskState.CRC;state.disks[3].smart.crc=24;pushDmesg("warn","/dev/sdd: UDMA CRC errors detected (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"crc_errors",goalHtml:`CRC errors: probabile cavo/connessione. Obiettivo: diagnosticare con <code>dmesg | tail</code> e <code>smartctl -a /dev/sdd</code> e concludere: <code>conclude sostituire cavo SATA</code>.`,solution:["dmesg | tail","smartctl -a /dev/sdd","conclude sostituire cavo SATA"],checkpoints:{diag:false,explained:false},done:false,startedAt:Date.now(),seed:"crc_errors"};termPrint("Scenario caricato: crc_errors","info");render(); },
overheat:()=>{ createArray();raidLevelEl.value="6";state.raidLevel=6;diskCountEl.value="5";state.disks=Array.from({length:5},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[2].state=DiskState.OVERHEAT;state.disks[2].smart.temp=60;pushDmesg("warn","/dev/sdc: temperature critical (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"overheat",goalHtml:`Disco in overheat. Obiettivo: diagnosi (<code>smartctl</code> + <code>dmesg | tail</code>) e conclusione: <code>conclude migliorare raffreddamento</code>.`,solution:["smartctl -a /dev/sdc","dmesg | tail","conclude migliorare raffreddamento"],checkpoints:{diag:false,explained:false},done:false,startedAt:Date.now(),seed:"overheat"};termPrint("Scenario caricato: overheat","info");render(); }
crc_errors:()=>{ createArray();raidLevelEl.value="5";state.raidLevel=5;diskCountEl.value="4";state.disks=Array.from({length:4},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[3].state=DiskState.CRC;state.disks[3].smart.crc=24;pushDmesg("warn","/dev/sdd: UDMA CRC errors detected (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"crc_errors",goalHtml:`CRC errors: probabile cavo/connessione. Diagnosi: <code>dmesg | tail</code> e <code>smartctl -a /dev/sdd</code>. Intervento: <code>replace cable /dev/sdd</code> oppure concludere con <code>conclude sostituire cavo SATA</code>.`,solution:["dmesg | tail","smartctl -a /dev/sdd","replace cable /dev/sdd"],checkpoints:{diag:false,explained:false,cableReplaced:false},done:false,startedAt:Date.now(),seed:"crc_errors"};termPrint("Scenario caricato: crc_errors","info");render(); },
overheat:()=>{ createArray();raidLevelEl.value="6";state.raidLevel=6;diskCountEl.value="5";state.disks=Array.from({length:5},(_,i)=>makeDisk(i,1000));diskSizeEl.value="1000";state.diskSizeGB=1000;state.disks[2].state=DiskState.OVERHEAT;state.disks[2].smart.temp=60;pushDmesg("warn","/dev/sdc: temperature critical (scenario)");state.mode="EXERCISE";state.exerciseOn=true;state.scenario={name:"overheat",goalHtml:`Disco in overheat. Diagnosi: <code>smartctl -a /dev/sdc</code> + <code>dmesg | tail</code>. Intervento: <code>cool down /dev/sdc</code> oppure concludere con <code>conclude migliorare raffreddamento</code>.`,solution:["smartctl -a /dev/sdc","dmesg | tail","cool down /dev/sdc"],checkpoints:{diag:false,explained:false,cooled:false},done:false,startedAt:Date.now(),seed:"overheat"};termPrint("Scenario caricato: overheat","info");render(); }
};
function loadScenario(name){ const fn=scenarios[name]; if(!fn)return termPrint(`scenario: '${name}' non trovato. Usa 'scenario list'.`,"err"); fn(); scenarioCheck(); }
@@ -3596,7 +3838,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)","","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 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","","Interventi hardware (simulati):"," replace cable /dev/sdX (sostituisce cavo SATA su disco in stato CRC)"," cool down /dev/sdX (intervento raffreddamento su disco in stato OVERHEAT)","","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:");
@@ -3623,7 +3865,6 @@ $ 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));
@@ -3635,19 +3876,6 @@ $ 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";
@@ -3760,6 +3988,62 @@ $ rm /mnt/verifica.txt
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(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==="replace"&&t[1]==="cable"&&t[2]){
const dev=t[2].replace(/^\/dev\//,"");
const d=state.disks.find(x=>x.dev==="/dev/"+dev)||state.disks.find(x=>x.dev===dev);
if(!d)return termPrint(`replace cable: /dev/${dev} non trovato nell'array.`,"err");
if(d.state!==DiskState.CRC)return termPrint(`replace cable: /dev/${dev} non è in stato CRC. Questo comando serve solo per dischi con errori CRC.`,"warn");
const oldCrc=d.smart.crc;
d.smart.crc=0;
d.state=DiskState.OK;
pushDmesg("info",`/dev/${dev}: SATA cable replaced — link reset, CRC counter cleared`);
pushDmesg("info",`ata${d.id+1}.00: SATA link up (cable substitution, ok)`);
termPrint(
`[sostituzione cavo SATA su /dev/${dev}]\n`+
` Cavo vecchio rimosso, nuovo cavo installato.\n`+
` Link SATA reset: il controller reinizializza il canale.\n`+
` UDMA_CRC_Error_Count: ${oldCrc} → 0 (azzerato dal reset)\n`+
` Stato disco: CRC → OK\n`+
`\n`+
`→ Monitorare nelle prossime ore con 'smartctl -a /dev/${dev}'\n`+
` Se CRC rimane 0: il problema era il cavo (risolto).\n`+
` Se CRC torna a crescere: il problema è nel disco o nel controller.`,
"ok"
);
if(state.scenario&&state.scenario.name==="crc_errors"){
if(!state.scenario.checkpoints.cableReplaced){state.scenario.checkpoints.cableReplaced=true;addScore(8,"(scenario) cavo SATA sostituito correttamente.");}
if(!state.scenario.checkpoints.explained){state.scenario.checkpoints.explained=true;addScore(6,"(scenario) risoluzione applicata.");}
}
render();scenarioCheck();return;
}
if(c0==="cool"&&t[1]==="down"&&t[2]){
const dev=t[2].replace(/^\/dev\//,"");
const d=state.disks.find(x=>x.dev==="/dev/"+dev)||state.disks.find(x=>x.dev===dev);
if(!d)return termPrint(`cool down: /dev/${dev} non trovato nell'array.`,"err");
if(d.state!==DiskState.OVERHEAT)return termPrint(`cool down: /dev/${dev} non è in stato OVERHEAT. Questo comando serve solo per dischi surriscaldati.`,"warn");
const oldTemp=d.smart.temp;
d.smart.temp=38+(d.id%4);
d.state=DiskState.OK;
pushDmesg("info",`/dev/${dev}: temperature normalized (${oldTemp}°C → ${d.smart.temp}°C)`);
pushDmesg("info",`ata${d.id+1}.00: temperature within safe range — thermal throttling removed`);
termPrint(
`[intervento di raffreddamento su /dev/${dev}]\n`+
` Azioni simulate: pulizia ventole, spaziatura dischi, miglioramento airflow.\n`+
` Temperatura: ${oldTemp}°C → ${d.smart.temp}°C (zona ottimale: 20-45°C)\n`+
` Throttling termico: rimosso\n`+
` Stato disco: OVERHEAT → OK\n`+
`\n`+
`→ Il disco è di nuovo operativo, ma va monitorato.\n`+
` Consiglio: pianificare sostituzione preventiva (disco già termicamente stressato).\n`+
` Usa 'smartctl -a /dev/${dev}' per verificare gli altri attributi SMART.`,
"ok"
);
if(state.scenario&&state.scenario.name==="overheat"){
if(!state.scenario.checkpoints.cooled){state.scenario.checkpoints.cooled=true;addScore(8,"(scenario) raffreddamento applicato correttamente.");}
if(!state.scenario.checkpoints.explained){state.scenario.checkpoints.explained=true;addScore(6,"(scenario) risoluzione applicata.");}
}
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");
@@ -3782,13 +4066,14 @@ $ rm /mnt/verifica.txt
}
function teacherDecode(raw){
try{
const s=raw.trim().toUpperCase().replace(/\s/g,"");
if(!s.startsWith("TC-"))return null;
const inner=s.slice(3);
const s=raw.trim().replace(/\s/g,"");
// Accetta sia maiuscolo che minuscolo per il prefisso TC-
if(!s.toUpperCase().startsWith("TC-"))return null;
const inner=s.slice(3); // preserva il case originale del base64
const lastDash=inner.lastIndexOf("-");
if(lastDash<0)return null;
const b64=inner.slice(0,lastDash);
const cs2=inner.slice(lastDash+1);
const b64=inner.slice(0,lastDash); // base64: case-sensitive, non toccare
const cs2=inner.slice(lastDash+1).toUpperCase(); // checksum: solo questo in maiuscolo
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];
@@ -3853,15 +4138,29 @@ $ rm /mnt/verifica.txt
capSpan.textContent=tfCapacityText(level,n,parseInt($("tfDiskSize").value)||1000)+" —";
preview.appendChild(capSpan);
// salva selezioni precedenti
// salva selezioni precedenti (leggi dal data-value del cdd-btn)
const existing={};
$("tfDiskFaultRows").querySelectorAll(".tf-fault-select").forEach(sel=>{
existing[sel.dataset.dev]=sel.value;
$("tfDiskFaultRows").querySelectorAll(".cdd-btn").forEach(btn=>{
existing[btn.dataset.dev]=btn.dataset.value||"";
});
// genera chip preview e righe fault
const faultGrid=$("tfDiskFaultRows");
faultGrid.innerHTML="";
// chiudi tutti i dropdown aperti al click fuori
if(!window._cddDocListener){
window._cddDocListener=true;
document.addEventListener("click",(e)=>{
if(!e.target.closest(".cdd-wrap")){
document.querySelectorAll(".cdd-menu.open").forEach(m=>{
m.classList.remove("open");
m.previousElementSibling&&m.previousElementSibling.classList.remove("open");
});
}
});
}
for(let i=0;i<n;i++){
const dev="/dev/"+devNames[i];
const chip=document.createElement("span");
@@ -3873,33 +4172,74 @@ $ rm /mnt/verifica.txt
// riga fault
const row=document.createElement("div");
row.className="tf-fault-row";
// label disco
const devLabel=document.createElement("span");
devLabel.className="tf-fault-dev";
devLabel.style.color="rgba(255,255,255,.92)";
devLabel.textContent=dev;
const sel=document.createElement("select");
sel.className="tf-fault-select";
sel.dataset.dev=dev;
sel.dataset.idx=String(i);
// ── custom dropdown ──
const savedVal=existing[dev]||"";
const wrap=document.createElement("div");
wrap.className="cdd-wrap";
const btn=document.createElement("div");
btn.className="cdd-btn"+(savedVal?` val-${savedVal}`:"");
btn.dataset.dev=dev;
btn.dataset.idx=String(i);
btn.dataset.value=savedVal;
btn.innerHTML=`<span class="cdd-label">${faultLabels[savedVal]||faultLabels[""]}</span>`+
`<svg class="cdd-arrow" width="11" height="7" viewBox="0 0 11 7" fill="none">`+
`<path d="M1 1l4.5 4.5L10 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`;
const menu=document.createElement("div");
menu.className="cdd-menu";
Object.entries(faultLabels).forEach(([val,label])=>{
const opt=document.createElement("option");
opt.value=val; opt.textContent=label;
sel.appendChild(opt);
const opt=document.createElement("div");
opt.className="cdd-opt"+(val===savedVal?" selected":"");
opt.dataset.val=val;
opt.textContent=label;
opt.addEventListener("click",(e)=>{
e.stopPropagation();
const prev=btn.dataset.value||"";
btn.dataset.value=val;
btn.querySelector(".cdd-label").textContent=label;
btn.className="cdd-btn"+(val?` val-${val}`:"");
menu.querySelectorAll(".cdd-opt").forEach(o=>o.classList.toggle("selected",o.dataset.val===val));
menu.classList.remove("open");
btn.classList.remove("open");
// aggiorna desc
desc.textContent=faultDescriptions[val]||"";
// aggiorna chip preview
const chipEl=$(`tf-chip-${i}`);
if(chipEl)chipEl.className="tf-disk-chip"+(val?` fault-${val}`:"");
});
menu.appendChild(opt);
});
// ripristina selezione precedente se stesso disco
if(existing[dev])sel.value=existing[dev];
btn.addEventListener("click",(e)=>{
e.stopPropagation();
const wasOpen=menu.classList.contains("open");
// chiudi tutti gli altri
document.querySelectorAll(".cdd-menu.open").forEach(m=>{
m.classList.remove("open");
m.previousElementSibling&&m.previousElementSibling.classList.remove("open");
});
if(!wasOpen){ menu.classList.add("open"); btn.classList.add("open"); }
});
wrap.appendChild(btn);
wrap.appendChild(menu);
// desc
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}`:"");
});
desc.style.color="rgba(255,255,255,.75)";
desc.textContent=faultDescriptions[savedVal]||"";
row.appendChild(devLabel);
row.appendChild(sel);
row.appendChild(wrap);
row.appendChild(desc);
faultGrid.appendChild(row);
}
@@ -3918,8 +4258,9 @@ $ rm /mnt/verifica.txt
// 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});
$("tfDiskFaultRows").querySelectorAll(".cdd-btn").forEach(btn=>{
const val=btn.dataset.value||"";
if(val) faults.push({idx:parseInt(btn.dataset.idx),dev:btn.dataset.dev,type:val});
});
// validazione
@@ -3977,7 +4318,7 @@ $ rm /mnt/verifica.txt
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.rebuild={active:false,start:0,etaSec:0,progress:0};
state.score=0;state.hints=0;state.actions=[];
state.mode="EXERCISE";state.exerciseOn=true;