Skip to content

Adds out of band control via adb to simplify wearing headset on neck scenario#312

Draft
yanziz-nvidia wants to merge 1 commit intomainfrom
yanziz/operator-dashboard
Draft

Adds out of band control via adb to simplify wearing headset on neck scenario#312
yanziz-nvidia wants to merge 1 commit intomainfrom
yanziz/operator-dashboard

Conversation

@yanziz-nvidia
Copy link
Copy Markdown
Contributor

@yanziz-nvidia yanziz-nvidia commented Mar 24, 2026

Adds an operator hub to enable controls on desktop. Added path /teleop/ for the control UI.

Summary by CodeRabbit

  • New Features

    • Operator dashboard UI and teleop hub for remote headset control, session commands, and metrics
    • Headset control channel with registration, reconnects, session commands, and periodic metrics reporting
    • CloudXR UI: apply incoming stream configuration to connection fields and persist selections
    • New CLI flags to run only the proxy and enable the hub
  • Documentation

    • Added full teleop control protocol reference and integration guidance
  • Tests

    • Added unit tests covering hub behavior, metrics, and authorization
  • Chores

    • Packaged operator static assets and test setup adjustments

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28d01653-6269-416c-948a-adabbf1f93c9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a bidirectional teleop control system: a WebSocket hub server, operator dashboard UI/assets, a headset-side control channel client, WSS proxy/HTTP integration, docs, tests, and App/UI wiring to route session commands, config updates, and periodic metrics between dashboard and headset.

Changes

