Run it locally

The shortest path from the repository root is:

bash
conda activate your-fastsim-environment
./demo/server/web_client/start_all.sh

Then open http://127.0.0.1:8090. The script starts FastSim Server and the independent static frontend together; Ctrl+C stops both. To keep their logs separate or restart one side independently, run start_server.sh and start_web.sh in two terminals.

After connecting, launch the bundled camera Run from the Server path tab:

  • Root: demos
  • Configuration path: server/web_client/run.yaml
  • Target state: running

The Run contains a controllable two-joint arm, a red cube, and two 1280 x 720 RGB scene cameras named sensors.overview and sensors.side. Their opposing viewpoints both include the robot and cube. The camera menu discovers them from the immutable Run Scenario, selects all by default, and can show either one or both.

The defaults are port 8010 for the backend and 8090 for the frontend. Override them without editing the scripts, for example:

bash
FASTSIM_SERVER_PORT=8011 FASTSIM_WEB_PORT=8091 \
  ./demo/server/web_client/start_all.sh

The scripts never assume a Conda environment name. They use fastsim-server from the currently active environment. If you deliberately want the script to enter a named Conda environment without activating it first, opt in explicitly:

bash
FASTSIM_CONDA_ENV=your-fastsim-environment \
  ./demo/server/web_client/start_all.sh

You need two independent terminals. The first terminal owns the simulation; the second only serves HTML, CSS, and JavaScript.

Terminal 1: start FastSim Server

Start an empty persistent Server and authorize one directory. The alias demos is public; the absolute directory remains Server-side:

bash
fastsim-server \
  --launch-root demos=/absolute/path/to/FastSim/demo \
  --output-root /absolute/path/to/FastSim/outputs \
  --planning-reads \
  --host 127.0.0.1 \
  --port 8000 \
  --browser-origin http://127.0.0.1:8080

--browser-origin is the exact origin that may call this Server from a browser. The scheme, host, and port must match the frontend URL exactly. Do not use * for a control API.

You may repeat --launch-root alias=/absolute/directory to expose more controlled roots. An existing positional startup remains supported, but it occupies the only Run slot until that Run is terminally closed.

--output-root is optional. When present, the Server gives every Run-slot generation its own private output directory and enables the Committed artifacts panel. Create the directory before starting the Server. It is an output destination, not a launch or asset root.

--planning-reads publishes FastSim's read-only scene, frame, and planning-geometry HTTP services. The Web Client uses the lightweight scene catalog and state services to populate its Scene inspector. The flag does not start a planner, grant scene mutation, or change control authority. It is opt-in on the general Server because geometry resources can be large and may reveal asset structure that a deployment does not intend to expose. The bundled loopback demo enables it deliberately; omit it for a deployment that does not need those reads. Camera image access is a separate capability and still requires a camera entity in the Run configuration.

Terminal 2: serve the static frontend

Python's static server is a convenient example, not a runtime dependency:

bash
python -m http.server 8080 --bind 127.0.0.1 --directory demo/server/web_client/static

Any equivalent static host works. Open http://127.0.0.1:8080, keep the default Server URL http://127.0.0.1:8000, and press Connect.

Keep this command running while the page is open. Pressing Ctrl+C stops the static site. If port 8080 is occupied, choose another port, such as 8081, and use that same origin in both --browser-origin http://127.0.0.1:8081 and the browser URL.

These commands are intentionally independent. Stopping the static server does not stop FastSim Server, and stopping FastSim Server does not affect how the frontend is hosted.

Workbench layout

The client uses a fixed engineering-workbench layout rather than a scrolling dashboard. The selected real scene-camera images are the primary viewport in the center and are always scaled to fit without cropping. A dropdown provides All cameras and one checkbox per discovered RGB camera; only checked cameras are requested, and multiple frames use an adaptive grid. Session, launch, lifecycle, and generic target controls are kept in the left rail. Scene state, plugins, operations, and committed outputs share the right inspector. The resizable bottom dock contains the event console, latest public state, per-joint command-versus-state curves, and process-performance charts. Opening a chart tab activates only the data stream that chart needs; collapsing or leaving it closes that demand.

