Run it locally
The shortest path from the repository root is:
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:
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:
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:
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:
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://orhttps://origin. Plain HTTP is accepted only for the exact loopback hostslocalhost, IPv4 addresses in127.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/liveandGET /health/ready. A200readiness response withapplication_opened: falsemeans 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/stopand 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
ControlTracksubmission; - 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
- Select Server path.
- Choose a root alias advertised by the Server.
- Enter a relative POSIX path below that root, for example
server/08_camera_rgb_and_png/run.yaml. - Keep Running, or deliberately choose Prepared or Open.
- 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
- Select Upload one config.
- Choose one
.yaml,.yml,.json, or.tomlfile. - 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:
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:
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:
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.