Cohort / File(s) Summary
Headset Control Channel
deps/cloudxr/webxr_client/helpers/controlChannel.ts
New HeadsetControlChannel WebSocket client: lifecycle, reconnection, register on open, JSON message routing (hello, config, sessionCommand, error), periodic clientMetrics emission, and dispose() behavior.
WebXR App & UI wiring
deps/cloudxr/webxr_client/src/App.tsx, deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
Integrates HeadsetControlChannel into App: extracts doConnect/disconnect refs, routes sessionCommand to connect/disconnect handlers, surfaces errors; adds CloudXR2DUI.setStreamConfig() for partial stream config updates + localStorage persistence; provides metrics snapshots for control channel.
Operator Dashboard Static UI
src/core/cloudxr/python/static/operator/index.html, src/core/cloudxr/python/static/operator/operator.js
Adds operator dashboard HTML and vanilla JS: registers as dashboard, polls /api/teleop/v1/state, renders headsets/metrics, sends setConfig and command messages, toasts, keyboard shortcuts, and copyable hub connection instructions.
Control Hub Server
src/core/cloudxr/python/teleop_ws_hub.py, src/core/cloudxr/python/wss.py, src/core/cloudxr/python/__main__.py
New TeleopControlHub with role register, token checks, setConfig (merge + configVersion), command routing (targeting/conflict handling), clientMetrics storage, snapshot API; WSS proxy extended to serve /teleop/*, route /teleop/v1/ws to hub, expose /api/teleop/v1/state, add LAN-aware dashboard URLs and --enable-hub / --wss-only CLI options.
Packaging & Static inclusion
src/core/cloudxr/python/CMakeLists.txt, src/core/python/MANIFEST.in, src/core/python/pyproject.toml.in
Include teleop_ws_hub.py and static/ assets in CloudXR Python packaging; add static/operator/* to package-data and manifest so operator UI is included in distributions.
Tests & Test infra
src/core/cloudxr_tests/python/conftest.py, src/core/cloudxr_tests/python/pyproject.toml, src/core/cloudxr_tests/python/test_teleop_ws_hub.py
Add pytest project config, import-path shim, and async tests covering registration, token auth, snapshot shape, config propagation, command routing/conflict handling, and metrics persistence.
Documentation
docs/source/references/teleop_control_protocol.rst, docs/source/index.rst
Add Sphinx reference documenting the teleop control protocol (message envelope, StreamConfig schema, HTTP/WebSocket endpoints, query params, behavior) and add it to the toctree.
Misc & logging
src/core/cloudxr/python/__main__.py, src/core/cloudxr/python/wss.py
CLI/help text and logging output adjustments; WSS run signature and logger handler tweaks; HTTP handler normalizes teleop paths and enforces hub-aware responses.

Sequence Diagram(s)

sequenceDiagram
    participant Dashboard as Operator Dashboard
    participant Hub as TeleopControlHub
    participant Headset as WebXR Headset
    participant App as WebXR App

    Dashboard->>Hub: WS register { role: "dashboard", token? }
    Headset->>Hub: WS register { role: "headset", token?, deviceLabel? }
    Hub->>Headset: hello { clientId, configVersion, config }
    Hub->>Dashboard: hello { clientId }

    Dashboard->>Hub: setConfig { config, targetClientId? }
    Hub->>Hub: Merge config, increment configVersion
    Hub->>Headset: config { config, configVersion }

    Dashboard->>Hub: command { action: "connect"/"disconnect", targetClientId? }
    Hub->>Headset: sessionCommand { action, requestId? }
    Headset->>App: onSessionCommand -> doConnect()/doDisconnect()

    Headset->>Hub: clientMetrics { cadence, metrics }
    Hub->>Hub: Store metrics by cadence
    Dashboard->>Hub: GET /api/teleop/v1/state (poll)
    Hub->>Dashboard: snapshot JSON { configVersion, headsets[{metrics}], ... }
Loading
sequenceDiagram
    participant WSS as WSS Proxy
    participant Browser as Operator Browser
    participant Hub as TeleopControlHub

    Browser->>WSS: GET /teleop -> index.html
    Browser->>WSS: GET /teleop/operator.js
    Browser->>WSS: WS /teleop/v1/ws
    WSS->>Hub: upgrade/forward WS connection
    Browser->>WSS: GET /api/teleop/v1/state
    WSS->>Hub: hub.get_snapshot()
    Hub->>WSS: snapshot JSON
    WSS->>Browser: 200 JSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped on the wire with a twitch and a grin,

I send configs and metrics, let the teleop begin.
Dashboards and headsets waltz over WS lines,
Packets like carrots, sweet telemetry signs.
Reconnect, reconcile — nimble hops and bright finds!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title describes adding out-of-band ADB control, but the changeset implements a desktop operator hub with WebSocket control, not ADB. Update the title to reflect the actual changes: 'Add operator hub for desktop control via WebSocket' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yanziz/operator-dashboard

Comment @coderabbitai help to get the list of available commands and usage tips.

@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch from 38c28cd to 6ec535a Compare March 24, 2026 20:53
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch from 6ec535a to 22ca5c8 Compare March 24, 2026 21:16
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 3 times, most recently from 4be6331 to 5cf801d Compare March 25, 2026 00:20
@yanziz-nvidia yanziz-nvidia marked this pull request as ready for review March 25, 2026 00:21
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch from 5cf801d to d81f3f3 Compare March 25, 2026 00:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx`:
- Around line 729-736: The setStreamConfig method signature declares
useSecureConnection but never applies it; either remove it from the parameter
list or apply it to the stored configuration. To fix: locate setStreamConfig and
either delete useSecureConnection from its parameter type and any callers, or
when config.useSecureConnection is defined assign it into the component's
configuration/state (e.g. this.configuration.useSecureConnection =
config.useSecureConnection) and ensure
updateConfiguration()/getDefaultConfiguration() and any WebSocket/connect logic
read that field so the runtime uses the new value; update any tests/call sites
accordingly.

In `@src/core/cloudxr/python/__main__.py`:
- Around line 95-98: Replace the try/except/pass around loop.add_signal_handler
with contextlib.suppress to make the intent explicit: import contextlib and wrap
the call to loop.add_signal_handler(sig, on_signal) inside
contextlib.suppress(NotImplementedError) so that NotImplementedError is quietly
ignored (e.g., contextlib.suppress(NotImplementedError):
loop.add_signal_handler(sig, on_signal)); keep the on_signal handler and loop
variables unchanged.

In `@src/core/cloudxr/python/static/operator/index.html`:
- Around line 360-372: Add explicit for attributes on each label to improve
accessibility: set the label for="headsetSelect" for the "Headset" label,
for="serverIP" for the "Server IP" label, for="port" for the "Port" label, and
for="proxyUrl" for the "Proxy URL" label so each label is explicitly linked to
its corresponding input/select elements (IDs: headsetSelect, serverIP, port,
proxyUrl).

In `@src/core/cloudxr/python/static/operator/operator.js`:
- Around line 318-336: The metric label/value are injected into HTML via tiles
and metricsPanelEl.innerHTML without escaping (see formatMetric and
humanizeMetricKey usage), which can lead to XSS; fix by escaping or avoiding
innerHTML: build DOM nodes with document.createElement and set textContent (or
run display.label/display.value through a sanitizer/escape function) when
creating metric tiles instead of interpolating raw strings into tiles and
metricsPanelEl.innerHTML so that humanizeMetricKey output cannot execute script.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 121-147: get_snapshot currently reads shared state
(self._headsets, self._dashboards) without synchronization which can produce
inconsistent snapshots under concurrent access; make get_snapshot an async def
and acquire the existing self._lock (await self._lock.acquire() / use async with
if it's an AsyncLock) around the construction of headsets/dashboards and the
returned dict, then release the lock, and update all callers to await
hub.get_snapshot() so callers hold the same async contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a34c4032-67b2-4aec-a8f7-a3e92b5aaab9

📥 Commits

Reviewing files that changed from the base of the PR and between ef5f878 and 5cf801d.

📒 Files selected for processing (16)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
  • docs/source/index.rst
  • docs/source/references/teleop_control_protocol.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/static/operator/index.html
  • src/core/cloudxr/python/static/operator/operator.js
  • src/core/cloudxr/python/teleop_ws_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_teleop_ws_hub.py
  • src/core/python/MANIFEST.in
  • src/core/python/pyproject.toml.in

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
src/core/cloudxr/python/static/operator/index.html (1)

360-372: ⚠️ Potential issue | 🟡 Minor

Add for attributes to labels for accessibility compliance.

The <label> elements lack explicit for attributes linking them to their corresponding inputs. While implicit association may work in some browsers, explicit association ensures reliable screen reader support.

♿ Proposed fix
-        <label>Headset</label>
+        <label for="headsetSelect">Headset</label>
         <select id="headsetSelect" ...>

-        <label>Server IP</label>
+        <label for="serverIP">Server IP</label>
         <input id="serverIP" type="text" ... />

-        <label>Port</label>
+        <label for="port">Port</label>
         <input id="port" type="number" ... />

-        <label>Proxy URL <span ...>(optional)</span></label>
+        <label for="proxyUrl">Proxy URL <span ...>(optional)</span></label>
         <input id="proxyUrl" type="text" ... />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/static/operator/index.html` around lines 360 - 372,
Add explicit for attributes on each label to link them to their inputs for
accessibility: update the <label> elements for the fields headsetSelect,
serverIP, port, and proxyUrl so they include for="headsetSelect",
for="serverIP", for="port", and for="proxyUrl" respectively, ensuring each label
matches the id of its corresponding <select> or <input> element (Headset →
headsetSelect, Server IP → serverIP, Port → port, Proxy URL → proxyUrl).
deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx (1)

729-736: ⚠️ Potential issue | 🟡 Minor

useSecureConnection parameter is declared but not handled.

The method signature includes useSecureConnection?: boolean but the method body doesn't use it. If the hub pushes this configuration, it will be silently ignored. Either remove the unused parameter or implement handling logic.

💡 Option: Remove unused parameter
   public setStreamConfig(config: {
     serverIP?: string;
     port?: number;
-    useSecureConnection?: boolean;
     proxyUrl?: string | null;
     mediaAddress?: string;
     mediaPort?: number;
   }): void {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx` around lines 729 - 736, The
setStreamConfig signature includes useSecureConnection but the method body
ignores it; update setStreamConfig to either remove the unused
useSecureConnection parameter or wire it into the component state/connection
config (e.g., assign to this.useSecureConnection or update whatever
connectionOptions object is used by CloudXR2DUI) so the hub-provided
secure/secure flag is honored when building URLs or opening sockets (use it to
choose ws:// vs wss:// or http:// vs https:// in connection setup); ensure any
code that constructs connection endpoints (connectionOptions, createConnection,
or similar methods) reads this flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deps/cloudxr/webxr_client/helpers/controlChannel.ts`:
- Around line 100-104: The _openWebSocket method can throw synchronously when
calling new WebSocket(this.opts.url) for malformed controlWsUrl values; wrap the
constructor in a try/catch inside _openWebSocket (guarding early return when
this.disposed) to catch constructor errors, log or forward the error via the
same error/close handling path used by the existing onerror/onclose handlers,
ensure this.ws is unset on failure, and trigger any reconnect/backoff logic so
the control-channel setup doesn't abort silently; update references inside
_openWebSocket (new WebSocket, this.ws, this.opts.url, and any existing
onerror/onclose/reconnect calls) to use the caught error path.

In `@deps/cloudxr/webxr_client/src/App.tsx`:
- Around line 640-643: The onSessionCommand handler currently calls
doConnectRef.current?.() and drops the returned promise, causing unhandled
rejections if doConnect (which calls enterAR/enterVR) fails; update the
'connect' branch to handle promise rejections by attaching a catch that calls
setErrorMessage(...) with a user-friendly error string (or await within an async
wrapper) so failures show in the UI; reference the onSessionCommand handler,
doConnectRef.current / doConnect(), enterAR()/enterVR(), and setErrorMessage
when making this change.

In `@docs/source/references/teleop_control_protocol.rst`:
- Around line 18-20: The docs claim teleop routes return 404 without
--enable-hub, but the WSS handler currently falls through to the certificate
page with HTTP 200; either update the doc to state the actual behavior or change
the handler to return 404 when the hub is disabled. Locate the request dispatch
logic in the WSS handler (e.g., functions/methods in the WSS class or request
handler in src/core/cloudxr/python/wss.py such as the route dispatch or
certificate page fallback) and either: 1) modify the fallback so that when the
enable-hub flag is false the handler responds with 404 for teleop paths, or 2)
update teleop_control_protocol.rst to reflect that teleop paths currently return
the certificate page with HTTP 200 when the hub is off.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 269-287: The code updates _stream_config and _config_version
before checking targetClientId, which can commit a config change even if the
specified headset doesn't exist; instead, validate target_id (from
payload.get("targetClientId")) against self._headsets/all_headsets and return
the NOT_FOUND via self._send_error if missing before mutating _stream_config or
bumping _config_version; only after target existence is confirmed should you
merge into self._stream_config, increment _config_version, take
config_snapshot/version, and proceed to notify targets.
- Around line 178-208: Holding self._lock while awaiting network I/O (await
self._send) can block other operations; modify register logic in the method that
contains the async with self._lock so that you only mutate shared state
(_headsets, _dashboards) and log inside the lock, but prepare the hello payload
(for headset: {"clientId": client_id, "configVersion": self._config_version,
"config": dict(self._stream_config)}; for dashboard: {"clientId": client_id})
and the target ws outside the lock, then release the lock and call await
self._send(ws, "hello", payload) afterwards; keep use of
_HeadsetState/_DashboardState, client_id, ws, and self._send as the locating
symbols.

In `@src/core/python/MANIFEST.in`:
- Line 30: The MANIFEST.in line currently uses "recursive-include
isaacteleop/cloudxr/static *" which diverges from pyproject.toml.in that only
includes "static/operator/*"; update either pyproject.toml.in to include the
broader static tree (e.g., static/**/* if supported) or make MANIFEST.in
explicit to match pyproject by replacing the recursive-include with
"recursive-include isaacteleop/cloudxr/static/operator *" so sdist and wheel
package-data remain consistent; adjust the file where "recursive-include
isaacteleop/cloudxr/static *" appears accordingly.