Rearranging these panels does not change the client/server boundary. Every displayed value and every control action still uses the public FastSim Server HTTP API.

Connect to a Server

The Connection panel contains:

  • FastSim Server URL — an absolute http:// or https:// origin. Plain HTTP is accepted only for the exact loopback hosts localhost, IPv4 addresses in 127.0.0.0/8 (including shorthand normalized by the URL parser), and [::1]. Every non-loopback Server must use HTTPS. URL validation runs before the page captures a bearer token or sends a request. The validated origin may be saved in browser local storage for convenience.
  • Bearer token — optional authentication for a secured Server. It is held only in module memory, cleared from the input after connection, cleared on disconnect, and never written to a URL, local storage, logs, or deployment config.
  • Live and Ready — separate results from GET /health/live and GET /health/ready. A 200 readiness response with application_opened: false means the Server is accepting work but no Run is open; the page displays Run cold while the connection itself remains Connected; it does not claim that a Run is ready.

After the health check, the page reads GET /api/v1/launch. It does not guess which roots, formats, or upload size the Server permits.

The page sends requests with omitted browser credentials, no referrer, and no redirect following. A connection failure remains visible; the page does not replace missing data with simulated telemetry. Every HTTP request also has a finite transport deadline: health and camera requests use 10 seconds, ordinary reads use 40 seconds, and asynchronous lifecycle/control admission uses 3605 seconds so the browser does not shorten the Server's normal application-open or control budget. A 202 Accepted operation belongs to the Run and is not cancelled when its originating HTTP request or browser connection later ends.

Disconnect is not simulation shutdown

Disconnect only stops this tab's requests and clears its in-memory token. It does not call stop, close, or cancel accepted operations. The remote simulation keeps its current state. Disconnect also aborts every outstanding browser request, so a Server or network that never responds cannot prevent a later connection from starting a fresh polling generation.

Simulation lifecycle buttons are explicit API commands:

  • Stop Run calls POST /api/v1/run/stop and drains active control;
  • Terminal close calls POST /api/v1/run/close, completes plugin post-processing, and releases application resources;
  • closing the page or losing the network calls neither endpoint.

This distinction matters for remote operators: a network disconnect must never silently destroy a Run.

Available control and observation

After connection, the client uses FastSim Server routes directly:

  • launch-source discovery plus asynchronous Run launch from an authorized path or a bounded text upload;
  • lifecycle: prepare, start, pause, resume, step, reset, stop, and close;
  • Run identity, coherent state, simulation tick, observed FPS, and real-time factor;
  • scene entity poses plus robot and general-articulation joint state;
  • plugin host and per-instance status, recent events, and bounded failure details;
  • committed Run outputs with safe preview or download for FSR, video, structured data, images, logs, and reports;
  • asynchronous operation history and cancellation;
  • portable control targets and generic ControlTrack submission;
  • a per-joint target editor with one named numeric row per axis, plus live comparison of authoritative backend-applied commands and measured joint state;
  • demand-driven FastSim Server process charts for simulation FPS, CPU, resident memory, and process-attributed GPU memory when NVML is available;
  • automatic RGB-camera discovery plus selected-camera JPEG (when advertised) or PNG frames from sensors.overview, sensors.side, or another Scenario camera entity;
  • a bounded cursor-based Server log stream in the bottom console, including cold and failed Run states.

Control is descriptor-driven. Targets, axes, units, command spaces, controllers, and current axis values come from /api/v1/control/targets and /api/v1/control/state; the frontend does not assume that every articulation is a robot. Camera frames are checked against the 1280×720 dimension, Run identity, entity, generation, tick, and media-type response headers. A new frame is decoded before its previous object URL is replaced, so a delayed or invalid response cannot flash black.

Trajectory time is optional. Leave it empty to apply the target immediately and hold it while the physical drive converges according to its gains and limits. Enter a time to ramp the requested values from the latest observed state to the target over that many simulation seconds. The time shapes the reference command; it does not by itself guarantee zero physical tracking error at the endpoint. The final target is held after either form of submission.

