From d2b667f5692b23264e1ecb850fd57517fd591fdf Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 24 Mar 2026 12:36:29 +0800 Subject: [PATCH] feat: polish linux lab cockpit and auth flow --- .gitignore | 4 + index.html | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 265 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 8fa5518..d03a13e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ __pycache__/ # OpenClaw interactive edit backups *.interactive.backup.* + +# Runtime logs +server.out.log +server.err.log diff --git a/index.html b/index.html index 07922d9..805bc98 100644 --- a/index.html +++ b/index.html @@ -342,6 +342,92 @@ font-size: 15px; line-height: 1.6; } + .auth-card { + padding: 18px; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255,255,255,0.18); + display: flex; + flex-direction: column; + gap: 12px; + } + .auth-card .auth-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + } + .auth-card form { + display: flex; + flex-direction: column; + gap: 8px; + } + .auth-card input { + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px 12px; + background: transparent; + color: var(--text); + } + .auth-status { + font-size: 13px; + font-weight: 700; + color: var(--accent); + } + .stage-map { + padding: 18px; + border-radius: 20px; + border: 1px solid var(--line); + background: rgba(255,255,255,0.2); + display: grid; + gap: 12px; + } + .stage-map .stage-step { + display: flex; + align-items: center; + gap: 12px; + } + .stage-map .stage-step::before { + content: ""; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--soft); + border: 2px solid var(--brand); + flex-shrink: 0; + } + .category-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + margin-top: 12px; + } + .category-card { + padding: 14px; + border-radius: 16px; + border: 1px solid var(--line); + background: rgba(255,255,255,0.2); + } + .category-card em { + font-size: 12px; + color: var(--muted); + } + .progress-track { + display: flex; + gap: 12px; + overflow-x: auto; + margin-top: 10px; + } + .progress-pill { + flex: 0 0 auto; + padding: 10px 16px; + border-radius: 999px; + border: 1px solid var(--line); + background: rgba(255,255,255,0.25); + font-size: 13px; + color: var(--text); + min-width: 160px; + } .variant-list { display: flex; flex-direction: column; @@ -448,6 +534,27 @@
Checking course and sandbox alignment...
+ +
+
登录培训入口
+

登录并解锁专属实验

+
+
+
+ 未登录 +

登录后可访问加速练习、保存进度、查看历史。

+
+ 公开访问 +
+
+ + + +
+ +
+
+
@@ -482,6 +589,21 @@
Master a lesson after you can explain the command, predict the output, and connect it to a real operations step.
+
+
Stage map
+

阶段路线清单

+

按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。

+
+
+
+ +
+
Command categories
+

命令家族可视化

+

了解各类命令在当前课程中的分布,以及建议的学习顺序。

+
+
+
Learning stages

System route from Linux basics to operations habits

@@ -1248,12 +1370,18 @@ } } + const persistedAuth = JSON.parse(localStorage.getItem('linux_lab_auth') || '{}'); 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') || '{}') + progress: JSON.parse(localStorage.getItem('linux_lab_progress') || '{}'), + auth: { + loggedIn: persistedAuth.loggedIn || false, + user: persistedAuth.user || '', + token: persistedAuth.token || '' + } }; document.addEventListener('DOMContentLoaded', async () => { @@ -1272,6 +1400,7 @@ }); document.getElementById('masteryBtn').addEventListener('click', toggleMastery); await loadOverview(); + renderAuthStatus(); }); function saveProgress() { @@ -1301,6 +1430,26 @@ renderLesson(); } + function persistAuth() { + localStorage.setItem('linux_lab_auth', JSON.stringify(state.auth)); + } + + function renderAuthStatus() { + const badge = document.getElementById('authStatusBadge'); + const modeLabel = document.getElementById('authModeLabel'); + const hint = document.getElementById('authHint'); + const form = document.getElementById('loginForm'); + const logoutBtn = document.getElementById('logoutBtn'); + badge.textContent = state.auth.loggedIn ? '已登录' : '公开访问'; + modeLabel.textContent = state.auth.loggedIn ? `当前登录:${state.auth.user}` : '未登录'; + hint.textContent = state.auth.loggedIn + ? '您已解锁全部实验和进度保存功能。' + : '登录后可解锁任务追踪、保存进度和补全排障建议提示。'; + form.style.display = state.auth.loggedIn ? 'none' : 'flex'; + logoutBtn.style.display = state.auth.loggedIn ? 'inline-flex' : 'none'; + badge.classList.toggle('auth-status', !!state.auth.loggedIn); + } + async function loadOverview() { const response = await fetch('/api/overview'); state.overview = await response.json(); @@ -1658,19 +1807,125 @@ document.getElementById('themeBtn').textContent = theme === 'light' ? uiText().themeToDark : uiText().themeToLight; } + 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 及路由。" }, + ]; + function escapeHtml(value) { return String(value) - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); } + function renderStageTimeline() { + const map = document.getElementById("stageMap"); + const modules = (state.overview?.modules || []).slice(0, 5); + if (!modules.length) { + map.innerHTML = "
尚无阶段数据
"; + return; + } + map.innerHTML = modules.map((module, index) => ` +
+ ${index + 1}. ${escapeHtml(localizedModuleTitle(module))} + ${(module.lesson_count || 0)} lessons · ${(module.exercise_count || 0)} exercises +
+ `).join(""); + const track = document.getElementById("progressTrack"); + const completion = completionRate(); + track.innerHTML = modules.map((module, index) => { + const masteredLessons = (module.lessons || []).filter((lesson) => isMastered(lesson.id)).length; + const totalLessons = module.lesson_count || (module.lessons || []).length; + const percent = totalLessons ? Math.round((masteredLessons / totalLessons) * 100) : 0; + return `${escapeHtml(localizedModuleTitle(module))}: ${percent}%`; + }).join(""); + } + + 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 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(""); + } + + async function loginToLab(event) { + event.preventDefault(); + const username = document.getElementById("authUser").value.trim(); + const password = document.getElementById("authPassword").value.trim(); + const messageNode = document.getElementById("authMessage"); + if (!username || !password) { + messageNode.textContent = "请输入用户名和密码"; + return; + } + try { + const response = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + const payload = await response.json(); + if (payload.success) { + state.auth = { loggedIn: true, user: username, token: payload.token }; + messageNode.textContent = payload.message || "登录成功"; + document.getElementById("authStatusBadge").textContent = "已登录"; + document.getElementById("authModeLabel").textContent = `当前登录:${username}`; + document.getElementById("logoutBtn").style.display = "inline-flex"; + document.getElementById("loginForm").style.display = "none"; + return; + } + messageNode.textContent = payload.error || "登录失败"; + } catch (error) { + messageNode.textContent = "网络异常,请稍后再试"; + } + } + + function logoutLab() { + state.auth = { loggedIn: false, user: "", token: "" }; + document.getElementById("authMessage").textContent = "已退出"; + document.getElementById("authStatusBadge").textContent = "仅公开"; + document.getElementById("authModeLabel").textContent = "未登录"; + document.getElementById("logoutBtn").style.display = "none"; + document.getElementById("loginForm").style.display = "flex"; + } + + function renderVisualModules() { + renderStageTimeline(); + renderCategoryGrid(); + } + + document.getElementById("loginForm").addEventListener("submit", loginToLab); + document.getElementById("logoutBtn").addEventListener("click", logoutLab); + window.openLesson = openLesson; window.runExercise = runExercise; window.runSearch = runSearch; window.resetSandbox = resetSandbox; + renderVisualModules();