---

Duplicate comments:
In `@deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx`:
- Around line 729-736: The setStreamConfig signature includes
useSecureConnection but the method body ignores it; update setStreamConfig to
either remove the unused useSecureConnection parameter or wire it into the
component state/connection config (e.g., assign to this.useSecureConnection or
update whatever connectionOptions object is used by CloudXR2DUI) so the
hub-provided secure/secure flag is honored when building URLs or opening sockets
(use it to choose ws:// vs wss:// or http:// vs https:// in connection setup);
ensure any code that constructs connection endpoints (connectionOptions,
createConnection, or similar methods) reads this flag.

In `@src/core/cloudxr/python/static/operator/index.html`:
- Around line 360-372: Add explicit for attributes on each label to link them to
their inputs for accessibility: update the <label> elements for the fields
headsetSelect, serverIP, port, and proxyUrl so they include for="headsetSelect",
for="serverIP", for="port", and for="proxyUrl" respectively, ensuring each label
matches the id of its corresponding <select> or <input> element (Headset →
headsetSelect, Server IP → serverIP, Port → port, Proxy URL → proxyUrl).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f16be14-37a6-46d3-8ae6-62d544c49718

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf801d and d81f3f3.

📒 Files selected for processing (16)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
  • docs/source/index.rst
  • docs/source/references/teleop_control_protocol.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/static/operator/index.html
  • src/core/cloudxr/python/static/operator/operator.js
  • src/core/cloudxr/python/teleop_ws_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_teleop_ws_hub.py
  • src/core/python/MANIFEST.in
  • src/core/python/pyproject.toml.in

@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 2 times, most recently from afd5a42 to 6699624 Compare March 25, 2026 03:44
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/core/cloudxr/python/static/operator/index.html (1)

360-372: ⚠️ Potential issue | 🟡 Minor

Add for attributes to labels for accessibility.

The labels lack explicit for attributes to associate them with their inputs. This was flagged by static analysis and a previous review.

♿ Proposed fix
-        <label>Headset</label>
+        <label for="headsetSelect">Headset</label>
         <select id="headsetSelect" ...>

-        <label>Server IP</label>
+        <label for="serverIP">Server IP</label>
         <input id="serverIP" type="text" .../>

-        <label>Port</label>
+        <label for="port">Port</label>
         <input id="port" type="number" .../>

-        <label>Proxy URL ...</label>
+        <label for="proxyUrl">Proxy URL ...</label>
         <input id="proxyUrl" type="text" .../>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/static/operator/index.html` around lines 360 - 372,
Add explicit for attributes to each label to improve accessibility by matching
them to their corresponding input/select element ids: set the Headset label's
for="headsetSelect", the Server IP label's for="serverIP", the Port label's
for="port", and the Proxy URL label's for="proxyUrl". Ensure each label's for
value exactly matches the id attribute (headsetSelect, serverIP, port, proxyUrl)
so clicking the label focuses the associated control.
src/core/cloudxr/python/teleop_ws_hub.py (2)

270-292: ⚠️ Potential issue | 🟠 Major

Config is committed before validating targetClientId.

Lines 276-277 update _stream_config and _config_version before lines 283-289 check if the target headset exists. If targetClientId is invalid, the config change is already committed and subsequent identical updates become no-ops.

Consider validating the target before mutating state:

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/teleop_ws_hub.py` around lines 270 - 292, The code
currently updates self._stream_config and increments self._config_version inside
the async with self._lock block before validating targetClientId (target_id),
which can commit an unwanted no-op change if the target headset (from
self._headsets) doesn't exist; fix by validating the target first (resolve
targets from self._headsets using target_id and call self._send_error if
missing) while holding the lock, and only after confirming the target(s) exist
assign self._stream_config = merged, increment self._config_version, snapshot
version/config, and then proceed with the rest of the logic so state is not
mutated on invalid targetClientId.

