Every session against the clock; skip what you know
Study mode is gone: no more untimed sessions with the reference solutions open from the start. The path is now objective drills against the clock, then a mock, then the fixed sets, and an objective is done when every task passed on its latest timed attempt. In the lobby "Study mode" becomes "Drill an objective" (the objective list, with one "Against the clock" button per objective); ./ksim study is removed and ./ksim next always drills. study-<track>.json keeps focus, known and the done stamps; results of old study sessions stay in it, read by nothing. Also in this commit, from the work before it: - ./ksim skip / unskip, and links beside each domain in the lobby: mark a domain or an objective as known, so the path, the no-argument drill and drill all leave it out; a mock still draws from every domain - DOMAIN:N picks N tasks of a domain, spread over its objectives - a command with neither --track nor KSIM_TRACK is for the track whose session is under way; ./ksim show says where the task's host resolves - CKAD Q60: the check names which file holds which run's output Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
This commit is contained in:
parent
f3d0e536b6
commit
c266eeb4b5
28
README.md
28
README.md
@ -6,7 +6,8 @@ terminal, Firefox limited to the documentation the exam allows), a task pane bes
|
|||||||
run since 2025, and the way killer.sh and sailor.sh present them.
|
run since 2025, and the way killer.sh and sailor.sh present them.
|
||||||
|
|
||||||
It grew out of `cka-sim`: the same task banks, per-check grading, clock, history, weak-spot log,
|
It grew out of `cka-sim`: the same task banks, per-check grading, clock, history, weak-spot log,
|
||||||
drills and study path, on a new foundation.
|
drills and a path through each curriculum, on a new foundation. Every session runs against the
|
||||||
|
clock.
|
||||||
|
|
||||||
```
|
```
|
||||||
browser ── http://127.0.0.1:8080 ──────────────────────────────────────────────┐
|
browser ── http://127.0.0.1:8080 ──────────────────────────────────────────────┐
|
||||||
@ -93,15 +94,21 @@ Firefox in the desktop opens only the allowed documentation for the track (`KSIM
|
|||||||
lifts that). Submit grades every task on its own host; the hosts stay up for review until the next
|
lifts that). Submit grades every task on its own host; the hosts stay up for review until the next
|
||||||
session starts.
|
session starts.
|
||||||
|
|
||||||
Everything is also available from a terminal:
|
Everything is also available from a terminal. A command is for the track named with `--track` or
|
||||||
|
`KSIM_TRACK`; with neither, for the track whose session is under way, and for CKA when there is
|
||||||
|
none. Task numbers are per track — CKA's task 13 and CKAD's task 13 are different tasks — and a
|
||||||
|
host name such as `ckad0013` only resolves in the desktop's terminal: from your own, it is
|
||||||
|
`./ksim ssh 13`.
|
||||||
|
|
||||||
```
|
```
|
||||||
./ksim --track ckad drill # next least-drilled domain, 5 tasks (--track or KSIM_TRACK)
|
./ksim --track ckad drill # next least-drilled domain, 5 tasks (--track or KSIM_TRACK)
|
||||||
./ksim drill weak | drill 2 17 20 | drill TSHOOT NET | drill 3.2
|
./ksim drill weak | drill 2 17 20 | drill TSHOOT NET | drill 3.2
|
||||||
./ksim start [--hard] # full mock, sampled by domain weight
|
./ksim start [--hard] # full mock, sampled by domain weight
|
||||||
./ksim sim c|d # a fixed simulator set (CKA: c, d · CKAD: a)
|
./ksim sim c|d # a fixed simulator set (CKA: c, d · CKAD: a)
|
||||||
./ksim objectives | next | focus # the curriculum as a study path
|
./ksim objectives | next | focus # the curriculum as a path, one objective at a time against the clock
|
||||||
./ksim study 3.2 | notes 3.2 # an objective: notes, then its tasks with no clock
|
./ksim notes 3.2 # an objective's notes: what to know, fastest commands, traps, docs
|
||||||
|
./ksim skip DESIGN | unskip DESIGN # "I am good with this domain (or objective 2.3)": off the path, and back
|
||||||
|
./ksim drill OBS:3 | next OBS:3 # three tasks of a domain, each from another objective - alone, or added
|
||||||
./ksim list | show N | grade N | solution N | timer | end
|
./ksim list | show N | grade N | solution N | timer | end
|
||||||
./ksim hosts [start] # the session's hosts; `start` brings them back after a reboot
|
./ksim hosts [start] # the session's hosts; `start` brings them back after a reboot
|
||||||
./ksim ssh N # a shell on the host of task N from your own terminal
|
./ksim ssh N # a shell on the host of task N from your own terminal
|
||||||
@ -112,6 +119,15 @@ Everything is also available from a terminal:
|
|||||||
# then solve it with its script and grade again (--settle N, --keep)
|
# then solve it with its script and grade again (--settle N, --keep)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Skipping what you know.** `./ksim skip DESIGN` (or the "I know this — skip it" link beside a
|
||||||
|
domain in the browser's objective list) marks a domain or an objective as known: the path, the
|
||||||
|
no-argument drill and `drill all` leave it out, and it no longer has to be drilled before a full
|
||||||
|
exam. It is a statement, not a result — nothing is written to the history, the statistics or the
|
||||||
|
weak list, the objective list shows `»` rather than `✓`, and `./ksim skip` prints what is on record
|
||||||
|
for it. A mock exam still draws from every domain, as the real one will. To keep such a domain
|
||||||
|
warm, `DOMAIN:N` takes a few of its tasks, each from another objective: `./ksim drill OBS:3`, or
|
||||||
|
`./ksim next OBS:3` to add them to the next step on the path.
|
||||||
|
|
||||||
## Tracks
|
## Tracks
|
||||||
|
|
||||||
Each track is a directory: `cka/`, `ckad/`, `cks/`.
|
Each track is a directory: `cka/`, `ckad/`, `cks/`.
|
||||||
@ -135,7 +151,7 @@ Each track is a directory: `cka/`, `ckad/`, `cks/`.
|
|||||||
**CKA, as audited on 2026-10-13:** all 131 tasks seed cleanly on a host of their own, score
|
**CKA, as audited on 2026-10-13:** all 131 tasks seed cleanly on a host of their own, score
|
||||||
nothing when graded untouched 45 seconds after seeding, and reach full marks when their scripted
|
nothing when graded untouched 45 seconds after seeding, and reach full marks when their scripted
|
||||||
solution is run on the host as `candidate` — for 66 of them that script is the reference solution
|
solution is run on the host as `candidate` — for 66 of them that script is the reference solution
|
||||||
exactly as a study session shows it.
|
exactly as a drill shows it once the task is graded.
|
||||||
|
|
||||||
**CKAD, as audited on 2026-10-12:** all 105 tasks seed cleanly on a host of their own, score
|
**CKAD, as audited on 2026-10-12:** all 105 tasks seed cleanly on a host of their own, score
|
||||||
nothing when graded untouched 45 seconds after seeding, and reach full marks when their scripted
|
nothing when graded untouched 45 seconds after seeding, and reach full marks when their scripted
|
||||||
@ -200,7 +216,7 @@ until a stopped kubelet shows as NotReady; `--keep` leaves the host up to look a
|
|||||||
|
|
||||||
In `cka/solve.py`, `AS_WRITTEN` lists the tasks whose reference solution is a script as it stands:
|
In `cka/solve.py`, `AS_WRITTEN` lists the tasks whose reference solution is a script as it stands:
|
||||||
for those the audit runs exactly the text the candidate is shown, so a solution that stops working
|
for those the audit runs exactly the text the candidate is shown, so a solution that stops working
|
||||||
with a new Kubernetes release fails the next audit instead of misleading you in a study session.
|
with a new Kubernetes release fails the next audit instead of misleading you after a drill.
|
||||||
|
|
||||||
## What is different from the real exam
|
## What is different from the real exam
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
"""CKA curriculum as a study path: the 27 objectives of the five domains, in the curriculum's own
|
"""CKA curriculum as a path to the exam: the 27 objectives of the five domains, in the curriculum's own
|
||||||
words and order (CKA_Curriculum_v1.35.pdf, cncf/curriculum), each with what to know, the fastest
|
words and order (CKA_Curriculum_v1.35.pdf, cncf/curriculum), each with what to know, the fastest
|
||||||
commands, the traps, the documentation pages the exam lets you open, and the tasks that practise it.
|
commands, the traps, the documentation pages the exam lets you open, and the tasks that practise it.
|
||||||
|
|
||||||
./ksim objectives the path, with your progress per objective
|
./ksim objectives the path, with your progress per objective
|
||||||
./ksim notes 4.4 the notes for one objective (id or key: 4.4, lifecycle)
|
./ksim notes 4.4 the notes for one objective (id or key: 4.4, lifecycle)
|
||||||
./ksim study 4.4 those notes, then its tasks with no clock and solutions visible
|
./ksim drill 4.4 its tasks against the clock
|
||||||
./ksim drill 4.4 the same tasks against the clock
|
|
||||||
./ksim next whatever is next on the path
|
./ksim next whatever is next on the path
|
||||||
|
|
||||||
"tasks" lists every task that really exercises the objective - the plain pool tasks first, then
|
"tasks" lists every task that really exercises the objective - the plain pool tasks first, then
|
||||||
|
|||||||
@ -575,7 +575,7 @@ eq "$(podman image inspect localhost/%{img}:1.1 --format '{{index .Config.Labels
|
|||||||
C("the Dockerfile itself sets ENTRYPOINT, USER and LABEL", 1, r"""
|
C("the Dockerfile itself sets ENTRYPOINT, USER and LABEL", 1, r"""
|
||||||
f="$WS/q60/image/Dockerfile"
|
f="$WS/q60/image/Dockerfile"
|
||||||
grep -Eiq '^ENTRYPOINT' "$f" 2>/dev/null && grep -Eiq '^USER[[:space:]]+10001' "$f" && grep -Eiq '^LABEL.*team' "$f" """),
|
grep -Eiq '^ENTRYPOINT' "$f" 2>/dev/null && grep -Eiq '^USER[[:space:]]+10001' "$f" && grep -Eiq '^LABEL.*team' "$f" """),
|
||||||
C("default.txt and %{arg2}.txt hold the two outputs", 1, r"""
|
C("default.txt holds the output of the run with defaults (%{arg}), %{arg2}.txt that of the run with the argument %{arg2}", 1, r"""
|
||||||
eq "$(tr -d '\r' < "$WS/q60/default.txt" 2>/dev/null)" "%{img} report: %{arg} (uid 10001)" \
|
eq "$(tr -d '\r' < "$WS/q60/default.txt" 2>/dev/null)" "%{img} report: %{arg} (uid 10001)" \
|
||||||
&& eq "$(tr -d '\r' < "$WS/q60/%{arg2}.txt" 2>/dev/null)" "%{img} report: %{arg2} (uid 10001)" """),
|
&& eq "$(tr -d '\r' < "$WS/q60/%{arg2}.txt" 2>/dev/null)" "%{img} report: %{arg2} (uid 10001)" """),
|
||||||
C("localhost/%{img}:1.0 is gone, 1.1 is there", 1, r"""
|
C("localhost/%{img}:1.0 is gone, 1.1 is there", 1, r"""
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
"""CKAD curriculum as a study path: the 24 objectives of the five domains, in the curriculum's own
|
"""CKAD curriculum as a path to the exam: the 24 objectives of the five domains, in the curriculum's own
|
||||||
words and order (CKAD_Curriculum_v1.37.pdf, cncf/curriculum), each with what to know, the fastest
|
words and order (CKAD_Curriculum_v1.37.pdf, cncf/curriculum), each with what to know, the fastest
|
||||||
commands, the traps, the documentation pages the exam lets you open, and the tasks that practise it.
|
commands, the traps, the documentation pages the exam lets you open, and the tasks that practise it.
|
||||||
|
|
||||||
./ksim objectives the path, with your progress per objective
|
./ksim objectives the path, with your progress per objective
|
||||||
./ksim notes 3.2 the notes for one objective (id or key: 3.2, probes)
|
./ksim notes 3.2 the notes for one objective (id or key: 3.2, probes)
|
||||||
./ksim study 3.2 those notes, then its tasks with no clock and solutions visible
|
./ksim drill 3.2 its tasks against the clock
|
||||||
./ksim drill 3.2 the same tasks against the clock
|
|
||||||
|
|
||||||
"tasks" lists every task that really exercises the objective, most direct first; a task may sit
|
"tasks" lists every task that really exercises the objective, most direct first; a task may sit
|
||||||
under several objectives. Study and drill sessions use the pool tasks; the Simulator Set A tasks
|
under several objectives. Drill sessions use the pool tasks; the Simulator Set A tasks
|
||||||
(38-57) are listed too but stay out unless you ask for them (--all), so the mock stays unspoiled.
|
(38-57) are listed too but stay out unless you ask for them (--all), so the mock stays unspoiled.
|
||||||
Every task must be listed at least once and every objective needs two pool tasks -
|
Every task must be listed at least once and every objective needs two pool tasks -
|
||||||
`./ksim objectives` says so when that stops being true.
|
`./ksim objectives` says so when that stops being true.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""CKS curriculum as a study path: the 26 objectives of the six domains, in the curriculum's own
|
"""CKS curriculum as a path to the exam: the 26 objectives of the six domains, in the curriculum's own
|
||||||
words and order, each with what to know, the fastest commands, the traps, the documentation pages
|
words and order, each with what to know, the fastest commands, the traps, the documentation pages
|
||||||
the exam lets you open, and the tasks that practise it.
|
the exam lets you open, and the tasks that practise it.
|
||||||
|
|
||||||
@ -9,8 +9,7 @@ documentation are not on it: for those tools the manual on the day is `--help`.
|
|||||||
|
|
||||||
./ksim --track cks objectives the path, with your progress per objective
|
./ksim --track cks objectives the path, with your progress per objective
|
||||||
./ksim --track cks notes 4.2 the notes for one objective (id or key: 4.2, secrets)
|
./ksim --track cks notes 4.2 the notes for one objective (id or key: 4.2, secrets)
|
||||||
./ksim --track cks study 4.2 those notes, then its tasks with no clock and solutions visible
|
./ksim --track cks drill 4.2 its tasks against the clock
|
||||||
./ksim --track cks drill 4.2 the same tasks against the clock
|
|
||||||
|
|
||||||
"tasks" lists every task that really exercises the objective, most direct first; a task may sit
|
"tasks" lists every task that really exercises the objective, most direct first; a task may sit
|
||||||
under several objectives. Every task must be listed at least once and every objective needs two
|
under several objectives. Every task must be listed at least once and every objective needs two
|
||||||
|
|||||||
@ -150,7 +150,9 @@
|
|||||||
details.obj[open] > summary { margin-bottom: 6px; }
|
details.obj[open] > summary { margin-bottom: 6px; }
|
||||||
details.obj .oid { font-family: var(--mono); color: var(--muted); }
|
details.obj .oid { font-family: var(--mono); color: var(--muted); }
|
||||||
.mk { font-family: var(--mono); font-weight: 600; text-align: center; }
|
.mk { font-family: var(--mono); font-weight: 600; text-align: center; }
|
||||||
.st-done, .st-passed { color: var(--ok); } .st-studied, .st-started { color: var(--warn); } .st-failed { color: var(--danger); } .st-new { color: var(--muted); }
|
.st-done, .st-passed { color: var(--ok); } .st-started, .st-focus { color: var(--warn); } .st-failed { color: var(--danger); } .st-new { color: var(--muted); } .st-known { color: var(--accent-hover); }
|
||||||
|
#objList h3 a.known, #objList h3 a.touch { text-transform: none; letter-spacing: 0; font-weight: 400; margin-left: 8px; }
|
||||||
|
.pill.known { color: var(--accent-hover); }
|
||||||
.chips { font: 12px var(--mono); white-space: nowrap; color: var(--muted); }
|
.chips { font: 12px var(--mono); white-space: nowrap; color: var(--muted); }
|
||||||
.chips span { margin-left: 6px; }
|
.chips span { margin-left: 6px; }
|
||||||
.ladder { display: grid; grid-template-columns: auto 1fr auto; gap: 2px 12px; font-size: 13px; margin: 0 0 14px; padding: 10px 12px;
|
.ladder { display: grid; grid-template-columns: auto 1fr auto; gap: 2px 12px; font-size: 13px; margin: 0 0 14px; padding: 10px 12px;
|
||||||
@ -206,14 +208,14 @@
|
|||||||
<div class="sub" id="lobbySub"></div>
|
<div class="sub" id="lobbySub"></div>
|
||||||
<div id="ver" style="position:absolute;right:28px;top:30px;color:var(--muted);font-size:12px"></div>
|
<div id="ver" style="position:absolute;right:28px;top:30px;color:var(--muted);font-size:12px"></div>
|
||||||
<div id="trackPick" style="display:flex;gap:6px;margin:0 0 14px"></div>
|
<div id="trackPick" style="display:flex;gap:6px;margin:0 0 14px"></div>
|
||||||
<div style="color:var(--muted);font-size:12px;margin:-8px 0 4px">Drill a domain (✓ = drilled at least once):</div>
|
<div style="color:var(--muted);font-size:12px;margin:-8px 0 4px">Drill a domain (✓ = drilled at least once, » = marked known):</div>
|
||||||
<div id="coverage" style="display:flex;gap:6px;margin:0 0 14px;flex-wrap:wrap"></div>
|
<div id="coverage" style="display:flex;gap:6px;margin:0 0 14px;flex-wrap:wrap"></div>
|
||||||
<label style="display:block;margin-bottom:14px"><input type="checkbox" id="hardMode"> <b>Hard mode</b>
|
<label style="display:block;margin-bottom:14px"><input type="checkbox" id="hardMode"> <b>Hard mode</b>
|
||||||
<span id="hardDesc" style="color:var(--muted)"> — killer.sh-style: more tasks in the same time, no hints, traps, exact-output tasks, compound failures, collateral checks.</span></label>
|
<span id="hardDesc" style="color:var(--muted)"> — killer.sh-style: more tasks in the same time, no hints, traps, exact-output tasks, compound failures, collateral checks.</span></label>
|
||||||
<div class="ladder hidden" id="ladder"></div>
|
<div class="ladder hidden" id="ladder"></div>
|
||||||
<div class="opts" id="lobbyOpts">
|
<div class="opts" id="lobbyOpts">
|
||||||
<button class="opt hidden" id="optPath" style="grid-column:1/3;border-color:var(--accent)"><b id="pathTitle">Continue the study path</b><span id="pathDesc"></span></button>
|
<button class="opt hidden" id="optPath" style="grid-column:1/3;border-color:var(--accent)"><b id="pathTitle">Continue the path</b><span id="pathDesc"></span></button>
|
||||||
<button class="opt" id="optStudy"><b>Study mode</b><span>5 tasks, no clock, reference solutions visible from the start. Nothing scored or recorded.</span></button>
|
<button class="opt hidden" id="optObjective"><b>Drill an objective</b><span id="objectiveDesc"></span></button>
|
||||||
<button class="opt" id="optNext"><b>Drill next domain</b><span id="nextDesc"></span></button>
|
<button class="opt" id="optNext"><b>Drill next domain</b><span id="nextDesc"></span></button>
|
||||||
<button class="opt" id="optAll"><b>Drill the whole bank</b><span id="allDesc"></span></button>
|
<button class="opt" id="optAll"><b>Drill the whole bank</b><span id="allDesc"></span></button>
|
||||||
<button class="opt" id="optDrill"><b>Drill: weak / pick</b><span>Tasks you're weak on, or choose your own. Feedback per task while you work.</span></button>
|
<button class="opt" id="optDrill"><b>Drill: weak / pick</b><span>Tasks you're weak on, or choose your own. Feedback per task while you work.</span></button>
|
||||||
@ -292,7 +294,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- curriculum ----
|
// ---- curriculum ----
|
||||||
const MARK = {done: '✓', passed: '✓', studied: '~', started: '-', failed: '✗', new: '·'};
|
const MARK = {done: '✓', passed: '✓', started: '-', failed: '✗', new: '·', known: '»'};
|
||||||
const mk = st => `<span class="mk st-${st}">${MARK[st] || '·'}</span>`;
|
const mk = st => `<span class="mk st-${st}">${MARK[st] || '·'}</span>`;
|
||||||
const inline = s => esc(s).replace(/`([^`]+)`/g, '<code>$1</code>');
|
const inline = s => esc(s).replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
function notesHtml(o) {
|
function notesHtml(o) {
|
||||||
@ -303,21 +305,19 @@
|
|||||||
}
|
}
|
||||||
function continuePath() {
|
function continuePath() {
|
||||||
const n = S.path && S.path.next; if (!n) return;
|
const n = S.path && S.path.next; if (!n) return;
|
||||||
if (n.kind === 'study') start('drill', null, false, false, null, true, n.objective);
|
if (n.objective) start('drill', null, false, false, null, n.objective);
|
||||||
else if (n.kind === 'drill') start('drill', null, false, false, null, false, n.objective);
|
|
||||||
else if (n.kind === 'sim') startSim(n.set);
|
else if (n.kind === 'sim') startSim(n.set);
|
||||||
else start('exam');
|
else start('exam');
|
||||||
}
|
}
|
||||||
function paintPath() {
|
function paintPath() {
|
||||||
const P = S.path, has = !!P;
|
const P = S.path, has = !!P;
|
||||||
$('#optPath').classList.toggle('hidden', !has); $('#ladder').classList.toggle('hidden', !has);
|
$('#optPath').classList.toggle('hidden', !has); $('#ladder').classList.toggle('hidden', !has);
|
||||||
$('#optStudy').querySelector('b').textContent = has ? 'Study an objective' : 'Study mode';
|
$('#optObjective').classList.toggle('hidden', !has);
|
||||||
$('#optStudy').querySelector('span').textContent = has
|
|
||||||
? `Pick any of the ${P.total} objectives: its notes, then its tasks with no clock and the solutions visible.`
|
|
||||||
: '5 tasks, no clock, reference solutions visible from the start. Nothing scored or recorded.';
|
|
||||||
if (!has) return;
|
if (!has) return;
|
||||||
|
$('#objectiveDesc').textContent = `Pick any of the ${P.total} objectives: read its notes, then drill its tasks against the clock.`;
|
||||||
const best = v => v == null ? 'not sat yet' : `best ${v}%`, k = P.next.kind;
|
const best = v => v == null ? 'not sat yet' : `best ${v}%`, k = P.next.kind;
|
||||||
const rows = [['study', 'Study', `${P.studied}/${P.total} objectives`], ['drill', 'Against the clock', `${P.done}/${P.total} objectives`],
|
const kn = P.known || 0, left = P.total - kn, aside = kn ? ` · ${kn} marked known` : '';
|
||||||
|
const rows = [['drill', 'Against the clock', `${P.done}/${left} objectives${aside}`],
|
||||||
['mock', 'Mock exam', best(P.mock_best)]].concat(Object.keys(S.sets || {}).map(x => ['sim:' + x, S.sets[x].name, best(P.set_best[x])]));
|
['mock', 'Mock exam', best(P.mock_best)]].concat(Object.keys(S.sets || {}).map(x => ['sim:' + x, S.sets[x].name, best(P.set_best[x])]));
|
||||||
$('#ladder').innerHTML = rows.map(([kind, name, where], i) => {
|
$('#ladder').innerHTML = rows.map(([kind, name, where], i) => {
|
||||||
const here = kind === k || kind === 'sim:' + P.next.set && k === 'sim';
|
const here = kind === k || kind === 'sim:' + P.next.set && k === 'sim';
|
||||||
@ -330,15 +330,26 @@
|
|||||||
const chips = o => o.tasks.filter(t => !t.set).map(chip).join('') +
|
const chips = o => o.tasks.filter(t => !t.set).map(chip).join('') +
|
||||||
(o.tasks.some(t => t.set) ? `<span>[</span>${o.tasks.filter(t => t.set).map(chip).join('').replace('<span', '<span style="margin-left:0"')}<span style="margin-left:0">]</span>` : '');
|
(o.tasks.some(t => t.set) ? `<span>[</span>${o.tasks.filter(t => t.set).map(chip).join('').replace('<span', '<span style="margin-left:0"')}<span style="margin-left:0">]</span>` : '');
|
||||||
const doms = [...new Set(S.objectives.map(o => o.domain))];
|
const doms = [...new Set(S.objectives.map(o => o.domain))];
|
||||||
$('#objSub').textContent = '✓ complete: every task passed against the clock. ~ studied. - started. Open an objective for its notes.';
|
$('#objSub').textContent = '✓ complete: every task passed against the clock. - started. » marked known, off the path. Open an objective for its notes.';
|
||||||
const focus = S.focus || []; // ./ksim focus: the domains the path walks first
|
const focus = S.focus || []; // ./ksim focus: the domains the path walks first
|
||||||
$('#objList').innerHTML = doms.map(d => `<h3>${esc(S.track.domains[d])}${focus.includes(d) ? ` <span class="st-studied" title="./ksim focus: the path walks this domain first">· focus ${focus.indexOf(d) + 1}</span>` : ''}</h3>` + S.objectives.filter(o => o.domain === d).map(o =>
|
const known = S.known || []; // ./ksim skip: what you said you know - the path leaves it out, nothing is recorded as passed
|
||||||
|
const knownTag = d => known.includes(d)
|
||||||
|
? ` <span class="st-known" title="./ksim skip ${d}: you said you are good with this domain, so the path leaves it out. Nothing is recorded as passed, and a mock exam still draws from it.">· known, off the path</span><a href="#" class="touch" data-touch="${d}" title="./ksim drill ${d}:3 — three of its tasks, each from another objective, against the clock: to keep a domain you know warm">3 of its questions</a><a href="#" class="known" data-known="${d}" data-on="0" title="./ksim unskip ${d}">put it back</a>`
|
||||||
|
: `<a href="#" class="known" data-known="${d}" data-on="1" title="./ksim skip ${d}: leave this domain out of the path. Nothing is recorded as passed, and a mock exam still draws from it.">I know this — skip it</a>`;
|
||||||
|
$('#objList').innerHTML = doms.map(d => `<h3>${esc(S.track.domains[d])}${focus.includes(d) ? ` <span class="st-focus" title="./ksim focus: the path walks this domain first">· focus ${focus.indexOf(d) + 1}</span>` : ''}${knownTag(d)}</h3>` + S.objectives.filter(o => o.domain === d).map(o =>
|
||||||
`<details class="obj"><summary>${mk(o.state)}<span class="oid">${o.id}</span><span class="otext">${esc(o.text)}</span><span class="chips">${chips(o)}</span></summary>
|
`<details class="obj"><summary>${mk(o.state)}<span class="oid">${o.id}</span><span class="otext">${esc(o.text)}</span><span class="chips">${chips(o)}</span></summary>
|
||||||
<div class="notes">${notesHtml(o)}</div>
|
<div class="notes">${notesHtml(o)}</div>
|
||||||
<div class="row" style="margin:10px 0 6px"><button class="btn primary" data-study="${o.id}">Study it · no clock</button><button class="btn" data-drill="${o.id}">Against the clock</button></div>
|
<div class="row" style="margin:10px 0 6px"><button class="btn primary" data-drill="${o.id}">Against the clock</button></div>
|
||||||
</details>`).join('')).join('');
|
</details>`).join('')).join('');
|
||||||
$('#objList').querySelectorAll('[data-study]').forEach(b => b.onclick = () => start('drill', null, false, false, null, true, b.dataset.study));
|
$('#objList').querySelectorAll('[data-drill]').forEach(b => b.onclick = () => start('drill', null, false, false, null, b.dataset.drill));
|
||||||
$('#objList').querySelectorAll('[data-drill]').forEach(b => b.onclick = () => start('drill', null, false, false, null, false, b.dataset.drill));
|
$('#objList').querySelectorAll('a.touch').forEach(a => a.onclick = e => {
|
||||||
|
e.preventDefault(); start('drill', null, false, false, null, null, [[a.dataset.touch, 3]]);
|
||||||
|
});
|
||||||
|
$('#objList').querySelectorAll('a.known').forEach(a => a.onclick = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
try { await api('/api/known', {mark: a.dataset.known, known: a.dataset.on === '1'}); await load(); }
|
||||||
|
catch (err) { alert(err.message === 'not found' ? 'This ./ksim web was started before "skip" existed. Stop it (Ctrl-C) and start it again.' : err.message); }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function qResultClass(q) {
|
function qResultClass(q) {
|
||||||
@ -350,11 +361,10 @@
|
|||||||
function paint() {
|
function paint() {
|
||||||
const sess = S.session, live = sess && !sess.ended_at;
|
const sess = S.session, live = sess && !sess.ended_at;
|
||||||
$('#trackName').textContent = `${S.track.name} · Kubernetes ${S.track.k8s}`;
|
$('#trackName').textContent = `${S.track.name} · Kubernetes ${S.track.k8s}`;
|
||||||
$('#modeLabel').textContent = sess ? (sess.untimed ? 'Study' : (sess.mode === 'drill' ? 'Drill' : 'Exam')) + (sess.hard ? ' · HARD' : '')
|
$('#modeLabel').textContent = sess ? (sess.mode === 'drill' ? 'Drill' : 'Exam') + (sess.hard ? ' · HARD' : '')
|
||||||
+ (sess.objective ? ' · objective ' + sess.objective : '') : '';
|
+ (sess.objective ? ' · objective ' + sess.objective : '') : '';
|
||||||
$('#btnEnd').classList.toggle('hidden', !live);
|
$('#btnEnd').classList.toggle('hidden', !live);
|
||||||
if (live && sess.untimed) $('#btnEnd').textContent = 'End study session';
|
$('#btnPause').classList.toggle('hidden', !live);
|
||||||
$('#btnPause').classList.toggle('hidden', !live || sess.untimed);
|
|
||||||
if (live) $('#btnPause').textContent = sess.paused ? 'Resume' : 'Pause';
|
if (live) $('#btnPause').textContent = sess.paused ? 'Resume' : 'Pause';
|
||||||
$('#btnCheck').classList.toggle('hidden', !(live && sess.mode === 'drill'));
|
$('#btnCheck').classList.toggle('hidden', !(live && sess.mode === 'drill'));
|
||||||
$('#lobby').classList.toggle('hidden', !!live || (sess && sess.ended_at && !$('#results').classList.contains('hidden')));
|
$('#lobby').classList.toggle('hidden', !!live || (sess && sess.ended_at && !$('#results').classList.contains('hidden')));
|
||||||
@ -418,7 +428,6 @@
|
|||||||
clearInterval(clockT);
|
clearInterval(clockT);
|
||||||
const sess = S.session, el = $('#clock');
|
const sess = S.session, el = $('#clock');
|
||||||
if (!sess || sess.ended_at) { el.textContent = sess ? 'ended' : '--:--:--'; el.className = ''; return; }
|
if (!sess || sess.ended_at) { el.textContent = sess ? 'ended' : '--:--:--'; el.className = ''; return; }
|
||||||
if (sess.untimed) { el.textContent = 'study'; el.className = 'paused'; return; }
|
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const now = Date.now()/1000 - skew;
|
const now = Date.now()/1000 - skew;
|
||||||
const pausedTotal = sess.paused_total + (sess.paused ? (now - sess.server_time) : 0);
|
const pausedTotal = sess.paused_total + (sess.paused ? (now - sess.server_time) : 0);
|
||||||
@ -438,11 +447,13 @@
|
|||||||
const hard = $('#hardMode').checked, qn = hard ? (S.track.quota_hard || {}) : (S.track.quota || {});
|
const hard = $('#hardMode').checked, qn = hard ? (S.track.quota_hard || {}) : (S.track.quota || {});
|
||||||
const total = Object.values(qn).reduce((a, b) => a + b, 0) || S.bank.length;
|
const total = Object.values(qn).reduce((a, b) => a + b, 0) || S.bank.length;
|
||||||
$('#examDesc').textContent = `${total} tasks sampled across the ${Object.keys(S.track.domains).length} domains, ${S.track.minutes} minutes, pass mark ${S.track.pass_pct}%. No feedback until you submit.`;
|
$('#examDesc').textContent = `${total} tasks sampled across the ${Object.keys(S.track.domains).length} domains, ${S.track.minutes} minutes, pass mark ${S.track.pass_pct}%. No feedback until you submit.`;
|
||||||
const drilled = S.drilled || {}, undone = Object.keys(S.track.domains).filter(d => !drilled[d]);
|
const drilled = S.drilled || {}, skipped = S.known || [];
|
||||||
|
const undone = Object.keys(S.track.domains).filter(d => !drilled[d] && !skipped.includes(d));
|
||||||
$('#coverage').innerHTML = Object.keys(S.track.domains).map(d =>
|
$('#coverage').innerHTML = Object.keys(S.track.domains).map(d =>
|
||||||
`<span class="pill pick ${drilled[d] ? 'done' : ''}" data-domain="${d}" title="Drill ${S.track.domains[d]} — 5 tasks">${d} ${drilled[d] ? '✓' : '·'}</span>`).join('');
|
`<span class="pill pick ${drilled[d] ? 'done' : skipped.includes(d) ? 'known' : ''}" data-domain="${d}" title="Drill ${S.track.domains[d]} — 5 tasks${skipped.includes(d) ? ' (you marked this domain as known)' : ''}">${d} ${drilled[d] ? '✓' : skipped.includes(d) ? '»' : '·'}</span>`).join('');
|
||||||
$('#coverage').querySelectorAll('[data-domain]').forEach(el => el.onclick = () => start('drill', null, false, false, el.dataset.domain));
|
$('#coverage').querySelectorAll('[data-domain]').forEach(el => el.onclick = () => start('drill', null, false, false, el.dataset.domain));
|
||||||
const nxt = Object.keys(S.track.domains).sort((a, b) => (drilled[a] || 0) - (drilled[b] || 0))[0];
|
const openDoms = Object.keys(S.track.domains).filter(d => !skipped.includes(d));
|
||||||
|
const nxt = (openDoms.length ? openDoms : Object.keys(S.track.domains)).sort((a, b) => (drilled[a] || 0) - (drilled[b] || 0))[0];
|
||||||
const eligible = S.bank.filter(q => hard || q.difficulty !== 'hard'), seenN = eligible.filter(q => (S.seen || []).includes(q.id)).length;
|
const eligible = S.bank.filter(q => hard || q.difficulty !== 'hard'), seenN = eligible.filter(q => (S.seen || []).includes(q.id)).length;
|
||||||
$('#allDesc').textContent = `${seenN}/${eligible.length} tasks attempted. 5 unseen tasks per session, round-robin across domains${hard ? ', hard first' : ''}; then whatever you failed.`;
|
$('#allDesc').textContent = `${seenN}/${eligible.length} tasks attempted. 5 unseen tasks per session, round-robin across domains${hard ? ', hard first' : ''}; then whatever you failed.`;
|
||||||
$('#nextDesc').textContent = `${S.track.domains[nxt]} — 5 tasks, ${hard ? 5 : 6} min each. Drills rotate through all domains before the exam.`;
|
$('#nextDesc').textContent = `${S.track.domains[nxt]} — 5 tasks, ${hard ? 5 : 6} min each. Drills rotate through all domains before the exam.`;
|
||||||
@ -489,12 +500,12 @@
|
|||||||
} finally { $('#seeding').classList.add('hidden'); }
|
} finally { $('#seeding').classList.add('hidden'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start(mode, ids, next, campaign, domain, study, objective) {
|
async function start(mode, ids, next, campaign, domain, objective, touch) {
|
||||||
$('#lobbyOpts').classList.add('hidden'); $('#drillPick').classList.add('hidden'); $('#objPick').classList.add('hidden'); $('#lobbyErr').classList.add('hidden');
|
$('#lobbyOpts').classList.add('hidden'); $('#drillPick').classList.add('hidden'); $('#objPick').classList.add('hidden'); $('#lobbyErr').classList.add('hidden');
|
||||||
$('#seeding').classList.remove('hidden'); $('#seedLog').textContent = 'starting…';
|
$('#seeding').classList.remove('hidden'); $('#seedLog').textContent = 'starting…';
|
||||||
try {
|
try {
|
||||||
await api('/api/start', {mode, ids, next: !!next, campaign: !!campaign, domain: domain || null, study: !!study,
|
await api('/api/start', {mode, ids, next: !!next, campaign: !!campaign, domain: domain || null,
|
||||||
objective: objective || null, hard: $('#hardMode').checked});
|
objective: objective || null, touch: touch || null, hard: $('#hardMode').checked});
|
||||||
// seeding runs in the background; stream its log until it finishes
|
// seeding runs in the background; stream its log until it finishes
|
||||||
for (;;) {
|
for (;;) {
|
||||||
await new Promise(r => setTimeout(r, 1200));
|
await new Promise(r => setTimeout(r, 1200));
|
||||||
@ -526,7 +537,7 @@
|
|||||||
function showResults() {
|
function showResults() {
|
||||||
const s = S.session, sc = s.score;
|
const s = S.session, sc = s.score;
|
||||||
$('#results').dataset.shown = 1;
|
$('#results').dataset.shown = 1;
|
||||||
$('#resSub').textContent = `${s.untimed ? 'Study session' : (s.mode === 'drill' ? 'Drill' : 'Full exam')} · ${fmt(s.ended_at - s.started_at - s.paused_total)} used${s.over_time ? ' (over time)' : ''}`;
|
$('#resSub').textContent = `${s.mode === 'drill' ? 'Drill' : 'Full exam'} · ${fmt(s.ended_at - s.started_at - s.paused_total)} used${s.over_time ? ' (over time)' : ''}`;
|
||||||
$('#resScore').innerHTML = `${sc.pct}% <small>${sc.earned} / ${sc.total} points</small>`;
|
$('#resScore').innerHTML = `${sc.pct}% <small>${sc.earned} / ${sc.total} points</small>`;
|
||||||
const v = $('#resVerdict'); v.textContent = sc.pct >= sc.pass_pct ? 'Pass' : 'Fail'; v.className = 'verdict ' + (sc.pct >= sc.pass_pct ? 'pass' : 'fail');
|
const v = $('#resVerdict'); v.textContent = sc.pct >= sc.pass_pct ? 'Pass' : 'Fail'; v.className = 'verdict ' + (sc.pct >= sc.pass_pct ? 'pass' : 'fail');
|
||||||
$('#resDomains').innerHTML = Object.entries(s.domains).map(([d, x]) =>
|
$('#resDomains').innerHTML = Object.entries(s.domains).map(([d, x]) =>
|
||||||
@ -556,8 +567,7 @@
|
|||||||
// ---- wiring ----
|
// ---- wiring ----
|
||||||
$('#optExam').onclick = () => start('exam');
|
$('#optExam').onclick = () => start('exam');
|
||||||
$('#optNext').onclick = () => start('drill', null, true);
|
$('#optNext').onclick = () => start('drill', null, true);
|
||||||
$('#optStudy').onclick = () => {
|
$('#optObjective').onclick = () => {
|
||||||
if (!S.path) return start('drill', null, true, false, null, true);
|
|
||||||
$('#lobbyOpts').classList.add('hidden'); $('#gate').classList.add('hidden'); $('#objPick').classList.remove('hidden');
|
$('#lobbyOpts').classList.add('hidden'); $('#gate').classList.add('hidden'); $('#objPick').classList.remove('hidden');
|
||||||
};
|
};
|
||||||
$('#optPath').onclick = continuePath;
|
$('#optPath').onclick = continuePath;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user