"pitfalls":["Assuming every shell opens in the same place","Running a file command before checking the path"],
"scenarios":["Verify where you are before editing a file","Confirm location before launching a script"],
"troubleshooting_flow":["Confirm the working context","Run pwd","Use the output to choose the next command"],
"related_commands":["pwd","cd","ls"],
"classic_view":"Treat pwd as the command that restores orientation before every other action.",
"takeaways":["Know your location before acting","Read the full path instead of guessing","Use location to decide the next safe step"],
"after_class":"Repeat pwd before every directory-changing command until it becomes automatic.",
"exercises":[
{"id":"m1_l1_e1","type":"understanding","question":"Which command prints the full current working directory?","answer":"pwd"},
{"id":"m1_l1_e2","type":"operation","title":"Print the current working directory","hint":"Run pwd directly.","success_test":"cmd == 'pwd'","solution":["pwd"],"success_msg":"You verified the shell location correctly."},
{"id":"m1_l1_e3","type":"scenario","question":"If a script fails with 'file not found', what should you check first?","answer":"Run pwd and verify the current path before anything else."}
"pitfalls":["Thinking a hidden file does not exist because plain ls missed it","Reading long output without understanding permissions or size"],
"scenarios":["Check what files exist before deleting anything","Inspect a config directory for hidden dotfiles"],
"troubleshooting_flow":["List the directory first","Switch to ls -la if details matter","Use the listing to choose the next file command"],
"related_commands":["ls","find","stat"],
"classic_view":"Observation comes before diagnosis, and ls is usually the first observation tool.",
"takeaways":["Use ls for broad visibility","Use ls -la for hidden files and metadata","Treat the listing as evidence"],
"after_class":"Practice comparing ls and ls -la until you can explain why the outputs differ.",
"exercises":[
{"id":"m1_l2_e1","type":"understanding","question":"Why does ls -a reveal more than plain ls?","answer":"Because it includes hidden dotfiles."},
{"id":"m1_l2_e2","type":"operation","title":"List the current directory","hint":"Start with ls.","success_test":"cmd == 'ls'","solution":["ls"],"success_msg":"You listed the current directory."},
{"id":"m1_l2_e3","type":"operation","title":"Show hidden files and details","hint":"Use ls -la.","success_test":"cmd == 'ls -la' or cmd == 'ls -al'","solution":["ls -la","ls -al"],"success_msg":"You switched from a simple listing to a full inspection view."}
"pitfalls":["Using cat on a directory","Changing directories without verifying where you landed","Ignoring echo as a debugging aid"],
"scenarios":["Enter a workspace and inspect a config file","Print quick checkpoints in a shell session"],
"troubleshooting_flow":["Change to the right location","Read the right file","Echo the key assumption you want to verify"],
"related_commands":["cd","cat","echo","pwd"],
"classic_view":"These commands make context and state visible before you do more powerful operations.",
"takeaways":["Use cd intentionally","Use cat for quick text inspection","Use echo to make invisible state explicit"],
"after_class":"Practice saying out loud what each command changed in your working context.",
"exercises":[
{"id":"m1_l3_e1","type":"operation","title":"Enter /tmp","hint":"Use cd /tmp.","success_test":"cmd == 'cd /tmp' and cwd == '/tmp'","solution":["cd /tmp"],"success_msg":"You moved to the target working directory."},
{"id":"m1_l3_e2","type":"operation","title":"Read the hosts file","hint":"Run cat /etc/hosts.","success_test":"cmd == 'cat /etc/hosts' and 'localhost' in output","solution":["cat /etc/hosts"],"success_msg":"You inspected a real text file in the sandbox."},
{"id":"m1_l3_e3","type":"operation","title":"Print a confirmation message","hint":"Run echo Hello Linux.","success_test":"cmd == 'echo Hello Linux' and 'Hello Linux' in output","solution":["echo Hello Linux"],"success_msg":"You used echo to make the current step explicit."}
"pitfalls":["Forgetting -p for nested paths","Trying to touch a file under a missing directory"],
"scenarios":["Prepare a clean lab directory","Create a placeholder log or notes file"],
"troubleshooting_flow":["Check the target path","Create directories first","Create files after the path exists"],
"related_commands":["mkdir","touch","ls"],
"classic_view":"File-system work is safer when you change one layer at a time: path first, file second.",
"takeaways":["Use mkdir for structure","Use touch for placeholders","Think about parent directories before file creation"],
"after_class":"Rebuild the same directory tree twice until the path feels natural.",
"exercises":[
{"id":"m2_l1_e1","type":"operation","title":"Create a lab directory","hint":"Run mkdir /tmp/lab.","success_test":"cmd == 'mkdir /tmp/lab' and exists('/tmp/lab')","solution":["mkdir /tmp/lab"],"success_msg":"Your lab directory now exists."},
{"id":"m2_l1_e2","type":"operation","title":"Create a nested directory tree","hint":"Run mkdir -p /tmp/lab/data/raw.","success_test":"cmd == 'mkdir -p /tmp/lab/data/raw' and exists('/tmp/lab/data/raw')","solution":["mkdir -p /tmp/lab/data/raw"],"success_msg":"You created the nested path correctly."},
{"id":"m2_l1_e3","type":"operation","title":"Create a note file","hint":"Run touch /tmp/lab/notes.txt.","success_test":"cmd == 'touch /tmp/lab/notes.txt' and exists('/tmp/lab/notes.txt')","solution":["touch /tmp/lab/notes.txt"],"success_msg":"You created a new empty file in the lab path."}
"pitfalls":["Deleting before backing up","Changing permissions without checking metadata first"],
"scenarios":["Back up a config before editing","Make a script or directory executable when needed"],
"troubleshooting_flow":["Copy before editing","Rename with a purpose","Inspect metadata before permission changes"],
"related_commands":["cp","mv","chmod","stat"],
"classic_view":"File operations are safe when you can clearly name the step: backup, rename, inspect, or permission change.",
"takeaways":["Back up first","Rename with purpose","Inspect before changing permissions"],
"after_class":"Repeat the flow copy -> rename -> stat -> chmod on a temporary file until the sequence feels obvious.",
"exercises":[
{"id":"m2_l2_e1","type":"operation","title":"Back up the hosts file","hint":"Run cp /etc/hosts /tmp/hosts.bak.","success_test":"cmd == 'cp /etc/hosts /tmp/hosts.bak' and exists('/tmp/hosts.bak')","solution":["cp /etc/hosts /tmp/hosts.bak"],"success_msg":"You created a backup copy before any change."},
{"id":"m2_l2_e2","type":"operation","title":"Rename the backup file","hint":"Run mv /tmp/hosts.bak /tmp/hosts.backup.","success_test":"cmd == 'mv /tmp/hosts.bak /tmp/hosts.backup' and exists('/tmp/hosts.backup')","solution":["mv /tmp/hosts.bak /tmp/hosts.backup"],"success_msg":"You renamed the file successfully."},
{"id":"m2_l2_e3","type":"operation","title":"Inspect hosts metadata","hint":"Run stat /etc/hosts.","success_test":"cmd == 'stat /etc/hosts' and 'File:' in output","solution":["stat /etc/hosts"],"success_msg":"You inspected the file metadata successfully."}
"pitfalls":["Deleting before confirming the file is disposable","Changing permissions without knowing why"],
"scenarios":["Remove temporary files after verification","Make a script or directory executable when needed"],
"troubleshooting_flow":["Verify the target path","Remove only temporary files","Inspect and then change permissions with intent"],
"related_commands":["rm","chmod","stat","ls"],
"classic_view":"Cleanup and permission work should be deliberate, not reactive.",
"takeaways":["Delete last","Use numeric and symbolic modes intentionally","Connect permissions to a real need"],
"after_class":"Repeat the sequence inspect -> remove or inspect -> chmod until it feels like a safe habit.",
"exercises":[
{"id":"m2_l3_e1","type":"operation","title":"Remove the renamed backup","hint":"Run rm /tmp/hosts.backup.","success_test":"cmd == 'rm /tmp/hosts.backup' and not exists('/tmp/hosts.backup')","solution":["rm /tmp/hosts.backup"],"success_msg":"You cleaned up the temporary backup safely."},
{"id":"m2_l3_e2","type":"operation","title":"Set the lab directory mode to 755","hint":"Run chmod 755 /tmp/lab.","success_test":"cmd == 'chmod 755 /tmp/lab'","solution":["chmod 755 /tmp/lab"],"success_msg":"You applied a numeric permission change."},
{"id":"m2_l3_e3","type":"operation","title":"Add execute permission to notes.txt","hint":"Run chmod +x /tmp/lab/notes.txt.","success_test":"cmd == 'chmod +x /tmp/lab/notes.txt'","solution":["chmod +x /tmp/lab/notes.txt"],"success_msg":"You used a symbolic permission change."}
"pitfalls":["Searching too broadly and ignoring context","Forgetting case-insensitive mode"],
"scenarios":["Find one user entry in passwd","Locate error lines in a log"],
"troubleshooting_flow":["Choose the right file first","Search for the smallest useful keyword","Use the matched lines to decide the next command"],
"related_commands":["grep","cat","tail"],
"classic_view":"grep turns a large text space into a small set of signals you can reason about.",
"takeaways":["Search deliberately","Narrow the signal before changing anything","Use matched lines as evidence"],
"after_class":"Practice moving from a broad keyword to a more precise keyword in the same file.",
"exercises":[
{"id":"m3_l1_e1","type":"operation","title":"Find sandbox_user in passwd","hint":"Run grep sandbox_user /etc/passwd.","success_test":"cmd == 'grep sandbox_user /etc/passwd' and 'sandbox_user' in output","solution":["grep sandbox_user /etc/passwd"],"success_msg":"You extracted the relevant passwd line."},
{"id":"m3_l1_e2","type":"operation","title":"Find error lines in syslog","hint":"Run grep -i error /var/log/syslog.","success_test":"cmd == 'grep -i error /var/log/syslog' and 'error' in output.lower()","solution":["grep -i error /var/log/syslog"],"success_msg":"You filtered the log down to the error lines."},
{"id":"m3_l1_e3","type":"scenario","question":"Why is grep usually faster than reading the whole log manually?","answer":"Because it narrows the file to only the lines that match the signal you care about."}
"pitfalls":["Searching from too high in the tree","Using cat on a large file when tail would answer faster"],
"scenarios":["Find config files under /etc","Inspect the newest auth log line"],
"troubleshooting_flow":["Pick the smallest reasonable root","Add a filter","Preview the most relevant file segment"],
"related_commands":["find","tail","head"],
"classic_view":"A good search ends with an exact path and a small, readable preview.",
"takeaways":["Use find for precise location","Use tail for fresh log evidence","Reduce noise early"],
"after_class":"Practice explaining the root path and every filter before you run find.",
"exercises":[
{"id":"m3_l2_e1","type":"operation","title":"Find configuration files under /etc","hint":"Run find /etc -name '*.conf'.","success_test":"cmd == \"find /etc -name '*.conf'\" and '.conf' in output","solution":["find /etc -name '*.conf'"],"success_msg":"You located configuration files recursively."},
{"id":"m3_l2_e2","type":"operation","title":"Find log files under /var/log","hint":"Run find /var/log -type f.","success_test":"cmd == 'find /var/log -type f' and '/var/log' in output","solution":["find /var/log -type f"],"success_msg":"You filtered the search to files under the log tree."},
{"id":"m3_l2_e3","type":"operation","title":"Read the newest auth log line","hint":"Run tail -n 1 /var/log/auth.log.","success_test":"cmd == 'tail -n 1 /var/log/auth.log' and 'sandbox_user' in output","solution":["tail -n 1 /var/log/auth.log"],"success_msg":"You previewed the most recent auth log line."}
"pitfalls":["Using cat on a large file when a preview is enough","Forgetting to change the line count for a focused question"],
"scenarios":["Check whether a file starts with the right structure","Inspect the last auth event in a log"],
"troubleshooting_flow":["Choose the right end of the file","Preview a small number of lines","Switch to grep or full reads only if needed"],
"related_commands":["head","tail","cat","grep"],
"classic_view":"Preview commands reduce noise so you can decide whether the file deserves deeper inspection.",
"takeaways":["Use head for structure","Use tail for recent activity","Use a pipeline when one command is not enough"],
"after_class":"Practice explaining why you chose head or tail before you run the command.",
"exercises":[
{"id":"m3_l3_e1","type":"operation","title":"Preview the top of passwd","hint":"Run head /etc/passwd.","success_test":"cmd == 'head /etc/passwd' and 'root' in output","solution":["head /etc/passwd"],"success_msg":"You previewed the beginning of the file."},
{"id":"m3_l3_e2","type":"operation","title":"Inspect the newest auth log line","hint":"Run tail -n 1 /var/log/auth.log.","success_test":"cmd == 'tail -n 1 /var/log/auth.log' and 'sandbox_user' in output","solution":["tail -n 1 /var/log/auth.log"],"success_msg":"You narrowed the file to the most recent line."},
{"id":"m3_l3_e3","type":"operation","title":"Use a simple pipeline to preview auth.log","hint":"Run cat /var/log/auth.log | tail -n 1.","success_test":"cmd == 'cat /var/log/auth.log | tail -n 1' and 'sandbox_user' in output","solution":["cat /var/log/auth.log | tail -n 1"],"success_msg":"You combined two commands to narrow the log quickly."}
"goal":"Spot busy processes and connect process views to system pressure.",
"why_it_matters":"Incidents often begin with 'the system feels slow', and process views turn that feeling into evidence.",
"concepts":["Process snapshots","CPU-heavy processes","Live system views"],
"command":"top / ps",
"examples":["top","ps aux --sort=-%cpu | head"],
"pitfalls":["Jumping straight to kill before understanding the process role","Looking at one snapshot without connecting it to symptoms"],
"scenarios":["Find the hottest process when CPU looks high","Explain which command gives the faster broad view"],
"troubleshooting_flow":["Look at top for the broad picture","Use ps sorting to confirm the hottest process","Only then decide the next action"],
"related_commands":["top","ps","kill"],
"classic_view":"System pressure is easier to reason about when you move from a broad live view to a specific process list.",
"takeaways":["Use top for the live picture","Use ps for a sortable snapshot","Diagnose before you terminate anything"],
"after_class":"Compare top and ps side by side and explain what each one is better at.",
"exercises":[
{"id":"m4_l1_e1","type":"operation","title":"Show a live overview","hint":"Run top.","success_test":"cmd == 'top' and 'load average' in output","solution":["top"],"success_msg":"You captured a live system overview."},
{"id":"m4_l1_e2","type":"operation","title":"List processes by CPU usage","hint":"Run ps aux --sort=-%cpu | head.","success_test":"cmd == 'ps aux --sort=-%cpu | head' and '%CPU' in output","solution":["ps aux --sort=-%cpu | head"],"success_msg":"You ranked processes by CPU usage."},
{"id":"m4_l1_e3","type":"understanding","question":"Why should kill come after observation rather than before it?","answer":"Because you need evidence about what the process is doing and whether it is safe to stop."}
"pitfalls":["Treating DNS, port, and app failures as one category","Checking a port without making a real request"],
"scenarios":["A local service looks up but the app still feels unavailable","You need to confirm whether the issue is address, reachability, port, or HTTP behavior"],
"troubleshooting_flow":["Check interface state with ip addr","Test connectivity with ping","Check sockets with ss","Validate the app layer with curl"],
"classic_view":"The cleanest network diagnosis moves from low-level reachability to the final application response.",
"takeaways":["Do not skip layers","Confirm a port before blaming the app","Use curl to verify user-facing behavior"],
"after_class":"Repeat the sequence ip -> ping -> ss -> curl until the layer transitions feel natural.",
"exercises":[
{"id":"m4_l2_e1","type":"operation","title":"Inspect interface addresses","hint":"Run ip addr.","success_test":"cmd == 'ip addr' and 'inet' in output","solution":["ip addr"],"success_msg":"You confirmed interface and address information."},
{"id":"m4_l2_e2","type":"operation","title":"Test local connectivity","hint":"Run ping 127.0.0.1.","success_test":"cmd == 'ping 127.0.0.1' and 'packet loss' in output","solution":["ping 127.0.0.1"],"success_msg":"You verified the local connectivity layer."},
{"id":"m4_l2_e3","type":"operation","title":"Validate the application response","hint":"Run curl http://127.0.0.1.","success_test":"cmd == 'curl http://127.0.0.1' and 'hello localhost' in output","solution":["curl http://127.0.0.1"],"success_msg":"You completed the network path all the way to the application layer."}
"classic_view":"Service status tells you what is happening; logs tell you why.",
"takeaways":["Start with status","Use logs for explanation","Restart only after you understand the failure"],
"after_class":"Say out loud what systemctl tells you that journalctl does not, and vice versa.",
"exercises":[
{"id":"m4_l3_e1","type":"operation","title":"Check nginx service status","hint":"Run systemctl status nginx.","success_test":"cmd == 'systemctl status nginx' and 'Active:' in output","solution":["systemctl status nginx"],"success_msg":"You inspected the service state."},
{"id":"m4_l3_e3","type":"operation","title":"Simulate a service restart command","hint":"Run systemctl restart nginx.","success_test":"cmd == 'systemctl restart nginx' and 'completed for nginx' in output","solution":["systemctl restart nginx"],"success_msg":"You practiced the restart command after inspection."}
]
}
]
},
{
"id":"module_5_incidents",
"title":"Module 5: Practice incident playbooks",
"summary":"Use the commands you learned in layered incident drills so the tools feel connected to real operations work.",
"lessons":[
{
"id":"m5_l1_disk",
"title":"Incident drill: investigate disk pressure",
"goal":"Move from broad disk visibility to specific directory evidence.",
"why_it_matters":"Disk pressure can break writes, logs, and services, so you need a repeatable inspection path.",
"pitfalls":["Stopping at df without drilling into the heavy directory","Deleting files before confirming what they are for"],
"scenarios":["A host reports that disk usage is full","A log directory appears to be growing too quickly"],
"troubleshooting_flow":["Confirm which filesystem is full","Measure the likely heavy directory","Locate concrete files before deciding cleanup"],
"related_commands":["df","du","find","sort"],
"classic_view":"Disk incidents are easier when you move from filesystem view to directory view to file view in order.",
"takeaways":["Use df for the broad picture","Use du for directory evidence","Use find before deleting anything"],
"after_class":"Practice saying which layer each command belongs to: filesystem, directory, or file.",
"exercises":[
{"id":"m5_l1_e1","type":"operation","title":"Check filesystem usage","hint":"Run df -h.","success_test":"cmd == 'df -h' and 'Filesystem' in output","solution":["df -h"],"success_msg":"You captured the broad disk usage picture."},
{"id":"m5_l1_e2","type":"operation","title":"Measure /var/log usage","hint":"Run du -sh /var/log.","success_test":"cmd == 'du -sh /var/log' and '/var/log' in output","solution":["du -sh /var/log"],"success_msg":"You narrowed the issue to a directory-level view."},
{"id":"m5_l1_e3","type":"operation","title":"Locate log files under /var/log","hint":"Run find /var/log -type f.","success_test":"cmd == 'find /var/log -type f' and '/var/log' in output","solution":["find /var/log -type f"],"success_msg":"You drilled down to the file layer."},
{"id":"m5_l1_e4","type":"scenario","question":"Why is deleting files before directory and file inspection risky?","answer":"Because you can remove useful evidence or the wrong data before you understand what is consuming space."}
]
},
{
"id":"m5_l2_auth",
"title":"Incident drill: investigate login or permission failures",
"goal":"Link identity, account records, and recent auth logs into one diagnosis sequence.",
"why_it_matters":"Access problems often mix user identity, account state, and authentication evidence.",
"pitfalls":["Guessing a password issue without checking logs","Ignoring account records and group membership"],
"scenarios":["A login fails repeatedly","A script exists but still cannot be used by the current user"],
"troubleshooting_flow":["Confirm the current identity","Verify the target account exists","Read the newest auth evidence"],
"related_commands":["whoami","id","grep","tail"],
"classic_view":"Access issues are easier when you inspect identity, account records, and logs as one chain.",
"takeaways":["Confirm the user context first","Use account files as evidence","Trust logs more than guesses"],
"after_class":"Repeat the identity -> account -> log sequence until it feels like one habit.",
"exercises":[
{"id":"m5_l2_e1","type":"operation","title":"Confirm your current identity","hint":"Run whoami.","success_test":"cmd == 'whoami' and 'sandbox_user' in output","solution":["whoami"],"success_msg":"You confirmed the current shell identity."},
{"id":"m5_l2_e2","type":"operation","title":"Inspect uid and group information","hint":"Run id.","success_test":"cmd == 'id' and 'uid=' in output","solution":["id"],"success_msg":"You inspected the uid and group context."},
{"id":"m5_l2_e3","type":"operation","title":"Check whether sandbox_user exists in passwd","hint":"Run grep sandbox_user /etc/passwd.","success_test":"cmd == 'grep sandbox_user /etc/passwd' and 'sandbox_user' in output","solution":["grep sandbox_user /etc/passwd"],"success_msg":"You verified the account record."},
"examples":["systemctl status nginx","ss -ltnp","curl http://127.0.0.1","journalctl -n 20"],
"pitfalls":["Trusting systemctl alone without checking sockets or a real request","Stopping at ss without making an application-layer call"],
"scenarios":["A team says the service is up but the page still feels unavailable","You need to prove whether the problem is service state, port state, or HTTP behavior"],
"troubleshooting_flow":["Check systemctl for service state","Check ss for listening ports","Use curl to verify the real response","Open journal logs if behavior still looks wrong"],
"classic_view":"Healthy service state does not guarantee healthy application behavior, so finish the chain with a real request.",
"takeaways":["Check multiple layers","Do not confuse a listening port with a healthy app","End the chain with a real request and logs when needed"],
"after_class":"Practice explaining which layer each command validates: service, socket, application, or logs.",
"exercises":[
{"id":"m5_l3_e1","type":"operation","title":"Check nginx service state","hint":"Run systemctl status nginx.","success_test":"cmd == 'systemctl status nginx' and 'Active:' in output","solution":["systemctl status nginx"],"success_msg":"You confirmed the service layer state."},
{"id":"m5_l3_e2","type":"operation","title":"Check listening sockets","hint":"Run ss -ltnp.","success_test":"cmd == 'ss -ltnp' and 'LISTEN' in output","solution":["ss -ltnp"],"success_msg":"You confirmed the socket layer."},
{"id":"m5_l3_e3","type":"operation","title":"Make a real application request","hint":"Run curl http://127.0.0.1.","success_test":"cmd == 'curl http://127.0.0.1' and 'hello localhost' in output","solution":["curl http://127.0.0.1"],"success_msg":"You verified the final application response layer."},
{"id":"m5_l3_e4","type":"operation","title":"Open recent journal lines","hint":"Run journalctl -n 20.","success_test":"cmd == 'journalctl -n 20' and 'Started nginx.service' in output","solution":["journalctl -n 20"],"success_msg":"You added log evidence to the service drill."}
"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.",
"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"],
"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"],
"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"],
"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.",
{"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.",
"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"],
"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.",
"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.",
"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"],
"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"],
"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"],
"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."}
"examples":["systemctl status nginx","ss -ltnp","curl http://127.0.0.1"],
"pitfalls":["Treating a running service as proof of healthy delivery","Stopping at a listening socket without making a request"],
"scenarios":["A service was restarted after deployment but the site still looks broken","You need to prove whether the issue is process, port, or response body"],
"troubleshooting_flow":["Check the service state","Check listening sockets","Send a real HTTP request","Use the result to decide whether you need logs next"],
"classic_view":"Healthy delivery means the request path works end to end, not just that one process is running.",
"takeaways":["Check multiple layers","Do not confuse a listening port with a correct response","Always include a real request in a delivery check"],
"after_class":"Practice saying which layer each command verifies: service, port, or HTTP response.",
"exercises":[
{"id":"m9_l1_e1","type":"operation","title":"Check nginx service state","hint":"Run systemctl status nginx.","success_test":"cmd == 'systemctl status nginx' and 'Active:' in output","solution":["systemctl status nginx"],"success_msg":"You verified the service layer."},
{"id":"m9_l1_e2","type":"operation","title":"Check listening sockets","hint":"Run ss -ltnp.","success_test":"cmd == 'ss -ltnp' and 'LISTEN' in output","solution":["ss -ltnp"],"success_msg":"You verified the socket layer."},
{"id":"m9_l1_e3","type":"operation","title":"Check the HTTP response","hint":"Run curl http://127.0.0.1.","success_test":"cmd == 'curl http://127.0.0.1' and 'hello localhost' in output","solution":["curl http://127.0.0.1"],"success_msg":"You verified the HTTP layer."},
{"id":"m9_l1_e4","type":"scenario","question":"If systemctl and ss look fine but curl still fails, which layer deserves your attention next?","answer":"The application response path and logs, because process and port checks alone do not prove healthy delivery."}
]
},
{
"id":"m9_l2_fetch_and_compare",
"title":"Fetch and compare remote content with curl and wget",
"goal":"Understand the difference between checking a response and saving an artifact.",
"why_it_matters":"Operations work often mixes quick request verification with actual file retrieval during delivery or recovery.",
"pitfalls":["Using wget when you only need a quick response check","Downloading a file without checking where it was saved"],
"scenarios":["Confirm a page is reachable","Fetch a package or static asset into a working directory"],
"troubleshooting_flow":["Use curl for quick HTTP inspection","Use wget when you need a saved file","Verify the saved path or body immediately"],
"related_commands":["curl","wget","ls","cat"],
"classic_view":"Choose between inspect and download deliberately instead of treating all HTTP tools as the same.",
"takeaways":["Use curl for response checks","Use wget for saved artifacts","Verify destination and content after retrieval"],
"after_class":"Explain why curl and wget answer different operational questions even when they both touch HTTP.",
"exercises":[
{"id":"m9_l2_e1","type":"operation","title":"Inspect the local web response","hint":"Run curl http://127.0.0.1.","success_test":"cmd == 'curl http://127.0.0.1' and 'hello localhost' in output","solution":["curl http://127.0.0.1"],"success_msg":"You used curl as a response-checking tool."},
{"id":"m9_l2_e2","type":"operation","title":"Simulate downloading an artifact","hint":"Run wget https://example.com/file.tar.gz.","success_test":"cmd == 'wget https://example.com/file.tar.gz' and 'saved' in output.lower()","solution":["wget https://example.com/file.tar.gz"],"success_msg":"You used wget as an artifact download tool."},
{"id":"m9_l2_e3","type":"understanding","question":"Why is curl often the better first tool during a health check?","answer":"Because it quickly shows the response without introducing file-placement concerns."}
]
},
{
"id":"m9_l3_config_runtime",
"title":"Inspect runtime config clues with cat, grep, and journalctl",
"goal":"Correlate config files, log output, and service behavior during a web issue.",
"why_it_matters":"Web incidents often require you to connect what the config says with what the logs report.",
"classic_view":"Config and logs make more sense when you read them as one story instead of separate artifacts.",
"takeaways":["Read before changing","Search logs for matching symptoms","Use config and logs together"],
"after_class":"Practice explaining how one config clue should influence what log pattern you search for.",
"exercises":[
{"id":"m9_l3_e1","type":"operation","title":"Read nginx.conf","hint":"Run cat /etc/nginx.conf.","success_test":"cmd == 'cat /etc/nginx.conf' and 'worker_processes' in output","solution":["cat /etc/nginx.conf"],"success_msg":"You inspected the active config file."},
{"id":"m9_l3_e2","type":"operation","title":"Search syslog for errors","hint":"Run grep error /var/log/syslog.","success_test":"cmd == 'grep error /var/log/syslog' and 'error' in output.lower()","solution":["grep error /var/log/syslog"],"success_msg":"You filtered log output down to the error signal."},
{"id":"m9_l3_e3","type":"operation","title":"Read recent journal entries","hint":"Run journalctl -n 20.","success_test":"cmd == 'journalctl -n 20' and 'nginx' in output.lower()","solution":["journalctl -n 20"],"success_msg":"You correlated service behavior with recent log evidence."}
]
}
]
},
{
"id":"module_10_backup_and_recovery",
"title":"Module 10: Backup, recovery, and safe change patterns",
"summary":"Practice the habits that make change safer: copy, archive, inspect, restore, and verify.",
"lessons":[
{
"id":"m10_l1_backup_patterns",
"title":"Create rollback options with cp and tar",
"goal":"Understand simple rollback preparation before changing files or directories.",
"why_it_matters":"A safe change is easier when you can return to a known-good file or archive quickly.",
"pitfalls":["Changing a file before creating any rollback artifact","Treating a backup as valid without verifying it exists"],
"scenarios":["Prepare for a risky config edit","Bundle a directory tree for later recovery"],
"troubleshooting_flow":["Create a copy or archive first","Verify the backup exists","Only then proceed with the risky change"],
"related_commands":["cp","tar","ls","stat"],
"classic_view":"Backups are what gives you permission to change safely.",
"takeaways":["Make rollback options first","Verify that the backup artifact exists","Choose copy vs archive based on scope"],
"after_class":"Before any file edit, say what your rollback object is and where it lives.",
"exercises":[
{"id":"m10_l1_e1","type":"operation","title":"Create a rollback copy","hint":"Run cp /etc/hosts /tmp/hosts.rollback.","success_test":"cmd == 'cp /etc/hosts /tmp/hosts.rollback' and exists('/tmp/hosts.rollback')","solution":["cp /etc/hosts /tmp/hosts.rollback"],"success_msg":"You created a rollback copy before change."},
{"id":"m10_l1_e2","type":"operation","title":"Create an archive snapshot","hint":"Run tar -czf /tmp/etc-snapshot.tar.gz /etc.","success_test":"cmd == 'tar -czf /tmp/etc-snapshot.tar.gz /etc' and exists('/tmp/etc-snapshot.tar.gz')","solution":["tar -czf /tmp/etc-snapshot.tar.gz /etc"],"success_msg":"You created an archive-based snapshot."},
{"id":"m10_l1_e3","type":"scenario","question":"Why is a verified backup more valuable than just planning to remember the old state?","answer":"Because rollback depends on an actual artifact, not memory or assumptions."}
]
},
{
"id":"m10_l2_restore_path",
"title":"Restore into a clean path and inspect the result",
"goal":"Practice extracting or copying into a clean recovery target instead of overwriting blindly.",
"why_it_matters":"Safe recovery often means restoring to a staging path first so you can inspect what you got back.",
"concepts":["Clean restore target","Artifact extraction","Verification after recovery"],
"pitfalls":["Extracting directly into a live path without inspection","Skipping path validation after a restore"],
"scenarios":["Recover a config tree into a staging directory","Inspect a restored artifact before using it"],
"troubleshooting_flow":["Create a clean target path","Extract into the clean path","List the restored result before using it"],
"related_commands":["mkdir","tar","ls","find"],
"classic_view":"Recovery is safer when you restore into a clean inspection space first.",
"takeaways":["Restore into a staging path when possible","Inspect restored content before reuse","Use ls or find to verify the recovery target"],
"after_class":"Practice saying where you would restore first and why that path is safer than the live target.",
"exercises":[
{"id":"m10_l2_e1","type":"operation","title":"Create a clean restore path","hint":"Run mkdir -p /tmp/restore/etc.","success_test":"cmd == 'mkdir -p /tmp/restore/etc' and exists('/tmp/restore/etc')","solution":["mkdir -p /tmp/restore/etc"],"success_msg":"You created a clean restore target."},
{"id":"m10_l2_e2","type":"operation","title":"Extract the snapshot into the restore path","hint":"Run tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc.","success_test":"cmd == 'tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc' and exists('/tmp/restore/etc')","solution":["tar -xzf /tmp/etc-snapshot.tar.gz -C /tmp/restore/etc"],"success_msg":"You restored the archive into a clean path."},
{"id":"m10_l2_e3","type":"operation","title":"List the restore target","hint":"Run ls /tmp/restore/etc.","success_test":"cmd == 'ls /tmp/restore/etc' and len(output) >= 0","solution":["ls /tmp/restore/etc"],"success_msg":"You inspected the restore target after extraction."}
]
},
{
"id":"m10_l3_change_window",
"title":"Plan a safe change window with date, cal, history, and journalctl",
"goal":"Think about change timing, traceability, and rollback evidence as part of routine operations work.",
"why_it_matters":"A safe operator tracks when a change happened and what happened immediately before or after it.",
"pitfalls":["Making a change without checking maintenance timing","Losing the command narrative that explains what happened"],
"scenarios":["Prepare for a maintenance window","Reconstruct the sequence around a recent restart or config change"],
"troubleshooting_flow":["Anchor the time","Check the calendar or window context","Review your command narrative","Read recent logs around the change"],
"classic_view":"Operational safety improves when every change has time context, command context, and log context.",
"takeaways":["Time-stamp your changes","Keep a readable command narrative","Pair history with logs for recovery thinking"],
"after_class":"Practice explaining a change using time, commands, and logs as one connected story.",
"exercises":[
{"id":"m10_l3_e1","type":"operation","title":"Anchor the current time","hint":"Run date.","success_test":"cmd == 'date' and 'CST' in output","solution":["date"],"success_msg":"You anchored the current time."},
{"id":"m10_l3_e2","type":"operation","title":"Review the calendar context","hint":"Run cal.","success_test":"cmd == 'cal' and 'March' in output","solution":["cal"],"success_msg":"You reviewed the calendar context."},
{"id":"m10_l3_e3","type":"operation","title":"Review your command narrative","hint":"Run history.","success_test":"cmd == 'history' and len(output) >= 0","solution":["history"],"success_msg":"You reviewed the shell command timeline."},
{"id":"m10_l3_e4","type":"operation","title":"Review recent journal lines","hint":"Run journalctl -n 20.","success_test":"cmd == 'journalctl -n 20' and 'nginx' in output.lower()","solution":["journalctl -n 20"],"success_msg":"You linked the change window to recent service logs."}