179-211: ⚠️ Potential issue | 🟠 Major

Network I/O while holding the lock can cause contention.

Lines 193-201 and 209 await self._send() while self._lock is held. A slow or backpressured client during registration can stall other register/config/command operations.

Consider preparing the hello payload inside the lock, then sending outside:

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/teleop_ws_hub.py` around lines 179 - 211, The
registration code is awaiting network I/O (await self._send) while holding
self._lock, which can block other operations; fix by creating and storing the
_HeadsetState or _DashboardState and preparing the hello payload and
clientId/config data while inside the lock (use self._headsets,
self._dashboards, self._stream_config, self._config_version to build the
payload), then release the lock and call await self._send(...) outside the
locked block (keep use of self._lock and the state assignments intact but move
the awaits so only non-blocking work is done while locked).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/cloudxr_tests/python/pyproject.toml`:
- Around line 10-18: The dev and test optional dependency groups are duplicated;
update the pyproject.toml so you don't repeat the same list: either remove one
group and keep a single optional-dependencies group (e.g., keep "dev") or make
the "test" group reference "dev" (or vice versa) so the same dependency list is
reused; edit the [project.optional-dependencies] section to eliminate the
duplicate arrays for dev and test and ensure tooling that reads these keys still
recognizes the intended group name.

In `@src/core/python/pyproject.toml.in`:
- Around line 66-72: The package data pattern for "isaacteleop.cloudxr"
currently uses "static/operator/*" which only includes files directly under
operator/; update this pattern to a recursive glob (e.g.,
"static/operator/**/*") to include any future subdirectories (requires
setuptools ≥62.3.0), or explicitly document/ensure assets remain flat; edit the
entry for "isaacteleop.cloudxr" in pyproject.toml.in to replace the
non-recursive pattern with the recursive one (or add additional patterns for
nested dirs) so nested assets like static/operator/assets/... are packaged.

---

Duplicate comments:
In `@src/core/cloudxr/python/static/operator/index.html`:
- Around line 360-372: Add explicit for attributes to each label to improve
accessibility by matching them to their corresponding input/select element ids:
set the Headset label's for="headsetSelect", the Server IP label's
for="serverIP", the Port label's for="port", and the Proxy URL label's
for="proxyUrl". Ensure each label's for value exactly matches the id attribute
(headsetSelect, serverIP, port, proxyUrl) so clicking the label focuses the
associated control.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 270-292: The code currently updates self._stream_config and
increments self._config_version inside the async with self._lock block before
validating targetClientId (target_id), which can commit an unwanted no-op change
if the target headset (from self._headsets) doesn't exist; fix by validating the
target first (resolve targets from self._headsets using target_id and call
self._send_error if missing) while holding the lock, and only after confirming
the target(s) exist assign self._stream_config = merged, increment
self._config_version, snapshot version/config, and then proceed with the rest of
the logic so state is not mutated on invalid targetClientId.
- Around line 179-211: The registration code is awaiting network I/O (await
self._send) while holding self._lock, which can block other operations; fix by
creating and storing the _HeadsetState or _DashboardState and preparing the
hello payload and clientId/config data while inside the lock (use
self._headsets, self._dashboards, self._stream_config, self._config_version to
build the payload), then release the lock and call await self._send(...) outside
the locked block (keep use of self._lock and the state assignments intact but
move the awaits so only non-blocking work is done while locked).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: adb6bf8d-1f95-4e1b-80c3-605d6b765d13

📥 Commits

Reviewing files that changed from the base of the PR and between d81f3f3 and afd5a42.

📒 Files selected for processing (16)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
  • docs/source/index.rst
  • docs/source/references/teleop_control_protocol.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/static/operator/index.html
  • src/core/cloudxr/python/static/operator/operator.js
  • src/core/cloudxr/python/teleop_ws_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_teleop_ws_hub.py
  • src/core/python/MANIFEST.in
  • src/core/python/pyproject.toml.in

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
src/core/cloudxr/python/static/operator/index.html (1)

360-372: ⚠️ Potential issue | 🟠 Major

Associate these labels with their controls.

These <label> elements are not wrapping the <select>/<input> elements, so screen readers will not announce the field names for the dashboard’s primary controls. Please add explicit for attributes for headsetSelect, serverIP, port, and proxyUrl.

♿ Suggested fix
-        <label>Headset</label>
+        <label for="headsetSelect">Headset</label>
         <select id="headsetSelect" style="width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.875rem;padding:7px 10px;outline:none;">
           <option value="">— no headset selected —</option>
         </select>

-        <label>Server IP</label>
+        <label for="serverIP">Server IP</label>
         <input id="serverIP" type="text" placeholder="192.168.1.100" />

-        <label>Port</label>
+        <label for="port">Port</label>
         <input id="port" type="number" placeholder="48322" min="1" max="65535" />