Every Run-dependent lifecycle and control write carries X-FastSim-Slot-Generation from the latest active launch status. The Server rejects a write with 409 stale_slot_generation if another client has already replaced that Run, preventing a delayed command for generation N from reaching generation N+1. Launch requests and operation queries or cancellation are supervisor-level calls and do not carry this header.

After transport discovery, one WebSocket session carries Run, operation, log, state, scene, control, and plugin updates. Selected scene cameras use WebRTC when the Server advertises it; compatibility snapshots use independent single-flight requests. The client does not keep periodic GET loops for data already supplied by the realtime session. Before realtime is available, the fallback lanes remain bounded and single-flight. An idle or unprepared Run never requests Run data-plane endpoints; those reads begin only in prepared, running, or paused.

Every active lifecycle.* operation closes a runtime read gate. The client stops and aborts in-flight Run status, simulation, plugin, control, scene, and camera polls; only health, launch-slot status, and operation status remain active. This applies both to a lifecycle command submitted by this page and to one discovered from another client. After a terminal operation, the client refreshes /api/v1/launch; it reads /api/v1/run once only when the slot remains active, then reopens the gate and restores the appropriate polling lanes. Submission errors restore the gate in finally, while disconnect and backend switches invalidate the old gate generation so a stale completion cannot restart polling on a new connection. Polling also pauses while the tab is hidden, and camera polling can be disabled independently.

Plugin diagnostics and Run outputs

The plugin panel reads the public host snapshot, exact status for each advertised instance, and the sequence-cursor event endpoint. All values are rendered as text; plugin payloads are never interpreted as HTML. Refresh performs an immediate read without changing the simulation lifecycle.

The output panel is enabled when the Server was started with --output-root. It lists only non-empty, allowlisted regular files from the current Run-slot generation: .fsr, .mp4, .webm, .json, .jsonl, .csv, .png, .jpg, .jpeg, .txt, .log, and .md. Hidden, temporary, unknown, symbolic-link, FIFO, and other special files do not appear. Text, data, and report previews are limited to 1 MiB; image and video previews use the lower of 64 MiB and the Server's advertised limit. FSR is downloaded and opened with fsr_viewer rather than parsed by this control plane.

The browser refreshes the list only when a Run becomes active or terminally returns to idle, or when the operator presses Refresh. There is no per-tick filesystem scan. Small downloads use a bounded Blob. Files above 64 MiB use 16 MiB HTTP Range chunks and the browser File System Access API, so the complete recording is not held in memory. Browsers without that API can still use the same authenticated ranged HTTP endpoint from an external download client.

Launch a Run

The Launch a simulation panel offers two sources.

Option A: authorized server path

  1. Select Server path.
  2. Choose a root alias advertised by the Server.
  3. Enter a relative POSIX path below that root, for example server/08_camera_rgb_and_png/run.yaml.
  4. Keep Running, or deliberately choose Prepared or Open.
  5. Press Launch Run.

The browser sends POST /api/v1/run/launch with an Idempotency-Key, then watches the returned lifecycle.launch operation. Absolute paths, Windows separators, empty segments, . and .. are rejected. The Server independently resolves the path below the selected root and rejects symlinks or escapes; browser validation is only early feedback, never the security boundary.

--launch-root authorizes which configuration file a client may select; it is not a sandbox around the configuration's complete transitive asset graph. The selected file is compiled as a trusted FastSim Project and may resolve registries, installed packages, declared trusted_roots, and digest-pinned HTTPS resources. Operators must therefore expose only Project trees they control and that untrusted users cannot write.

Option B: upload one configuration

  1. Select Upload one config.
  2. Choose one .yaml, .yml, .json, or .toml file.
  3. Press Launch Run.

The accepted suffixes and maximum UTF-8 byte count come from the Server. The browser checks the file size before reading it, decodes strict UTF-8, rejects NUL bytes, and sends {filename, content} in the launch JSON. It does not upload directories, archives, executables, project metadata, or lock files. A standalone uploaded Run must therefore resolve through installed catalogs or resources allowed by the isolated Server compiler; use an authorized server path when the Run depends on sibling local files.

