Compare commits
17 Commits
8519ba1854
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c36f43760
|
|||
|
4dd19b1104
|
|||
|
3e21620d1b
|
|||
|
ecee75af4a
|
|||
|
1233d7b441
|
|||
|
128df31840
|
|||
|
abcb6539fc
|
|||
| 06a0c2dd99 | |||
|
078998668e
|
|||
|
0a4cb932c6
|
|||
| b9675d03b0 | |||
| e0fe2a53e9 | |||
| 98e15d2e6c | |||
| 53460bd70e | |||
| 7f083f187d | |||
| 20f4531ad3 | |||
| 6cf5839194 |
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM nginx
|
||||
|
||||
# Copia pagine HTML
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
COPY simulatore.html /usr/share/nginx/html/simulatore.html
|
||||
COPY guida.html /usr/share/nginx/html/guida.html
|
||||
|
||||
EXPOSE 80
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
simulator:
|
||||
image: git.fiorencis.eu/eduprojects/dicampi-raid-simulator:latest
|
||||
ports:
|
||||
- 8080:80
|
||||
restart: unless-stopped
|
||||
network_mode: bridge
|
||||
48
index.html
Normal file
48
index.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simulatore RAID Web</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="margin-bottom: 0;">Simulatore RAID Web</h1>
|
||||
<p style="margin-bottom: 0;">by Marco di Campi, forked and tweaked by Andrea Fiorencis<p>
|
||||
<div class="buttons">
|
||||
<a href="simulatore.html" target="_blank">Simulatore</a>
|
||||
<a href="guida.html" target="_blank">Guida</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
616
simulatore.html
616
simulatore.html
@@ -106,10 +106,11 @@
|
||||
label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)}
|
||||
select,input[type="number"],input[type="text"]{
|
||||
height:40px;border-radius:12px;border:1px solid rgba(255,255,255,.14);
|
||||
background:rgba(10,14,28,.55);color:rgba(255,255,255,.92);
|
||||
background:#0d1220;color:rgba(255,255,255,.92);
|
||||
padding:0 12px;outline:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.04);
|
||||
font-family:var(--sans)
|
||||
}
|
||||
select option{background:#0d1220;color:rgba(255,255,255,.92);}
|
||||
select:focus,input:focus{border-color:rgba(167,139,250,.55);box-shadow:0 0 0 4px rgba(167,139,250,.15)}
|
||||
.controls{display:grid;grid-template-columns:1.1fr .7fr .7fr;gap:10px}
|
||||
@media (max-width: 720px){.controls{grid-template-columns:1fr}}
|
||||
@@ -895,6 +896,106 @@
|
||||
.server-example .se-icon{ font-size: 18px; flex-shrink: 0; }
|
||||
.server-example .se-text{ font-size: 12.5px; color: rgba(255,255,255,.75); line-height: 1.55; }
|
||||
.server-example .se-text b{ color: rgba(255,255,255,.92); }
|
||||
|
||||
/* ── PANNELLO DOCENTE ── */
|
||||
.teacher-form{
|
||||
display:flex;flex-direction:column;gap:12px;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:14px;padding:16px 18px;
|
||||
}
|
||||
.tf-row{ display:flex;gap:14px;flex-wrap:wrap; }
|
||||
.tf-field{ display:flex;flex-direction:column;gap:5px;flex:1;min-width:180px; }
|
||||
.tf-label{ font-size:11.5px;color:var(--muted2);font-weight:700;letter-spacing:.3px;text-transform:uppercase; }
|
||||
.tf-input,.tf-select{
|
||||
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);
|
||||
border-radius:10px;padding:9px 12px;color:var(--text);font-family:var(--sans);font-size:13px;
|
||||
outline:none;transition:border-color .2s;
|
||||
}
|
||||
.tf-select{ appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='rgba(255,255,255,.5)' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px; }
|
||||
.tf-input:focus,.tf-select:focus{ border-color:rgba(167,139,250,.55);box-shadow:0 0 0 3px rgba(167,139,250,.12); }
|
||||
.tf-hint{ font-size:11.5px;color:var(--muted2);line-height:1.45; }
|
||||
|
||||
.teacher-code-box{
|
||||
background:linear-gradient(135deg,rgba(167,139,250,.12),rgba(34,211,238,.08));
|
||||
border:1px solid rgba(167,139,250,.35);border-radius:14px;padding:16px 18px;
|
||||
}
|
||||
.tcb-header{ display:flex;justify-content:space-between;align-items:center;margin-bottom:12px; }
|
||||
.tcb-label{ font-size:13px;font-weight:700;color:var(--accent); }
|
||||
.tcb-code{
|
||||
font-family:var(--mono);font-size:15px;letter-spacing:2px;color:var(--text);
|
||||
background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.12);
|
||||
border-radius:10px;padding:12px 16px;word-break:break-all;line-height:1.6;
|
||||
user-select:all;cursor:text;
|
||||
}
|
||||
.tcb-meta{ margin-top:8px;font-size:11.5px;color:var(--muted2);line-height:1.5; }
|
||||
|
||||
/* section titles inside teacher form */
|
||||
.tf-section-title{
|
||||
font-size:12px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;
|
||||
color:var(--accent);padding:6px 0 4px;border-bottom:1px solid rgba(167,139,250,.2);
|
||||
margin-bottom:10px;
|
||||
}
|
||||
|
||||
/* array preview chips */
|
||||
.tf-array-preview{
|
||||
display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;padding:10px 12px;
|
||||
background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.08);border-radius:10px;
|
||||
min-height:38px;align-items:center;
|
||||
}
|
||||
.tf-disk-chip{
|
||||
display:inline-flex;align-items:center;gap:5px;
|
||||
padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;font-family:var(--mono);
|
||||
background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.3);color:var(--ok);
|
||||
transition:background .2s,border-color .2s,color .2s;
|
||||
}
|
||||
.tf-disk-chip.fault-FAILED{background:rgba(251,113,133,.15);border-color:rgba(251,113,133,.4);color:var(--bad);}
|
||||
.tf-disk-chip.fault-CRC{background:rgba(251,191,36,.12);border-color:rgba(251,191,36,.35);color:var(--warn);}
|
||||
.tf-disk-chip.fault-OVERHEAT{background:rgba(251,146,60,.12);border-color:rgba(251,146,60,.35);color:#fb923c;}
|
||||
.tf-disk-chip.fault-SLOW{background:rgba(96,165,250,.12);border-color:rgba(96,165,250,.3);color:var(--info);}
|
||||
|
||||
/* fault configuration grid */
|
||||
.tf-fault-grid{display:flex;flex-direction:column;gap:6px;}
|
||||
.tf-fault-row{
|
||||
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:10px;padding:8px 12px;
|
||||
}
|
||||
.tf-fault-dev{
|
||||
font-family:var(--mono);font-size:13px;font-weight:700;color:var(--text);
|
||||
min-width:52px;flex-shrink:0;
|
||||
}
|
||||
.tf-fault-select{
|
||||
background:#111827;border:1px solid rgba(255,255,255,.18);
|
||||
border-radius:8px;padding:6px 28px 6px 10px;color:rgba(255,255,255,.92);font-size:12.5px;
|
||||
font-family:var(--sans);outline:none;cursor:pointer;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='rgba(255,255,255,.55)' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-repeat:no-repeat;background-position:right 9px center;appearance:none;
|
||||
transition:border-color .2s;
|
||||
}
|
||||
.tf-fault-select option{background:#111827;color:rgba(255,255,255,.92);}
|
||||
.tf-fault-select:focus{border-color:rgba(167,139,250,.6);}
|
||||
.tf-fault-select.sel-FAILED{border-color:rgba(251,113,133,.6);color:var(--bad);}
|
||||
.tf-fault-select.sel-FAILED option{color:rgba(255,255,255,.92);}
|
||||
.tf-fault-select.sel-CRC{border-color:rgba(251,191,36,.6);color:var(--warn);}
|
||||
.tf-fault-select.sel-CRC option{color:rgba(255,255,255,.92);}
|
||||
.tf-fault-select.sel-OVERHEAT{border-color:rgba(251,146,60,.6);color:#fb923c;}
|
||||
.tf-fault-select.sel-OVERHEAT option{color:rgba(255,255,255,.92);}
|
||||
.tf-fault-select.sel-SLOW{border-color:rgba(96,165,250,.55);color:var(--info);}
|
||||
.tf-fault-select.sel-SLOW option{color:rgba(255,255,255,.92);}
|
||||
.tf-fault-desc{font-size:11.5px;color:var(--muted2);flex:1;line-height:1.35;}
|
||||
|
||||
/* Rebuild didactic banner */
|
||||
.rebuild-banner{
|
||||
margin-top:10px;padding:12px 14px;border-radius:16px;
|
||||
border:1px solid rgba(251,191,36,.40);
|
||||
background:linear-gradient(135deg, rgba(251,191,36,.10), rgba(251,146,60,.07));
|
||||
display:flex;align-items:flex-start;gap:12px;
|
||||
animation: rb-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes rb-pulse{0%,100%{border-color:rgba(251,191,36,.35)}50%{border-color:rgba(251,191,36,.70)}}
|
||||
.rb-icon{font-size:20px;flex-shrink:0;line-height:1}
|
||||
.rb-body{font-size:12.5px;color:rgba(255,255,255,.88);line-height:1.6}
|
||||
.rb-body b{color:#FBBF24}
|
||||
</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,7 +1156,10 @@
|
||||
<div class="card">
|
||||
<div class="head">
|
||||
<h2>💻 Terminale Linux (simulato)</h2>
|
||||
<div class="pill"><strong>Mode</strong> <span id="pillMode">LAB</span></div>
|
||||
<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">
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user