-        <label>Proxy URL <span style="color:var(--text-muted);font-weight:400">(optional)</span></label>
+        <label for="proxyUrl">Proxy URL <span style="color:var(--text-muted);font-weight:400">(optional)</span></label>
         <input id="proxyUrl" type="text" placeholder="https://proxy.example.com/" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/static/operator/index.html` around lines 360 - 372,
Add explicit for attributes to each label so they associate with their
corresponding controls: set the label for="headsetSelect" for the select with id
"headsetSelect", label for="serverIP" for the input id "serverIP", label
for="port" for the input id "port", and label for="proxyUrl" for the input id
"proxyUrl" in the operator UI (ids: headsetSelect, serverIP, port, proxyUrl) to
ensure screen readers announce the field names.
src/core/cloudxr/python/teleop_ws_hub.py (1)

274-277: ⚠️ Potential issue | 🟠 Major

Check targetClientId before the no-op fast path.

The early merged == self._stream_config return still runs before target validation. That means a stale/disconnected targetClientId combined with {} or an identical config is treated as a silent success instead of NOT_FOUND, so targeted updates become indistinguishable from a real no-op.

♻️ Proposed fix
         async with self._lock:
-            merged = {**self._stream_config, **new_config}
-            if merged == self._stream_config:
-                return  # no-op: empty {}, redundant values, or no change
-
             all_headsets = list(self._headsets.values())
 
             if target_id is not None:
                 targets = [s for s in all_headsets if s.client_id == target_id]
                 if not targets:
                     missing_target = target_id
-                else:
-                    self._stream_config = merged
-                    self._config_version += 1
-                    version = self._config_version
-                    config_snapshot = dict(self._stream_config)
             else:
                 targets = all_headsets
+
+            if missing_target is None:
+                merged = {**self._stream_config, **new_config}
+                if merged == self._stream_config:
+                    return
                 self._stream_config = merged
                 self._config_version += 1
                 version = self._config_version
                 config_snapshot = dict(self._stream_config)

Also applies to: 279-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/teleop_ws_hub.py` around lines 274 - 277, The early
no-op check returns before validating a requested target, causing
stale/disconnected targetClientId updates to be treated as success; inside the
same async with self._lock block (around the merged = {**self._stream_config,
**new_config} logic) validate any requested targetClientId from new_config or
merged before taking the fast-path return: if a targetClientId is present,
confirm it refers to a currently connected/known client (use your hub’s client
lookup, e.g. self._clients or self._client_by_id / equivalent check), and if not
found return the NOT_FOUND error instead of returning early; apply the same
targetClientId validation to the other similar fast-paths noted (the block
covering lines 279–301).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deps/cloudxr/webxr_client/src/App.tsx`:
- Around line 301-336: doConnect currently only validates per-eye resolution and
then calls store.enterAR()/enterVR(), letting control-channel triggered connects
bypass the button path guards; refactor so doConnect enforces the same preflight
checks the UI connect button uses: verify reprojection-grid validation,
capability/immersive-support readiness, and that there is no active XR session
before calling store.enterAR()/store.enterVR(); implement this by invoking the
existing button preflight validation helper(s) (or extract the button’s guard
logic into a shared function like validatePreconnect or ensureConnectAllowed) at
the top of doConnect (using ui.getConfiguration() and whatever guard functions
the button path uses), aborting early and updating UI state if any guard fails,
then proceed to setFrameRate as currently done.

In `@src/core/cloudxr/python/CMakeLists.txt`:
- Around line 112-115: Add a removal step to purge the staged static tree before
the copy so stale/renamed files don’t survive incremental builds: before the
existing COMMAND that runs ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/static" "${CLOUDXR_PYTHON_DIR}/static", insert a
COMMAND invoking ${CMAKE_COMMAND} -E rm -rf on "${CLOUDXR_PYTHON_DIR}/static"
(similar to the existing cleanup for "${CLOUDXR_PYTHON_DIR}/__pycache__") so the
static directory is cleared prior to copying.

In `@src/core/cloudxr/python/static/operator/index.html`:
- Around line 382-385: The comment says the displayed shortcut hint claiming C/D
only work when focus is not in a field is false because the keydown listener on
document in src/core/cloudxr/python/static/operator/operator.js does not ignore
focused inputs; update the keydown handler (the function attached via
document.addEventListener('keydown', ...) or the handler named handleKeyDown) to
early-return when document.activeElement is an input, textarea, select, or any
element with contenteditable=true (use element.tagName and
element.isContentEditable), so typing in the stream-target form won't trigger
connect/disconnect; adjust the handler logic and unit/behavior tests if present.

In `@src/core/cloudxr/python/static/operator/operator.js`:
- Around line 21-24: WS_URL is conditionally overridden by params.get('wsUrl')
but STATE_URL still uses window.location.host; update STATE_URL to derive its
host from the same wsUrl override so both socket and polling endpoints point to
the hub target. Parse the host/authority from params.get('wsUrl') (if present)
and build STATE_URL using that origin (fallback to window.location.host) so
STATE_URL and WS_URL remain consistent; touch the constants WS_URL and STATE_URL
in operator.js and ensure parsing handles wss/wss/https scheme differences.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 259-264: The handler _handle_set_config currently accepts any JSON
object from payload.get("config") without validation; update _handle_set_config
to validate the config schema before persisting or forwarding: verify required
fields exist and have correct types (e.g., "port" is an integer within 1–65535,
"serverIP" is a non-empty string matching an IP/hostname pattern, booleans are
bools, numeric ranges are enforced), and on any invalid field call
_send_error(ws, "BAD_REQUEST", "<field> invalid") and return; centralize the
rules (a small helper validate_config(config) or inline checks) so only
validated config is stored and forwarded.

---

Duplicate comments:
In `@src/core/cloudxr/python/static/operator/index.html`:
- Around line 360-372: Add explicit for attributes to each label so they
associate with their corresponding controls: set the label for="headsetSelect"
for the select with id "headsetSelect", label for="serverIP" for the input id
"serverIP", label for="port" for the input id "port", and label for="proxyUrl"
for the input id "proxyUrl" in the operator UI (ids: headsetSelect, serverIP,
port, proxyUrl) to ensure screen readers announce the field names.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 274-277: The early no-op check returns before validating a
requested target, causing stale/disconnected targetClientId updates to be
treated as success; inside the same async with self._lock block (around the
merged = {**self._stream_config, **new_config} logic) validate any requested
targetClientId from new_config or merged before taking the fast-path return: if
a targetClientId is present, confirm it refers to a currently connected/known
client (use your hub’s client lookup, e.g. self._clients or self._client_by_id /
equivalent check), and if not found return the NOT_FOUND error instead of
returning early; apply the same targetClientId validation to the other similar
fast-paths noted (the block covering lines 279–301).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c4c2226-a49e-484c-b2b0-39ae357922b8

📥 Commits

Reviewing files that changed from the base of the PR and between afd5a42 and 6699624.

📒 Files selected for processing (16)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
  • docs/source/index.rst
  • docs/source/references/teleop_control_protocol.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/static/operator/index.html
  • src/core/cloudxr/python/static/operator/operator.js
  • src/core/cloudxr/python/teleop_ws_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_teleop_ws_hub.py
  • src/core/python/MANIFEST.in
  • src/core/python/pyproject.toml.in

Comment on lines +301 to +336
const doConnect = async () => {
const config = ui.getConfiguration();
const resolutionError = getResolutionValidationError(
config.perEyeWidth,
config.perEyeHeight
);
if (resolutionError) {
ui.updateConnectButtonState();
return;
}
// Start XR session
if (config.immersiveMode === 'ar') {
await store.enterAR();
} else if (config.immersiveMode === 'vr') {
await store.enterVR();
} else {
setErrorMessage('Unrecognized immersive mode');
}
store.setFrameRate((supportedFrameRates: ArrayLike<number>): number | false => {
let frameRate = ui.getConfiguration().deviceFrameRate;
let found = false;
for (let i = 0; i < supportedFrameRates.length; ++i) {
if (supportedFrameRates[i] === frameRate) {
found = true;
break;
}
}
// Start XR session
if (config.immersiveMode === 'ar') {
await store.enterAR();
} else if (config.immersiveMode === 'vr') {
await store.enterVR();
if (found) {
console.info('Changed frame rate to', frameRate);
return frameRate;
} else {
setErrorMessage('Unrecognized immersive mode');
console.error('Failed to change frame rate to', frameRate);
return false;
}
store.setFrameRate((supportedFrameRates: ArrayLike<number>): number | false => {
let frameRate = ui.getConfiguration().deviceFrameRate;
let found = false;
for (let i = 0; i < supportedFrameRates.length; ++i) {
if (supportedFrameRates[i] === frameRate) {
found = true;
break;
}
}
if (found) {
console.info('Changed frame rate to', frameRate);
return frameRate;
} else {
console.error('Failed to change frame rate to', frameRate);
return false;
}
});
},
(error: Error) => {
setErrorMessage(`Failed to start XR session: ${error}`);
}
);
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make doConnect() enforce the same preflight checks as the button path.

After the control-channel path started calling doConnectRef.current directly, this function became the shared connect entry point. It still only checks per-eye resolution, so hub-triggered connects bypass the button’s other guards: reprojection-grid validation, capability/immersive-support readiness, and the “XR session already active” gate. That means the operator can trigger enterAR()/enterVR() while the UI correctly shows CONNECT as disabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/cloudxr/webxr_client/src/App.tsx` around lines 301 - 336, doConnect
currently only validates per-eye resolution and then calls
store.enterAR()/enterVR(), letting control-channel triggered connects bypass the
button path guards; refactor so doConnect enforces the same preflight checks the
UI connect button uses: verify reprojection-grid validation,
capability/immersive-support readiness, and that there is no active XR session
before calling store.enterAR()/store.enterVR(); implement this by invoking the
existing button preflight validation helper(s) (or extract the button’s guard
logic into a shared function like validatePreconnect or ensureConnectAllowed) at
the top of doConnect (using ui.getConfiguration() and whatever guard functions
the button path uses), aborting early and updating UI state if any guard fails,
then proceed to setFrameRate as currently done.

