forked from admin/linux-practice
Compare commits
4 Commits
3f8d4c0ce6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5aafbe8b | ||
|
|
f61376aa8a | ||
|
|
83831b8622 | ||
|
|
9effdee625 |
2719
COURSE_TASKS.json
2719
COURSE_TASKS.json
File diff suppressed because it is too large
Load Diff
285
index.html
285
index.html
@@ -94,6 +94,25 @@
|
||||
}
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||||
.stat strong { font-size: 22px; }
|
||||
.mini-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.mini-stat {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.mini-stat span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.mini-stat strong { font-size: 20px; }
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -231,16 +250,90 @@
|
||||
.feedback.show { display: block; }
|
||||
.feedback.success { background: rgba(29,155,108,0.1); border: 1px solid rgba(29,155,108,0.22); color: #187653; }
|
||||
.feedback.warn { background: rgba(240,180,41,0.1); border: 1px solid rgba(240,180,41,0.24); color: #a26b04; }
|
||||
.mastery-note {
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.18);
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.diagnostic {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.18);
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.module-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.module-summary-card {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255,255,255,0.18);
|
||||
}
|
||||
.module-summary-card h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.module-summary-card p {
|
||||
margin: 0 0 8px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.stage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.stage-card {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255,255,255,0.18);
|
||||
}
|
||||
.stage-card h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.stage-card p {
|
||||
margin: 0 0 8px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.lesson-btn.done {
|
||||
border-color: rgba(29, 155, 108, 0.4);
|
||||
background: rgba(29, 155, 108, 0.1);
|
||||
}
|
||||
.ordered {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.9;
|
||||
}
|
||||
.empty { padding: 12px; border: 1px dashed var(--line); border-radius: 14px; color: var(--muted); text-align: center; }
|
||||
@media (max-width: 1180px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.sidebar { position: static; }
|
||||
.hero-grid, .detail-grid { grid-template-columns: 1fr; }
|
||||
.mini-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.module-summary-grid { grid-template-columns: 1fr; }
|
||||
.stage-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.shell { padding: 14px; }
|
||||
.topbar, .hero-head, .search, .terminal-input { flex-direction: column; align-items: stretch; }
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
.mini-stats { grid-template-columns: 1fr; }
|
||||
.stage-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -270,6 +363,8 @@
|
||||
<div class="stat"><span>Lessons</span><strong id="lessonCount">0</strong></div>
|
||||
<div class="stat"><span>Exercises</span><strong id="exerciseCount">0</strong></div>
|
||||
<div class="stat"><span>Commands</span><strong id="commandCount">0</strong></div>
|
||||
<div class="stat"><span>Mastered</span><strong id="masteredCount">0</strong></div>
|
||||
<div class="stat"><span>Completion</span><strong id="completionRate">0%</strong></div>
|
||||
</div>
|
||||
<div class="chip-list" id="commandTags"></div>
|
||||
<p class="muted" id="metaLine" style="margin-top: 12px;">Version --</p>
|
||||
@@ -290,6 +385,18 @@
|
||||
<h2>Course map</h2>
|
||||
<div class="modules" id="moduleList"><div class="empty">Loading modules...</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="eyebrow">Diagnostics</div>
|
||||
<h2>Course coverage check</h2>
|
||||
<div class="mini-stats">
|
||||
<div class="mini-stat"><span>Coverage</span><strong id="coverageRate">0%</strong></div>
|
||||
<div class="mini-stat"><span>Operations</span><strong id="operationCount">0</strong></div>
|
||||
<div class="mini-stat"><span>Reflection</span><strong id="reflectionCount">0</strong></div>
|
||||
<div class="mini-stat"><span>Unsupported</span><strong id="unsupportedCount">0</strong></div>
|
||||
</div>
|
||||
<div class="diagnostic" id="diagnosticNote" style="margin-top: 12px;">Checking course and sandbox alignment...</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
@@ -301,6 +408,7 @@
|
||||
<p class="muted" id="lessonGoal">Choose a lesson from the left to see goals, examples, common pitfalls, and runnable exercises.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-soft" type="button" id="masteryBtn">Mark mastered</button>
|
||||
<button class="btn-ghost" type="button" id="prevBtn">Previous</button>
|
||||
<button class="btn" type="button" id="nextBtn">Next</button>
|
||||
</div>
|
||||
@@ -320,6 +428,45 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mastery-note" id="masteryNote">Master a lesson after you can explain the command, predict the output, and connect it to a real operations step.</div>
|
||||
</section>
|
||||
|
||||
<section class="detail card">
|
||||
<div class="eyebrow">Learning stages</div>
|
||||
<h2 style="margin: 8px 0 0;">System route from Linux basics to operations habits</h2>
|
||||
<p class="muted">This view helps you study in phases instead of bouncing between unrelated commands.</p>
|
||||
<div class="stage-grid" id="stageGrid"></div>
|
||||
</section>
|
||||
|
||||
<section class="detail card">
|
||||
<div class="eyebrow">Module heatmap</div>
|
||||
<h2 style="margin: 8px 0 0;">See how the learning path is distributed</h2>
|
||||
<p class="muted">This panel helps you check whether the project really teaches both command basics and operations workflows.</p>
|
||||
<div class="module-summary-grid" id="moduleSummaryGrid"></div>
|
||||
</section>
|
||||
|
||||
<section class="detail card">
|
||||
<div class="eyebrow">Learning cockpit</div>
|
||||
<h2 style="margin: 8px 0 0;">Visual study map for the current lesson</h2>
|
||||
<p class="muted">These cards help you move from memorizing a command to understanding the workflow around it.</p>
|
||||
<div class="detail-grid" style="margin-top: 16px;">
|
||||
<article class="panel">
|
||||
<h3>Lesson radar</h3>
|
||||
<div class="mini-stats" id="lessonRadar"></div>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<h3>Experiment ladder</h3>
|
||||
<ol class="ordered" id="experimentList"></ol>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<h3>Observation checklist</h3>
|
||||
<ul id="observationList"></ul>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<h3>Related commands</h3>
|
||||
<div class="chip-list" id="relatedCommandList"></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail card">
|
||||
@@ -371,7 +518,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const state = { overview: null, lesson: null, theme: localStorage.getItem('linux_lab_theme') || 'light' };
|
||||
const state = {
|
||||
overview: null,
|
||||
lesson: null,
|
||||
theme: localStorage.getItem('linux_lab_theme') || 'light',
|
||||
progress: JSON.parse(localStorage.getItem('linux_lab_progress') || '{}')
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
setTheme(state.theme);
|
||||
@@ -385,9 +537,37 @@
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
if (state.lesson?.next_lesson) openLesson(state.lesson.next_lesson.id);
|
||||
});
|
||||
document.getElementById('masteryBtn').addEventListener('click', toggleMastery);
|
||||
await loadOverview();
|
||||
});
|
||||
|
||||
function saveProgress() {
|
||||
localStorage.setItem('linux_lab_progress', JSON.stringify(state.progress));
|
||||
}
|
||||
|
||||
function masteredCount() {
|
||||
return Object.values(state.progress).filter(Boolean).length;
|
||||
}
|
||||
|
||||
function isMastered(lessonId) {
|
||||
return Boolean(state.progress[lessonId]);
|
||||
}
|
||||
|
||||
function completionRate() {
|
||||
const total = state.overview?.meta?.lesson_count || 0;
|
||||
if (!total) return 0;
|
||||
return Math.round((masteredCount() / total) * 100);
|
||||
}
|
||||
|
||||
function toggleMastery() {
|
||||
if (!state.lesson?.id) return;
|
||||
state.progress[state.lesson.id] = !state.progress[state.lesson.id];
|
||||
saveProgress();
|
||||
renderOverview();
|
||||
renderModules();
|
||||
renderLesson();
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
const response = await fetch('/api/overview');
|
||||
state.overview = await response.json();
|
||||
@@ -404,10 +584,21 @@
|
||||
document.getElementById('lessonCount').textContent = meta.lesson_count || 0;
|
||||
document.getElementById('exerciseCount').textContent = meta.exercise_count || 0;
|
||||
document.getElementById('commandCount').textContent = meta.command_count || 0;
|
||||
document.getElementById('masteredCount').textContent = masteredCount();
|
||||
document.getElementById('completionRate').textContent = completionRate() + '%';
|
||||
document.getElementById('coverageRate').textContent = (state.overview.diagnostics?.coverage_rate ?? 0) + '%';
|
||||
document.getElementById('operationCount').textContent = state.overview.diagnostics?.operation_exercises ?? 0;
|
||||
document.getElementById('reflectionCount').textContent = state.overview.diagnostics?.reflection_exercises ?? 0;
|
||||
document.getElementById('unsupportedCount').textContent = state.overview.diagnostics?.unsupported_count ?? 0;
|
||||
document.getElementById('diagnosticNote').textContent = (state.overview.diagnostics?.unsupported_count ?? 0) === 0
|
||||
? 'All commands referenced by the course are covered by the sandbox command set.'
|
||||
: 'Some course commands are not fully supported by the sandbox yet.';
|
||||
document.getElementById('metaLine').textContent = `Version ${meta.version || '4.0'} | Updated ${meta.updated || '--'}`;
|
||||
renderRuntime(state.overview.runtime || {});
|
||||
renderCommandTags(state.overview.commands || []);
|
||||
renderModules();
|
||||
renderStageGrid();
|
||||
renderModuleSummaryGrid();
|
||||
}
|
||||
|
||||
function renderRuntime(runtime) {
|
||||
@@ -437,8 +628,8 @@
|
||||
<p>${escapeHtml(module.display_summary)}</p>
|
||||
<div class="lesson-list">
|
||||
${(module.lessons || []).map((lesson) => `
|
||||
<button class="lesson-btn ${state.lesson?.id === lesson.id ? 'active' : ''}" type="button" onclick="openLesson('${lesson.id}')">
|
||||
<strong>${escapeHtml(lesson.display_title)}</strong>
|
||||
<button class="lesson-btn ${state.lesson?.id === lesson.id ? 'active' : ''} ${isMastered(lesson.id) ? 'done' : ''}" type="button" onclick="openLesson('${lesson.id}')">
|
||||
<strong>${isMastered(lesson.id) ? 'Completed - ' : ''}${escapeHtml(lesson.display_title)}</strong>
|
||||
<span class="muted">${escapeHtml((lesson.command_tokens || []).join(' | ') || lesson.command || 'mixed commands')} | ${lesson.exercise_count || 0} exercises</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
@@ -447,6 +638,55 @@
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderModuleSummaryGrid() {
|
||||
const modules = state.overview.modules || [];
|
||||
const target = document.getElementById('moduleSummaryGrid');
|
||||
if (!modules.length) {
|
||||
target.innerHTML = '<div class="empty">No module diagnostics found.</div>';
|
||||
return;
|
||||
}
|
||||
target.innerHTML = modules.map((module) => `
|
||||
<article class="module-summary-card">
|
||||
<h4>${escapeHtml(module.display_title)}</h4>
|
||||
<p>${escapeHtml(module.display_summary)}</p>
|
||||
<div class="mini-stats">
|
||||
<div class="mini-stat"><span>Lessons</span><strong>${module.lesson_count || 0}</strong></div>
|
||||
<div class="mini-stat"><span>Exercises</span><strong>${module.exercise_count || 0}</strong></div>
|
||||
<div class="mini-stat"><span>Ops</span><strong>${module.operation_count || 0}</strong></div>
|
||||
<div class="mini-stat"><span>Commands</span><strong>${module.command_count || 0}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderStageGrid() {
|
||||
const groups = [
|
||||
{ title: 'Stage 1: Foundations', summary: 'Orientation, listing, file operations, and permissions.', range: [0, 1] },
|
||||
{ title: 'Stage 2: Search & Observation', summary: 'Text search, log preview, process, service, and network inspection.', range: [2, 3] },
|
||||
{ title: 'Stage 3: Incidents', summary: 'Disk, auth, and service-path drills that connect commands into playbooks.', range: [4, 4] },
|
||||
{ title: 'Stage 4: Automation & Platform', summary: 'Shell state, package tools, archives, monitoring, and scheduling.', range: [5, 7] }
|
||||
];
|
||||
const modules = state.overview.modules || [];
|
||||
const target = document.getElementById('stageGrid');
|
||||
target.innerHTML = groups.map((group) => {
|
||||
const selectedModules = modules.slice(group.range[0], group.range[1] + 1);
|
||||
const lessonTotal = selectedModules.reduce((sum, item) => sum + (item.lesson_count || 0), 0);
|
||||
const exerciseTotal = selectedModules.reduce((sum, item) => sum + (item.exercise_count || 0), 0);
|
||||
return `
|
||||
<article class="stage-card">
|
||||
<h4>${escapeHtml(group.title)}</h4>
|
||||
<p>${escapeHtml(group.summary)}</p>
|
||||
<div class="mini-stats">
|
||||
<div class="mini-stat"><span>Modules</span><strong>${selectedModules.length}</strong></div>
|
||||
<div class="mini-stat"><span>Lessons</span><strong>${lessonTotal}</strong></div>
|
||||
<div class="mini-stat"><span>Exercises</span><strong>${exerciseTotal}</strong></div>
|
||||
<div class="mini-stat"><span>Mastered</span><strong>${selectedModules.reduce((sum, item) => sum + (item.lessons || []).filter((lesson) => isMastered(lesson.id)).length, 0)}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function openLesson(lessonId, focusExerciseId = '') {
|
||||
const response = await fetch('/api/lesson?id=' + encodeURIComponent(lessonId));
|
||||
const payload = await response.json();
|
||||
@@ -470,9 +710,14 @@
|
||||
document.getElementById('lessonWhy').textContent = lesson.display_why || '';
|
||||
document.getElementById('classicView').textContent = lesson.classic_view || '';
|
||||
document.getElementById('afterClass').textContent = lesson.after_class || '';
|
||||
document.getElementById('masteryBtn').textContent = isMastered(lesson.id) ? 'Mark for review' : 'Mark mastered';
|
||||
document.getElementById('masteryNote').textContent = isMastered(lesson.id)
|
||||
? 'This lesson is marked as mastered. Revisit it if you cannot explain the command chain from memory.'
|
||||
: 'Mark a lesson as mastered after you can explain the command, predict the output, and choose the next troubleshooting step.';
|
||||
document.getElementById('prevBtn').disabled = !lesson.previous_lesson;
|
||||
document.getElementById('nextBtn').disabled = !lesson.next_lesson;
|
||||
document.getElementById('lessonCommands').innerHTML = (lesson.command_tokens || [lesson.command || 'mixed commands']).map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join('');
|
||||
renderLessonCockpit(lesson);
|
||||
renderList('conceptList', lesson.concepts, 'No concept notes yet.');
|
||||
renderList('scenarioList', lesson.scenarios, 'No scenario notes yet.');
|
||||
renderList('pitfallList', lesson.pitfalls, 'No pitfall notes yet.');
|
||||
@@ -483,6 +728,40 @@
|
||||
renderExercises(lesson.exercises || []);
|
||||
}
|
||||
|
||||
function renderLessonCockpit(lesson) {
|
||||
document.getElementById('lessonRadar').innerHTML = [
|
||||
{ label: 'Commands', value: lesson.command_tokens?.length || 1 },
|
||||
{ label: 'Exercises', value: lesson.exercise_count || 0 },
|
||||
{ label: 'Pitfalls', value: lesson.pitfalls?.length || 0 },
|
||||
{ label: 'Related', value: lesson.related_commands?.length || 0 }
|
||||
].map((item) => `
|
||||
<div class="mini-stat">
|
||||
<span>${escapeHtml(item.label)}</span>
|
||||
<strong>${item.value}</strong>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const experimentSteps = [
|
||||
`Confirm the goal: ${lesson.display_goal || 'Understand the command objective.'}`,
|
||||
`Run ${lesson.command || 'the main command'} and inspect the immediate output.`,
|
||||
`Compare the output with one example or scenario from this lesson.`,
|
||||
`Choose the next troubleshooting step based on what the command revealed.`
|
||||
];
|
||||
document.getElementById('experimentList').innerHTML = experimentSteps.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
||||
|
||||
const observationItems = [
|
||||
`Before running: confirm the current context and target path.`,
|
||||
`During output review: look for the field or line that answers the lesson goal.`,
|
||||
`After running: explain what should happen next in a real operations flow.`
|
||||
];
|
||||
document.getElementById('observationList').innerHTML = observationItems.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
||||
|
||||
const related = lesson.related_commands || [];
|
||||
document.getElementById('relatedCommandList').innerHTML = related.length
|
||||
? related.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join('')
|
||||
: '<span class="chip">No related commands yet</span>';
|
||||
}
|
||||
|
||||
function renderList(id, items, emptyText) {
|
||||
const target = document.getElementById(id);
|
||||
target.innerHTML = (items && items.length) ? items.map((item) => `<li>${escapeHtml(item)}</li>`).join('') : `<li>${escapeHtml(emptyText)}</li>`;
|
||||
|
||||
14
sandbox.py
14
sandbox.py
@@ -75,6 +75,17 @@ COMMAND_INDEX = {
|
||||
"python3": "/usr/bin/python3",
|
||||
}
|
||||
|
||||
SUPPORTED_COMMANDS = {
|
||||
"alias", "apt", "awk", "bash", "bc", "cat", "cd", "chmod", "chgrp", "chown",
|
||||
"clear", "cp", "crontab", "curl", "cut", "date", "df", "dig", "du", "echo",
|
||||
"env", "export", "fdisk", "find", "free", "grep", "head", "history", "id",
|
||||
"ifconfig", "ip", "journalctl", "kill", "last", "less", "ls", "lsof", "mkdir",
|
||||
"more", "mount", "mv", "netstat", "nohup", "passwd", "ping", "pkill", "ps",
|
||||
"pwd", "rm", "sed", "service", "sort", "ss", "stat", "su", "systemctl", "tail",
|
||||
"tar", "top", "touch", "traceroute", "uniq", "uptime", "vi", "vim", "w",
|
||||
"wc", "wget", "whereis", "which", "yum"
|
||||
}
|
||||
|
||||
|
||||
class LinuxSandbox:
|
||||
def __init__(self):
|
||||
@@ -121,6 +132,9 @@ class LinuxSandbox:
|
||||
node = self.get_node(self.resolve_path(path))
|
||||
return bool(node and len(node.get("perm", "")) >= 3 and node["perm"][2] == "1" or node and "x" in node.get("perm_human", ""))
|
||||
|
||||
def supported_commands(self) -> set[str]:
|
||||
return set(SUPPORTED_COMMANDS)
|
||||
|
||||
def _perm_human(self, perm: str, is_dir: bool) -> str:
|
||||
mapping = {
|
||||
"0": "---", "1": "--x", "2": "-w-", "3": "-wx",
|
||||
|
||||
79
server.py
79
server.py
@@ -28,6 +28,7 @@ PUBLIC_GET_PATHS = {
|
||||
"/privacy.html",
|
||||
"/api/course",
|
||||
"/api/course/search",
|
||||
"/api/diagnostics",
|
||||
"/api/health",
|
||||
"/api/lesson",
|
||||
"/api/overview",
|
||||
@@ -243,16 +244,80 @@ def build_lesson_preview(module: dict[str, Any], lesson: dict[str, Any]) -> dict
|
||||
|
||||
def build_module_preview(module: dict[str, Any]) -> dict[str, Any]:
|
||||
lessons = [build_lesson_preview(module, lesson) for lesson in module.get("lessons", [])]
|
||||
exercises = [exercise for lesson in module.get("lessons", []) for exercise in lesson.get("exercises", [])]
|
||||
operations = [exercise for exercise in exercises if exercise.get("type") == "operation"]
|
||||
reflections = [exercise for exercise in exercises if exercise.get("type") != "operation"]
|
||||
commands = sorted(
|
||||
{
|
||||
command
|
||||
for lesson in module.get("lessons", [])
|
||||
for command in split_commands(lesson.get("command"), lesson.get("id"))
|
||||
if command
|
||||
}
|
||||
)
|
||||
return {
|
||||
"id": module.get("id"),
|
||||
"display_title": module_title(module),
|
||||
"display_summary": module_summary(module),
|
||||
"lesson_count": len(module.get("lessons", [])),
|
||||
"exercise_count": sum(len(lesson.get("exercises", [])) for lesson in module.get("lessons", [])),
|
||||
"exercise_count": len(exercises),
|
||||
"operation_count": len(operations),
|
||||
"reflection_count": len(reflections),
|
||||
"command_count": len(commands),
|
||||
"command_tokens": commands,
|
||||
"lessons": lessons,
|
||||
}
|
||||
|
||||
|
||||
def extract_solution_commands() -> list[str]:
|
||||
commands: list[str] = []
|
||||
for module in COURSE.get("modules", []):
|
||||
for lesson in module.get("lessons", []):
|
||||
for exercise in lesson.get("exercises", []):
|
||||
for solution in exercise.get("solution", []):
|
||||
for segment in str(solution).split("|"):
|
||||
head = segment.strip().split(" ")[0].strip()
|
||||
if head:
|
||||
commands.append(head)
|
||||
return commands
|
||||
|
||||
|
||||
def build_diagnostics() -> dict[str, Any]:
|
||||
lessons = flatten_lessons()
|
||||
course_commands = sorted(
|
||||
{
|
||||
command
|
||||
for lesson in lessons
|
||||
for command in split_commands(lesson.get("command"), lesson.get("id"))
|
||||
if command
|
||||
}
|
||||
)
|
||||
solution_commands = sorted(set(extract_solution_commands()))
|
||||
required_commands = sorted(set(course_commands) | set(solution_commands))
|
||||
supported_commands = sorted(SANDBOX.supported_commands())
|
||||
unsupported_commands = [command for command in required_commands if command not in SANDBOX.supported_commands()]
|
||||
total_exercises = sum(len(lesson.get("exercises", [])) for lesson in lessons)
|
||||
operation_exercises = sum(
|
||||
1
|
||||
for lesson in lessons
|
||||
for exercise in lesson.get("exercises", [])
|
||||
if exercise.get("type") == "operation"
|
||||
)
|
||||
reflection_exercises = total_exercises - operation_exercises
|
||||
|
||||
return {
|
||||
"supported_command_count": len(supported_commands),
|
||||
"required_command_count": len(required_commands),
|
||||
"covered_command_count": len(required_commands) - len(unsupported_commands),
|
||||
"coverage_rate": 100 if not required_commands else round(((len(required_commands) - len(unsupported_commands)) / len(required_commands)) * 100)),
|
||||
"unsupported_commands": unsupported_commands,
|
||||
"course_commands": required_commands,
|
||||
"operation_exercises": operation_exercises,
|
||||
"reflection_exercises": reflection_exercises,
|
||||
"module_checks": [build_module_preview(module) for module in COURSE.get("modules", [])],
|
||||
}
|
||||
|
||||
|
||||
def build_exercise(exercise: dict[str, Any], lesson_stub: dict[str, Any]) -> dict[str, Any]:
|
||||
commands = split_commands(lesson_stub.get("command"), lesson_stub.get("id"))
|
||||
label = command_label(commands)
|
||||
@@ -343,6 +408,7 @@ def build_overview() -> dict[str, Any]:
|
||||
)
|
||||
exercise_count = sum(len(lesson.get("exercises", [])) for lesson in lessons)
|
||||
meta = COURSE.get("meta", {})
|
||||
diagnostics = build_diagnostics()
|
||||
|
||||
return {
|
||||
"meta": {
|
||||
@@ -362,6 +428,12 @@ def build_overview() -> dict[str, Any]:
|
||||
"cwd": SANDBOX.cwd,
|
||||
"user": SANDBOX.user,
|
||||
},
|
||||
"diagnostics": {
|
||||
"coverage_rate": diagnostics["coverage_rate"],
|
||||
"unsupported_count": len(diagnostics["unsupported_commands"]),
|
||||
"operation_exercises": diagnostics["operation_exercises"],
|
||||
"reflection_exercises": diagnostics["reflection_exercises"],
|
||||
},
|
||||
"modules": modules,
|
||||
"commands": commands[:24],
|
||||
}
|
||||
@@ -531,6 +603,7 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
"module_count": overview["meta"]["module_count"],
|
||||
"lesson_count": overview["meta"]["lesson_count"],
|
||||
"exercise_count": overview["meta"]["exercise_count"],
|
||||
"coverage_rate": overview["diagnostics"]["coverage_rate"],
|
||||
}
|
||||
)
|
||||
return
|
||||
@@ -543,6 +616,10 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_json(build_overview())
|
||||
return
|
||||
|
||||
if path == "/api/diagnostics":
|
||||
self.send_json(build_diagnostics())
|
||||
return
|
||||
|
||||
if path == "/api/lesson":
|
||||
lesson_id = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
|
||||
if not lesson_id:
|
||||
|
||||
Reference in New Issue
Block a user