Compare commits

..

No commits in common. "fe86b4be35a7ae7c2a172c9a76b2d48f19f4bbfd" and "accb26159ef5b40ccbe00b0035d5d68902d08a91" have entirely different histories.

3 changed files with 1 additions and 32 deletions

View File

@ -4,18 +4,6 @@ Notable changes to kubestronaut-sim, newest first. A version is the `VERSION` st
(year.month.day.n); `./ksim help` and the top bar of the web page show the one you are running.
Every entry names the commit it came in.
## 2026.10.26.3 — 2026-10-26
Everything below came in `95f9d1b`.
### Fixed
- Choosing *Teach a domain* could start a full exam. `./ksim web` reads `web/index.html` afresh on
every request but keeps running the code it started with, so a server from before the update
read the new request as its default, a full exam. The server now reports when its code has
changed on disk since it started (a server from before this check counts as changed). The
lobby then asks you to restart it and starts nothing until you do.
## 2026.10.26.2 — 2026-10-26
Everything below came in `e852419`.

15
ksim
View File

@ -22,7 +22,7 @@ from collections.abc import Callable
from datetime import datetime
from typing import Any
VERSION = "2026.10.26.3"
VERSION = "2026.10.26.2"
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT)
from sim import desktop, envs # noqa: E402
@ -2165,17 +2165,9 @@ def cmd_reset(args):
print(("rebuilt " if not problem else "problem rebuilding ") + f"Q{q['id']:02d}: {q['title']}")
# --------------------------------------------------------------- web ----
def code_stamp():
"""When the simulator's own code (ksim, sim/*.py) last changed on disk."""
sim_dir = os.path.join(ROOT, "sim")
return max(os.path.getmtime(f) for f in [os.path.join(ROOT, "ksim")]
+ [os.path.join(sim_dir, n) for n in os.listdir(sim_dir) if n.endswith(".py")])
class Api(http.server.BaseHTTPRequestHandler):
"""Serves web/index.html plus a small JSON API. State lives in the same files the CLI uses."""
lock = threading.Lock()
started_code = None # code_stamp() when ./ksim web started
log_lines = []
seeding = False
seed_error = None
@ -2353,10 +2345,6 @@ class Api(http.server.BaseHTTPRequestHandler):
"k8s": k8s_version(), "pending": len(PENDING), "drilled": drilled_domains(), "sets": {k: {"name": v, "count": len(set_questions(k))} for k, v in SETS.items()},
"seen": sorted(last_seen()),
"seeding": Api.seeding, "seed_error": Api.seed_error,
# index.html is read from disk on every request, the server's code only when it started:
# after an update the page may ask for what this server does not know (an old one read
# "teach a domain" as a full exam), so the page starts nothing until it is restarted
"stale": Api.started_code is not None and code_stamp() > Api.started_code,
"session": None,
}
payload.update(self.path_payload())
@ -2456,7 +2444,6 @@ def cmd_web(args):
except envs.EnvError as e:
Api.desktop_error = str(e)
print(yellow(f" no desktop: {e}"))
Api.started_code = code_stamp()
http.server.ThreadingHTTPServer.allow_reuse_address = True
srv = http.server.ThreadingHTTPServer(("127.0.0.1", port), Api) # type: ignore[arg-type]
print(bold(f"\n {TRACK_ID.upper()} exam UI: http://127.0.0.1:{port} (ksim {VERSION})\n")

View File

@ -397,7 +397,6 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
<div class="scrollhint">SCROLL FOR YOUR PATH AND OBJECTIVES</div>
</section>
<main class="wrap">
<div class="alert alert-warning hidden" id="staleNote"></div>
<div class="alert alert-danger hidden" id="lobbyErr"></div>
<div class="alert alert-info hidden" id="lobbyNote"></div>
<section class="card" id="pathCard">
@ -597,9 +596,6 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
const fmtHMS = s => { s = Math.max(0, Math.floor(s)); return [s / 3600, s % 3600 / 60, s % 60].map(pad).join(':'); };
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}[c]));
const oldServer = what => `This ./ksim web was started before "${what}" existed. Stop it (Ctrl-C) and start it again.`;
// a server that sends no "stale" predates the check, so it is older than this page as well
const STALE = 'The simulator was updated after this ./ksim web started, and the server still runs the old code: it would misread what this page asks for (an old one starts a full exam instead of a domain\'s lessons). Stop it (Ctrl-C), start it again, and reload this page.';
const stale = () => S && S.stale !== false;
const openModal = id => $(id).classList.remove('hidden');
const closeModal = id => $(id).classList.add('hidden');
const live = () => !!(S && S.session && !S.session.ended_at);
@ -687,7 +683,6 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
$$('#trackPick [data-track]').forEach(el => el.onclick = () => switchTrack(el.dataset.track));
$('#ver').textContent = 'ksim ' + (S.version || '?') + (S.pending ? ` · ${S.pending} task(s) held back` : '');
$('#foot').textContent = `ksim ${S.version || '?'} · Apache-2.0 · not affiliated with the CNCF or the Linux Foundation`;
$('#staleNote').textContent = STALE; $('#staleNote').classList.toggle('hidden', !stale());
$('#heroTitle').textContent = T.name;
$('#heroSub').textContent = `Kubernetes ${T.k8s}. Every task runs on its own cluster that you reach with ssh from a remote desktop, and every session runs against the clock.`;
$('#hardHero').checked = hard;
@ -896,7 +891,6 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
}
}
async function launch(body) {
if (stale()) { closeModal('#mPicker'); return lobbyError(STALE); }
closeModal('#mPicker'); $('#lobbyErr').classList.add('hidden'); $('#lobbyNote').classList.add('hidden');
$('#prepMsg').textContent = 'starting…'; $('#prepLog').textContent = ''; openModal('#mPrep'); polling = true;
try { await api('/api/start', body); await waitSeeded(); ready(true); }