Comment on lines 112 to 115
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/static"
"${CLOUDXR_PYTHON_DIR}/static"
COMMAND ${CMAKE_COMMAND} -E rm -rf "${CLOUDXR_PYTHON_DIR}/__pycache__"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Purge the staged static/ tree before copying it.

copy_directory only overlays files. If an operator asset gets renamed or deleted, the old file survives under ${CLOUDXR_PYTHON_DIR}/static and can leak into incremental packages/builds. You already clean __pycache__; do the same for static/ before the copy.

🧹 Suggested fix
 add_custom_target(cloudxr_python ALL
     COMMAND ${CMAKE_COMMAND} -E make_directory "${CLOUDXR_PYTHON_DIR}"
+    COMMAND ${CMAKE_COMMAND} -E rm -rf "${CLOUDXR_PYTHON_DIR}/static"
     COMMAND ${CMAKE_COMMAND} -E copy
         "${CMAKE_CURRENT_SOURCE_DIR}/__init__.py"
         "${CMAKE_CURRENT_SOURCE_DIR}/__main__.py"
         "${CMAKE_CURRENT_SOURCE_DIR}/env_config.py"
         "${CMAKE_CURRENT_SOURCE_DIR}/runtime.py"
         "${CMAKE_CURRENT_SOURCE_DIR}/wss.py"
         "${CMAKE_CURRENT_SOURCE_DIR}/teleop_ws_hub.py"
         "${CLOUDXR_PYTHON_DIR}/"
     COMMAND ${CMAKE_COMMAND} -E copy_directory
         "${CMAKE_CURRENT_SOURCE_DIR}/static"
         "${CLOUDXR_PYTHON_DIR}/static"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/CMakeLists.txt` around lines 112 - 115, Add a removal
step to purge the staged static tree before the copy so stale/renamed files
don’t survive incremental builds: before the existing COMMAND that runs
${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/static"
"${CLOUDXR_PYTHON_DIR}/static", insert a COMMAND invoking ${CMAKE_COMMAND} -E rm
-rf on "${CLOUDXR_PYTHON_DIR}/static" (similar to the existing cleanup for
"${CLOUDXR_PYTHON_DIR}/__pycache__") so the static directory is cleared prior to
copying.

@jiwenc-nv jiwenc-nv requested a review from nvddr March 25, 2026 16:37
Teleop Control Protocol
=======================

Two-way control channel between a **PC operator dashboard** and an **XR headset** running the
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the term "operator" maybe a bit overloaded.


An unknown ``type`` causes the server to send an ``error`` message and ignore the payload.

Messages: Client → Server
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the "Isaac Teleop web client"? or the new UI meant to be running on a separate workstation?

.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

Teleop Control Protocol
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the architecture is getting complex now, we have 3 computer in this case:

  1. XR headset the human is wearing
  2. A PC the human is using for command & control
  3. The compute runs CXR Runtime & Teleop Session (Cloud VM, on robot, OR a separate workstation)

the rest of the doc mentions client <-> server which is not painting the whole picture and can get a reader confused about which computer is talking to which.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
src/core/cloudxr/python/teleop_ws_hub.py (2)

270-287: ⚠️ Potential issue | 🟠 Major

Resolve targetClientId before the no-op fast path.

merged == self._stream_config returns before any target lookup, so an empty/identical update aimed at a stale targetClientId is silently accepted instead of returning NOT_FOUND. That makes stale dashboard selections look successful even though no headset exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/teleop_ws_hub.py` around lines 270 - 287, Resolve the
requested targetClientId before taking the no-op fast path: inside the async
with self._lock block, compute target_id = payload.get("targetClientId") and
derive all_headsets = list(self._headsets.values()) and targets = [s for s in
all_headsets if s.client_id == target_id] (set missing_target when not found)
before computing merged = {**self._stream_config, **new_config} and checking
merged == self._stream_config; if missing_target is set, return the NOT_FOUND
response immediately so stale targetClientId is rejected even for
no-op/identical updates.

262-267: ⚠️ Potential issue | 🟠 Major

Validate the stream-config schema before merging it.

Checking only isinstance(new_config, dict) lets values like {"port": "48322"} or {"serverIP": []} become authoritative hub state and get pushed to headsets. The dashboard form validates locally, but this endpoint is still a public protocol surface and needs server-side field/range checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/teleop_ws_hub.py` around lines 262 - 267, In
_handle_set_config, do full server-side schema validation instead of only
isinstance(new_config, dict): validate expected keys (e.g., "port", "serverIP",
any known config fields), enforce types (port must be an int), ranges (port
within 1-65535), and formats (serverIP is a valid IP/hostname string), reject
unknown keys, and only merge into hub state if validation passes; if validation
fails, call self._send_error(ws, "BAD_REQUEST", "<specific message>") with a
clear reason. Ensure you update the merge path that currently applies new_config
so it only executes after successful validation performed inside
_handle_set_config (refer to function name _handle_set_config and the existing
_send_error helper).
src/core/cloudxr/python/wss.py (1)

470-479: ⚠️ Potential issue | 🔴 Critical

Do not start the control hub in an unauthenticated mode by default.

When enable_hub is true and CONTROL_TOKEN is unset, this exposes /teleop/, /api/teleop/v1/state, and /teleop/v1/ws with no credential check. Any browser that can reach the proxy port can then register a dashboard/headset and issue setConfig / command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/wss.py` around lines 470 - 479, Currently the code
will start TeleopControlHub when enable_hub is true even if CONTROL_TOKEN is
unset, enabling unauthenticated access; change the logic in wss.py so
TeleopControlHub is only instantiated when a valid control token is provided
(i.e., check os.environ.get("CONTROL_TOKEN") and refuse/skip starting the hub if
it is None or empty), and log a clear warning/error instead of enabling the hub
in unauthenticated mode; reference the enable_hub variable, the CONTROL_TOKEN
env read, and the TeleopControlHub constructor to locate and update the code
path.
deps/cloudxr/webxr_client/src/App.tsx (1)

301-336: ⚠️ Potential issue | 🟠 Major

Reuse the full connect-button preflight in doConnect().

doConnect() is now the shared entry point for both the local button and remote sessionCommand.connect, but it still only revalidates per-eye resolution. The button path in CloudXR2DUI.setupConnectButtonHandler() rejects other disabled states too, so hub-triggered connects can still call enterAR() / enterVR() while the local UI would keep CONNECT disabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/cloudxr/webxr_client/src/App.tsx` around lines 301 - 336, doConnect
currently only revalidates per-eye resolution but must run the same full
"connect" preflight used by the local button handler to avoid hub-triggered
connects bypassing UI disable rules; modify doConnect to invoke the same
validation routine used by CloudXR2DUI.setupConnectButtonHandler (e.g., call the
shared preflight function or the same checks that updateConnectButtonState
uses), and bail out if that preflight indicates connect should be disabled
before calling store.enterAR()/enterVR(); keep the existing resolution check but
replace the simple updateConnectButtonState() call with the full connect-enabled
check so both button and remote triggers share identical validation.
src/core/cloudxr/python/static/operator/operator.js (1)

21-24: ⚠️ Potential issue | 🟠 Major

Derive STATE_URL from the same hub target as WS_URL.

Overriding ?wsUrl= only changes the WebSocket endpoint. Snapshot polling still goes to window.location.host, so a dashboard hosted on one origin cannot render headsets, config, or metrics from a hub on another origin.

♻️ Suggested fix
   const WS_URL =
     params.get('wsUrl') ||
     `wss://${window.location.host}/teleop/v1/ws`;
-  const STATE_URL = `https://${window.location.host}/api/teleop/v1/state`;
+  const HUB_ORIGIN = (() => {
+    const u = new URL(WS_URL, window.location.href);
+    return `${u.protocol === 'wss:' ? 'https:' : 'http:'}//${u.host}`;
+  })();
+  const STATE_URL = `${HUB_ORIGIN}/api/teleop/v1/state`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/cloudxr/python/static/operator/operator.js` around lines 21 - 24,
STATE_URL is still built from window.location.host while WS_URL can be
overridden by params.get('wsUrl'), causing polling to hit the wrong host; change
the code to derive STATE_URL from the same hub target used for WS_URL: read
params.get('wsUrl') into the same variable used to set WS_URL, parse it with the
URL constructor (or equivalent) to extract host and protocol, convert ws/wss ->
http/https accordingly, and build STATE_URL as
`${protocol}//${host}/api/teleop/v1/state` (falling back to window.location.host
if no wsUrl provided) so both WS_URL and STATE_URL point at the same hub.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/source/references/teleop_control_protocol.rst`:
- Around line 203-227: The subsection underlines for the headings "setConfig
(control console / ``dashboard`` role only)" and "command (control console /
``dashboard`` role only)" are too short; update the RST underline rows (the
lines of ~ characters) so each underline is exactly the same length as its title
text (use the same ~ character) to restore proper heading formatting for the two
subsections in teleop_control_protocol.rst.
- Around line 223-225: Update the docs to state that configVersion is only
incremented when the merged StreamConfig actually changes: explain that
payload.config is shallow-merged over the current StreamConfig but the hub's
setConfig path (see src/core/cloudxr/python/teleop_ws_hub.py) returns early for
empty or identical merges, leaving configVersion unchanged; rephrase the current
sentence to reflect this no-op behavior and mention that tests assert
empty/identical updates do not bump the version.

In `@src/core/cloudxr/python/static/operator/index.html`:
- Line 7: Remove the restriction that disables pinch-zoom by editing the
viewport meta tag in index.html: replace the current meta element that contains
"user-scalable=no" with a viewport string that allows zooming (for example keep
"width=device-width, initial-scale=1.0" and remove "user-scalable=no" or add
"maximum-scale=5, minimum-scale=1") so the control console supports browser zoom
for accessibility.

In `@src/core/cloudxr/python/static/operator/operator.js`:
- Around line 150-167: When the websocket closes (ws.onclose) you must
immediately disable headset-dependent UI and prevent false success toasts: in
the ws.onclose handler (where setStatusConnected(false), stopPolling(),
reconnectTimer and ws nulling happen) also clear the last snapshot and current
selection and call updateHeadsetDependentButtons() so buttons reflect
disconnection; then modify send(type,payload) to return a boolean or throw when
not connected (instead of only showing a toast) and update all action handlers
that call send(...) to check its return/exception and only show success toasts
on actual successful sends. Ensure references to wsConnected, send,
updateHeadsetDependentButtons, and the ws.onclose handler are updated
accordingly.

In `@src/core/cloudxr/python/wss.py`:
- Around line 67-79: control_console_urls() advertises HTTPS URLs using the LAN
IP, but ensure_certificate() only creates a cert for CN=localhost causing
hostname mismatch; update ensure_certificate() to include Subject Alternative
Names (SANs) that cover 127.0.0.1 and the guessed LAN IPv4 (from
_guess_lan_ipv4()) and/or the machine hostname so the generated self-signed cert
matches the URLs returned by control_console_urls() and the
wss://<LAN-IP>/teleop/v1/ws clients; ensure wss_proxy_port() usage is unchanged
and fallback behavior remains if no LAN IP is found.

---

Duplicate comments:
In `@deps/cloudxr/webxr_client/src/App.tsx`:
- Around line 301-336: doConnect currently only revalidates per-eye resolution
but must run the same full "connect" preflight used by the local button handler
to avoid hub-triggered connects bypassing UI disable rules; modify doConnect to
invoke the same validation routine used by CloudXR2DUI.setupConnectButtonHandler
(e.g., call the shared preflight function or the same checks that
updateConnectButtonState uses), and bail out if that preflight indicates connect
should be disabled before calling store.enterAR()/enterVR(); keep the existing
resolution check but replace the simple updateConnectButtonState() call with the
full connect-enabled check so both button and remote triggers share identical
validation.

In `@src/core/cloudxr/python/static/operator/operator.js`:
- Around line 21-24: STATE_URL is still built from window.location.host while
WS_URL can be overridden by params.get('wsUrl'), causing polling to hit the
wrong host; change the code to derive STATE_URL from the same hub target used
for WS_URL: read params.get('wsUrl') into the same variable used to set WS_URL,
parse it with the URL constructor (or equivalent) to extract host and protocol,
convert ws/wss -> http/https accordingly, and build STATE_URL as
`${protocol}//${host}/api/teleop/v1/state` (falling back to window.location.host
if no wsUrl provided) so both WS_URL and STATE_URL point at the same hub.

