Browser UI in the style of CK-X: landing page, exam screen, results
Rebuilt web/index.html after the layout of CK-X (itself modelled on the Linux Foundation exam UI), written from scratch and loading nothing from the network: - landing page: track switch in the top bar, a Start exam dialog to pick the session (path, objective, domain, next domain, whole bank, picked tasks, simulator sets, full exam with its gate, hard mode), the path, a card per domain, every objective with its notes and skip links, and an overlay with the clusters' progress while a session is prepared - exam screen: dark top bar with the clock and the Exam Interface / Exam Controls / Exam Information menus, a light question panel with < Question N >, "Solve this question on instance", Flag for review, Satisfied with answer, and Check this task in drills; the remote desktop with reconnect and fullscreen beside it - results page: points, pass or fail, domains, every task's checks and reference solution, answers, drill what I missed, review on the hosts A session started in the browser opens a dialog whose Start puts the full time on the clock. New POST /api/terminate backs Terminate session (an open session ends ungraded, as ./ksim abort) and Delete the hosts (a finished session loses its hosts); both delete that session's hosts only. The session payload says whether a session was aborted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
This commit is contained in:
parent
c244bd9c75
commit
bab85d14b7
31
README.md
31
README.md
@ -85,15 +85,34 @@ desktop; the VM host adds a 3.5 GB base disk and the 0.6 GB Ubuntu cloud image u
|
|||||||
```
|
```
|
||||||
./ksim build all # once: the exam host image per Kubernetes minor, and the desktop image
|
./ksim build all # once: the exam host image per Kubernetes minor, and the desktop image
|
||||||
./ksim build cks --vm # once, for CKS: also the VM host its Falco and AppArmor tasks need (~10 min)
|
./ksim build cks --vm # once, for CKS: also the VM host its Falco and AppArmor tasks need (~10 min)
|
||||||
./ksim web # http://127.0.0.1:8080 — pick a track and a session in the lobby
|
./ksim web # http://127.0.0.1:8080 — pick a track and a session on the landing page
|
||||||
```
|
```
|
||||||
|
|
||||||
In the browser: the task on the left names its host; click `ssh cka0037` (or any highlighted
|
The browser UI follows the layout of [CK-X](https://github.com/sailor-sh/CK-X) (itself modelled on
|
||||||
value) to put it on the desktop's clipboard and paste in the terminal with **Ctrl+Shift+V**.
|
the Linux Foundation's exam UI), written anew for this simulator:
|
||||||
|
|
||||||
|
- **Landing page** — the track (CKA, CKAD, CKS) in the top bar; **Start exam** opens a dialog to
|
||||||
|
pick the session: the next step on the path, an objective, a domain, the next domain, the whole
|
||||||
|
bank, tasks of your choice, a simulator set or a full exam, with or without hard mode. Below it:
|
||||||
|
your path, a card per domain, and every objective with its notes and the "I know this — skip it"
|
||||||
|
links. While the clusters are built, an overlay shows their progress and the log.
|
||||||
|
- **Exam screen** — a dark top bar with the clock and three menus: *Exam Interface* (fullscreen,
|
||||||
|
panel sizes, reconnect the desktop), *Exam Controls* (submit, pause, **Restart clock**,
|
||||||
|
**Terminate session**) and *Exam Information* (exam instructions, Kubernetes docs, your history).
|
||||||
|
On the left, `< Question N >` and each task headed by its host: click `ssh ckad0037` (or any
|
||||||
|
highlighted value) to put it on the desktop's clipboard and paste in the terminal with
|
||||||
|
**Ctrl+Shift+V**. **Flag for review** and **Satisfied with answer** sit under the task; a drill
|
||||||
|
adds **Check this task**, and a task's reference solution opens once it is graded. On the right,
|
||||||
|
the remote desktop. The clock starts when you click **Start** in the dialog that opens the session.
|
||||||
|
- **Results page** — points and pass or fail, every task with its checks, the answers, what to drill
|
||||||
|
next, and a way back to the tasks to review them on their hosts.
|
||||||
|
|
||||||
Firefox in the desktop opens only the allowed documentation for the track (`KSIM_OPEN_WEB=1`
|
Firefox in the desktop opens only the allowed documentation for the track (`KSIM_OPEN_WEB=1`
|
||||||
lifts that). Submit grades every task on its own host; the hosts stay up for review until the next
|
lifts that). Submitting grades every task on its own host; the hosts stay up for review until the
|
||||||
session starts. **Restart clock** in the top bar (`./ksim restart`) puts the session's full time
|
next session starts, or until **Delete the hosts** on the results page. **Restart clock**
|
||||||
back on the clock without touching its hosts or your work; like Pause, it is recorded nowhere.
|
(`./ksim restart`) puts the session's full time back on the clock without touching its hosts or
|
||||||
|
your work; like Pause, it is recorded nowhere. **Terminate session** ends a session without grading
|
||||||
|
it and deletes its hosts, as `./ksim abort` does.
|
||||||
|
|
||||||
Everything is also available from a terminal. A command is for the track named with `--track` or
|
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
|
`KSIM_TRACK`; with neither, for the track whose session is under way, and for CKA when there is
|
||||||
|
|||||||
19
ksim
19
ksim
@ -22,7 +22,7 @@ from collections.abc import Callable
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
VERSION = "2026.10.15.2"
|
VERSION = "2026.10.15.3"
|
||||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, ROOT)
|
sys.path.insert(0, ROOT)
|
||||||
from sim import desktop, envs # noqa: E402
|
from sim import desktop, envs # noqa: E402
|
||||||
@ -1340,6 +1340,15 @@ def clear_hosts(keep=()):
|
|||||||
list(pool.map(envs.destroy, old))
|
list(pool.map(envs.destroy, old))
|
||||||
return old
|
return old
|
||||||
|
|
||||||
|
def drop_hosts(st):
|
||||||
|
"""Delete the hosts of one session's tasks and nothing else - the browser's Terminate session
|
||||||
|
and Delete the hosts, which must not reach another track's session."""
|
||||||
|
mine = {host_of(q) for q in session_questions(st)}
|
||||||
|
gone = [h for h in envs.existing() if h in mine]
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=PARALLEL) as pool:
|
||||||
|
list(pool.map(envs.destroy, gone))
|
||||||
|
return gone
|
||||||
|
|
||||||
def open_sessions_elsewhere():
|
def open_sessions_elsewhere():
|
||||||
"""Tracks other than this one with a session that has not ended. One session at a time: they
|
"""Tracks other than this one with a session that has not ended. One session at a time: they
|
||||||
share the desktop, and a new session clears the hosts of the last one."""
|
share the desktop, and a new session clears the hosts of the last one."""
|
||||||
@ -1862,6 +1871,12 @@ class Api(http.server.BaseHTTPRequestHandler):
|
|||||||
if st.get("ended_at"):
|
if st.get("ended_at"):
|
||||||
return self._json({"error": "This session has ended - there is no clock to restart."}, 400)
|
return self._json({"error": "This session has ended - there is no clock to restart."}, 400)
|
||||||
restart_clock(st)
|
restart_clock(st)
|
||||||
|
elif p.path == "/api/terminate": # an open session ends ungraded, as ./ksim abort; an ended one only loses its hosts
|
||||||
|
if Api.seeding:
|
||||||
|
return self._json({"error": "A session is being prepared - wait until it is ready, then terminate it."}, 409)
|
||||||
|
if not st.get("ended_at"):
|
||||||
|
st["ended_at"] = time.time(); st["aborted"] = True; save_state(st)
|
||||||
|
drop_hosts(st)
|
||||||
elif p.path == "/api/notes":
|
elif p.path == "/api/notes":
|
||||||
st["notes"] = body.get("notes", ""); save_state(st)
|
st["notes"] = body.get("notes", ""); save_state(st)
|
||||||
elif p.path == "/api/grade":
|
elif p.path == "/api/grade":
|
||||||
@ -1911,7 +1926,7 @@ class Api(http.server.BaseHTTPRequestHandler):
|
|||||||
"over_time": st.get("over_time", False), "flags": st.get("flags", []),
|
"over_time": st.get("over_time", False), "flags": st.get("flags", []),
|
||||||
"paused": bool(st.get("paused_at")), "paused_total": paused_total(st),
|
"paused": bool(st.get("paused_at")), "paused_total": paused_total(st),
|
||||||
"notes": st.get("notes", ""),
|
"notes": st.get("notes", ""),
|
||||||
"objective": st.get("objective"), "milestones": st.get("milestones", []),
|
"objective": st.get("objective"), "milestones": st.get("milestones", []), "aborted": bool(st.get("aborted")),
|
||||||
"results": st["results"], "score": {"earned": earned, "total": total, "pct": pct, "pass_pct": PASS_PCT},
|
"results": st["results"], "score": {"earned": earned, "total": total, "pct": pct, "pass_pct": PASS_PCT},
|
||||||
"domains": self.domain_scores(st), "server_time": time.time()}
|
"domains": self.domain_scores(st), "server_time": time.time()}
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
1408
web/index.html
1408
web/index.html
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user