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:
vumas3r 2026-10-15 16:36:26 -06:00
parent c244bd9c75
commit bab85d14b7
3 changed files with 954 additions and 514 deletions

View File

@ -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 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
value) to put it on the desktop's clipboard and paste in the terminal with **Ctrl+Shift+V**.
The browser UI follows the layout of [CK-X](https://github.com/sailor-sh/CK-X) (itself modelled on
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`
lifts that). Submit grades every task on its own host; the hosts stay up for review until the next
session starts. **Restart clock** in the top bar (`./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.
lifts that). Submitting grades every task on its own host; the hosts stay up for review until the
next session starts, or until **Delete the hosts** on the results page. **Restart clock**
(`./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
`KSIM_TRACK`; with neither, for the track whose session is under way, and for CKA when there is

19
ksim
View File

@ -22,7 +22,7 @@ from collections.abc import Callable
from datetime import datetime
from typing import Any
VERSION = "2026.10.15.2"
VERSION = "2026.10.15.3"
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT)
from sim import desktop, envs # noqa: E402
@ -1340,6 +1340,15 @@ def clear_hosts(keep=()):
list(pool.map(envs.destroy, 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():
"""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."""
@ -1862,6 +1871,12 @@ class Api(http.server.BaseHTTPRequestHandler):
if st.get("ended_at"):
return self._json({"error": "This session has ended - there is no clock to restart."}, 400)
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":
st["notes"] = body.get("notes", ""); save_state(st)
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", []),
"paused": bool(st.get("paused_at")), "paused_total": paused_total(st),
"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},
"domains": self.domain_scores(st), "server_time": time.time()}
return payload

File diff suppressed because it is too large Load Diff