> ## Documentation Index
> Fetch the complete documentation index at: https://docs.almond.bot/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Control Panel

> Drive Axol from a browser — connect the Axol host and robot, assign the cameras, then run any operation without touching the terminal.

The **web control panel** is a browser UI for driving Axol without the terminal. It wraps the core operations — **teleoperation**, **gravity compensation**, **data collection**, **dataset replay**, and **policy inference** — behind connect/run buttons, with live status for the robot motors and the camera setup.

It's served by [`axol serve`](/cli/serve) and runs each operation **in-process** inside that server, so it's a drop-in alternative to the per-command CLI workflow.

## Start the server

With the [one-command install](/installation), the server is already running on the robot machine (the computer wired to the robot, with the ZED cameras attached) as a root systemd service that starts at boot and restarts automatically — open [axol.almond.bot](https://axol.almond.bot) and connect to the host.

On a development install, start it yourself:

```bash theme={null}
axol serve
```

This prints a local and a LAN URL (HTTPS by default):

```
Axol control panel:
  Local : https://localhost:8001
  LAN   : https://192.168.1.50:8001
```

Open the **Local** URL on the host, or the **LAN** URL from another machine on the same network (accept the self-signed certificate once). The control panel lives at `/control`. The layout is **responsive**, so you can run it from a phone or tablet on the same network, not just a desktop.

<Note>
  For a development install, the local UI must be built once (`cd web && npm install`, then `npm run build` for the `axol-vr-client` and `app` workspaces) — or skip the build and use the hosted UI at [axol.almond.bot](https://axol.almond.bot), which connects to your `axol serve` directly. See [Development install](/advanced/development-install#build-the-web-ui).
</Note>

<Tip>
  The nav bar's **Quickstart** button — in the **☰** menu on a phone — has copyable commands for installing the CLI and launching the server.
</Tip>

## Connections

The top of the panel is a pair of tiles — **Axol Host** and **Axol** — each with a colored status dot. (Cameras and the Quest USB link are configured in the [Settings](#settings) tabs right below.)

### Axol Host

The **Axol Host** is the machine running `axol serve` — the computer wired to the robot, with the ZED cameras attached. Press **Connect** on the tile to open the host dialog and enter its address (just the IP — port `8001` is assumed); if the host can't be reached the attempt gives up after a couple of seconds with a *can't reach the host* message instead of leaving the tile stuck on *Connecting…*. When connected, the tile shows the host's name, its installed release version (e.g. `axol v0.1.2`), and a green dot. Teleop and gravity comp need only this connection.

A connected tile carries three buttons:

* **Restart** reboots the robot host (`shutdown -r now`) behind a confirmation dialog. The panel loses its connection while the host reboots — reconnect once it's back up, typically under a minute.
* **Shut down** powers the host off (`shutdown -h now`), also behind a confirmation. Turning the robot back on requires physical access to it.
* **Disconnect** (the unplug button) drops **this browser only** — a client-side disconnect that leaves the server, the robot link, and any running operation untouched, so another operator driving the same host isn't affected.

Restart and shut down are **refused while any operation or session is running** — cutting power mid-run would drop the arms — so stop what's in flight first; the confirm button stays disabled until the host is idle. (The one-command install runs as root and powers off directly; a development `axol serve` escalates with `sudo -n`, so a host with no passwordless sudo reports that it couldn't and stays up.)

#### Updates

When a newer [release](https://github.com/almond-bot/axol/releases) than the installed one exists, a banner appears above the tiles showing the version jump (e.g. `v0.1.2 → v0.1.3`) with a link to the release notes. Press **Update** to apply it: the host reinstalls at the new release, re-provisions its system dependencies, and restarts once idle — the button is disabled while an operation is running, since applying an update restarts the server. Nothing updates without that button press.

The same banner area also warns when the **control panel UI and the host backend are running different code** — the web bundle bakes in the git commit and release version it was built from, the host reports its own via `/api/info`, and a mismatch raises the warning (a released host is compared by version, a dev checkout by commit). This catches a stale hosted tab, or an unbuilt local `web/app/dist`, silently driving a newer or older backend; clear it by updating the host to the matching release or rebuilding the local UI.

### Axol

The panel connects the robot **automatically** once the host comes online (one attempt per host session, so a manual disconnect isn't immediately undone); the **Connect** button is the manual fallback / reconnect. Bringing up the robot:

* Enables the CAN interfaces — running [`can.setup`](/cli/can-setup) automatically the first time when the Axol Hub adapter is used and the host hasn't been configured yet. A non-hub SocketCAN adapter (chosen per arm in the **Robot** settings tab, or via the diagnostics dashboard's [CAN adapter picker](/guides/diagnostics-dashboard#choosing-can-adapters)) is brought up directly instead.
* Pings all 16 motors once per second while idle, so the tile reflects live motor health as two rows of squares (grouped **L** / **R**): the top row is **CAN reachability**, the bottom row the motor's **error status**. Hover any square for the details (status, temperature).

**Motor faults block everything.** The *N/16 motors* count only includes motors that are both reachable **and** error-free. While any motor is unreachable or reporting an error (over-temperature, over-current, stall, encoder error, …), every hardware operation's **Start** is blocked — both in the panel (a per-fault entry under *Before you can start*) and by the server itself — since driving through a faulted motor risks the arm. Sim teleop stays available.

The server runs as root under the one-command install's systemd service, so CAN bring-up needs no password. While a task is running the 1 Hz polling pauses and the robot is handed to the task.

<Warning>
  Closing the browser tab — or disconnecting the **host** — does **not** stop anything: the robot link and any running operation live in the `serve` process and keep running. Use **Stop**, **Robot Disconnect**, or stop `axol serve` to tear them down.
</Warning>

## Settings

Almost nothing you configure is actually per-run — camera assignment, arm stiffness, teleop rate, recording fps and codec, the inference server address — so it all lives in one **Settings** card, tabbed directly on the page below the connection tiles:

| Tab             | What's in it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Cameras**     | The camera setup described [below](#cameras).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| **Quest USB**   | The optional [Quest-over-USB](#quest-usb) controller-pose link.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **Robot**       | Left/right arm stiffness sliders, CAN channels, gripper torque limit and max speed (both grippers), gravity-comp damping and rate, the **contact-stop** thresholds (the always-on reset guard plus the opt-in teleop and policy watchdogs — each a switch that arms it at a suggested value), and the **Powered cart** toggle (drives the [powered cart](/guides/vr-interface#powered-cart) from the headset thumbsticks in teleop and data collection).                                                                                                                                                                                |
| **Teleop & VR** | Teleop control rate, position and rotation multipliers, and the **hold-to-engage** grip scheme.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **Rest pose**   | An **interactive 3D pose editor**: the robot model (loaded from the host's URDF) rendered live, with a slider per joint for each arm. The pose you set here is where the arms start from — and return to — in teleop and data collection.                                                                                                                                                                                                                                                                                                                                                                                               |
| **Kinematics**  | The IK solver's main cost weights (position / orientation / elbow / rest) and the per-step joint delta cap.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **Recording**   | Dataset fps, video codec, dataset root, push-to-hub, Rerun viewer address. Setting the recording fps above the cameras' default 60 automatically raises the recording cameras' capture rate to match (the ZED X only supports rates above 60 at SVGA).                                                                                                                                                                                                                                                                                                                                                                                  |
| **Inference**   | Device, remote inference server host/port, episode length, chunking and aggregation.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **System**      | Log level.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| **Advanced**    | Every remaining config field, organized by subsystem (*Axol*, *Teleop*, *Kinematics*, *Cart*, *VR server*, *LeRobot robot*) with a search box across all of it. There is **one value per knob, applied to every operation** — the host translates it to each operation's config path, so there are never different per-task copies of the same setting. Runs of identical sub-configs — the per-joint gains — render as **one table per arm** (a row per joint, a column per parameter), and vector values like a link's CoM are entered as separate **x / y / z** fields, never raw arrays. Nothing is hidden behind collapsed groups. |

Values are stored **on the Axol host** (`~/.almond/settings.json`) — not in your browser — so they're shared by every operation and every operator device, and they're folded into each operation start automatically (a per-run field always wins over a shared one). Edits stage locally until you press **Save** (or **Discard**). Unset fields show the config's own default as a placeholder; edited ones get a reset button. **Export** / **Import** in the card footer move the whole settings file as JSON for backup or copying between robots.

### Cameras

Data collection and policy inference also need the ZED cameras attached to the host — they're assigned in the **Cameras** settings tab.

The Cameras tab **detects the cameras** plugged into the host and shows one card per camera: its serial, its type (**stereo** ZED X / **mono** ZED-X One — detected automatically, never flagged manually), the model, and a **live preview frame**, so you can tell at a glance which physical camera each serial belongs to. Assign each one to its slot (overhead / left arm / right arm) — an assigned slot repeats the camera's preview thumbnail next to its name. Press **Refresh** to re-query the daemon's device list and grab fresh preview frames, or **Restart daemon** to restart the ZED X daemon so it re-scans the GMSL links — needed to pick up a camera plugged in or unplugged since boot. (Previews are unavailable while an operation is running, since it owns the cameras.)

The Cameras tab is the single source of truth for all camera settings — these fields appear nowhere else. It controls two independent branches:

* **Stream** — what is relayed to the VR headset over WebRTC (used by teleop and collect-data). Run policy relays no video, so it ignores this branch entirely.
* **Record** — what is written to the dataset during data collection (downscaled on the GPU's VIC, independently of the stream). Because **run policy** streams no headset video, it also captures its policy observations at this resolution — the size the dataset was recorded at and the policy was trained on, rather than grabbing a larger headset frame it would only throw away.

Each branch has its own **resolution** dropdown (`SVGA` / `HD1080` / `HD1200`, or **Off** to disable that branch globally) — so the headset can see a high-resolution feed while the dataset stores a lighter `SVGA` one, or vice versa. Each assigned camera then has independent **Stream** and **Record** controls: a single on/off for a mono camera, or per-eye **L** / **R** checkboxes for a stereo one (none checked = off for that branch). So you can stream a camera without recording it (or the reverse), choose which mono cameras feed the headset vs. the dataset, stream both eyes of a stereo camera for depth while recording only one, or turn either branch off entirely.

A stereo **overhead** captures both eyes from one grab; with both eyes selected it's rendered per-lens in the headset (streamed as one side-by-side feed) and recorded as the two observation keys `overhead_left` / `overhead_right`. A single eye — or a stereo **wrist** — is exposed under the plain slot name, so it costs and reads exactly like a mono camera.

The assignments and stream/record selection persist **on the host** with the rest of the [shared settings](#settings), so every operation and every operator device sees the same camera setup. Teleop streams whichever cameras have streaming enabled; **collect data and run policy need at least one camera with recording enabled** (they error early otherwise, since a policy needs the cameras it was trained on). Starting **collect data** or **run policy** also re-verifies that every assigned camera is physically present and **blocks the start with an alert** naming the missing serial if one isn't — reconnect it, then **Refresh** (or **Restart daemon**) in the Cameras tab. Teleop streams whatever is configured and is never blocked by this check.

### Quest USB

The **Quest USB** tab manages the optional [Quest-over-USB](/guides/quest-over-usb) link, which streams controller poses over a USB cable instead of Wi-Fi for lower latency. When a headset is plugged into the host and authorized (accept the **Allow USB debugging?** prompt the first time), the host sets up the `adb reverse` tunnel **automatically** and turns green (*Controller over USB*); the **Connect** button is a manual fallback / reconnect. Camera video keeps using the LAN — USB carries poses only. The headset sends every pose over both the cable and Wi-Fi, so a dropped cable **fails over to Wi-Fi instantly** with no reconnect, and the cable reclaims the link when it returns.

The tab also carries the [headless](/guides/quest-headless) **Keep awake** / **Restore** buttons: with the headset plugged in and authorized, **Keep awake** disables its proximity sensor over adb so the VR session keeps running with the headset set down, and **Restore** brings normal sleep behavior back (a headset reboot also resets it).

## Operations

Pick an operation from the selector. Each panel asks only for its **per-run inputs** — the dataset repo id and task for collect data, the policy path/type for run policy, the episode to replay — plus **Start**/**Stop**. Everything reusable lives in [Settings](#settings). Hover the **ⓘ** next to a field for its description, pulled from the CLI's own help.

| Operation                | Needs                   | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Teleoperation**        | Axol, sim, or cart-only | Drive the arms from the VR headset. Toggle **sim** to use the browser visualizer with no hardware. If the robot has the [powered cart](/guides/vr-interface#powered-cart), the **Powered cart** setting (Robot tab) also lets the thumbsticks drive it; tick **Cart only** to drive *just* the cart with the arms untouched — no Axol connection or motor check needed. If camera slots are assigned, the feeds are relayed to the headset automatically — see [Camera views](/guides/vr-interface#camera-views) — and mirrored in the panel's [operator view](#operator-view). |
| **Gravity compensation** | Axol                    | Hold the arms weightless for hand-guiding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **Waypoints**            | Axol or sim             | Teach-and-repeat: hand-guide the arms and press **Record waypoint** for each pose, then **Play** to replay the path as straight-line moves. **Undo**, **Clear**, and a gripper toggle are there while teaching; **Stop playback** interrupts a run. Toggle **sim** to preview a saved path in the browser visualizer. See [`waypoints`](/cli/waypoints).                                                                                                                                                                                                                        |
| **Collect data**         | Axol + Cameras          | Record VR teleop episodes to a LeRobot dataset with the cameras. The **repo id** field is a picker offering the datasets already on the host (with episode counts) so you can resume one, or type a new id. The feeds are mirrored in the panel's [operator view](#operator-view) and the [episode controls](#episode-controls) can start and end takes, so a session can be driven with the headset off.                                                                                                                                                                       |
| **Replay dataset**       | Axol                    | Replay a recorded [`collect-data`](/cli/replay-dataset) episode on the arms, then return to rest. No cameras needed. The **repo id** field is a picker offering the datasets on the host (with episode counts), and also accepts a bare id or a filesystem path. Toggle **loop** to replay continuously until **Stop**, and **interpolate** to smooth playback by upsampling the recorded actions to \~120 Hz commands.                                                                                                                                                         |
| **Run policy**           | Axol + Cameras          | Run a trained policy via LeRobot async inference — locally or on a remote [inference server](/cli/inference-server).                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

### Before you can start

The **Start** button stays disabled until everything an operation needs is ready, and a "Before you can start" list spells out what's missing — e.g. *Connect Axol*, *Fix motor fault: R elbow — over temperature*, *Assign at least one camera serial in the Cameras settings tab*, or *Set \<required field>*.

Per-run values (repo ids, task names, …) are remembered by your browser so they reappear on reload; everything else persists on the host with the shared settings.

### Running, logs, and stopping

Starting an operation streams its logs into the console at the bottom of the page and shows a status badge. The console captures not just the operation's Python output but also **native-library and subprocess output** — the ZED SDK, GStreamer, and CUDA, plus the spawned video-relay and recorder processes — so a camera or ZED failure surfaces its actual reason in the UI instead of a bare "exited". The console **auto-follows** new output only while you're scrolled to the bottom; scroll up to read earlier lines and it stops yanking you back down. While you're scrolled up, a **floating arrow button** in the lower-right of the log area jumps you back to the latest line and re-engages auto-follow. The **download** button in the Logs header saves everything currently in the console to a timestamped `.log` file (disabled when there's no output) — handy for attaching to a bug report. Press **Stop** to end it — the request returns immediately and the badge and button flip to **Stopping…** while a background watchdog tears the op down (force-killing its child subprocesses after a short grace if a save or startup is still blocking). The panel polls until the op is truly gone, then the robot link returns to idle 1 Hz polling. **Robot Disconnect** likewise stops the running op and waits for it to finish tearing down before releasing the robot link; the host **Disconnect**, by contrast, is a client-side drop that leaves any running op in place server-side.

## Operator view

While **teleoperation** or **collect data** streams camera video to the headset (camera slots assigned), the control panel mirrors those **live camera feeds**, so you can watch the robot's point of view on the dashboard and not only in VR. The panel joins the running operation's video stream as one more **view-only** client and shows the same tracks the headset receives — no extra work for the robot, and the [Cameras settings tab](#cameras) still decides what streams. A side-by-side stereo overhead is shown cropped to its left eye. (A **sim** or **Cart only** teleop run shows no mirrored feeds — neither relays headset video.)

The **fullscreen** toggle on the camera-feeds header expands the feeds to a full-screen operator display that fits without scrolling; **Esc** or **Exit fullscreen** leaves. If the feeds never connect, the panel offers a one-tap button to **authorize** the stream server's self-signed certificate — the same one-time step the [VR app](/guides/vr-interface) uses.

## Episode controls

While **Run policy** or **Collect data** is live, an **Episode control** box appears in the operation card, led by a colored **phase badge** (Ready, Recording, Saving, Resetting, …) with the episode number and saved count. The box is driven by the host, so it shows the same phase on **every** connected computer, not just the tab that started the run. To end the run entirely, use **Stop** in the card header.

For **Run policy**:

| Button             | Effect                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Start episode**  | Begin the next rollout (advances the between-episode "reset the scene" gate).                                                                                                                                                                                                                                                                                                 |
| **Save**           | Save the current episode and return to the reset gate — press **Start episode** for the next one.                                                                                                                                                                                                                                                                             |
| **Discard**        | Drop the current episode. The arms go **limp** for hand-repositioning — a failed rollout usually leaves them somewhere they shouldn't be, so they don't drive home from that pose — then **Return to rest** before the next take.                                                                                                                                             |
| **Return to rest** | Send the limp arms home, replanning the collision-aware return from where they are. Appears whenever the arms are left limp: after a **Discard** (phase **Limp**), when the guarded between-episode return meets resistance, or (with the opt-in policy contact stop enabled) when a rollout hits something and is aborted (phase **Contact**). Clear the arms by hand first. |

These mirror the `s` / `r` keystrokes and the Enter prompts the [`run-policy`](/cli/run-policy) CLI reads from stdin.

For **Collect data**, the buttons do exactly what the VR controller does, so a session can run with the headset off — pair them with the mirrored [camera feeds](#operator-view) above:

| Button              | Effect                                                                                                                                                                                                                                                                                 |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Start recording** | Begin the next take after a 3-second countdown, mirroring the in-headset one so you can pick the controllers back up. Clicking again cancels.                                                                                                                                          |
| **Save episode**    | End the take and save it, like confirming **A** in the headset. Click twice to confirm — the panel's stand-in for the controller's double-press.                                                                                                                                       |
| **Discard episode** | End the take and drop it to re-record, like confirming **X**. Also click-to-confirm.                                                                                                                                                                                                   |
| **Return to rest**  | During a **Contact** hold — the guarded return met resistance, or (with the opt-in teleop contact stop enabled) a take hit something and was discarded — the arms are limp. Clear them by hand, then press this (or **X** on the controller) to replan the return from where they are. |

The VR controller stays live throughout — the panel is a parallel input, not a replacement, and whichever arrives first ends the take. Anything the operator arms on the controller (the **Save episode?** / **Discard episode?** confirmation and the record countdown) is mirrored into the panel as a popup, so a headset worn off the face is still followable.

## Next steps

<CardGroup cols={2}>
  <Card title="Diagnostics dashboard" icon="chart-line" href="/guides/diagnostics-dashboard">
    Live motor telemetry, health tiles, and diagnostics scripts — the sibling `/diagnostics` page.
  </Card>

  <Card title="serve reference" icon="terminal" href="/cli/serve">
    Flags, TLS, and what the server exposes.
  </Card>

  <Card title="VR interface" icon="vr-cardboard" href="/guides/vr-interface">
    The in-repo WebXR app that drives teleop and recording.
  </Card>
</CardGroup>