In `@src/core/cloudxr/python/teleop_ws_hub.py`:
- Around line 270-287: Resolve the requested targetClientId before taking the
no-op fast path: inside the async with self._lock block, compute target_id =
payload.get("targetClientId") and derive all_headsets =
list(self._headsets.values()) and targets = [s for s in all_headsets if
s.client_id == target_id] (set missing_target when not found) before computing
merged = {**self._stream_config, **new_config} and checking merged ==
self._stream_config; if missing_target is set, return the NOT_FOUND response
immediately so stale targetClientId is rejected even for no-op/identical
updates.
- Around line 262-267: In _handle_set_config, do full server-side schema
validation instead of only isinstance(new_config, dict): validate expected keys
(e.g., "port", "serverIP", any known config fields), enforce types (port must be
an int), ranges (port within 1-65535), and formats (serverIP is a valid
IP/hostname string), reject unknown keys, and only merge into hub state if
validation passes; if validation fails, call self._send_error(ws, "BAD_REQUEST",
"<specific message>") with a clear reason. Ensure you update the merge path that
currently applies new_config so it only executes after successful validation
performed inside _handle_set_config (refer to function name _handle_set_config
and the existing _send_error helper).

In `@src/core/cloudxr/python/wss.py`:
- Around line 470-479: Currently the code will start TeleopControlHub when
enable_hub is true even if CONTROL_TOKEN is unset, enabling unauthenticated
access; change the logic in wss.py so TeleopControlHub is only instantiated when
a valid control token is provided (i.e., check os.environ.get("CONTROL_TOKEN")
and refuse/skip starting the hub if it is None or empty), and log a clear
warning/error instead of enabling the hub in unauthenticated mode; reference the
enable_hub variable, the CONTROL_TOKEN env read, and the TeleopControlHub
constructor to locate and update the code path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 014f905b-ad78-4d08-bc94-69ec43acf6fe

