From c244bd9c75ed23b495ed430bd1bd75ccdb57ddbf Mon Sep 17 00:00:00 2001 From: vumas3r Date: Thu, 15 Oct 2026 15:36:09 -0600 Subject: [PATCH] Restart the clock: ./ksim restart and a top-bar button Puts the session's full time back on the clock and starts it running, keeping the hosts, the work on them, results, flags and notes. Like pause it is recorded nowhere: the attempt's time used counts from the restart. Refused once a session has been submitted. POST /api/restart backs the button, which asks before it resets; a ./ksim web started before this says so instead of failing silently. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz --- README.md | 4 +++- ksim | 24 ++++++++++++++++++++++-- web/index.html | 7 +++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a61def..5f2c926 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ In the browser: the task on the left names its host; click `ssh cka0037` (or any value) to put it on the desktop's clipboard and paste in the terminal with **Ctrl+Shift+V**. 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. +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. 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 @@ -110,6 +111,7 @@ host name such as `ckad0013` only resolves in the desktop's terminal: from your ./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 pause | resume | restart # stop the clock, or put the session's full time back (hosts and work kept) ./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 reset N # rebuild the host of task N from scratch diff --git a/ksim b/ksim index bb161d6..175ef46 100755 --- a/ksim +++ b/ksim @@ -22,7 +22,7 @@ from collections.abc import Callable from datetime import datetime from typing import Any -VERSION = "2026.10.15.1" +VERSION = "2026.10.15.2" ROOT = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, ROOT) from sim import desktop, envs # noqa: E402 @@ -299,6 +299,12 @@ def set_paused(st: dict[str, Any], on: bool) -> None: st["paused_total"] = float(st.get("paused_total", 0)) + (time.time() - float(st["paused_at"])); st["paused_at"] = None save_state(st) +def restart_clock(st: dict[str, Any]) -> None: + """The session's full time again, from now and running. Hosts, the work on them, results, + flags and notes stay as they are.""" + st["started_at"] = time.time(); st["paused_total"] = 0.0; st["paused_at"] = None + save_state(st) + def fmt(secs: float | int) -> str: secs_int = max(0, int(secs)) return f"{secs_int // 3600:02d}:{(secs_int % 3600) // 60:02d}:{secs_int % 60:02d}" @@ -1643,6 +1649,15 @@ def cmd_resume(args): st = require_started() set_paused(st, False); print(status_line(st)) +def cmd_restart(args): + """Start the clock again from the session's full time, keeping its hosts and your work. Like + pause, not recorded anywhere: the attempt's time used counts from the restart.""" + st = require_started() + if st.get("ended_at"): + die("Session already ended - nothing to restart. Start a new one: ./ksim drill, ./ksim start, ./ksim next") + restart_clock(st) + print(status_line(st) + dim(" clock restarted - the hosts and your work are as they were")) + def cmd_end(args): st = require_started() if st.get("ended_at"): @@ -1843,6 +1858,10 @@ class Api(http.server.BaseHTTPRequestHandler): flags.symmetric_difference_update({qid}); st["flags"] = sorted(flags); save_state(st) elif p.path == "/api/pause": set_paused(st, bool(body.get("paused", True))) + elif p.path == "/api/restart": + 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/notes": st["notes"] = body.get("notes", ""); save_state(st) elif p.path == "/api/grade": @@ -2215,6 +2234,7 @@ def cmd_help(args): grade N|all grade one task (or all) per check - drills only (--force to peek in an exam) status / timer time remaining (timer = live countdown) pause / resume stop the clock (not recorded anywhere - but the real exam has no pause) + restart the clock from the top: the session's full time again, hosts and work kept (not recorded) end submit: grade per check, score, log attempt, update weak-spots weak what you keep getting wrong, with the failing checks history every attempt, scores, and per-domain trend @@ -2228,7 +2248,7 @@ def cmd_help(args): """) COMMANDS = {"start": cmd_start, "drill": cmd_drill, "list": cmd_list, "show": cmd_show, "grade": cmd_grade, - "status": cmd_status, "timer": cmd_timer, "pause": cmd_pause, "resume": cmd_resume, "end": cmd_end, "weak": cmd_weak, "history": cmd_history, + "status": cmd_status, "timer": cmd_timer, "pause": cmd_pause, "resume": cmd_resume, "restart": cmd_restart, "end": cmd_end, "weak": cmd_weak, "history": cmd_history, "reset": cmd_reset, "web": cmd_web, "abort": cmd_abort, "progress": cmd_progress, "solution": cmd_solution, "sim": cmd_sim, "version": cmd_version, "help": cmd_help, "build": cmd_build, "hosts": cmd_hosts, "ssh": cmd_ssh, "down": cmd_down, "audit": cmd_audit, "coverage": cmd_coverage, "objectives": cmd_objectives, "curriculum": cmd_objectives, "notes": cmd_notes, "next": cmd_next, "focus": cmd_focus, diff --git a/web/index.html b/web/index.html index bfd0795..60b87f0 100644 --- a/web/index.html +++ b/web/index.html @@ -170,6 +170,7 @@ --:--:-- + @@ -365,6 +366,7 @@ + (sess.objective ? ' ยท objective ' + sess.objective : '') : ''; $('#btnEnd').classList.toggle('hidden', !live); $('#btnPause').classList.toggle('hidden', !live); + $('#btnRestart').classList.toggle('hidden', !live); if (live) $('#btnPause').textContent = sess.paused ? 'Resume' : 'Pause'; $('#btnCheck').classList.toggle('hidden', !(live && sess.mode === 'drill')); $('#lobby').classList.toggle('hidden', !!live || (sess && sess.ended_at && !$('#results').classList.contains('hidden'))); @@ -598,6 +600,11 @@ $('#btnNewExam').onclick = () => { $('#results').classList.add('hidden'); $('#lobby').classList.remove('hidden'); $('#lobbyOpts').classList.remove('hidden'); }; $('#btnHistory').onclick = showHistory; $('#btnPause').onclick = async () => { S = await api('/api/pause', {paused: !S.session.paused}); skew = Date.now()/1000 - S.session.server_time; paint(); }; + $('#btnRestart').onclick = async () => { + if (!confirm(`Restart the clock at ${fmt(S.session.duration)}? The hosts and your work stay as they are.`)) return; + try { S = await api('/api/restart', {}); skew = Date.now()/1000 - S.session.server_time; autoSubmitted = false; paint(); } + catch (e) { alert(e.message === 'not found' ? 'This ./ksim web was started before "restart" existed. Stop it (Ctrl-C) and start it again.' : e.message); } + }; $('#btnCloseHist').onclick = () => $('#histOv').classList.add('hidden'); // draggable split const handle = $('#handle'), main = document.querySelector('main');