feat: extend linux operations curriculum
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
"author": "Codex",
|
||||
"updated": "2026-03-19",
|
||||
"description": "A rebuilt Linux learning path that moves from command basics to real operations troubleshooting with sandbox exercises.",
|
||||
"module_count": 5,
|
||||
"total_lessons": 15,
|
||||
"total_exercises": 48,
|
||||
"module_count": 8,
|
||||
"total_lessons": 24,
|
||||
"total_exercises": 82,
|
||||
"pedagogy": "learning-first",
|
||||
"orientation": "ops-workflow"
|
||||
},
|
||||
@@ -364,6 +364,223 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module_6_shell_environment",
|
||||
"title": "Module 6: Work faster in the shell",
|
||||
"summary": "Use environment variables, aliases, history, and simple text pipelines to make Linux work feel repeatable instead of manual.",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m6_l1_env_alias",
|
||||
"title": "Control shell context with env, export, and alias",
|
||||
"goal": "Understand how shell state changes affect later commands and sessions.",
|
||||
"why_it_matters": "A lot of Linux productivity comes from shaping the shell so common work is easier to repeat safely.",
|
||||
"concepts": ["Environment variables", "Exported shell state", "Command aliases"],
|
||||
"command": "env / export / alias",
|
||||
"examples": ["env", "export APP_MODE=lab", "alias ll='ls -l'"],
|
||||
"pitfalls": ["Assuming an echoed value is the same as an exported variable", "Creating aliases without understanding the original command"],
|
||||
"scenarios": ["Prepare a temporary runtime flag", "Shorten a repeated command pattern"],
|
||||
"troubleshooting_flow": ["Inspect the current shell state", "Change one variable or alias", "Verify the shell reflects the change"],
|
||||
"related_commands": ["env", "export", "alias", "echo"],
|
||||
"classic_view": "The shell is not just a place to type commands; it is a working environment you can shape.",
|
||||
"takeaways": ["Use env to inspect state", "Use export to persist a shell variable for child processes", "Use aliases carefully and intentionally"],
|
||||
"after_class": "Practice changing one variable and one alias, then explain what effect each change has.",
|
||||
"exercises": [
|
||||
{ "id": "m6_l1_e1", "type": "operation", "title": "Inspect the current environment", "hint": "Run env.", "success_test": "cmd == 'env' and 'HOME=' in output", "solution": ["env"], "success_msg": "You inspected the current shell environment." },
|
||||
{ "id": "m6_l1_e2", "type": "operation", "title": "Export a temporary variable", "hint": "Run export APP_MODE=lab.", "success_test": "cmd == 'export APP_MODE=lab' and 'APP_MODE=lab' in output", "solution": ["export APP_MODE=lab"], "success_msg": "You exported a shell variable successfully." },
|
||||
{ "id": "m6_l1_e3", "type": "operation", "title": "Create a listing alias", "hint": "Run alias ll='ls -l'.", "success_test": "cmd == \"alias ll='ls -l'\" and \"alias ll='ls -l'\" in output", "solution": ["alias ll='ls -l'"], "success_msg": "You created a shell alias." },
|
||||
{ "id": "m6_l1_e4", "type": "scenario", "question": "Why is export different from simply printing a value with echo?", "answer": "Because export changes the shell environment for later commands, while echo only prints text once." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m6_l2_history_replay",
|
||||
"title": "Use history, date, and clear to manage command sessions",
|
||||
"goal": "Review what you ran, time-stamp your work, and keep the terminal readable during longer sessions.",
|
||||
"why_it_matters": "Operations work often depends on replaying what happened and keeping a clean command narrative.",
|
||||
"concepts": ["Command history", "Session readability", "Time awareness"],
|
||||
"command": "history / date / clear",
|
||||
"examples": ["history", "date", "clear"],
|
||||
"pitfalls": ["Running many commands without keeping track of sequence", "Treating a terminal as disposable when the command history is useful evidence"],
|
||||
"scenarios": ["Reconstruct what you tried during an incident", "Record the time before a change or restart"],
|
||||
"troubleshooting_flow": ["Review your recent commands", "Record or inspect the current time", "Clear the screen only after capturing what matters"],
|
||||
"related_commands": ["history", "date", "clear", "echo"],
|
||||
"classic_view": "A strong operator can explain not just what they ran, but in what order and when.",
|
||||
"takeaways": ["Use history as lightweight evidence", "Use date to anchor actions in time", "Clear the screen only when it helps readability"],
|
||||
"after_class": "Practice summarizing a small command session from history without looking at your notes.",
|
||||
"exercises": [
|
||||
{ "id": "m6_l2_e1", "type": "operation", "title": "Show your recent command history", "hint": "Run history.", "success_test": "cmd == 'history' and len(output) >= 0", "solution": ["history"], "success_msg": "You reviewed the shell history." },
|
||||
{ "id": "m6_l2_e2", "type": "operation", "title": "Print the current date and time", "hint": "Run date.", "success_test": "cmd == 'date' and 'CST' in output", "solution": ["date"], "success_msg": "You time-stamped the session." },
|
||||
{ "id": "m6_l2_e3", "type": "operation", "title": "Clear the terminal view", "hint": "Run clear.", "success_test": "cmd == 'clear' and 'screen cleared' in output", "solution": ["clear"], "success_msg": "You reset the visible terminal output." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m6_l3_text_pipeline",
|
||||
"title": "Reduce noise with sort, uniq, cut, awk, and sed",
|
||||
"goal": "Use small text-processing tools to transform output instead of reading everything manually.",
|
||||
"why_it_matters": "Linux operations is easier when you can shape output into something small enough to reason about.",
|
||||
"concepts": ["Pipeline thinking", "Sorting and deduplication", "Field extraction", "Quick text transforms"],
|
||||
"command": "sort / uniq / cut / awk / sed",
|
||||
"examples": ["cat /var/log/auth.log | sort", "cat /var/log/auth.log | uniq", "cat /etc/passwd | cut -d: -f1"],
|
||||
"pitfalls": ["Running multiple commands manually when a pipeline would be clearer", "Transforming output without understanding what field you are extracting"],
|
||||
"scenarios": ["Normalize repeated lines before inspection", "Extract usernames from passwd"],
|
||||
"troubleshooting_flow": ["Start with raw output", "Apply one transform at a time", "Stop when the output is small enough to answer the question"],
|
||||
"related_commands": ["sort", "uniq", "cut", "awk", "sed", "grep"],
|
||||
"classic_view": "Pipelines are how Linux turns a large question into a sequence of smaller, readable steps.",
|
||||
"takeaways": ["Use sort and uniq to reduce repetition", "Use cut for simple field extraction", "Treat awk and sed as focused shaping tools rather than magic"],
|
||||
"after_class": "Practice explaining what each pipeline stage contributes before you run the full chain.",
|
||||
"exercises": [
|
||||
{ "id": "m6_l3_e1", "type": "operation", "title": "Sort auth log lines", "hint": "Run cat /var/log/auth.log | sort.", "success_test": "cmd == 'cat /var/log/auth.log | sort' and len(output) > 0", "solution": ["cat /var/log/auth.log | sort"], "success_msg": "You sorted the auth log lines." },
|
||||
{ "id": "m6_l3_e2", "type": "operation", "title": "Deduplicate auth log lines", "hint": "Run cat /var/log/auth.log | uniq.", "success_test": "cmd == 'cat /var/log/auth.log | uniq' and len(output) > 0", "solution": ["cat /var/log/auth.log | uniq"], "success_msg": "You reduced duplicate lines with uniq." },
|
||||
{ "id": "m6_l3_e3", "type": "operation", "title": "Extract the first passwd field", "hint": "Run cat /etc/passwd | cut -d: -f1.", "success_test": "cmd == 'cat /etc/passwd | cut -d: -f1' and len(output) > 0", "solution": ["cat /etc/passwd | cut -d: -f1"], "success_msg": "You extracted a single field from passwd." },
|
||||
{ "id": "m6_l3_e4", "type": "scenario", "question": "Why do pipelines help more than manually copying output into another tool?", "answer": "Because they preserve a clear, repeatable transformation chain directly in the shell." }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module_7_packages_and_delivery",
|
||||
"title": "Module 7: Locate tools, inspect packages, and move artifacts",
|
||||
"summary": "Learn where commands live, how package managers differ, and how archives or background jobs support basic delivery work.",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m7_l1_locate_tools",
|
||||
"title": "Locate binaries with which and whereis",
|
||||
"goal": "Understand where commands come from before changing PATHs or package assumptions.",
|
||||
"why_it_matters": "Operators often need to prove which binary is running and where supporting docs or manpages live.",
|
||||
"concepts": ["Binary location", "PATH lookup", "Tool discovery"],
|
||||
"command": "which / whereis",
|
||||
"examples": ["which curl", "which ls", "whereis grep"],
|
||||
"pitfalls": ["Assuming the same command path on every server", "Changing PATH without first proving what is currently used"],
|
||||
"scenarios": ["Check where curl is installed", "Find the shell-visible ls binary"],
|
||||
"troubleshooting_flow": ["Ask which binary the shell resolves", "Use whereis for broader discovery", "Only then adjust your assumptions or PATH"],
|
||||
"related_commands": ["which", "whereis", "env"],
|
||||
"classic_view": "Before you replace a binary or adjust PATH, prove what the shell is already resolving.",
|
||||
"takeaways": ["Use which for shell resolution", "Use whereis for broader lookup", "Do not guess command paths"],
|
||||
"after_class": "Compare which and whereis on two commands and explain why their outputs differ.",
|
||||
"exercises": [
|
||||
{ "id": "m7_l1_e1", "type": "operation", "title": "Locate curl", "hint": "Run which curl.", "success_test": "cmd == 'which curl' and '/usr/bin/curl' in output", "solution": ["which curl"], "success_msg": "You located the curl binary." },
|
||||
{ "id": "m7_l1_e2", "type": "operation", "title": "Locate ls", "hint": "Run which ls.", "success_test": "cmd == 'which ls' and '/bin/ls' in output", "solution": ["which ls"], "success_msg": "You located the ls binary." },
|
||||
{ "id": "m7_l1_e3", "type": "operation", "title": "Broaden the search for grep", "hint": "Run whereis grep.", "success_test": "cmd == 'whereis grep' and 'grep:' in output", "solution": ["whereis grep"], "success_msg": "You used whereis for broader command discovery." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m7_l2_package_basics",
|
||||
"title": "Read package tool output with apt, yum, rpm, and dpkg",
|
||||
"goal": "Recognize the common package tools you will encounter across distributions.",
|
||||
"why_it_matters": "Even if you automate package installs later, you still need to understand the native package vocabulary of the host.",
|
||||
"concepts": ["Debian vs RPM ecosystems", "Package listing output", "Distribution-specific tooling"],
|
||||
"command": "apt / yum / rpm / dpkg",
|
||||
"examples": ["apt", "yum", "rpm", "dpkg"],
|
||||
"pitfalls": ["Assuming apt exists on every Linux server", "Reading package output without identifying the distro context first"],
|
||||
"scenarios": ["Check which package tool family a server likely uses", "Read enough package output to orient yourself before change work"],
|
||||
"troubleshooting_flow": ["Identify the package family", "Read listing output", "Only then plan installs or version checks"],
|
||||
"related_commands": ["apt", "yum", "rpm", "dpkg", "which"],
|
||||
"classic_view": "Package commands are not interchangeable, so distribution context matters before action.",
|
||||
"takeaways": ["Recognize Debian-style and RPM-style tooling", "Read package output without guessing", "Use package family as part of host context"],
|
||||
"after_class": "Practice explaining which systems are more likely to use apt vs yum or rpm.",
|
||||
"exercises": [
|
||||
{ "id": "m7_l2_e1", "type": "operation", "title": "Inspect apt output", "hint": "Run apt.", "success_test": "cmd == 'apt' and 'Reading package lists' in output", "solution": ["apt"], "success_msg": "You inspected Debian-style package manager output." },
|
||||
{ "id": "m7_l2_e2", "type": "operation", "title": "Inspect yum output", "hint": "Run yum.", "success_test": "cmd == 'yum' and 'Installed Packages' in output", "solution": ["yum"], "success_msg": "You inspected RPM-style package manager output." },
|
||||
{ "id": "m7_l2_e3", "type": "operation", "title": "Inspect rpm output", "hint": "Run rpm.", "success_test": "cmd == 'rpm' and 'bash-' in output", "solution": ["rpm"], "success_msg": "You inspected a package-level rpm response." },
|
||||
{ "id": "m7_l2_e4", "type": "operation", "title": "Inspect dpkg output", "hint": "Run dpkg.", "success_test": "cmd == 'dpkg' and 'ii bash' in output", "solution": ["dpkg"], "success_msg": "You inspected a Debian package-level response." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m7_l3_archives_and_background",
|
||||
"title": "Handle archives and background jobs with tar and nohup",
|
||||
"goal": "Prepare simple deployment artifacts and understand why background execution changes how you observe a job.",
|
||||
"why_it_matters": "Basic delivery work often includes packaging files, extracting them, and running something outside the current terminal.",
|
||||
"concepts": ["Archive creation", "Archive extraction", "Background job output"],
|
||||
"command": "tar / nohup",
|
||||
"examples": ["tar -czf /tmp/lab.tar.gz /tmp/lab", "tar -xzf /tmp/lab.tar.gz -C /tmp/lab_restore", "nohup python3 /projects/backend/app.py"],
|
||||
"pitfalls": ["Treating an archive like a backup without verifying extraction", "Starting a job with nohup without thinking about its output destination"],
|
||||
"scenarios": ["Bundle a small workspace", "Extract an artifact into a staging path", "Run a long task after disconnecting"],
|
||||
"troubleshooting_flow": ["Create the artifact", "Extract it into a clean target", "Use nohup only when the process should survive the terminal"],
|
||||
"related_commands": ["tar", "nohup", "ps", "ls"],
|
||||
"classic_view": "Packaging and background execution are useful, but only when you stay aware of where files and output go.",
|
||||
"takeaways": ["Use tar for simple packaging", "Verify extraction paths", "Use nohup deliberately, not by habit"],
|
||||
"after_class": "Practice explaining what changed in the filesystem after create and extract operations.",
|
||||
"exercises": [
|
||||
{ "id": "m7_l3_e1", "type": "operation", "title": "Create a tar archive", "hint": "Run tar -czf /tmp/lab.tar.gz /tmp/lab.", "success_test": "cmd == 'tar -czf /tmp/lab.tar.gz /tmp/lab' and exists('/tmp/lab.tar.gz')", "solution": ["tar -czf /tmp/lab.tar.gz /tmp/lab"], "success_msg": "You created an archive artifact." },
|
||||
{ "id": "m7_l3_e2", "type": "operation", "title": "Extract the tar archive", "hint": "Run tar -xzf /tmp/lab.tar.gz -C /tmp/lab_restore.", "success_test": "cmd == 'tar -xzf /tmp/lab.tar.gz -C /tmp/lab_restore' and exists('/tmp/lab_restore')", "solution": ["tar -xzf /tmp/lab.tar.gz -C /tmp/lab_restore"], "success_msg": "You extracted the archive into a clean target directory." },
|
||||
{ "id": "m7_l3_e3", "type": "operation", "title": "Start a background-friendly command", "hint": "Run nohup python3 /projects/backend/app.py.", "success_test": "cmd == 'nohup python3 /projects/backend/app.py' and 'nohup.out' in output", "solution": ["nohup python3 /projects/backend/app.py"], "success_msg": "You practiced a nohup-style launch." },
|
||||
{ "id": "m7_l3_e4", "type": "scenario", "question": "Why should you still verify extraction or output paths after using tar or nohup?", "answer": "Because packaging or backgrounding can succeed syntactically while still writing to the wrong place." }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module_8_monitoring_and_scheduling",
|
||||
"title": "Module 8: Read system health and time-based execution",
|
||||
"summary": "Learn the commands that help you interpret a host snapshot and think about recurring work.",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m8_l1_system_snapshot",
|
||||
"title": "Take a quick host snapshot with uptime, free, w, and last",
|
||||
"goal": "Build a fast first-pass host summary before deeper debugging.",
|
||||
"why_it_matters": "A good operator can summarize load, memory, active sessions, and recent logins in under a minute.",
|
||||
"concepts": ["Load averages", "Memory pressure", "Active users", "Login history"],
|
||||
"command": "uptime / free / w / last",
|
||||
"examples": ["uptime", "free", "w", "last"],
|
||||
"pitfalls": ["Reading one metric in isolation", "Ignoring user/session context during troubleshooting"],
|
||||
"scenarios": ["A host feels slow but you need a broad snapshot first", "You need to know whether other users are active on the system"],
|
||||
"troubleshooting_flow": ["Check load with uptime", "Check memory with free", "Check active sessions with w", "Review recent logins with last"],
|
||||
"related_commands": ["uptime", "free", "w", "last", "top"],
|
||||
"classic_view": "Before deep investigation, collect a fast baseline of load, memory, users, and logins.",
|
||||
"takeaways": ["Use multiple quick metrics together", "Include session context in your diagnosis", "Build a snapshot before diving deeper"],
|
||||
"after_class": "Practice summarizing the host in one sentence using all four commands together.",
|
||||
"exercises": [
|
||||
{ "id": "m8_l1_e1", "type": "operation", "title": "Check uptime", "hint": "Run uptime.", "success_test": "cmd == 'uptime' and 'load average' in output", "solution": ["uptime"], "success_msg": "You read the broad load snapshot." },
|
||||
{ "id": "m8_l1_e2", "type": "operation", "title": "Check memory usage", "hint": "Run free.", "success_test": "cmd == 'free' and 'Mem:' in output", "solution": ["free"], "success_msg": "You checked memory pressure." },
|
||||
{ "id": "m8_l1_e3", "type": "operation", "title": "Check active users", "hint": "Run w.", "success_test": "cmd == 'w' and 'USER' in output", "solution": ["w"], "success_msg": "You reviewed active shell users." },
|
||||
{ "id": "m8_l1_e4", "type": "operation", "title": "Check recent login history", "hint": "Run last.", "success_test": "cmd == 'last' and 'sandbox_user' in output", "solution": ["last"], "success_msg": "You reviewed recent login history." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m8_l2_ports_and_paths",
|
||||
"title": "Inspect open paths with lsof, netstat, traceroute, and dig",
|
||||
"goal": "Use deeper network tools when the simple service or socket checks are not enough.",
|
||||
"why_it_matters": "Real network debugging often requires more than ping and curl.",
|
||||
"concepts": ["Open file and socket ownership", "Legacy vs modern network listing", "Route tracing", "DNS lookup"],
|
||||
"command": "lsof / netstat / traceroute / dig",
|
||||
"examples": ["lsof", "netstat", "traceroute 8.8.8.8", "dig example.com"],
|
||||
"pitfalls": ["Treating all network questions as port-only problems", "Ignoring route or DNS evidence when connectivity looks inconsistent"],
|
||||
"scenarios": ["Confirm which process owns a listening socket", "Trace whether the path to a remote target looks reachable", "Check if DNS is the real failure point"],
|
||||
"troubleshooting_flow": ["Use lsof or netstat to inspect local ownership", "Use traceroute to reason about path", "Use dig to isolate DNS problems"],
|
||||
"related_commands": ["lsof", "netstat", "traceroute", "dig", "ss"],
|
||||
"classic_view": "When simple checks stop being enough, move to ownership, route, and DNS visibility.",
|
||||
"takeaways": ["Use lsof for ownership clues", "Use traceroute for route intuition", "Use dig to separate DNS from transport issues"],
|
||||
"after_class": "Practice describing which question each command answers before you run it.",
|
||||
"exercises": [
|
||||
{ "id": "m8_l2_e1", "type": "operation", "title": "Inspect open files and sockets", "hint": "Run lsof.", "success_test": "cmd == 'lsof' and 'LISTEN' in output", "solution": ["lsof"], "success_msg": "You inspected file and socket ownership." },
|
||||
{ "id": "m8_l2_e2", "type": "operation", "title": "Inspect listening network state", "hint": "Run netstat.", "success_test": "cmd == 'netstat' and 'LISTEN' in output", "solution": ["netstat"], "success_msg": "You inspected network listening state with netstat." },
|
||||
{ "id": "m8_l2_e3", "type": "operation", "title": "Trace a network path", "hint": "Run traceroute 8.8.8.8.", "success_test": "cmd == 'traceroute 8.8.8.8' and 'traceroute' in output", "solution": ["traceroute 8.8.8.8"], "success_msg": "You traced a network path." },
|
||||
{ "id": "m8_l2_e4", "type": "operation", "title": "Check DNS resolution", "hint": "Run dig example.com.", "success_test": "cmd == 'dig example.com' and 'ANSWER SECTION' in output", "solution": ["dig example.com"], "success_msg": "You separated DNS lookup from the rest of the network path." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m8_l3_time_and_schedule",
|
||||
"title": "Read scheduling context with date, cal, and crontab",
|
||||
"goal": "Think about when work runs, not just what runs.",
|
||||
"why_it_matters": "A lot of operations work is time-based: jobs, cutovers, maintenance windows, and recurring checks.",
|
||||
"concepts": ["Current system time", "Calendar context", "Recurring jobs"],
|
||||
"command": "date / cal / crontab",
|
||||
"examples": ["date", "cal", "crontab -l"],
|
||||
"pitfalls": ["Ignoring system time when reading logs or scheduling changes", "Treating cron like magic instead of planned automation"],
|
||||
"scenarios": ["Confirm the current host time before correlating logs", "Check whether a user has scheduled jobs", "Think about maintenance windows"],
|
||||
"troubleshooting_flow": ["Read current time first", "Check the calendar context", "Inspect scheduled jobs before assuming nothing runs automatically"],
|
||||
"related_commands": ["date", "cal", "crontab", "history"],
|
||||
"classic_view": "Time is part of system state, and operations mistakes often come from forgetting that.",
|
||||
"takeaways": ["Time-stamp your thinking", "Use calendar context during planning", "Check cron before assuming a task is manual"],
|
||||
"after_class": "Explain how wrong time assumptions can distort a log or incident timeline.",
|
||||
"exercises": [
|
||||
{ "id": "m8_l3_e1", "type": "operation", "title": "Read the current system time", "hint": "Run date.", "success_test": "cmd == 'date' and 'CST' in output", "solution": ["date"], "success_msg": "You checked the system time." },
|
||||
{ "id": "m8_l3_e2", "type": "operation", "title": "Open the calendar view", "hint": "Run cal.", "success_test": "cmd == 'cal' and 'March' in output", "solution": ["cal"], "success_msg": "You opened a calendar view for planning context." },
|
||||
{ "id": "m8_l3_e3", "type": "operation", "title": "Inspect scheduled jobs", "hint": "Run crontab -l.", "success_test": "cmd == 'crontab -l' and 'crontab' in output", "solution": ["crontab -l"], "success_msg": "You checked for scheduled cron jobs." },
|
||||
{ "id": "m8_l3_e4", "type": "scenario", "question": "Why should you check date before correlating logs from an incident?", "answer": "Because a wrong time assumption can make the event sequence look incorrect even if the commands are right." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
59
index.html
59
index.html
@@ -288,6 +288,27 @@
|
||||
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);
|
||||
@@ -305,12 +326,14 @@
|
||||
.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>
|
||||
@@ -408,6 +431,13 @@
|
||||
<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>
|
||||
@@ -567,6 +597,7 @@
|
||||
renderRuntime(state.overview.runtime || {});
|
||||
renderCommandTags(state.overview.commands || []);
|
||||
renderModules();
|
||||
renderStageGrid();
|
||||
renderModuleSummaryGrid();
|
||||
}
|
||||
|
||||
@@ -628,6 +659,34 @@
|
||||
`).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();
|
||||
|
||||
Reference in New Issue
Block a user