# L4D2 Job Pages and Cancellation Follow-Up ## Goal Make queued and running lifecycle jobs easier to inspect and stop from the web UI. ## Scope - Add job list navigation for server pages and admin pages. - Add a job detail page with persisted command logs streamed through the existing SSE endpoint. - Add cancellation for queued jobs first. - Add best-effort cancellation for running jobs by terminating the subprocess owned by `l4d2host.process.run_command()`. ## Slice 1: Job Browsing and Queued Cancel ### Behavior - `/servers/` shows recent jobs for that server and links to the full job history. - `/servers//jobs` shows all jobs for that server, newest first. - `/jobs/` shows job metadata and live/replayed logs. - `/admin/jobs` reuses the same job table markup and links every job to its detail page. - `POST /jobs//cancel` cancels queued jobs only. - Owners can view/cancel their own jobs. - Admins can view/cancel any job. ### Implementation Notes - Use one reusable Jinja partial for job tables. - Show cancel buttons only for `queued` jobs in this slice. - Cancelling a queued job sets `state="cancelled"`, `finished_at`, `updated_at`, and `exit_code=1`. - Append a `stderr` job-log line explaining that the job was cancelled before execution. - Do not revert `Server.desired_state`; cancellation prevents execution but is not rollback. ### Verification - `pytest l4d2web/tests/test_pages.py -q` - `pytest l4d2web/tests/test_job_logs.py -q` - `pytest l4d2web/tests -q` ## Slice 2: Running Job Cancellation ### Behavior - Running jobs expose the same cancel action. - Cancelling a running job marks it `cancelling` while the subprocess is being terminated. - Once the subprocess exits because of cancellation, the job finishes as `cancelled`. - Cancellation is best-effort and is not rollback; partial runtime state may remain. - Server actual state is refreshed after a cancelled server job when possible. ### Implementation Notes - Add cancellation primitives in `l4d2host.process`. - Launch subprocesses in their own process group/session when a cancel token is supplied. - On cancellation, send terminate, wait briefly, then force kill. - Thread the cancel token through `l4d2host` lifecycle APIs, `l4d2web.services.l4d2_facade`, and `l4d2web.services.job_worker`. - Keep v1 single-process assumptions; cancellation requests are DB-backed, while process handles stay process-local. ### Verification - `pytest l4d2host/tests/test_process.py -q` - `pytest l4d2host/tests -q` - `pytest l4d2web/tests/test_job_worker.py -q` - `pytest l4d2web/tests/test_job_logs.py -q` - `pytest l4d2web/tests -q`