Linux Ops Learning Lab
@@ -792,6 +854,7 @@
overviewEyebrow: '总览',
overviewTitle: '课程总览',
overviewText: '课程按模块和命令家族分组,重点是把摘要讲清楚、把沙箱练习串起来。',
+ navMoreLessons: '本模块还有 {count} 节课程可跳转。',
moduleCountLabel: '模块',
lessonCountLabel: '课程',
exerciseCountLabel: '练习',
@@ -921,6 +984,8 @@
requestFailed: '请求失败,请稍后再试。',
sandboxResetComplete: '沙箱已重置。',
commandFallback: '命令',
+ commandFamilyCountLabel: '课程命令数',
+ commandFamilyCommandsHint: '代表命令',
nameOrigin: '名字来源',
learningHook: '记忆钩子',
noOrigin: '暂无来源说明。',
@@ -974,6 +1039,7 @@
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}`,
+ navMoreLessons: '{count} more lessons inside this module.',
waitingCommandIndex: 'Waiting for command index',
noModulesFound: 'No modules found.',
completedPrefix: 'Completed - ',
@@ -1077,6 +1143,8 @@
requestFailed: 'The request failed. Please try again.',
sandboxResetComplete: 'Sandbox reset complete.',
commandFallback: 'command',
+ commandFamilyCountLabel: 'Commands in course',
+ commandFamilyCommandsHint: 'Representative commands',
nameOrigin: 'Name origin',
learningHook: 'Learning hook',
noOrigin: 'No origin note yet.',
@@ -1548,12 +1616,20 @@
return fetch(path, Object.assign({}, options, { headers }));
}
+ function setExperienceLock(locked) {
+ const shell = document.getElementById('contentShell');
+ if (!shell) return;
+ shell.classList.toggle('locked', locked);
+ }
+
function showAuthGate() {
+ setExperienceLock(true);
document.getElementById('authGate').classList.remove('hidden');
}
function hideAuthGate() {
document.getElementById('authGate').classList.add('hidden');
+ setExperienceLock(false);
}
async function loadOverview() {
@@ -1915,12 +1991,14 @@
}
const COMMAND_CATEGORY_META = [
- { id: "navigation", title: "导航", description: "帮你定位在系统中的上下文、目录和状态。" },
- { id: "filesystem", title: "文件系统", description: "读写/移动/备份关键资产的安全操作。" },
- { id: "text", title: "文本处理", description: "过滤日志、提取字段、整理数据。" },
- { id: "observation", title: "观测", description: "实时洞察服务、进程、资源使用。" },
- { id: "service", title: "服务控制", description: "管理 systemd 单元、日志和守护进程。" },
- { id: "network", title: "网络", description: "检查连接、响应、DNS 及路由。" },
+ { id: "navigation", title: "导航", description: "帮你定位在系统中的上下文、目录和状态。", commands: ["pwd","ls","cd","which","whereis","clear"] },
+ { id: "filesystem", title: "文件系统", description: "读写/移动/备份关键资产的安全操作。", commands: ["cat","echo","mkdir","touch","cp","mv","rm","chmod","stat","tar"] },
+ { id: "text", title: "文本处理", description: "过滤日志、提取字段、整理数据。", commands: ["grep","find","head","tail","sort","uniq","cut","awk","sed"] },
+ { id: "observation", title: "观测", description: "实时洞察服务、进程、资源使用。", commands: ["ps","top","uptime","free","df","du","lsof","last","w","history","date"] },
+ { id: "service", title: "服务控制", description: "管理 systemd 单元、日志和守护进程。", commands: ["systemctl","journalctl","nohup","crontab"] },
+ { id: "network", title: "网络", description: "检查连接、响应、DNS 及路由。", commands: ["ip","ping","ss","curl","wget","netstat","traceroute","dig"] },
+ { id: "identity", title: "身份", description: "了解当前身份、环境变量与别名设置。", commands: ["whoami","id","env","export","alias"] },
+ { id: "package", title: "包管理", description: "检查、安装与验证平台级软件。", commands: ["apt","yum","rpm","dpkg"] },
];
function escapeHtml(value) {
@@ -1970,51 +2048,81 @@
function renderCategoryGrid() {
const container = document.getElementById("categoryGrid");
- const commandSet = new Set(state.overview?.commands || []);
- const counts = {};
- commandSet.forEach((cmd) => {
- for (const category of COMMAND_CATEGORY_META) {
- if (!counts[category.title]) {
- counts[category.title] = 0;
+ const text = uiText();
+ const commands = (state.overview?.commands || [])
+ .map((cmd) => String(cmd).trim().toLowerCase())
+ .filter(Boolean);
+ const counts = COMMAND_CATEGORY_META.reduce((acc, category) => {
+ acc[category.id] = 0;
+ return acc;
+ }, {});
+ commands.forEach((cmd) => {
+ COMMAND_CATEGORY_META.forEach((category) => {
+ const family = (category.commands || []).map((item) => String(item).toLowerCase());
+ if (family.includes(cmd)) {
+ counts[category.id] = (counts[category.id] || 0) + 1;
}
- const family = category.id;
- const supported = window.COMMAND_FAMILIES?.[family] || [];
- if (supported.includes(cmd)) {
- counts[category.title]++;
- }
- }
+ });
});
- container.innerHTML = COMMAND_CATEGORY_META.map((category) => `
-
- ${escapeHtml(category.title)}
- ${escapeHtml(category.description)}
- 课程命令:${counts[category.title] || 0}
-
- `).join("");
+ container.innerHTML = COMMAND_CATEGORY_META.map((category) => {
+ const count = counts[category.id] || 0;
+ const samples = (category.commands || []).slice(0, 3);
+ return `
+
+ ${escapeHtml(category.title)}
+ ${escapeHtml(category.description)}
+
+ ${escapeHtml(text.commandFamilyCountLabel)} ${count}
+ ${samples.length ? `${escapeHtml(text.commandFamilyCommandsHint)} ${escapeHtml(samples.join(", "))}` : ""}
+
+
+ `;
+ }).join("");
}
function renderNavLessons() {
const container = document.getElementById("navLessonResults");
const modules = state.overview?.modules || [];
const exerciseCount = modules.reduce((sum, module) => sum + (module.exercise_count || 0), 0);
- const lessonCountEl = document.getElementById("navLessonCount");
const exerciseCountEl = document.getElementById("navExerciseCount");
- if (lessonCountEl) {
- lessonCountEl.textContent = modules.length;
- }
+ const text = uiText();
if (exerciseCountEl) {
exerciseCountEl.textContent = exerciseCount;
}
if (!modules.length) {
- container.innerHTML = `
${escapeHtml(uiText().noModulesFound)}
`;
+ container.innerHTML = `
${escapeHtml(text.noModulesFound)}
`;
return;
}
- container.innerHTML = modules.map((module) => `
-
- `).join("");
+ const visibleModules = modules.slice(0, 6);
+ container.innerHTML = visibleModules.map((module) => {
+ const lessons = (module.lessons || []).slice(0, 3);
+ const lessonButtons = lessons.map((lesson) => `
+
+ `).join("");
+ const declaredLessons = module.lesson_count || (module.lessons || []).length;
+ const extraLessons = Math.max(0, declaredLessons - lessons.length);
+ return `
+
+
+
+ ${lessonButtons}
+
+ ${extraLessons ? `` : ""}
+
+ `;
+ }).join("");
}
async function loginToLab(event) {
@@ -2049,10 +2157,15 @@
}
}
- function logoutLab() {
+ async function logoutLab() {
const zh = state.language === 'zh';
state.auth = { loggedIn: false, user: "", token: "" };
persistAuth();
+ try {
+ await apiFetch('/api/logout', { method: 'POST' });
+ } catch (error) {
+ console.warn('Logout request failed', error);
+ }
showAuthGate();
renderAuthControls();
document.getElementById("gateMessage").textContent = zh ? "已退出,重新登录即可继续。" : "Logged out. Sign in again to continue.";