Launch state is shown as Idle, Launching, Active, Closing, or Error. Run, plugin, scene, control, and camera polling is stopped while launch work is active; health, launch status, and operation progress remain available. On success the normal lifecycle and data panels attach to the new Run.

Browser data boundary

Only the normalized Server URL is stored in local storage. The bearer token, selected server path, uploaded filename, uploaded content, and launch request are not persisted. The token and selected inputs are cleared on disconnect; the upload input is also cleared after every submission attempt. Configuration content is never copied into the event log or diagnostic state panel.

Bearer authentication

Create a private token file and start the Server with the same browser-origin rule:

bash
fastsim-server \
  --launch-root demos=/absolute/path/to/FastSim/demo \
  --host 127.0.0.1 \
  --port 8000 \
  --browser-origin http://127.0.0.1:8080 \
  --token-file ./fastsim-server.token

Paste the file's token into the password field before connecting. Do not put the token in a query string or commit the token file. The Web Client enforces the same RFC6750 b64token grammar as FastSim Server: 32 to 4096 ASCII characters, a body of A-Z, a-z, 0-9, -, ., _, ~, +, or /, followed by optional = padding only at the end. Unicode, NUL/control characters, whitespace, internal =, and other punctuation are rejected. An empty field means that authentication is not configured.

Remote deployment

For a non-loopback Server, enable TLS and bearer authentication. The client rejects every non-loopback http:// URL even when the frontend itself is served over HTTP:

bash
fastsim-server \
  --launch-root runs=/srv/fastsim \
  --host simulation.example.com \
  --port 8443 \
  --browser-origin https://control.example.com \
  --token-file /run/secrets/fastsim-server.token \
  --tls-cert-file /run/secrets/server.crt \
  --tls-key-file /run/secrets/server.key

Deploy the contents of static/ at https://control.example.com and enter the Server origin https://simulation.example.com:8443. The hostname passed to --host must resolve to an address owned by that machine, and the browser-visible Server authority must use that same hostname and --port; the TLS certificate must cover it. This version does not claim transparent support for NAT, a reverse proxy, or external TLS termination. An HTTPS frontend deliberately refuses an HTTP backend because browsers block mixed active content.

Configure the static host with a production Content Security Policy whose connect-src names only the intended Server origin. The included portable HTML has a broader http: https: connection policy because the Server URL is selected at runtime; deployment headers should narrow it. The static host must also send both Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY to prevent clickjacking. A CSP delivered through an HTML <meta> element cannot enforce frame-ancestors. As defense in depth, the application also refuses to bootstrap when window.top !== window.self; an embedded page sends no Server request and shows an explicit blocked state. FastSim Server must return its exact CORS origin and the exposed FastSim identity/range headers. It must never allow a credentialed wildcard origin.

Packaging and performance

The source distribution and FastSim wheel include this static application under share/fastsim/demo/server/web_client/static. Those files remain ordinary static assets: serving or modifying them does not require importing FastSim or installing a simulator in the frontend host.

The Web Client is pay-for-play. When no browser is connected, FastSim Core has no Web polling cost. With a page connected, lower state and camera rates, select fewer cameras, or disable camera polling to reduce HTTP serialization and image encoding. A single selected camera still issues exactly one frame request per single-flight cycle.

Test the frontend

The tests require Node.js but no Python package and no simulator:

bash
node --test demo/server/web_client/tests/test_state_parsing.mjs

They cover URL validation, direct endpoint construction, memory-only bearer handling, launch discovery and its five UI states, exact launch payloads, root/path traversal rejection, strict UTF-8 upload and advertised byte limits, forced fetch security options, HTTP error propagation, state parsing, generic Track construction, PNG validation, polling single-flight/generation behavior, idle/cold-Run suppression, and lifecycle data-plane gating and cancellation. They also validate artifact generation fences, safe paths and bounded Range chunks, catalog descriptors, plugin diagnostics, and the required output-panel DOM surface.

Return to the Server demo index.