📥 Commits

Reviewing files that changed from the base of the PR and between 6699624 and 1cfd56d.

📒 Files selected for processing (16)
  • deps/cloudxr/webxr_client/helpers/controlChannel.ts
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXR2DUI.tsx
  • docs/source/index.rst
  • docs/source/references/teleop_control_protocol.rst
  • src/core/cloudxr/python/CMakeLists.txt
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/static/operator/index.html
  • src/core/cloudxr/python/static/operator/operator.js
  • src/core/cloudxr/python/teleop_ws_hub.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/conftest.py
  • src/core/cloudxr_tests/python/pyproject.toml
  • src/core/cloudxr_tests/python/test_teleop_ws_hub.py
  • src/core/python/MANIFEST.in
  • src/core/python/pyproject.toml.in

Comment on lines +223 to +225
``payload.config`` may be **partial**; the hub shallow-merges it over the current
``StreamConfig``. ``configVersion`` is always monotonically incremented by the hub.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the no-op setConfig behavior accurately.

The hub does not always increment configVersion: src/core/cloudxr/python/teleop_ws_hub.py returns early when the merged config is unchanged, and the new tests assert that empty/identical updates keep the version unchanged. This sentence currently contradicts the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/source/references/teleop_control_protocol.rst` around lines 223 - 225,
Update the docs to state that configVersion is only incremented when the merged
StreamConfig actually changes: explain that payload.config is shallow-merged
over the current StreamConfig but the hub's setConfig path (see
src/core/cloudxr/python/teleop_ws_hub.py) returns early for empty or identical
merges, leaving configVersion unchanged; rephrase the current sentence to
reflect this no-op behavior and mention that tests assert empty/identical
updates do not bump the version.

@yanziz-nvidia yanziz-nvidia marked this pull request as draft March 31, 2026 18:32
@yanziz-nvidia
Copy link
Copy Markdown
Contributor Author

per discussion we want to have a more automated minimal no UI solution, will think about it

@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 2 times, most recently from e345f6a to 4eced4e Compare April 4, 2026 04:50
@yanziz-nvidia yanziz-nvidia changed the title Adds a hub for operator to run on desktop Adds out of band control via adb to simplify wearing headset on neck scenario Apr 4, 2026
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 6 times, most recently from 9c68695 to 78bf0df Compare April 7, 2026 19:53
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 4 times, most recently from ea1d556 to 1d11c6b Compare April 10, 2026 22:26
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch 3 times, most recently from 7d8fc23 to 0008919 Compare April 11, 2026 02:38
@yanziz-nvidia yanziz-nvidia force-pushed the yanziz/operator-dashboard branch from 0008919 to dff8f49 Compare April 11, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants