T6-2 (#38): Rust CODE panel inside the normal game UI #39

Merged
sleepy merged 7 commits from task/38-in-game-rust-panel into main 2026-09-24 13:45:05 +02:00
Owner

What

The Rust CODE panel now lives inside the normal game UI (issue #38, Phase 6): the CODE button opens a three-element .codeui group — a tab strip, the legacy JS editor (#codeui, untouched), and the new Rust pane (#rustcodeui) — so the character is on screen while a Rust process drives it.

One partial, one client, two hosts (the "reuse, don't fork" rule from the brief):

file role
htmls/contents/rust_serve_panel.html the markup — no ids, no interpolation, editor + log pane open empty
js/rust_code_panel.js the client both hosts mount: POST /compile|/run|/stop|/reload, GET /status|/options|/source, 1.5 s poll, one instance per mount
js/al_code_panes.js code_pane("js"|"rust") + code_ui_sync(), called from toggle_code()
htmls/rust_code.html now a thin wrapper around the same partial + client (T4-3's page keeps working)
htmls/index.html, css/al_serve_panel.css, js/functions.js the tab strip, the pane, the styles, one added code_ui_sync() call

The endpoint is chosen in the browser (data-endpoint → ?serve= → localStorage → window.AL_SERVE_DEFAULT_ENDPOINT → 127.0.0.1:8192); the backend renders no endpoint, no crate list and no Rust source, and the auth token never crosses the page or the backend. The RUST pane mounts lazily on first tab click — no request, no poll until someone asks for it — and stops polling when its tab or the CODE panel closes.

Live verification (the part the previous two sessions did not reach)

Headless chromium over CDP against the worktree backend :8091 and al serve :8212 (--creds rust/dev/.local, i.e. Rusty). The browser plays UserTesting and watches Rusty through the game's own observer (observe_character) — deliberately: the server refuses a second login for a character already in game (msince(last_sync) < 120 → ex("ingame"), node/server.js:10911), and al serve runs the crate as Rusty.

  • lazy mount: 0 requests to al serve at page load, panel("game") absent → after the RUST tab click it is mounted, polling, endpoint() === http://127.0.0.1:8212 (resolved through the ?serve= step), crate list [examples/hello, examples/farm_basic, examples/capture], editor loaded 24 080 chars from GET /source.
  • Compile: cargo inside al serve → Finished \release` profile [optimized] target(s) in 2.85s/[al] built …/target/release/hello`.
  • Run: panel status Running · pid 2630111 · restarts 0 · examples/hello; ring hello: logged in to ws://127.0.0.1:7192/socket.io/ as "Rusty"; the log pane streamed x 2.4 y -21.2 → x -11.0 y 98.1 → x -24.3 y 217.3.
  • the character visibly moved: the observing tab saw Rusty at (-20.7, 185) moving=true → (-24.3, 217.3) with its camera following (main,-11,218 → main,-24,337); the server's own store went (2.4,-21.2) → (-24.3,217.3) = 240 px, arrived=true.
  • Stop: Stopped → stopped · clean exit (code 0), /status running:false pid:null.
  • standalone /rust-code?serve=… on the same :8091: panel rust-code, polling, al serve is up, 18 hooks, 24 080 chars.
  • legacy JS editor: CODE opens it alone, CodeMirror alive (1118 chars), a snippet engaged (code_active=true, runner frame up); switching to RUST hides it and stops the panel's poll; closing CODE closes both and stops the poll.

The one bug the live run found (bb4b06a)

js/game.js rewrites the address bar as soon as the character loads (history.replaceState({}, title, "/character/<name>/in/…"), ~line 1608; the auto-reload navigates to that URL). Because the pane mounts lazily, location.search was already empty when resolve_endpoint ran — the player's ?serve=http://127.0.0.1:8212 fell out of the chain and the panel quietly started polling the shipped default :8192: a different al serve, with a different crate allow-list and a different character's credentials. Fix: capture location.search when the script loads and use it when the live search no longer answers the parameter. A host that never touches its URL behaves exactly as before (live search still wins), and the chain's order is unchanged. Two tests pin both halves.

Tests

  • panel suites: 37 pass / 0 fail (node/test/rust_serve_panel.test.js, node/test/rust_code_page.test.js) — 35 from the previous session + 2 for the URL rewrite;
  • full node --test node/test/: 210 pass / 0 fail;
  • the T4-3 guarantees are still asserted, on both hosts: source never reaches the DOM as markup, no interpolation of visitor-controlled data, the unsaved-edits crate switch is refused, a refused /source leaves the editor alone, and nothing (source, crate list, build log) passes through the backend.

node_modules (symlinks, untracked) and the auto-bumped version.js are not included. hello/src/main.rs is byte-identical — Compile wrote back what it was given (main.rs.good is al serve's own backup, untracked).

Note for #17 (T5-1), not a T6-2 bug

hello's target_from walks toward main's origin from wherever the character stands; from some spots that line crosses a wall, the server jails him on the first leg (#C cheater: Rusty … at main), and from jail every further leg answers new_map. That is the jail-safe travel redesign #17 owns. A run starting on walkable ground (main(3,-30), main(-23,209)) arrives cleanly.

## What The Rust CODE panel now lives **inside the normal game UI** (issue #38, Phase 6): the CODE button opens a three-element `.codeui` group — a tab strip, the legacy JS editor (`#codeui`, untouched), and the new Rust pane (`#rustcodeui`) — so the character is on screen while a Rust process drives it. One partial, one client, two hosts (the "reuse, don't fork" rule from the brief): | file | role | |---|---| | `htmls/contents/rust_serve_panel.html` | the markup — no ids, no interpolation, editor + log pane open empty | | `js/rust_code_panel.js` | the client both hosts mount: `POST /compile\|/run\|/stop\|/reload`, `GET /status\|/options\|/source`, 1.5 s poll, one instance per mount | | `js/al_code_panes.js` | `code_pane("js"\|"rust")` + `code_ui_sync()`, called from `toggle_code()` | | `htmls/rust_code.html` | now a thin wrapper around the same partial + client (T4-3's page keeps working) | | `htmls/index.html`, `css/al_serve_panel.css`, `js/functions.js` | the tab strip, the pane, the styles, one added `code_ui_sync()` call | The endpoint is chosen **in the browser** (`data-endpoint` → `?serve=` → localStorage → `window.AL_SERVE_DEFAULT_ENDPOINT` → `127.0.0.1:8192`); the backend renders no endpoint, no crate list and no Rust source, and the auth token never crosses the page or the backend. The RUST pane **mounts lazily on first tab click** — no request, no poll until someone asks for it — and stops polling when its tab or the CODE panel closes. ## Live verification (the part the previous two sessions did not reach) Headless chromium over CDP against the worktree backend **:8091** and `al serve` **:8212** (`--creds rust/dev/.local`, i.e. Rusty). The browser plays **UserTesting** and watches Rusty through the game's *own* observer (`observe_character`) — deliberately: the server refuses a second login for a character already in game (`msince(last_sync) < 120 → ex("ingame")`, `node/server.js:10911`), and `al serve` runs the crate as Rusty. - **lazy mount**: 0 requests to `al serve` at page load, `panel("game")` absent → after the RUST tab click it is mounted, polling, `endpoint() === http://127.0.0.1:8212` (resolved through the `?serve=` step), crate list `[examples/hello, examples/farm_basic, examples/capture]`, editor loaded 24 080 chars from `GET /source`. - **Compile**: cargo inside `al serve` → `Finished \`release\` profile [optimized] target(s) in 2.85s` / `[al] built …/target/release/hello`. - **Run**: panel status `Running · pid 2630111 · restarts 0 · examples/hello`; ring `hello: logged in to ws://127.0.0.1:7192/socket.io/ as "Rusty"`; the log pane streamed `x 2.4 y -21.2` → `x -11.0 y 98.1` → `x -24.3 y 217.3`. - **the character visibly moved**: the observing tab saw Rusty at `(-20.7, 185) moving=true` → `(-24.3, 217.3)` with its camera following (`main,-11,218` → `main,-24,337`); the server's own store went `(2.4,-21.2)` → `(-24.3,217.3)` = **240 px**, `arrived=true`. - **Stop**: `Stopped` → `stopped · clean exit (code 0)`, `/status` `running:false pid:null`. - **standalone `/rust-code?serve=…`** on the same :8091: panel `rust-code`, polling, `al serve is up`, 18 hooks, 24 080 chars. - **legacy JS editor**: CODE opens it alone, CodeMirror alive (1118 chars), a snippet engaged (`code_active=true`, runner frame up); switching to RUST hides it and stops the panel's poll; closing CODE closes both and stops the poll. ## The one bug the live run found (`bb4b06a`) `js/game.js` rewrites the address bar as soon as the character loads (`history.replaceState({}, title, "/character/<name>/in/…")`, ~line 1608; the auto-reload navigates to that URL). Because the pane mounts lazily, `location.search` was already empty when `resolve_endpoint` ran — the player's `?serve=http://127.0.0.1:8212` fell out of the chain and the panel quietly started polling the shipped default **:8192**: a different `al serve`, with a different crate allow-list and a different character's credentials. Fix: capture `location.search` when the script loads and use it when the live search no longer answers the parameter. A host that never touches its URL behaves exactly as before (live search still wins), and the chain's order is unchanged. Two tests pin both halves. ## Tests - panel suites: **37 pass / 0 fail** (`node/test/rust_serve_panel.test.js`, `node/test/rust_code_page.test.js`) — 35 from the previous session + 2 for the URL rewrite; - full `node --test node/test/`: **210 pass / 0 fail**; - the T4-3 guarantees are still asserted, on both hosts: source never reaches the DOM as markup, no interpolation of visitor-controlled data, the unsaved-edits crate switch is refused, a refused `/source` leaves the editor alone, and nothing (source, crate list, build log) passes through the backend. `node_modules` (symlinks, untracked) and the auto-bumped `version.js` are not included. `hello/src/main.rs` is byte-identical — Compile wrote back what it was given (`main.rs.good` is `al serve`'s own backup, untracked). ## Note for #17 (T5-1), not a T6-2 bug `hello`'s `target_from` walks toward `main`'s origin from wherever the character stands; from some spots that line crosses a wall, the server jails him on the first leg (`#C cheater: Rusty … at main`), and from jail every further leg answers `new_map`. That is the jail-safe travel redesign #17 owns. A run starting on walkable ground (`main(3,-30)`, `main(-23,209)`) arrives cleanly.
Continues 38adde3 (al_t62b died pre-commit; verified by orchestrator).

js/rust_code_panel.js:
- query_value(): keep the slash in crate names unescaped (al serve's query
  parser compares whole strings, so examples%2Fhello is refused as an unknown
  crate); everything encodeURIComponent exists for still escapes.
- claim_pane(): clear the last_tail sentinel so a button's answer owns the log
  pane only until the next poll has anything to say — a build log parked in the
  pane while the character is already walking is a pane lying about now. The
  unchanged-tail case still must not rewrite the pane.

node/test: expanded helpers (al_code_panes_dom) + rust_code_page/rust_serve_panel
suites; RUST tab mounts the client lazily on first click, CODE close stops the
poll, and the T4-3 guarantees (source never as markup, no interpolation of
visitor data, unsaved-edits guard, refused /source leaves editor alone) hold.

node --test node/test/rust_serve_panel.test.js node/test/rust_code_page.test.js
=> 35 pass / 0 fail
Driving the real page (headless chromium against the worktree backend on :8091, `al
serve` on :8212) found the one thing the Node suite could not see: the RUST pane
mounts lazily on tab click, but js/game.js rewrites the address bar the moment the
character loads (`history.replaceState({}, title, "/character/<name>/in/…")`, and the
auto-reload navigates to that URL). So `location.search` was empty by the time
`resolve_endpoint` ran, the player's `?serve=http://127.0.0.1:8212` fell out of the
chain, and the panel started polling the shipped default :8192 — a different `al
serve`, with a different crate allow-list and a different character's credentials.

Fix: capture `location.search` when the script loads and use it when the live search
no longer answers the parameter. A host that never touches its URL behaves exactly as
before (live search still wins), and nothing in the chain's order changes.

Two tests in node/test/rust_serve_panel.test.js pin it: the override survives a load →
rewrite → click sequence, and no `?serve=` at load leaves the rest of the chain in
charge (a stored edit still wins, no phantom value is invented). Suite for the panel:
37 pass / 0 fail; full node suite: 210 pass / 0 fail.
T6-2 (#38): PROGRESS entry + PLAN status — in-game Rust panel done, verified live
Some checks are pending
Code Quality / prettier (push) Waiting to run
Code Quality / prettier (pull_request) Waiting to run
7c280df061
The entry carries the hour (2026-09-24 12:00 local) and, per the log's habit, the
things a later session would otherwise rediscover the slow way:

* the live evidence, in coordinates: Compile (`Finished release profile in 2.85s`)
  → Run (`pid 2630111`, `hello: logged in … as "Rusty"`) → the log pane streaming
  `x 2.4 y -21.2` → `x -11.0 y 98.1` → `x -24.3 y 217.3` → Stop
  (`clean exit (code 0)`), with a second tab watching Rusty through the game's own
  observer (`moving=true` at (-20.7,185), camera following) and the server's store
  confirming the 240 px move;
* the one real bug the live run found and what fixed it (the game page's
  `history.replaceState` erasing `?serve=` before the lazy mount reads it);
* two operational traps: the server refuses a second login for a character it still
  holds (`ex("ingame")` for 120 min on `last_sync`), so a driver that leaves its tab
  open poisons the next run — hence the driver's preflight; and `hello`'s
  toward-origin walk jails the character from some spawn points, which is #17's
  jail-safe travel work, not a panel bug.

Panel suite 37/37, full node suite 210/210. PLAN: #38 `[x]`.
Merge main into task/38-in-game-rust-panel
Some checks are pending
Code Quality / prettier (push) Waiting to run
Code Quality / prettier (pull_request) Waiting to run
c473be107c
main moved ahead while this branch was under review (T5-1 #17, `farm_basic`,
merged as PR #36 / main `19977d6`, plus its docs commit), so PR #39 reported
itself unmergeable. Brought the branch up to date rather than rebasing: no
history rewrite on a branch with an open PR.

One real conflict, in PROGRESS.md: both sides added a newest-first entry at the
top. Kept both, this one above theirs (12:00 vs 10:42), no content edited.
PLAN.md merged itself — main's `[x] #17` line and this branch's `[x] #38` line
are different lines.

`node --test node/test/` 210/210 and `cargo fmt --check` on the merged tree.
T6-2: refuse to Compile an empty editor (it truncates the crate before cargo runs)
Some checks are pending
Code Quality / prettier (push) Waiting to run
Code Quality / prettier (pull_request) Waiting to run
ab0fd65c48
`al serve`'s `POST /compile` writes whatever body it is given to
`<crate>/src/main.rs` and *then* builds, so an empty source is not "a build
that fails" — it is the player's crate at 0 bytes. Nothing refused it, and the
empty box is the ordinary shape of a bad load: `load_source` deliberately
leaves the editor alone when `/source` answers `ok:false` (a crate off this
endpoint's allow-list, an endpoint that moved under the tab). The recovery,
`src/main.rs.good`, is written on success only, so a fresh crate's first
Compile has nothing beside it to restore.

This is not hypothetical — a hand-rolled `curl -X POST /compile` during the
acceptance pass truncated `examples/hello/src/main.rs` to 0 bytes. Restored
from git; the file is clean (24116 bytes, `git diff` empty).

The endpoint's contract is T4-3's and already merged (#16), so the refusal
lands on the side that knows whether a human meant it: Compile and Reload
(its pre-restart compile reaches the same write) refuse an empty or
whitespace-only editor, claim the log pane to explain the mechanism, show the
Restore row — `{"restore": true}` is the honest way to replace a file — and
park the note on `disk-state`, the slot the 1.5 s poll never reclaims, so it is
still readable after the pane gives way to the ring. Loaded crates, and Reload
with a clean box, behave exactly as before.

Verified live on :8091 + :8212 (`/tmp/guard.js`, no game login): emptied box →
Compile → `Compile refused`, `al serve`'s own ring byte-identical across the
click (no build ran), `main.rs` still 24116 bytes → whitespace → same →
Restore → `[al] built`, editor refilled from `on_disk` → reload → Compile on
the loaded crate builds normally. Note for the next browser driver: Chromium
serves `/js/*.js?v=…` from memory across navigations in one target, so the
first pass exercised the *previous* client until `Network.setCacheDisabled`.

Test: "an empty editor is not compilable…" — panel suite 38/38, full
`node --test node/test/` 211/211. PROGRESS carries the hazard, the fix, what
is still open in the endpoint, and the two traps.
Author
Owner

Two more commits after review-relevant testing, both from re-running the flow by hand rather than from the fake DOM.

ab0fd65 — Compile no longer truncates the crate. al serve's POST /compile writes the body it is given to <crate>/src/main.rs before cargo runs, and an empty source is a legal thing to send a compiler, so nothing refused it. An empty editor is the ordinary shape of a bad load — load_source deliberately leaves the box alone when /source answers ok:false (crate off that endpoint's allow-list, endpoint moved under the tab) — so "something went wrong on the way in" plus one Compile equals the player's crate at 0 bytes. src/main.rs.good recovers it byte-exactly, but only if a build has succeeded once: the copy is written on success.

This happened to me during the pass below: a hand-rolled curl -X POST /compile truncated examples/hello/src/main.rs to 0 bytes. Restored from git (24116 bytes, git diff empty).

The refusal belongs on the side that knows whether a human meant it, because the endpoint's contract is T4-3's and already merged (#16): Compile — and Reload, whose pre-restart compile reaches the same write — refuses an empty or whitespace-only editor, claims the log pane to say why, shows the Restore row ({"restore": true} is the honest way to replace a file), and parks the note on disk-state, the slot the 1.5 s poll never reclaims. Loaded crates and a clean-box Reload behave exactly as before. Still open, deliberately: the endpoint itself will take the same body from curl or an IDE bridge and truncate; a whitespace-only source refusal in control.rs::compile (unless restore) is the companion fix, and nothing in crates/al-tool/tests/ covers an empty source either. Say the word and I'll do it here or in a follow-up issue.

Live check on :8091 + :8212 (/tmp/guard.js, no game login → no character conflict): emptied box → Compile → Compile refused, al serve's own ring byte-identical across the click (so no build ran), main.rs still 24116 bytes → whitespace box → same refusal → Restore → [al] built …/target/release/hello, editor refilled from on_disk → reload → Compile on the loaded crate builds normally.

c473be1 — main merged in, because the PR reported itself unmergeable once T5-1 landed. One conflict: both sides put a newest-first entry at the top of rust/PROGRESS.md; both kept, no content edited.

Panel suite 38/38, full node --test node/test/ 211/211, cargo fmt --check clean on the merged tree. Driver note for whoever re-runs the browser flow next: Chromium serves /js/*.js?v=… from memory across navigations in one target — the first pass exercised the previous client until Network.setCacheDisabled.

Two more commits after review-relevant testing, both from re-running the flow by hand rather than from the fake DOM. **`ab0fd65` — Compile no longer truncates the crate.** `al serve`'s `POST /compile` writes the body it is given to `<crate>/src/main.rs` *before* cargo runs, and an empty source is a legal thing to send a compiler, so nothing refused it. An empty editor is the ordinary shape of a bad load — `load_source` deliberately leaves the box alone when `/source` answers `ok:false` (crate off that endpoint's allow-list, endpoint moved under the tab) — so "something went wrong on the way in" plus one Compile equals the player's crate at 0 bytes. `src/main.rs.good` recovers it byte-exactly, but only if a build has succeeded once: the copy is written on success. This happened to me during the pass below: a hand-rolled `curl -X POST /compile` truncated `examples/hello/src/main.rs` to 0 bytes. Restored from git (24116 bytes, `git diff` empty). The refusal belongs on the side that knows whether a human meant it, because the endpoint's contract is T4-3's and already merged (#16): Compile — and Reload, whose pre-restart compile reaches the same write — refuses an empty or whitespace-only editor, claims the log pane to say why, shows the Restore row (`{"restore": true}` is the honest way to replace a file), and parks the note on `disk-state`, the slot the 1.5 s poll never reclaims. Loaded crates and a clean-box Reload behave exactly as before. **Still open, deliberately:** the endpoint itself will take the same body from curl or an IDE bridge and truncate; a whitespace-only `source` refusal in `control.rs::compile` (unless `restore`) is the companion fix, and nothing in `crates/al-tool/tests/` covers an empty source either. Say the word and I'll do it here or in a follow-up issue. Live check on :8091 + :8212 (`/tmp/guard.js`, no game login → no character conflict): emptied box → Compile → `Compile refused`, `al serve`'s own ring byte-identical across the click (so no build ran), `main.rs` still 24116 bytes → whitespace box → same refusal → Restore → `[al] built …/target/release/hello`, editor refilled from `on_disk` → reload → Compile on the loaded crate builds normally. **`c473be1` — main merged in**, because the PR reported itself unmergeable once T5-1 landed. One conflict: both sides put a newest-first entry at the top of `rust/PROGRESS.md`; both kept, no content edited. Panel suite **38/38**, full `node --test node/test/` **211/211**, `cargo fmt --check` clean on the merged tree. Driver note for whoever re-runs the browser flow next: Chromium serves `/js/*.js?v=…` from memory across navigations in one target — the first pass exercised the previous client until `Network.setCacheDisabled`.
gitignore: the two stray artifacts this branch kept stepping near
Some checks failed
Code Quality / prettier (push) Has been cancelled
Code Quality / prettier (pull_request) Has been cancelled
a4c3b8e590
`git status` on a worktree is where you find out what the ignore file actually
covers, and on this one it kept showing three things nobody should ever commit:

* `node_modules` and `node/node_modules` — here they are *symlinks* into the
  main checkout's install, and `/node_modules/` with a trailing slash matches a
  directory only, so the link itself was untracked-visible and one `git add -A`
  away from landing in history as a 40-character blob. The rule the file already
  uses for `/common` and `/secretsandconfig` (slash and no-slash) applied here.
* `rust/examples/hello/src/main.rs.good` — `al serve` writes it beside the
  source on every successful build (`GOOD_SUFFIX`, control.rs:139), so running
  the panel leaves an untracked file next to a tracked one *in the tree the
  panel is compiling*. Never tracked, and it must stay that way: it is a
  runtime backup, and the one this session needed for recovery was a different
  crate's copy of the same idea.

`git status --short` is now empty on a dirty branch.
sleepy merged commit 27931c55f2 into main 2026-09-24 13:45:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sleepy/adventureland_mongodb!39
No description provided.