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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
This commit is contained in:
parent
c266eeb4b5
commit
c244bd9c75
@ -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
|
||||
|
||||
24
ksim
24
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,
|
||||
|
||||
@ -170,6 +170,7 @@
|
||||
<span class="spacer"></span>
|
||||
<span id="clock">--:--:--</span>
|
||||
<button class="btn ghost" id="btnPause">Pause</button>
|
||||
<button class="btn ghost" id="btnRestart" title="Back to the session's full time - the hosts and your work stay as they are">Restart clock</button>
|
||||
<button class="btn ghost" id="btnHistory">History</button>
|
||||
<button class="btn danger" id="btnEnd">Submit exam</button>
|
||||
</header>
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user