Files
linux-practice/index.html
2026-03-23 16:03:32 +08:00

1677 lines
77 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Linux Learning Lab</title>
<style>
:root {
--bg: #f3f7fb;
--panel: rgba(255, 255, 255, 0.94);
--line: #d8e2ee;
--text: #102033;
--muted: #5d7187;
--brand: #0f6db6;
--accent: #1d9b6c;
--soft: #e9f3ff;
--terminal: #0e1724;
--shadow: 0 18px 40px rgba(16, 32, 51, 0.12);
}
[data-theme="dark"] {
--bg: #08111d;
--panel: rgba(12, 24, 38, 0.94);
--line: #1d3245;
--text: #edf4fb;
--muted: #9bb0c3;
--brand: #67baff;
--accent: #62d6ab;
--soft: rgba(103, 186, 255, 0.1);
--terminal: #06101c;
--shadow: 0 20px 44px rgba(0, 0, 0, 0.34);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top right, rgba(15, 109, 182, 0.15), transparent 25%),
radial-gradient(circle at bottom left, rgba(29, 155, 108, 0.12), transparent 20%),
var(--bg);
}
button, input { font: inherit; }
.shell { max-width: 1480px; margin: 0 auto; padding: 20px; }
.topbar, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 24px;
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 20px 24px;
margin-bottom: 16px;
align-items: center;
}
.title h1 { margin: 0 0 8px; font-size: 30px; }
.title p, .muted { color: var(--muted); line-height: 1.8; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
.btn, .btn-soft, .btn-ghost {
border: 0;
border-radius: 999px;
padding: 11px 16px;
cursor: pointer;
font-weight: 700;
}
.btn { color: #fff; background: linear-gradient(135deg, var(--brand), #37a1ff); }
.btn-soft { color: var(--brand); background: var(--soft); }
.btn-ghost { color: var(--text); background: transparent; border: 1px solid var(--line); }
.layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.sidebar { padding: 20px; position: sticky; top: 20px; }
.section { margin-bottom: 18px; }
.eyebrow { font-size: 12px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: var(--brand); }
h2 { margin: 8px 0 10px; font-size: 18px; }
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 12px;
}
.stat {
padding: 14px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255,255,255,0.3);
}
.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;
margin-top: 12px;
}
.search input {
flex: 1;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--line);
background: transparent;
color: var(--text);
outline: none;
}
.chip-list, .cmd-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.chip {
display: inline-flex;
align-items: center;
padding: 7px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: var(--soft);
color: var(--brand);
}
.results, .modules { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; max-height: 34vh; overflow: auto; }
.result, .lesson-btn, .module-card {
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255,255,255,0.18);
}
.result, .lesson-btn {
width: 100%;
text-align: left;
padding: 12px 14px;
color: var(--text);
cursor: pointer;
}
.result strong, .lesson-btn strong { display: block; margin-bottom: 4px; font-size: 14px; }
.module-card { padding: 14px; }
.module-card h3 { margin: 0 0 8px; font-size: 16px; }
.module-card p { margin: 0 0 10px; color: var(--muted); line-height: 1.7; }
.lesson-list { display: flex; flex-direction: column; gap: 8px; }
.lesson-btn.active { background: rgba(15, 109, 182, 0.12); border-color: rgba(15, 109, 182, 0.28); }
.main { display: flex; flex-direction: column; gap: 16px; }
.hero, .detail, .practice { padding: 22px; }
.hero-head {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 14px;
}
.hero-head h2 { margin: 8px 0 10px; font-size: 34px; }
.hero-grid, .detail-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 14px;
}
.panel {
border: 1px solid var(--line);
border-radius: 18px;
padding: 16px;
background: rgba(255,255,255,0.12);
}
.panel h3 { margin: 0 0 10px; font-size: 17px; }
.panel p, .panel li { color: var(--muted); line-height: 1.9; }
.panel ul { margin: 0; padding-left: 18px; }
.detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.code {
margin: 0;
padding: 13px 14px;
border-radius: 14px;
background: var(--terminal);
color: #d8e8ff;
font-family: Consolas, "Courier New", monospace;
font-size: 14px;
overflow: auto;
}
.examples { display: flex; flex-direction: column; gap: 10px; }
.exercise-list { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.exercise {
border: 1px solid var(--line);
border-radius: 18px;
padding: 16px;
background: rgba(255,255,255,0.12);
}
.badge {
display: inline-flex;
margin-bottom: 10px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(29, 155, 108, 0.12);
color: var(--accent);
font-size: 12px;
font-weight: 800;
}
.exercise h4 { margin: 0 0 10px; font-size: 20px; }
.terminal {
margin-top: 12px;
border-radius: 16px;
overflow: hidden;
border: 1px solid rgba(151,177,201,0.18);
background: linear-gradient(180deg, var(--terminal), #172435);
}
.terminal-head { padding: 11px 14px; color: #b8d4ef; border-bottom: 1px solid rgba(151,177,201,0.16); }
.terminal-input { display: flex; align-items: center; gap: 8px; padding: 12px 14px; }
.prompt { color: #62d6ab; font-family: Consolas, "Courier New", monospace; font-weight: 800; }
.terminal-input input {
flex: 1;
border: 0;
outline: none;
background: transparent;
color: #f4fbff;
font-family: Consolas, "Courier New", monospace;
}
.terminal-input button {
border: 0;
border-radius: 12px;
padding: 9px 13px;
background: linear-gradient(135deg, var(--brand), #37a1ff);
color: #fff;
cursor: pointer;
font-weight: 800;
}
.output {
margin: 0;
min-height: 100px;
padding: 14px;
white-space: pre-wrap;
color: #d8e8ff;
font-family: Consolas, "Courier New", monospace;
}
.feedback { display: none; margin-top: 10px; padding: 11px 13px; border-radius: 14px; line-height: 1.7; font-weight: 700; }
.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(auto-fit, minmax(240px, 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(auto-fit, minmax(210px, 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;
}
.guide-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.guide-card {
padding: 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.16);
}
.guide-card h4 { margin: 0 0 8px; font-size: 17px; }
.guide-card p { margin: 6px 0 0; }
.guide-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
}
.guide-meta .mini-stat strong {
font-size: 15px;
line-height: 1.6;
}
.variant-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.variant {
padding: 10px 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.18);
}
.variant code {
display: inline-block;
margin-bottom: 6px;
padding: 4px 8px;
border-radius: 999px;
background: var(--terminal);
color: #dceaff;
font-family: Consolas, "Courier New", monospace;
font-size: 13px;
}
.variant strong {
display: block;
font-size: 14px;
margin-bottom: 4px;
}
.span-2 { grid-column: span 2; }
.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)); }
.span-2 { grid-column: span 1; }
}
@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; }
.guide-meta { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="title">
<div class="eyebrow" id="topEyebrow">Linux Ops Learning Lab</div>
<h1 id="courseTitle">Linux Learning Lab</h1>
<p id="courseDescription">Search Linux lessons, practice commands in a sandbox, and build better troubleshooting habits.</p>
</div>
<div class="actions">
<button class="btn-ghost" type="button" id="themeBtn">Toggle theme</button>
<button class="btn-ghost" type="button" id="languageBtn">EN</button>
<button class="btn-soft" type="button" id="resetBtn" onclick="resetSandbox()">Reset sandbox</button>
<a class="btn-ghost" id="privacyLink" href="/privacy" target="_blank" rel="noreferrer">Privacy</a>
</div>
</header>
<div class="layout">
<aside class="sidebar card">
<section class="section">
<div class="eyebrow" id="overviewEyebrow">Overview</div>
<h2 id="overviewTitle">Course overview</h2>
<p class="muted" id="overviewText">Lessons are grouped by module and command family. The page focuses on readable summaries and guided sandbox practice.</p>
<div class="stats">
<div class="stat"><span id="moduleCountLabel">Modules</span><strong id="moduleCount">0</strong></div>
<div class="stat"><span id="lessonCountLabel">Lessons</span><strong id="lessonCount">0</strong></div>
<div class="stat"><span id="exerciseCountLabel">Exercises</span><strong id="exerciseCount">0</strong></div>
<div class="stat"><span id="commandCountLabel">Commands</span><strong id="commandCount">0</strong></div>
<div class="stat"><span id="masteredCountLabel">Mastered</span><strong id="masteredCount">0</strong></div>
<div class="stat"><span id="completionRateLabel">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>
</section>
<section class="section">
<div class="eyebrow" id="searchEyebrow">Search</div>
<h2 id="searchTitle">Quick lookup</h2>
<div class="search">
<input id="searchInput" type="text" placeholder="Try pwd, grep, module_1, or m1_l1_pwd" />
<button class="btn" type="button" id="searchBtn" onclick="runSearch()">Search</button>
</div>
<div class="results" id="searchResults"><div class="empty">Search by command, lesson id, or module id.</div></div>
</section>
<section class="section">
<div class="eyebrow" id="modulesEyebrow">Modules</div>
<h2 id="modulesTitle">Course map</h2>
<div class="modules" id="moduleList"><div class="empty">Loading modules...</div></div>
</section>
<section class="section">
<div class="eyebrow" id="diagnosticsEyebrow">Diagnostics</div>
<h2 id="diagnosticsTitle">Course coverage check</h2>
<div class="mini-stats">
<div class="mini-stat"><span id="coverageRateLabel">Coverage</span><strong id="coverageRate">0%</strong></div>
<div class="mini-stat"><span id="operationCountLabel">Operations</span><strong id="operationCount">0</strong></div>
<div class="mini-stat"><span id="reflectionCountLabel">Reflection</span><strong id="reflectionCount">0</strong></div>
<div class="mini-stat"><span id="unsupportedCountLabel">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">
<section class="hero card">
<div class="hero-head">
<div>
<div class="eyebrow" id="moduleLabel">Pick a lesson to begin</div>
<h2 id="lessonTitle">Turn Linux practice into workflow thinking</h2>
<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>
</div>
<div class="cmd-list" id="lessonCommands"></div>
<div class="hero-grid">
<div class="panel">
<h3 id="moduleSummaryTitle">Module summary</h3>
<p id="moduleSummary">This card shows the learning direction of the current module.</p>
</div>
<div class="panel">
<h3 id="runtimeTitle">Sandbox runtime</h3>
<p id="runtimeText">You can execute commands directly below. The platform returns simulated output and exercise feedback.</p>
<ul>
<li><span id="runtimeCwdLabel">Current directory</span>: <span id="runtimeCwd">/</span></li>
<li><span id="runtimeUserLabel">Current user</span>: <span id="runtimeUser">sandbox_user</span></li>
</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" id="stagesEyebrow">Learning stages</div>
<h2 id="stagesTitle" style="margin: 8px 0 0;">System route from Linux basics to operations habits</h2>
<p class="muted" id="stagesText">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" id="heatmapEyebrow">Module heatmap</div>
<h2 id="heatmapTitle" style="margin: 8px 0 0;">See how the learning path is distributed</h2>
<p class="muted" id="heatmapText">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" id="cockpitEyebrow">Learning cockpit</div>
<h2 id="cockpitTitle" style="margin: 8px 0 0;">Visual study map for the current lesson</h2>
<p class="muted" id="cockpitText">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 id="lessonRadarTitle">Lesson radar</h3>
<div class="mini-stats" id="lessonRadar"></div>
</article>
<article class="panel">
<h3 id="experimentTitle">Experiment ladder</h3>
<ol class="ordered" id="experimentList"></ol>
</article>
<article class="panel">
<h3 id="observationTitle">Observation checklist</h3>
<ul id="observationList"></ul>
</article>
<article class="panel">
<h3 id="relatedCommandsTitle">Related commands</h3>
<div class="chip-list" id="relatedCommandList"></div>
</article>
</div>
</section>
<section class="detail card">
<div class="eyebrow" id="depthEyebrow">Depth Layer</div>
<h2 id="depthTitle" style="margin: 8px 0 0;">Parameters, command history, and reinforcement</h2>
<p class="muted" id="depthText">Use this layer to extend beyond the base syntax into variants, Unix naming history, and review prompts that stick.</p>
<div class="detail-grid" style="margin-top: 16px;">
<article class="panel span-2">
<h3 id="commandDossierTitle">Command dossier</h3>
<div class="guide-stack" id="commandGuideList"></div>
</article>
<article class="panel">
<h3 id="opsExtensionTitle">Operations extension</h3>
<ul id="opsExtensionList"></ul>
</article>
<article class="panel">
<h3 id="reviewPromptsTitle">Review prompts</h3>
<ol class="ordered" id="reviewPromptList"></ol>
</article>
</div>
</section>
<section class="detail card">
<div class="detail-grid">
<article class="panel">
<h3 id="whyThisMattersTitle">Why this matters</h3>
<p id="lessonWhy">Understanding when to use a command matters more than memorizing flags in isolation.</p>
</article>
<article class="panel">
<h3 id="teachingAngleTitle">Teaching angle</h3>
<p id="classicView">Each lesson connects the command to a broader troubleshooting flow.</p>
</article>
<article class="panel">
<h3 id="coreIdeasTitle">Core ideas</h3>
<ul id="conceptList"></ul>
</article>
<article class="panel">
<h3 id="commonScenariosTitle">Common scenarios</h3>
<ul id="scenarioList"></ul>
</article>
<article class="panel">
<h3 id="commonPitfallsTitle">Common pitfalls</h3>
<ul id="pitfallList"></ul>
</article>
<article class="panel">
<h3 id="troubleshootingFlowTitle">Troubleshooting flow</h3>
<ul id="flowList"></ul>
</article>
<article class="panel">
<h3 id="takeawaysTitle">Takeaways</h3>
<ul id="takeawayList"></ul>
<p id="afterClass" style="margin-top: 10px;"></p>
</article>
<article class="panel">
<h3 id="exampleCommandsTitle">Example commands</h3>
<div class="examples" id="exampleList"></div>
</article>
</div>
</section>
<section class="practice card">
<div class="eyebrow" id="practiceEyebrow">Practice</div>
<h2 id="practiceTitle" style="margin: 8px 0 0;">Exercises and sandbox terminal</h2>
<p class="muted" id="practiceText">Operation tasks run in the simulated terminal. Reflection tasks keep the learning flow readable even when source data is noisy.</p>
<div class="exercise-list" id="exerciseList"></div>
</section>
</main>
</div>
</div>
<script>
const I18N = {
zh: {
topEyebrow: 'Linux 运维学习实验室',
courseTitle: 'Linux 学习实验室',
courseDescription: '搜索 Linux 课程、在沙箱里练命令,并把零散命令串成真正的排障习惯。',
themeToDark: '深色模式',
themeToLight: '浅色模式',
resetBtn: '重置沙箱',
privacyLink: '隐私说明',
overviewEyebrow: '总览',
overviewTitle: '课程总览',
overviewText: '课程按模块和命令家族分组,重点是把摘要讲清楚、把沙箱练习串起来。',
moduleCountLabel: '模块',
lessonCountLabel: '课程',
exerciseCountLabel: '练习',
commandCountLabel: '命令',
masteredCountLabel: '已掌握',
completionRateLabel: '完成率',
searchEyebrow: '搜索',
searchTitle: '快速定位',
searchPlaceholder: '试试 pwd、grep、module_1 或 m1_l1_pwd',
searchBtn: '搜索',
searchEmpty: '可以按命令名、课程 ID 或模块 ID 搜索。',
searchLoading: '正在搜索...',
searchNoResult: '没有找到结果,试试命令名或课程 ID。',
modulesEyebrow: '模块',
modulesTitle: '课程地图',
modulesLoading: '正在加载模块...',
diagnosticsEyebrow: '诊断',
diagnosticsTitle: '课程覆盖检查',
coverageRateLabel: '覆盖率',
operationCountLabel: '操作题',
reflectionCountLabel: '反思题',
unsupportedCountLabel: '未覆盖',
diagnosticChecking: '正在检查课程和沙箱的一致性...',
diagnosticSupported: '课程引用到的命令都已经被沙箱命令集覆盖。',
diagnosticUnsupported: '课程里仍有部分命令没有被沙箱完全支持。',
metaLine: (version, updated) => `版本 ${version} | 更新于 ${updated}`,
waitingCommandIndex: '等待命令索引...',
noModulesFound: '没有找到模块。',
completedPrefix: '已完成 - ',
exerciseUnit: '个练习',
mixedCommands: '混合命令',
noModuleDiagnostics: '暂无模块诊断数据。',
moduleSummaryLessons: '课程',
moduleSummaryExercises: '练习',
moduleSummaryOps: '操作题',
moduleSummaryCommands: '命令',
stagesEyebrow: '学习阶段',
stagesTitle: '从 Linux 基础走到运维习惯的学习路线',
stagesText: '这个视图帮助你按阶段推进,而不是在零散命令之间来回跳。',
stageGroups: [
{ title: '阶段 1基础定位', summary: '先建立目录、列表、文件操作和权限的直觉。', range: [0, 1] },
{ title: '阶段 2搜索与观察', summary: '进入文本检索、日志预览、进程、服务与网络观察。', range: [2, 3] },
{ title: '阶段 3故障处置', summary: '把磁盘、认证与服务链路故障串成完整排障流程。', range: [4, 4] },
{ title: '阶段 4自动化与平台', summary: '覆盖 shell 状态、包管理、归档、监控与定时任务。', range: [5, 7] },
{ title: '阶段 5交付与恢复', summary: '把 Web 交付、备份与恢复变成可演练的运维套路。', range: [8, 9] }
],
stageModules: '模块',
stageLessons: '课程',
stageExercises: '练习',
stageMastered: '已掌握',
heatmapEyebrow: '模块热力图',
heatmapTitle: '看看学习路径是怎么分布的',
heatmapText: '这个面板帮助你判断项目是否同时覆盖了命令基础和运维工作流。',
cockpitEyebrow: '学习驾驶舱',
cockpitTitle: '当前课程的可视化学习地图',
cockpitText: '这些卡片帮助你从记命令,走到理解命令周围的排障流程。',
lessonRadarTitle: '课程雷达',
experimentTitle: '实验阶梯',
observationTitle: '观察清单',
relatedCommandsTitle: '相关命令',
depthEyebrow: '深入层',
depthTitle: '参数、命令历史和强化记忆',
depthText: '这一层把基础语法继续延伸到常见变体、Unix 名称来源和复盘提示。',
commandDossierTitle: '命令档案',
opsExtensionTitle: '运维扩展',
reviewPromptsTitle: '复习提示',
whyThisMattersTitle: '为什么重要',
teachingAngleTitle: '教学视角',
coreIdeasTitle: '核心概念',
commonScenariosTitle: '常见场景',
commonPitfallsTitle: '常见坑点',
troubleshootingFlowTitle: '排障流程',
takeawaysTitle: '本课收获',
exampleCommandsTitle: '示例命令',
practiceEyebrow: '练习',
practiceTitle: '练习题与沙箱终端',
practiceText: '操作题会直接在模拟终端里运行;反思题则保留成清晰的学习链路,避免被脏数据打断。',
initialModuleLabel: '先选一节课开始',
initialLessonTitle: '把 Linux 练习变成流程化思维',
initialLessonGoal: '从左侧选择一节课,查看目标、示例、常见坑点和可执行练习。',
moduleSummaryTitle: '模块摘要',
moduleSummaryPlaceholder: '这里会显示当前模块的学习方向。',
runtimeTitle: '沙箱运行态',
runtimeText: '你可以直接在下方执行命令,平台会返回模拟输出和练习反馈。',
runtimeCwdLabel: '当前目录',
runtimeUserLabel: '当前用户',
masteryDefault: '标记已掌握',
masteryReview: '标记待复习',
masteryPendingNote: '当你能解释命令目标、预判输出,并说出下一步排障动作时,再标记掌握。',
masteryDoneNote: '这节课已经标记为已掌握;如果你已经说不清命令链路,就回头复习。',
currentLessonFallback: '当前课程',
linuxLessonFallback: 'Linux 课程',
radarCommands: '命令',
radarExercises: '练习',
radarPitfalls: '坑点',
radarRelated: '相关项',
experimentStep1: (goal) => `先确认目标:${goal || '先搞清楚这条命令要解决什么问题。'}`,
experimentStep2: (command) => `执行 ${command || '主命令'},先看最直接的输出。`,
experimentStep3: '把输出和本课的一个示例或场景对照起来。',
experimentStep4: '根据命令揭示出来的状态,选择下一步排障动作。',
observationStep1: '执行前:先确认当前上下文和目标路径。',
observationStep2: '看输出时:找到真正回答本课目标的那一行或那个字段。',
observationStep3: '执行后:说明在真实运维流程里下一步该做什么。',
noRelatedCommands: '暂无相关命令',
noConcepts: '暂无概念说明。',
noScenarios: '暂无场景说明。',
noPitfalls: '暂无坑点说明。',
noFlow: '暂无流程说明。',
noTakeaways: '暂无本课收获。',
noExamples: '暂无示例命令。',
noCommandGuides: '暂无命令档案说明。',
noOpsExtensions: '暂无扩展练习。',
noReviewPrompts: '暂无复习提示。',
noExercises: '这节课暂时还没有练习。',
operationTask: '操作题',
scenarioReflection: '场景反思',
understandingTask: '理解题',
exerciseTitleFallback: '操作题',
reflectionTaskFallback: '反思题',
exerciseHintFallback: '输入一条命令并观察输出。',
terminalHead: 'Linux 沙箱终端',
runBtn: '运行',
waitingForCommand: '等待输入命令...',
running: '运行中...',
noOutput: '(没有输出)',
requestFailedPrefix: '请求失败:',
requestFailed: '请求失败,请稍后再试。',
sandboxResetComplete: '沙箱已重置。',
commandFallback: '命令',
nameOrigin: '名字来源',
learningHook: '记忆钩子',
noOrigin: '暂无来源说明。',
noFunFact: '暂无强化提示。',
examplePrefix: '示例:',
nextSuggestionPrefix: '下一步:',
prevBtn: '上一课',
nextBtn: '下一课',
moduleType: '模块',
lessonType: '课程',
exerciseType: '练习',
tryFirstPrefix: '可先尝试:',
originalHintLabel: '原始提示',
originalQuestionLabel: '原题',
referenceAnswerLabel: '参考答案'
},
en: {
topEyebrow: 'Linux Ops Learning Lab',
courseTitle: 'Linux Learning Lab',
courseDescription: 'Search Linux lessons, practice commands in a sandbox, and build better troubleshooting habits.',
themeToDark: 'Dark mode',
themeToLight: 'Light mode',
resetBtn: 'Reset sandbox',
privacyLink: 'Privacy',
overviewEyebrow: 'Overview',
overviewTitle: 'Course overview',
overviewText: 'Lessons are grouped by module and command family. The page focuses on readable summaries and guided sandbox practice.',
moduleCountLabel: 'Modules',
lessonCountLabel: 'Lessons',
exerciseCountLabel: 'Exercises',
commandCountLabel: 'Commands',
masteredCountLabel: 'Mastered',
completionRateLabel: 'Completion',
searchEyebrow: 'Search',
searchTitle: 'Quick lookup',
searchPlaceholder: 'Try pwd, grep, module_1, or m1_l1_pwd',
searchBtn: 'Search',
searchEmpty: 'Search by command, lesson id, or module id.',
searchLoading: 'Searching...',
searchNoResult: 'No results found. Try a command name or lesson id.',
modulesEyebrow: 'Modules',
modulesTitle: 'Course map',
modulesLoading: 'Loading modules...',
diagnosticsEyebrow: 'Diagnostics',
diagnosticsTitle: 'Course coverage check',
coverageRateLabel: 'Coverage',
operationCountLabel: 'Operations',
reflectionCountLabel: 'Reflection',
unsupportedCountLabel: 'Unsupported',
diagnosticChecking: 'Checking course and sandbox alignment...',
diagnosticSupported: 'All commands referenced by the course are covered by the sandbox command set.',
diagnosticUnsupported: 'Some course commands are not fully supported by the sandbox yet.',
metaLine: (version, updated) => `Version ${version} | Updated ${updated}`,
waitingCommandIndex: 'Waiting for command index',
noModulesFound: 'No modules found.',
completedPrefix: 'Completed - ',
exerciseUnit: 'exercises',
mixedCommands: 'mixed commands',
noModuleDiagnostics: 'No module diagnostics found.',
moduleSummaryLessons: 'Lessons',
moduleSummaryExercises: 'Exercises',
moduleSummaryOps: 'Ops',
moduleSummaryCommands: 'Commands',
stagesEyebrow: 'Learning stages',
stagesTitle: 'System route from Linux basics to operations habits',
stagesText: 'This view helps you study in phases instead of bouncing between unrelated commands.',
stageGroups: [
{ 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] },
{ title: 'Stage 5: Delivery & Recovery', summary: 'Web delivery checks, backups, restore paths, and recovery discipline.', range: [8, 9] }
],
stageModules: 'Modules',
stageLessons: 'Lessons',
stageExercises: 'Exercises',
stageMastered: 'Mastered',
heatmapEyebrow: 'Module heatmap',
heatmapTitle: 'See how the learning path is distributed',
heatmapText: 'This panel helps you check whether the project really teaches both command basics and operations workflows.',
cockpitEyebrow: 'Learning cockpit',
cockpitTitle: 'Visual study map for the current lesson',
cockpitText: 'These cards help you move from memorizing a command to understanding the workflow around it.',
lessonRadarTitle: 'Lesson radar',
experimentTitle: 'Experiment ladder',
observationTitle: 'Observation checklist',
relatedCommandsTitle: 'Related commands',
depthEyebrow: 'Depth layer',
depthTitle: 'Parameters, command history, and reinforcement',
depthText: 'Use this layer to extend beyond the base syntax into variants, Unix naming history, and review prompts that stick.',
commandDossierTitle: 'Command dossier',
opsExtensionTitle: 'Operations extension',
reviewPromptsTitle: 'Review prompts',
whyThisMattersTitle: 'Why this matters',
teachingAngleTitle: 'Teaching angle',
coreIdeasTitle: 'Core ideas',
commonScenariosTitle: 'Common scenarios',
commonPitfallsTitle: 'Common pitfalls',
troubleshootingFlowTitle: 'Troubleshooting flow',
takeawaysTitle: 'Takeaways',
exampleCommandsTitle: 'Example commands',
practiceEyebrow: 'Practice',
practiceTitle: 'Exercises and sandbox terminal',
practiceText: 'Operation tasks run in the simulated terminal. Reflection tasks keep the learning flow readable even when source data is noisy.',
initialModuleLabel: 'Pick a lesson to begin',
initialLessonTitle: 'Turn Linux practice into workflow thinking',
initialLessonGoal: 'Choose a lesson from the left to see goals, examples, common pitfalls, and runnable exercises.',
moduleSummaryTitle: 'Module summary',
moduleSummaryPlaceholder: 'This card shows the learning direction of the current module.',
runtimeTitle: 'Sandbox runtime',
runtimeText: 'You can execute commands directly below. The platform returns simulated output and exercise feedback.',
runtimeCwdLabel: 'Current directory',
runtimeUserLabel: 'Current user',
masteryDefault: 'Mark mastered',
masteryReview: 'Mark for review',
masteryPendingNote: 'Mark a lesson as mastered after you can explain the command, predict the output, and choose the next troubleshooting step.',
masteryDoneNote: 'This lesson is marked as mastered. Revisit it if you cannot explain the command chain from memory.',
currentLessonFallback: 'Current lesson',
linuxLessonFallback: 'Linux lesson',
radarCommands: 'Commands',
radarExercises: 'Exercises',
radarPitfalls: 'Pitfalls',
radarRelated: 'Related',
experimentStep1: (goal) => `Confirm the goal: ${goal || 'Understand the command objective.'}`,
experimentStep2: (command) => `Run ${command || 'the main command'} and inspect the immediate output.`,
experimentStep3: 'Compare the output with one example or scenario from this lesson.',
experimentStep4: 'Choose the next troubleshooting step based on what the command revealed.',
observationStep1: 'Before running: confirm the current context and target path.',
observationStep2: 'During output review: look for the field or line that answers the lesson goal.',
observationStep3: 'After running: explain what should happen next in a real operations flow.',
noRelatedCommands: 'No related commands yet',
noConcepts: 'No concept notes yet.',
noScenarios: 'No scenario notes yet.',
noPitfalls: 'No pitfall notes yet.',
noFlow: 'No flow notes yet.',
noTakeaways: 'No takeaways yet.',
noExamples: 'No example commands yet.',
noCommandGuides: 'No command guide notes yet.',
noOpsExtensions: 'No extension drills yet.',
noReviewPrompts: 'No review prompts yet.',
noExercises: 'This lesson does not have exercises yet.',
operationTask: 'Operation task',
scenarioReflection: 'Scenario reflection',
understandingTask: 'Understanding task',
exerciseTitleFallback: 'Operation task',
reflectionTaskFallback: 'Reflection task',
exerciseHintFallback: 'Enter a command and inspect the output.',
terminalHead: 'Linux sandbox terminal',
runBtn: 'Run',
waitingForCommand: 'Waiting for a command...',
running: 'Running...',
noOutput: '(no output)',
requestFailedPrefix: 'Request failed: ',
requestFailed: 'The request failed. Please try again.',
sandboxResetComplete: 'Sandbox reset complete.',
commandFallback: 'command',
nameOrigin: 'Name origin',
learningHook: 'Learning hook',
noOrigin: 'No origin note yet.',
noFunFact: 'No reinforcement note yet.',
examplePrefix: 'Example:',
nextSuggestionPrefix: 'Next up:',
prevBtn: 'Previous',
nextBtn: 'Next',
moduleType: 'Module',
lessonType: 'Lesson',
exerciseType: 'Exercise',
tryFirstPrefix: 'Try first:',
originalHintLabel: 'Original hint',
originalQuestionLabel: 'Original prompt',
referenceAnswerLabel: 'Reference answer'
}
};
const MODULE_ZH = {
module_1_foundation: {
title: '模块 1建立 Linux 空间感',
summary: '从定位与列表开始,让后续每一步都有上下文。'
},
module_2_filesystem: {
title: '模块 2安全操作文件',
summary: '练习创建、复制、移动与权限调整,把文件系统操作做稳。'
},
module_3_search: {
title: '模块 3用证据做搜索与预览',
summary: '从大范围搜索走到精确答案,减少噪音并保留关键线索。'
},
module_4_operations: {
title: '模块 4把命令串成运维流程',
summary: '从进程、网络到服务状态,建立系统化观察路径。'
},
module_5_incidents: {
title: '模块 5故障排查演练',
summary: '围绕磁盘、认证与服务链路,练习完整的事故处理套路。'
},
module_6_shell_environment: {
title: '模块 6提高 Shell 操作效率',
summary: '用环境变量、历史与文本管道,把命令行工作做顺手。'
},
module_7_packages_and_delivery: {
title: '模块 7定位工具、检查软件与交付制品',
summary: '学会找到命令来源、读懂包管理器输出,并处理归档与后台任务。'
},
module_8_monitoring_and_scheduling: {
title: '模块 8系统监控与计划任务',
summary: '用系统快照、端口路径与时间调度建立主机健康视角。'
},
module_9_web_delivery: {
title: '模块 9Web 交付与运行验证',
summary: '从服务状态走到 HTTP 验证,贴近真实上线与检查过程。'
},
module_10_backup_and_recovery: {
title: '模块 10备份、恢复与安全变更',
summary: '把回滚、恢复和变更窗口纳入日常运维基本功。'
}
};
const LESSON_ZH = {
m1_l1_pwd: '用 pwd 确认当前位置',
m1_l2_ls: '用 ls 查看目录内容',
m1_l3_cd_cat_echo: '结合 cd、cat、echo 完成基础流程',
m2_l1_create: '用 mkdir 和 touch 搭建结构',
m2_l2_copy_move_permission: '用 cp、mv、chmod、stat 安全处理文件',
m2_l3_cleanup_and_modes: '用 rm 和 chmod 做清理与权限调整',
m3_l1_grep: '用 grep 搜索文件内容',
m3_l2_find_preview: '用 find 定位文件并用 tail 预览',
m3_l3_preview_and_pipeline: '用 head、tail 与简单管道预览文件',
m4_l1_process: '用 top 和 ps 观察系统压力',
m4_l2_network: '用 ip、ping、ss、curl 检查网络链路',
m4_l3_service: '用 systemctl 和 journalctl 追踪服务健康',
m5_l1_disk: '故障演练:排查磁盘压力',
m5_l2_auth: '故障演练:排查登录或权限失败',
m5_l3_service_path: '故障演练:服务正常但应用仍异常',
m6_l1_env_alias: '用 env、export、alias 管好 Shell 上下文',
m6_l2_history_replay: '用 history、date、clear 管理命令会话',
m6_l3_text_pipeline: '用 sort、uniq、cut、awk、sed 降噪',
m7_l1_locate_tools: '用 which 和 whereis 定位工具',
m7_l2_package_basics: '读懂 apt、yum、rpm、dpkg 输出',
m7_l3_archives_and_background: '用 tar 和 nohup 处理归档与后台任务',
m8_l1_system_snapshot: '用 uptime、free、w、last 做主机快照',
m8_l2_ports_and_paths: '用 lsof、netstat、traceroute、dig 观察链路',
m8_l3_time_and_schedule: '用 date、cal、crontab 理解时间与调度',
m9_l1_service_to_http: '从服务状态走到 HTTP 验证',
m9_l2_fetch_and_compare: '用 curl 和 wget 拉取并对比远端内容',
m9_l3_config_runtime: '用 cat、grep、journalctl 观察运行配置线索',
m10_l1_backup_patterns: '用 cp 和 tar 建立回滚余地',
m10_l2_restore_path: '恢复到干净路径并检查结果',
m10_l3_change_window: '用 date、cal、history、journalctl 规划安全变更窗口'
};
function uiText() {
return I18N[state.language] || I18N.zh;
}
function lessonCommands(lesson) {
if (Array.isArray(lesson?.command_tokens) && lesson.command_tokens.length) {
return lesson.command_tokens.filter(Boolean);
}
return String(lesson?.command || '')
.split('/')
.map((item) => item.trim())
.filter(Boolean);
}
function localizedCommandLabel(lesson) {
const commands = lessonCommands(lesson);
return commands.length ? commands.join(' / ') : uiText().commandFallback;
}
function localizedModuleTitle(module) {
if (state.language === 'zh') {
return MODULE_ZH[module?.id]?.title || module?.display_title || module?.title || uiText().moduleType;
}
return module?.display_title || module?.title || uiText().moduleType;
}
function localizedModuleSummary(module) {
if (state.language === 'zh') {
return MODULE_ZH[module?.id]?.summary
|| `围绕 ${localizedCommandLabel(module)} 建立命令理解、证据判断与排障衔接。`;
}
return module?.display_summary || module?.summary || '';
}
function localizedLessonTitle(lesson) {
if (state.language === 'zh') {
return LESSON_ZH[lesson?.id] || `学习 ${localizedCommandLabel(lesson)}`;
}
return lesson?.display_title || lesson?.title || uiText().linuxLessonFallback;
}
function localizedLessonGoal(lesson) {
if (state.language === 'zh') {
return `围绕 ${localizedCommandLabel(lesson)} 练习“先确认上下文、再观察输出、再决定下一步”的 Linux 操作思路。`;
}
return lesson?.display_goal || lesson?.goal || '';
}
function localizedLessonWhy(lesson) {
if (state.language === 'zh') {
return `在真实运维里,${localizedCommandLabel(lesson)} 往往不是终点,而是帮助你判断后续检查、修复或验证动作的起点。`;
}
return lesson?.display_why || lesson?.why_it_matters || '';
}
function localizedClassicView(lesson) {
if (state.language === 'zh') {
return `不要把 ${localizedCommandLabel(lesson)} 当成孤立命令来背,而要把它放进完整的排障链路里理解。`;
}
return lesson?.classic_view || '';
}
function localizedAfterClass(lesson) {
if (state.language === 'zh') {
return `回到沙箱把 ${localizedCommandLabel(lesson)} 再练 2 到 3 遍,并尝试口述每一步的目的、输出与下一步动作。`;
}
return lesson?.after_class || '';
}
function findModulePreview(moduleId) {
return (state.overview?.modules || []).find((module) => module.id === moduleId) || null;
}
function findLessonPreview(lessonId) {
for (const module of state.overview?.modules || []) {
const lesson = (module.lessons || []).find((item) => item.id === lessonId);
if (lesson) {
return { ...lesson, module_id: module.id };
}
}
return null;
}
function localizedSearchTitle(item) {
if (state.language !== 'zh') {
return item.title;
}
if (item.type === 'module') {
const module = findModulePreview(item.module_id);
return module ? localizedModuleTitle(module) : item.title;
}
const lesson = findLessonPreview(item.lesson_id);
if (!lesson) {
return item.title;
}
if (item.type === 'exercise') {
return `${localizedLessonTitle(lesson)} / ${uiText().exerciseType}`;
}
return localizedLessonTitle(lesson);
}
function localizedSearchSubtitle(item) {
if (state.language !== 'zh') {
return item.subtitle || item.type;
}
const text = uiText();
const typeLabel = item.type === 'module'
? text.moduleType
: item.type === 'lesson'
? text.lessonType
: text.exerciseType;
return [typeLabel, item.subtitle].filter(Boolean).join(' | ');
}
function formatMultiline(value) {
return escapeHtml(value).replaceAll('\n', '<br />');
}
function localizedExerciseHeading(exercise) {
if (state.language !== 'zh') {
return exercise.type === 'operation'
? exercise.title || uiText().exerciseTitleFallback
: exercise.question || uiText().reflectionTaskFallback;
}
if (exercise.type === 'operation') {
const primary = exercise.solution?.[0] || state.lesson?.command || localizedCommandLabel(state.lesson || {});
return `动手练习:${primary}`;
}
if (exercise.type === 'scenario') {
return `场景思考:${localizedLessonTitle(state.lesson || {})}`;
}
return `理解检查:${localizedLessonTitle(state.lesson || {})}`;
}
function localizedExerciseBody(exercise) {
const text = uiText();
if (state.language !== 'zh') {
return exercise.type === 'operation'
? exercise.hint || text.exerciseHintFallback
: exercise.answer || 'Summarize the purpose, output, and next step in your own words.';
}
if (exercise.type === 'operation') {
const primary = exercise.solution?.[0] || state.lesson?.command || 'pwd';
const lines = [`${text.tryFirstPrefix} ${primary}`];
if (exercise.hint) {
lines.push(`${text.originalHintLabel}${exercise.hint}`);
}
return lines.join('\n');
}
const lines = [];
if (exercise.question) {
lines.push(`${text.originalQuestionLabel}${exercise.question}`);
}
if (exercise.answer) {
lines.push(`${text.referenceAnswerLabel}${exercise.answer}`);
}
return lines.join('\n') || text.exerciseHintFallback;
}
function setLanguage(language) {
state.language = language === 'en' ? 'en' : 'zh';
document.documentElement.lang = state.language === 'zh' ? 'zh-CN' : 'en';
localStorage.setItem('linux_lab_language', state.language);
renderStaticText();
if (state.overview) {
renderOverview();
}
if (state.lesson) {
renderLesson();
}
runSearch();
}
function renderStaticText() {
const text = uiText();
document.title = text.courseTitle;
document.getElementById('topEyebrow').textContent = text.topEyebrow;
document.getElementById('themeBtn').textContent = state.theme === 'light' ? text.themeToDark : text.themeToLight;
document.getElementById('languageBtn').textContent = state.language === 'zh' ? 'EN' : '中文';
document.getElementById('resetBtn').textContent = text.resetBtn;
document.getElementById('privacyLink').textContent = text.privacyLink;
document.getElementById('overviewEyebrow').textContent = text.overviewEyebrow;
document.getElementById('overviewTitle').textContent = text.overviewTitle;
document.getElementById('overviewText').textContent = text.overviewText;
document.getElementById('moduleCountLabel').textContent = text.moduleCountLabel;
document.getElementById('lessonCountLabel').textContent = text.lessonCountLabel;
document.getElementById('exerciseCountLabel').textContent = text.exerciseCountLabel;
document.getElementById('commandCountLabel').textContent = text.commandCountLabel;
document.getElementById('masteredCountLabel').textContent = text.masteredCountLabel;
document.getElementById('completionRateLabel').textContent = text.completionRateLabel;
document.getElementById('searchEyebrow').textContent = text.searchEyebrow;
document.getElementById('searchTitle').textContent = text.searchTitle;
document.getElementById('searchInput').setAttribute('placeholder', text.searchPlaceholder);
document.getElementById('searchBtn').textContent = text.searchBtn;
document.getElementById('modulesEyebrow').textContent = text.modulesEyebrow;
document.getElementById('modulesTitle').textContent = text.modulesTitle;
document.getElementById('diagnosticsEyebrow').textContent = text.diagnosticsEyebrow;
document.getElementById('diagnosticsTitle').textContent = text.diagnosticsTitle;
document.getElementById('coverageRateLabel').textContent = text.coverageRateLabel;
document.getElementById('operationCountLabel').textContent = text.operationCountLabel;
document.getElementById('reflectionCountLabel').textContent = text.reflectionCountLabel;
document.getElementById('unsupportedCountLabel').textContent = text.unsupportedCountLabel;
document.getElementById('moduleSummaryTitle').textContent = text.moduleSummaryTitle;
document.getElementById('runtimeTitle').textContent = text.runtimeTitle;
document.getElementById('runtimeText').textContent = text.runtimeText;
document.getElementById('runtimeCwdLabel').textContent = text.runtimeCwdLabel;
document.getElementById('runtimeUserLabel').textContent = text.runtimeUserLabel;
document.getElementById('stagesEyebrow').textContent = text.stagesEyebrow;
document.getElementById('stagesTitle').textContent = text.stagesTitle;
document.getElementById('stagesText').textContent = text.stagesText;
document.getElementById('heatmapEyebrow').textContent = text.heatmapEyebrow;
document.getElementById('heatmapTitle').textContent = text.heatmapTitle;
document.getElementById('heatmapText').textContent = text.heatmapText;
document.getElementById('cockpitEyebrow').textContent = text.cockpitEyebrow;
document.getElementById('cockpitTitle').textContent = text.cockpitTitle;
document.getElementById('cockpitText').textContent = text.cockpitText;
document.getElementById('lessonRadarTitle').textContent = text.lessonRadarTitle;
document.getElementById('experimentTitle').textContent = text.experimentTitle;
document.getElementById('observationTitle').textContent = text.observationTitle;
document.getElementById('relatedCommandsTitle').textContent = text.relatedCommandsTitle;
document.getElementById('depthEyebrow').textContent = text.depthEyebrow;
document.getElementById('depthTitle').textContent = text.depthTitle;
document.getElementById('depthText').textContent = text.depthText;
document.getElementById('commandDossierTitle').textContent = text.commandDossierTitle;
document.getElementById('opsExtensionTitle').textContent = text.opsExtensionTitle;
document.getElementById('reviewPromptsTitle').textContent = text.reviewPromptsTitle;
document.getElementById('whyThisMattersTitle').textContent = text.whyThisMattersTitle;
document.getElementById('teachingAngleTitle').textContent = text.teachingAngleTitle;
document.getElementById('coreIdeasTitle').textContent = text.coreIdeasTitle;
document.getElementById('commonScenariosTitle').textContent = text.commonScenariosTitle;
document.getElementById('commonPitfallsTitle').textContent = text.commonPitfallsTitle;
document.getElementById('troubleshootingFlowTitle').textContent = text.troubleshootingFlowTitle;
document.getElementById('takeawaysTitle').textContent = text.takeawaysTitle;
document.getElementById('exampleCommandsTitle').textContent = text.exampleCommandsTitle;
document.getElementById('practiceEyebrow').textContent = text.practiceEyebrow;
document.getElementById('practiceTitle').textContent = text.practiceTitle;
document.getElementById('practiceText').textContent = text.practiceText;
document.getElementById('prevBtn').textContent = text.prevBtn;
document.getElementById('nextBtn').textContent = text.nextBtn;
if (!state.overview) {
document.getElementById('courseTitle').textContent = text.courseTitle;
document.getElementById('courseDescription').textContent = text.courseDescription;
document.getElementById('metaLine').textContent = text.metaLine('--', '--');
document.getElementById('commandTags').innerHTML = `<span class="chip">${escapeHtml(text.waitingCommandIndex)}</span>`;
document.getElementById('searchResults').innerHTML = `<div class="empty">${escapeHtml(text.searchEmpty)}</div>`;
document.getElementById('moduleList').innerHTML = `<div class="empty">${escapeHtml(text.modulesLoading)}</div>`;
document.getElementById('diagnosticNote').textContent = text.diagnosticChecking;
}
if (!state.lesson) {
document.getElementById('moduleLabel').textContent = text.initialModuleLabel;
document.getElementById('lessonTitle').textContent = text.initialLessonTitle;
document.getElementById('lessonGoal').textContent = text.initialLessonGoal;
document.getElementById('moduleSummary').textContent = text.moduleSummaryPlaceholder;
document.getElementById('masteryBtn').textContent = text.masteryDefault;
document.getElementById('masteryNote').textContent = text.masteryPendingNote;
document.getElementById('lessonWhy').textContent = state.language === 'zh'
? '理解什么时候该用一条命令,比孤立地背参数更重要。'
: 'Understanding when to use a command matters more than memorizing flags in isolation.';
document.getElementById('classicView').textContent = state.language === 'zh'
? '每节课都会把命令连接回更大的排障流程。'
: 'Each lesson connects the command to a broader troubleshooting flow.';
}
}
const state = {
overview: null,
lesson: null,
language: localStorage.getItem('linux_lab_language') || 'zh',
theme: localStorage.getItem('linux_lab_theme') || 'light',
progress: JSON.parse(localStorage.getItem('linux_lab_progress') || '{}')
};
document.addEventListener('DOMContentLoaded', async () => {
setTheme(state.theme);
renderStaticText();
document.getElementById('themeBtn').addEventListener('click', () => setTheme(state.theme === 'light' ? 'dark' : 'light'));
document.getElementById('languageBtn').addEventListener('click', () => setLanguage(state.language === 'zh' ? 'en' : 'zh'));
document.getElementById('searchInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter') runSearch();
});
document.getElementById('prevBtn').addEventListener('click', () => {
if (state.lesson?.previous_lesson) openLesson(state.lesson.previous_lesson.id);
});
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();
renderOverview();
const firstLesson = state.overview.modules?.[0]?.lessons?.[0];
if (firstLesson) await openLesson(firstLesson.id);
}
function renderOverview() {
const text = uiText();
const meta = state.overview.meta || {};
document.getElementById('courseTitle').textContent = state.language === 'zh' ? text.courseTitle : (meta.title || text.courseTitle);
document.getElementById('courseDescription').textContent = state.language === 'zh' ? text.courseDescription : (meta.description || '');
document.getElementById('moduleCount').textContent = meta.module_count || 0;
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
? text.diagnosticSupported
: text.diagnosticUnsupported;
document.getElementById('metaLine').textContent = text.metaLine(meta.version || '4.0', meta.updated || '--');
renderRuntime(state.overview.runtime || {});
renderCommandTags(state.overview.commands || []);
renderModules();
renderStageGrid();
renderModuleSummaryGrid();
}
function renderRuntime(runtime) {
document.getElementById('runtimeCwd').textContent = runtime.cwd || '/';
document.getElementById('runtimeUser').textContent = runtime.user || 'sandbox_user';
}
function renderCommandTags(commands) {
const text = uiText();
const container = document.getElementById('commandTags');
if (!commands.length) {
container.innerHTML = `<span class="chip">${escapeHtml(text.waitingCommandIndex)}</span>`;
return;
}
container.innerHTML = commands.slice(0, 10).map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join('');
}
function renderModules() {
const text = uiText();
const modules = state.overview.modules || [];
const container = document.getElementById('moduleList');
if (!modules.length) {
container.innerHTML = `<div class="empty">${escapeHtml(text.noModulesFound)}</div>`;
return;
}
container.innerHTML = modules.map((module) => `
<article class="module-card">
<h3>${escapeHtml(localizedModuleTitle(module))}</h3>
<p>${escapeHtml(localizedModuleSummary(module))}</p>
<div class="lesson-list">
${(module.lessons || []).map((lesson) => `
<button class="lesson-btn ${state.lesson?.id === lesson.id ? 'active' : ''} ${isMastered(lesson.id) ? 'done' : ''}" type="button" onclick="openLesson('${lesson.id}')">
<strong>${isMastered(lesson.id) ? escapeHtml(text.completedPrefix) : ''}${escapeHtml(localizedLessonTitle(lesson))}</strong>
<span class="muted">${escapeHtml((lesson.command_tokens || []).join(' | ') || lesson.command || text.mixedCommands)} | ${lesson.exercise_count || 0} ${escapeHtml(text.exerciseUnit)}</span>
</button>
`).join('')}
</div>
</article>
`).join('');
}
function renderModuleSummaryGrid() {
const text = uiText();
const modules = state.overview.modules || [];
const target = document.getElementById('moduleSummaryGrid');
if (!modules.length) {
target.innerHTML = `<div class="empty">${escapeHtml(text.noModuleDiagnostics)}</div>`;
return;
}
target.innerHTML = modules.map((module) => `
<article class="module-summary-card">
<h4>${escapeHtml(localizedModuleTitle(module))}</h4>
<p>${escapeHtml(localizedModuleSummary(module))}</p>
<div class="mini-stats">
<div class="mini-stat"><span>${escapeHtml(text.moduleSummaryLessons)}</span><strong>${module.lesson_count || 0}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.moduleSummaryExercises)}</span><strong>${module.exercise_count || 0}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.moduleSummaryOps)}</span><strong>${module.operation_count || 0}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.moduleSummaryCommands)}</span><strong>${module.command_count || 0}</strong></div>
</div>
</article>
`).join('');
}
function renderStageGrid() {
const text = uiText();
const groups = text.stageGroups || [];
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>${escapeHtml(text.stageModules)}</span><strong>${selectedModules.length}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.stageLessons)}</span><strong>${lessonTotal}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.stageExercises)}</span><strong>${exerciseTotal}</strong></div>
<div class="mini-stat"><span>${escapeHtml(text.stageMastered)}</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();
state.lesson = payload.lesson;
renderModules();
renderLesson();
if (focusExerciseId) {
setTimeout(() => {
document.getElementById('exercise-' + focusExerciseId)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 120);
}
}
function renderLesson() {
const text = uiText();
const lesson = state.lesson;
if (!lesson) return;
document.getElementById('moduleLabel').textContent = state.language === 'zh'
? localizedModuleTitle({ id: lesson.module_id, display_title: lesson.display_module_title })
: (lesson.display_module_title || text.currentLessonFallback);
document.getElementById('lessonTitle').textContent = localizedLessonTitle(lesson);
document.getElementById('lessonGoal').textContent = localizedLessonGoal(lesson);
document.getElementById('moduleSummary').textContent = localizedModuleSummary({
id: lesson.module_id,
display_summary: lesson.display_module_summary,
command_tokens: lesson.command_tokens
});
document.getElementById('lessonWhy').textContent = localizedLessonWhy(lesson);
document.getElementById('classicView').textContent = localizedClassicView(lesson);
document.getElementById('afterClass').textContent = localizedAfterClass(lesson);
document.getElementById('masteryBtn').textContent = isMastered(lesson.id) ? text.masteryReview : text.masteryDefault;
document.getElementById('masteryNote').textContent = isMastered(lesson.id)
? text.masteryDoneNote
: text.masteryPendingNote;
document.getElementById('prevBtn').disabled = !lesson.previous_lesson;
document.getElementById('nextBtn').disabled = !lesson.next_lesson;
document.getElementById('lessonCommands').innerHTML = (lesson.command_tokens || [lesson.command || text.mixedCommands]).map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join('');
renderLessonCockpit(lesson);
renderList('conceptList', lesson.concepts, text.noConcepts);
renderList('scenarioList', lesson.scenarios, text.noScenarios);
renderList('pitfallList', lesson.pitfalls, text.noPitfalls);
renderList('flowList', lesson.troubleshooting_flow, text.noFlow);
renderList('takeawayList', lesson.takeaways, text.noTakeaways);
renderCommandGuides(lesson.command_guides || []);
renderList('opsExtensionList', lesson.ops_extensions, text.noOpsExtensions);
renderList('reviewPromptList', lesson.review_prompts, text.noReviewPrompts);
const examples = lesson.examples || [];
document.getElementById('exampleList').innerHTML = examples.length
? examples.map((item) => `<pre class="code">${escapeHtml(item)}</pre>`).join('')
: `<div class="empty">${escapeHtml(text.noExamples)}</div>`;
renderExercises(lesson.exercises || []);
}
function renderLessonCockpit(lesson) {
const text = uiText();
document.getElementById('lessonRadar').innerHTML = [
{ label: text.radarCommands, value: lesson.command_tokens?.length || 1 },
{ label: text.radarExercises, value: lesson.exercise_count || 0 },
{ label: text.radarPitfalls, value: lesson.pitfalls?.length || 0 },
{ label: text.radarRelated, 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 = [
text.experimentStep1(localizedLessonGoal(lesson)),
text.experimentStep2(lesson.command || localizedCommandLabel(lesson)),
text.experimentStep3,
text.experimentStep4
];
document.getElementById('experimentList').innerHTML = experimentSteps.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
const observationItems = [
text.observationStep1,
text.observationStep2,
text.observationStep3
];
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">${escapeHtml(text.noRelatedCommands)}</span>`;
}
function renderCommandGuides(guides) {
const text = uiText();
const target = document.getElementById('commandGuideList');
if (!guides.length) {
target.innerHTML = `<div class="empty">${escapeHtml(text.noCommandGuides)}</div>`;
return;
}
target.innerHTML = guides.map((guide) => `
<article class="guide-card">
<h4>${escapeHtml(guide.command || text.commandFallback)}</h4>
<p>${escapeHtml(guide.role || '')}</p>
<div class="guide-meta">
<div class="mini-stat">
<span>${escapeHtml(text.nameOrigin)}</span>
<strong>${escapeHtml(guide.origin || text.noOrigin)}</strong>
</div>
<div class="mini-stat">
<span>${escapeHtml(text.learningHook)}</span>
<strong>${escapeHtml(guide.fun_fact || text.noFunFact)}</strong>
</div>
</div>
<div class="variant-list">
${(guide.variants || []).map((variant) => `
<div class="variant">
<code>${escapeHtml(variant.syntax || '')}</code>
<strong>${escapeHtml(variant.purpose || '')}</strong>
<span class="muted">${escapeHtml(text.examplePrefix)} ${escapeHtml(variant.example || '')}</span>
</div>
`).join('')}
</div>
</article>
`).join('');
}
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>`;
}
function renderExercises(exercises) {
const text = uiText();
const container = document.getElementById('exerciseList');
if (!exercises.length) {
container.innerHTML = `<div class="empty">${escapeHtml(text.noExercises)}</div>`;
return;
}
container.innerHTML = exercises.map((exercise) => {
const label = exercise.type === 'operation'
? text.operationTask
: exercise.type === 'scenario'
? text.scenarioReflection
: text.understandingTask;
if (exercise.type === 'operation') {
const placeholder = exercise.solution?.[0] || state.lesson.command || 'pwd';
return `
<article class="exercise" id="exercise-${exercise.id}">
<span class="badge">${label}</span>
<h4>${escapeHtml(localizedExerciseHeading(exercise))}</h4>
<p class="muted" style="white-space: pre-line;">${formatMultiline(localizedExerciseBody(exercise))}</p>
<div class="terminal">
<div class="terminal-head">${escapeHtml(text.terminalHead)}</div>
<div class="terminal-input">
<span class="prompt">$</span>
<input id="input-${exercise.id}" type="text" placeholder="${escapeHtml(placeholder)}" onkeydown="if(event.key==='Enter'){runExercise('${exercise.id}')}" />
<button type="button" onclick="runExercise('${exercise.id}')">${escapeHtml(text.runBtn)}</button>
</div>
<pre class="output" id="output-${exercise.id}">${escapeHtml(text.waitingForCommand)}</pre>
</div>
<div class="feedback" id="feedback-${exercise.id}"></div>
</article>
`;
}
return `
<article class="exercise" id="exercise-${exercise.id}">
<span class="badge">${label}</span>
<h4>${escapeHtml(localizedExerciseHeading(exercise))}</h4>
<p style="white-space: pre-line;">${formatMultiline(localizedExerciseBody(exercise))}</p>
</article>
`;
}).join('');
}
async function runExercise(exerciseId) {
const text = uiText();
const input = document.getElementById('input-' + exerciseId);
const output = document.getElementById('output-' + exerciseId);
const feedback = document.getElementById('feedback-' + exerciseId);
const cmd = input.value.trim();
if (!cmd) return;
output.textContent = `$ ${cmd}\n\n${text.running}`;
feedback.className = 'feedback';
feedback.textContent = '';
try {
const runResponse = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const runData = await runResponse.json();
output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || text.noOutput}`;
renderRuntime({ cwd: runData.cwd || '/', user: state.overview?.runtime?.user || 'sandbox_user' });
const checkResponse = await fetch('/api/check?exercise_id=' + encodeURIComponent(exerciseId) + '&last_cmd=' + encodeURIComponent(cmd) + '&output=' + encodeURIComponent(runData.output || ''));
const checkData = await checkResponse.json();
feedback.className = 'feedback show ' + (checkData.success ? 'success' : 'warn');
feedback.textContent = checkData.message + (checkData.next_suggestion ? '\n' + checkData.next_suggestion : '');
} catch (error) {
output.textContent = `$ ${cmd}\n\n${text.requestFailedPrefix}${error.message}`;
feedback.className = 'feedback show warn';
feedback.textContent = text.requestFailed;
}
}
async function resetSandbox() {
const text = uiText();
await fetch('/api/reset', { method: 'POST' });
renderRuntime({ cwd: '/', user: 'sandbox_user' });
alert(text.sandboxResetComplete);
}
async function runSearch() {
const text = uiText();
const query = document.getElementById('searchInput').value.trim();
const container = document.getElementById('searchResults');
if (!query) {
container.innerHTML = `<div class="empty">${escapeHtml(text.searchEmpty)}</div>`;
return;
}
container.innerHTML = `<div class="empty">${escapeHtml(text.searchLoading)}</div>`;
try {
const response = await fetch('/api/course/search?q=' + encodeURIComponent(query));
const payload = await response.json();
const results = payload.results || [];
if (!results.length) {
container.innerHTML = `<div class="empty">${escapeHtml(text.searchNoResult)}</div>`;
return;
}
container.innerHTML = results.map((item) => `
<button class="result" type="button" onclick="openLesson('${item.lesson_id}', '${item.exercise_id || ''}')">
<strong>${escapeHtml(localizedSearchTitle(item))}</strong>
<span class="muted">${escapeHtml(localizedSearchSubtitle(item))}</span>
</button>
`).join('');
} catch (error) {
container.innerHTML = `<div class="empty">${escapeHtml(text.requestFailedPrefix + error.message)}</div>`;
}
}
function setTheme(theme) {
state.theme = theme;
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_lab_theme', theme);
document.getElementById('themeBtn').textContent = theme === 'light' ? uiText().themeToDark : uiText().themeToLight;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
window.openLesson = openLesson;
window.runExercise = runExercise;
window.runSearch = runSearch;
window.resetSandbox = resetSandbox;
</script>
</body>
</html>