aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 15:00:25 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 15:00:25 +0200
commitbc199e350ea4ab000df03c9e732bf3478c9c43b6 (patch)
treedd75d59ec1acfeac2eae40de8f860ffcb34220f5
parent3966a870b13ab0896e6cfa7fb3bca2003846b7e5 (diff)
course: fix P0 content errors + completion/mobile/keyboard UX (review A)
-rw-r--r--course.html86
-rw-r--r--course/chapters/02-architecture.json32
-rw-r--r--course/chapters/03-commands.json28
-rw-r--r--course/chapters/05-modules.json12
-rw-r--r--course/chapters/06-files-cloud.json4
-rw-r--r--course/chapters/07-automation.json9
-rw-r--r--course/chapters/08-build.json8
7 files changed, 115 insertions, 64 deletions
diff --git a/course.html b/course.html
index c14979c..03ada60 100644
--- a/course.html
+++ b/course.html
@@ -20,7 +20,15 @@ header h1{font-size:1.05rem;letter-spacing:.3px}
.btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));border:0;color:#0b0b18;font-weight:700}
.grow{flex:1}
.layout{display:grid;grid-template-columns:280px minmax(0,1fr) 300px;gap:18px;padding:18px;max-width:1500px;margin:0 auto}
-@media(max-width:1050px){.layout{grid-template-columns:1fr}.rail{display:none}}
+.rail-toggle{display:none}
+@media(max-width:1050px){.layout{grid-template-columns:1fr}
+ .rail{display:block;position:fixed;left:0;right:0;bottom:0;max-height:82vh;overflow:auto;z-index:40;border-radius:16px 16px 0 0;transform:translateY(105%);transition:transform .25s;padding-bottom:28px}
+ .rail.open{transform:none}.rail-toggle{display:inline-block}}
+.backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:35}.backdrop.show{display:block}
+:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
+button.chapter,button.lesson{width:100%;background:none;border:0;text-align:left;font-family:inherit;color:inherit}
+.lesson.active{color:var(--text);background:var(--bg3);border-left-color:var(--accent)}
+#reader{scroll-margin-top:70px}
.panel{background:rgba(20,20,40,.72);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
.side h3,.rail h3{font-size:.75rem;text-transform:uppercase;letter-spacing:.12em;color:var(--text2);margin-bottom:10px}
.chapter{display:flex;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:9px;cursor:pointer;font-size:.85rem}
@@ -55,6 +63,8 @@ main h2{font-size:1.35rem;margin-bottom:6px}
<span class="chip" id="ver">loading…</span>
<span class="chip" id="xp">0 XP</span>
<div class="grow"></div>
+ <button class="btn rail-toggle" onclick="toggleRail('atlas')">Atlas</button>
+ <button class="btn rail-toggle" onclick="toggleRail('tutor')">Tutor</button>
<button class="btn" onclick="showProjects()">Projects</button>
<button class="btn" onclick="showFeed()">Daily</button>
<a class="btn" href="main.html">⌨ Terminal</a>
@@ -74,16 +84,18 @@ main h2{font-size:1.35rem;margin-bottom:6px}
</div>
</aside>
<main class="panel" id="reader"><p class="small">Loading course…</p></main>
- <aside class="panel rail">
- <h3>Command atlas</h3>
- <input class="search" id="atlas-q" placeholder="filter 344 commands…" autocomplete="off">
+ <aside class="panel rail" id="rail">
+ <button class="btn" style="float:right" onclick="closeRail()">Close</button>
+ <h3 id="atlas-head">Command atlas</h3>
+ <input class="search" id="atlas-q" placeholder="filter commands…" autocomplete="off">
<div id="atlas" style="margin-top:8px;max-height:400px;overflow:auto"></div>
- <h3 style="margin-top:16px">Ask the tutor</h3>
+ <h3 id="tutor-head" style="margin-top:16px">Ask the tutor</h3>
<div id="tutor-log"></div>
<input class="search" id="tutor-q" placeholder="ask about this lesson…" autocomplete="off">
<button class="btn primary" style="width:100%;margin-top:8px" id="tutor-send">Ask AI tutor</button>
</aside>
</div>
+<div class="backdrop" id="backdrop" onclick="closeRail()"></div>
<script>
const BASE = '/spaces/newos4/course/';
const KEY = 'webx11_course_progress_v1';
@@ -103,6 +115,7 @@ async function boot(){
MANIFEST = m; ATLAS = a;
$('ver').textContent = 'v' + m.version;
$('telemetry').checked = localStorage.getItem('webx11_course_telemetry') !== 'off';
+ $('atlas-q').placeholder = 'filter ' + Object.keys((a&&a.commands)||{}).length + ' commands…';
renderChapters(); renderAtlas(); renderProgress();
}catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Course content unreachable: '+esc(e.message)+'</p>'; }
}
@@ -114,36 +127,45 @@ function renderProgress(){
function renderAtlas(q){
if(!ATLAS){ $('atlas').innerHTML='<p class="small" style="color:var(--rose)">atlas.json missing</p>'; return; }
const items = Object.entries(ATLAS.commands).filter(([c,e])=> !q || (c+' '+e.desc+' '+e.module+' '+e.category).toLowerCase().includes(q.toLowerCase()));
- $('atlas').innerHTML = '<p class="small" style="margin-bottom:6px">'+items.length+' / '+Object.keys(ATLAS.commands).length+' commands</p>'
+ const total = Object.keys(ATLAS.commands).length;
+ $('atlas').innerHTML = '<p class="small" style="margin-bottom:6px">Showing '+Math.min(items.length,60)+' of '+total+' commands'+(items.length>60?' — refine to narrow':'')+'</p>'
+ items.slice(0,60).map(([c,e])=>'<div class="atlas-item"><code>'+esc(c)+'</code><span>'+esc(e.desc||e.module)+' · '+esc(e.tier)+'</span></div>').join('');
}
-async function renderChapters(){
- const p = prog(), wrap = $('chapters'); wrap.innerHTML = '';
+async function renderChapters(autoOpen = true){
+ const p = prog(), wrap = $('chapters'); wrap.innerHTML = ''; const boxes = {}, chBtns = {};
for(const ch of MANIFEST.chapters||[]){
- const d = document.createElement('div'); d.className='chapter';
+ const d = document.createElement('button'); d.type='button'; d.className='chapter'; d.setAttribute('aria-expanded','false');
d.innerHTML = '<span>'+esc(ch.icon||'📘')+' '+esc(ch.title)+'</span><small>'+ (ch.lessons||[]).filter(l=>p.completed[l.id]).length +'/'+(ch.lessons||[]).length+'</small>';
- wrap.appendChild(d);
- const box = document.createElement('div'); wrap.appendChild(box);
- d.onclick = () => { d.classList.toggle('active'); box.style.display = box.style.display==='none'?'block':'none'; };
+ const box = document.createElement('div'); box.style.display='none';
+ wrap.appendChild(d); wrap.appendChild(box); boxes[ch.id] = box; chBtns[ch.id] = d;
+ d.onclick = () => { const open = box.style.display==='none'; box.style.display = open?'block':'none'; d.classList.toggle('active', open); d.setAttribute('aria-expanded', String(open)); };
for(const l of ch.lessons||[]){
- const e = document.createElement('div'); e.className='lesson'+(p.completed[l.id]?' done':''); e.textContent = l.id+' · '+l.title;
+ const e = document.createElement('button'); e.type='button'; e.className='lesson'+(p.completed[l.id]?' done':''); e.dataset.lesson = l.id;
+ e.setAttribute('aria-current','false'); e.textContent = l.id+' · '+l.title;
e.onclick = () => openLesson(ch.id, l.id); box.appendChild(e);
}
}
- const p2 = prog(); const all = (MANIFEST.chapters||[]).flatMap(c=>(c.lessons||[]).map(l=>({c,l})));
- const first = all.find(x=>!p2.completed[x.l.id]) || all[0];
- if(first) openLesson(first.c.id, first.l.id, false);
+ if(autoOpen){
+ const p2 = prog(); const all = (MANIFEST.chapters||[]).flatMap(c=>(c.lessons||[]).map(l=>({c,l})));
+ const first = all.find(x=>!p2.completed[x.l.id]) || all[0];
+ if(first){ if(boxes[first.c.id]){ boxes[first.c.id].style.display='block'; chBtns[first.c.id].classList.add('active'); chBtns[first.c.id].setAttribute('aria-expanded','true'); } openLesson(first.c.id, first.l.id, false); }
+ }
+ if(ACTIVE && ACTIVE.ch && boxes[ACTIVE.ch.id]){ boxes[ACTIVE.ch.id].style.display='block'; chBtns[ACTIVE.ch.id].classList.add('active'); chBtns[ACTIVE.ch.id].setAttribute('aria-expanded','true'); }
+ if(ACTIVE && ACTIVE.l){ const a = document.querySelector('.lesson[data-lesson="'+ACTIVE.l.id+'"]'); if(a){ a.classList.add('active'); a.setAttribute('aria-current','true'); } }
}
+let OPEN_SEQ = 0;
async function openLesson(chId, lessonId, doTrack = true){
+ const seq = ++OPEN_SEQ;
$('reader').innerHTML = '<p class="small">Loading lesson…</p>';
try{
const ch = await fetch(BASE+'chapters/'+chId+'.json?v='+V).then(r=>r.json());
const l = (ch.lessons||[]).find(x=>x.id===lessonId); if(!l) throw new Error('lesson not found');
+ if(seq !== OPEN_SEQ) return;
ACTIVE = {ch, l};
const p = prog();
- let html = '<h2>'+esc(l.title)+'</h2><div class="meta">'+esc(l.objective||'')+' · ⏱ '+(l.minutes||5)+' min · 📘 '+esc(ch.title)+'</div>';
+ let html = '<h2>'+esc(l.title)+'</h2><div class="meta">'+esc(l.objective||'')+' · ⏱ '+esc(l.minutes||5)+' min · 📘 '+esc(ch.title)+'</div>';
for(const b of l.blocks||[]){
- if(b.type==='cmd') html += '<div class="block cmd"><span>$ '+esc(b.text)+'</span><button class="btn" data-copy="'+esc(b.text)+'">Copy</button></div>';
+ if(b.type==='cmd') html += '<div class="block cmd"><span>$ '+esc(b.text)+'</span><button class="btn" aria-label="Copy command" data-copy="'+esc(b.text)+'">Copy</button></div>';
else if(b.type==='code') html += '<pre class="block cmd">'+esc(b.text)+'</pre>';
else if(b.type==='tip') html += '<div class="block tip">💡 '+esc(b.text)+'</div>';
else if(b.type==='warn') html += '<div class="block warn">⚠️ '+esc(b.text)+'</div>';
@@ -159,24 +181,34 @@ async function openLesson(chId, lessonId, doTrack = true){
if(l.quiz){
html += '<div class="quiz"><strong>❓ '+esc(l.quiz.q)+'</strong>';
(l.quiz.options||[]).forEach((o,i)=>{ html += '<button class="opt" data-opt="'+i+'">'+(i+1)+') '+esc(o)+'</button>'; });
- html += '<p id="quiz-msg" style="margin-top:6px"></p></div>';
+ html += '<p id="quiz-msg" role="status" aria-live="polite" style="margin-top:6px"></p></div>';
}
$('reader').innerHTML = html;
+ document.querySelectorAll('.lesson').forEach(x=>{ x.classList.remove('active'); x.setAttribute('aria-current','false'); });
+ const cur = document.querySelector('.lesson[data-lesson="'+lessonId+'"]');
+ if(cur){ cur.classList.add('active'); cur.setAttribute('aria-current','true'); }
+ if(doTrack){
+ $('reader').scrollIntoView({block:'start'});
+ const h = $('reader').querySelector('h2'); if(h){ h.tabIndex = -1; h.focus({preventScroll:true}); }
+ }
$('reader').querySelectorAll('[data-copy]').forEach(b=>b.onclick=()=>{ navigator.clipboard&&navigator.clipboard.writeText(b.dataset.copy); b.textContent='Copied'; });
- if($('hint-btn')) $('hint-btn').onclick=()=>{ $('hint').style.display='block'; };
+ if($('hint-btn')){ $('hint-btn').setAttribute('aria-expanded','false'); $('hint-btn').setAttribute('aria-controls','hint'); $('hint-btn').onclick=()=>{ const h=$('hint'), open=h.style.display!=='block'; h.style.display=open?'block':'none'; $('hint-btn').setAttribute('aria-expanded',String(open)); }; }
if($('done-btn')) $('done-btn').onclick=()=>{ completeLesson(l.id); };
$('reader').querySelectorAll('[data-opt]').forEach(b=>b.onclick=()=>{
const ok = parseInt(b.dataset.opt,10) === l.quiz.answer;
$('quiz-msg').textContent = ok ? '✅ Correct (+5 XP)' : '❌ Not quite — re-read the lesson.';
$('quiz-msg').style.color = ok ? 'var(--green)' : 'var(--rose)';
- if(ok) completeLesson(l.id, 5);
+ if(ok){ const before = prog().completed[l.id]; completeLesson(l.id, 5); $('quiz-msg').textContent = before ? '✅ Correct' : '✅ Correct (+5 XP)'; }
});
if (doTrack) track('lesson_open', {lesson:l.id});
}catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Failed: '+esc(e.message)+'</p>'; }
}
function completeLesson(id, xp){
- const p = prog(); if(p.completed[id]) return;
- p.completed[id]=new Date().toISOString(); p.xp=(p.xp||0)+(xp||10); save(p); renderProgress(); renderChapters();
+ const p = prog();
+ if(p.completed[id]){ if($('done-btn')) $('done-btn').textContent='✓ Done'; return; }
+ const award = xp || 10;
+ p.completed[id]=new Date().toISOString(); p.xp=(p.xp||0)+award; save(p); renderProgress(); renderChapters(false);
+ if($('done-btn')){ $('done-btn').textContent='✓ Done (+'+award+' XP)'; $('done-btn').disabled=true; }
track('lesson_complete',{lesson:id});
}
function track(type, detail){
@@ -246,6 +278,14 @@ async function showFeed(){
$('atlas-q').addEventListener('input', e => renderAtlas(e.target.value.trim()));
$('tutor-send').onclick = askTutor;
$('tutor-q').addEventListener('keydown', e => { if(e.key==='Enter') askTutor(); });
+function toggleRail(which){
+ const r = $('rail'), b = $('backdrop');
+ if(r.classList.contains('open') && r.dataset.section === which){ closeRail(); return; }
+ r.dataset.section = which; r.classList.add('open'); b.classList.add('show');
+ const t = $(which === 'atlas' ? 'atlas-head' : 'tutor-head');
+ if(t) setTimeout(()=>t.scrollIntoView({block:'start'}), 60);
+}
+function closeRail(){ $('rail').classList.remove('open'); $('backdrop').classList.remove('show'); }
boot();
</script>
</body>
diff --git a/course/chapters/02-architecture.json b/course/chapters/02-architecture.json
index 7dee513..8e8826b 100644
--- a/course/chapters/02-architecture.json
+++ b/course/chapters/02-architecture.json
@@ -7,12 +7,12 @@
{
"id": "2.1",
"title": "Terminal, modules, commands",
- "objective": "See how the terminal, module manager and command registry fit together.",
+ "objective": "See what is loaded, why a command can be listed but broken, and what the system is doing.",
"minutes": 9,
"commands": [
"modules",
- "status",
- "help"
+ "doctor",
+ "status"
],
"blocks": [
{
@@ -37,23 +37,24 @@
},
{
"type": "cmd",
- "text": "check"
+ "text": "doctor"
},
{
"type": "tip",
- "text": "A command that exists in `help` but fails at runtime usually has an unmet module requirement that `check` reports."
+ "text": "A command that exists in `help` but fails at runtime usually has an unmet module requirement that `doctor` reports."
}
],
"exercise": {
- "instruction": "List the loaded modules.",
+ "instruction": "Run `doctor` and see how it ranks module problems.",
"accept": [
+ "doctor",
"modules",
- "status"
+ "module-info *"
],
"check": {
"type": "command"
},
- "hint": "Type: modules"
+ "hint": "Type: doctor"
},
"quiz": {
"q": "Who executes a command after you press Enter?",
@@ -76,9 +77,11 @@
"objective": "Understand where files live and why a module edit sometimes needs a rebuild and sometimes does not.",
"minutes": 10,
"commands": [
- "status",
- "help",
- "load-local"
+ "pwd",
+ "cd",
+ "tree",
+ "health-check",
+ "mem"
],
"blocks": [
{
@@ -103,14 +106,15 @@
}
],
"exercise": {
- "instruction": "Run `status` and find the VFS size line.",
+ "instruction": "Run `health-check` and find the VFS size line.",
"accept": [
- "status"
+ "health-check",
+ "mem"
],
"check": {
"type": "command"
},
- "hint": "Type: status"
+ "hint": "Type: health-check"
},
"quiz": {
"q": "Which tier needs `rebuild-bundles.py` after an edit?",
diff --git a/course/chapters/03-commands.json b/course/chapters/03-commands.json
index 10807cb..40fa2ad 100644
--- a/course/chapters/03-commands.json
+++ b/course/chapters/03-commands.json
@@ -7,16 +7,17 @@
{
"id": "3.1",
"title": "Find any command fast",
- "objective": "Use help, commands and check to discover what exists before guessing.",
+ "objective": "Use help, doctor and status to discover what exists before guessing.",
"minutes": 7,
"commands": [
"help",
+ "doctor",
"status"
],
"blocks": [
{
"type": "text",
- "text": "`help` groups all 344 commands by the module that owns them."
+ "text": "`help` groups every registered command by the module that owns them."
},
{
"type": "cmd",
@@ -28,18 +29,20 @@
},
{
"type": "tip",
- "text": "`check` shows module requirements — the fastest way to explain a command that is listed but not working."
+ "text": "`doctor` shows module requirements — the fastest way to explain a command that is listed but not working."
}
],
"exercise": {
- "instruction": "Open the help for `webdav-push`.",
+ "instruction": "Run `doctor` and read its severity ranking.",
"accept": [
- "help webdav-push"
+ "doctor",
+ "help",
+ "status"
],
"check": {
"type": "command"
},
- "hint": "Type: help webdav-push"
+ "hint": "Type: doctor"
},
"quiz": {
"q": "Which command shows usage for one command?",
@@ -74,11 +77,11 @@
},
{
"type": "cmd",
- "text": "ls | grep txt"
+ "text": "grep hello hello.txt"
},
{
"type": "warn",
- "text": "`sed s/x/y/` substitutes the first match per LINE, not per file. `sed -i` is flagged destructive and needs confirmation."
+ "text": "`sed s/x/y/` substitutes the first match per LINE, not per file. `sed -i` overwrites the file immediately — checkpoint first (`checkpoint before-sed`). Pipes are partial in this build: `ls | grep x` may print the unfiltered listing; use `grep pattern file`."
},
{
"type": "text",
@@ -90,15 +93,16 @@
}
],
"exercise": {
- "instruction": "Search the current listing for `txt`.",
+ "instruction": "Search a file for a word (create `hello.txt` with `edit` if needed).",
"accept": [
- "ls | grep txt",
- "grep txt"
+ "grep hello hello.txt",
+ "grep * hello.txt",
+ "grep hello *"
],
"check": {
"type": "command"
},
- "hint": "Type: ls | grep txt"
+ "hint": "Type: grep hello hello.txt"
},
"quiz": {
"q": "`sed s/a/b/` replaces…",
diff --git a/course/chapters/05-modules.json b/course/chapters/05-modules.json
index 4b68f42..7829553 100644
--- a/course/chapters/05-modules.json
+++ b/course/chapters/05-modules.json
@@ -17,7 +17,7 @@
"blocks": [
{
"type": "text",
- "text": "`wpm` is the package manager for terminal modules: list the index, install a module from `packages.json` or a URL."
+ "text": "`wpm` is the package manager for terminal modules: list the index, install a package by name, inspect it with `wpm info`."
},
{
"type": "cmd",
@@ -29,22 +29,22 @@
},
{
"type": "text",
- "text": "Every ext module in the default build auto-loads. There is no separate “needs loadmod” tier."
+ "text": "Most ext modules auto-load via `module-loader.js`. Manual modules such as `test-suite` need `load-local`."
},
{
"type": "cmd",
- "text": "load-local modules/ext/calc.js"
+ "text": "load-local modules/test/test-suite.js"
}
],
"exercise": {
- "instruction": "List available packages.",
+ "instruction": "Inspect the calc package metadata.",
"accept": [
- "wpm list"
+ "wpm info calc"
],
"check": {
"type": "command"
},
- "hint": "Type: wpm list"
+ "hint": "Type: wpm info calc"
},
"quiz": {
"q": "What does wpm install?",
diff --git a/course/chapters/06-files-cloud.json b/course/chapters/06-files-cloud.json
index 697a5f5..e997b9c 100644
--- a/course/chapters/06-files-cloud.json
+++ b/course/chapters/06-files-cloud.json
@@ -18,7 +18,7 @@
"blocks": [
{
"type": "text",
- "text": "`edit` opens the VFS editor. `diff` compares two files. `find` searches names; `filemgr` opens the visual browser."
+ "text": "`edit` opens the VFS editor. `diff` compares two files. `find` searches names; `files` opens the visual browser."
},
{
"type": "cmd",
@@ -55,7 +55,7 @@
"quiz": {
"q": "Which command opens the visual file manager?",
"options": [
- "filemgr",
+ "files",
"ls",
"cat"
],
diff --git a/course/chapters/07-automation.json b/course/chapters/07-automation.json
index 3345fc2..b7eadb4 100644
--- a/course/chapters/07-automation.json
+++ b/course/chapters/07-automation.json
@@ -102,19 +102,20 @@
},
{
"type": "warn",
- "text": "`rollback` replaces current VFS state — checkpoint before experiments."
+ "text": "`rollback` replaces current VFS state — checkpoint before experiments. Only the 3 most recent checkpoints are kept; `checkpoint-list` shows what survived."
}
],
"exercise": {
- "instruction": "Create a checkpoint.",
+ "instruction": "Create a checkpoint, then confirm it is listed with `checkpoint-list`.",
"accept": [
"checkpoint *",
- "checkpoint"
+ "checkpoint",
+ "checkpoint-list"
],
"check": {
"type": "command"
},
- "hint": "Type: checkpoint my-test"
+ "hint": "Type: checkpoint my-test, then checkpoint-list"
},
"quiz": {
"q": "What does checkpoint do?",
diff --git a/course/chapters/08-build.json b/course/chapters/08-build.json
index 48a60bd..c09d89d 100644
--- a/course/chapters/08-build.json
+++ b/course/chapters/08-build.json
@@ -33,14 +33,16 @@
}
],
"exercise": {
- "instruction": "Run the built-in test suite.",
+ "instruction": "Load the test suite, then run it: `load-local modules/test/test-suite.js` then `test auto`.",
"accept": [
- "status"
+ "test auto",
+ "test *",
+ "load-local *test-suite*"
],
"check": {
"type": "command"
},
- "hint": "Type: test auto"
+ "hint": "Type: load-local modules/test/test-suite.js, then test auto"
},
"quiz": {
"q": "Which two places carry the version?",