Skip to content

fix(web): make Go2 teleop UIs LAN-aware#2377

Open
kezaer wants to merge 3 commits into
dimensionalOS:mainfrom
kezaer:fix/go2-lan-web-ui
Open

fix(web): make Go2 teleop UIs LAN-aware#2377
kezaer wants to merge 3 commits into
dimensionalOS:mainfrom
kezaer:fix/go2-lan-web-ui

Conversation

@kezaer

@kezaer kezaer commented Jun 6, 2026

Copy link
Copy Markdown

Summary

  • Build Rerun dashboard iframe URLs from the current page host instead of hardcoded localhost or stale Rerun ports.
  • Thread GlobalConfig.listen_host into phone teleop, WebsocketVis, and RobotWebInterface server construction.
  • Add focused tests for dashboard URL generation, server bind-host propagation, and explicit RobotWebInterface(host=...) handling.
  • Narrow the RobotWebInterface host test so it does not require optional python-multipart support in CI.

Why

Phone and browser teleop must work from another device on the same trusted LAN. Hardcoded localhost URLs and loopback-only server binding make the UI appear broken even when the robot connection is healthy.

Safety

  • LAN exposure remains explicit via --listen-host 0.0.0.0; this PR does not silently expose motion-capable browser UIs.
  • No robot motion commands were sent while validating this change.

Validation

  • uv run pytest dimos/web/test_robot_web_interface.py dimos/web/templates/test_rerun_dashboard.py dimos/web/websocket_vis/test_websocket_vis_module.py dimos/teleop/phone/test_phone_teleop_module.py -q
  • uv run ruff check dimos/web/test_robot_web_interface.py dimos/web/robot_web_interface.py dimos/web/templates/test_rerun_dashboard.py dimos/web/websocket_vis/websocket_vis_module.py dimos/web/websocket_vis/test_websocket_vis_module.py dimos/teleop/phone/phone_teleop_module.py dimos/teleop/phone/test_phone_teleop_module.py

Review Notes

  • Addresses the earlier Codecov failure in test_robot_web_interface_accepts_explicit_host by avoiding full FastAPI route registration in a unit test whose purpose is only host forwarding.

@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes the Go2 teleop web UIs LAN-aware by replacing hardcoded localhost URLs in the Rerun dashboard template with dynamic client-side URL construction, and propagating GlobalConfig.listen_host into both the phone teleop and WebSocket visualization servers.

  • rerun_dashboard.html: Both iframes now build their src URLs from window.location.hostname/origin/protocol, with query-param or DIMOS_RERUN_CONFIG overrides for host, ports (defaulting to 9877/9878), and protocol. An IPv6 bracketing helper and normalizeHttpProtocol guard are included.
  • robot_web_interface.py / phone_teleop_module.py / websocket_vis_module.py: host is now an explicit keyword argument forwarded to FastAPIServer, sourced from the module's own config g.listen_host rather than a process-wide singleton.
  • Focused test coverage is added for all three changes (dashboard URL generation, RobotWebInterface host parameter, and server host propagation in both web modules).

Confidence Score: 5/5

Safe to merge — changes are additive, well-tested, and address previously identified issues around host binding and hardcoded URLs.

All three change surfaces (dashboard URL generation, RobotWebInterface host forwarding, and uvicorn host sourcing) are covered by new focused tests. The JavaScript URL construction handles IPv6, protocol normalization, and query-param overrides correctly. No regressions to existing behaviour are expected since the default listen_host of 127.0.0.1 preserves the prior localhost-only binding.

No files require special attention.

Important Files Changed

Filename Overview
dimos/web/templates/rerun_dashboard.html Replaces hardcoded localhost URLs for both iframes with dynamic JavaScript URL construction using the current page host, protocol, and configurable Rerun port defaults (9877/9878); includes IPv6 bracketing and protocol normalization helpers.
dimos/web/robot_web_interface.py Adds explicit keyword-only `host: str
dimos/teleop/phone/phone_teleop_module.py Extracts web server construction into _create_web_server() and passes self.config.g.listen_host so the embedded FastAPI server binds to the module-level global config host instead of the implicit default.
dimos/web/websocket_vis/websocket_vis_module.py Replaces global_config.listen_host singleton reference with self.config.g.listen_host in _run_uvicorn_server, making the bind host testable and scoped to the module's own config.
dimos/web/templates/test_rerun_dashboard.py New test file verifying dashboard HTML uses current-host URL generation and no longer contains stale hardcoded localhost/port references or a hardcoded http:// scheme.
dimos/teleop/phone/test_phone_teleop_module.py New test verifying PhoneTeleopModule._create_web_server() passes listen_host and server_port from config to RobotWebInterface.
dimos/web/websocket_vis/test_websocket_vis_module.py New test verifying WebsocketVisModule._run_uvicorn_server() binds to listen_host from the module's own config rather than the global singleton.
dimos/web/test_robot_web_interface.py New test ensuring an explicit host argument is stored on RobotWebInterface.host and not leaked into the streams dict.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant DashboardHTML as rerun_dashboard.html
    participant DIMOS as DIMOS Server (WebsocketVisModule)
    participant Rerun as Rerun Viewer (separate port)

    Browser->>DashboardHTML: GET /dashboard
    Note over DashboardHTML: Reads window.location.hostname<br/>+ query/config overrides
    DashboardHTML->>Browser: "Sets commandCenter.src = origin + /command-center"
    DashboardHTML->>Browser: "Sets rerun.src = rerunWebProtocol://rerunHost:rerunWebPort/?url=rerun+grpc://..."

    Browser->>DIMOS: iframe GET /command-center (same origin)
    Browser->>Rerun: iframe GET on resolved rerun URL

    Note over DIMOS: Bound to GlobalConfig.listen_host<br/>(default 127.0.0.1, override 0.0.0.0 for LAN)
Loading

Reviews (4): Last reviewed commit: "test: avoid multipart dependency in web ..." | Re-trigger Greptile

Comment thread dimos/web/templates/rerun_dashboard.html Outdated
Comment thread dimos/teleop/phone/phone_teleop_module.py
@codecov

codecov Bot commented Jun 6, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1840 1 1839 151
View the top 1 failed test(s) by shortest run time
dimos.web.test_robot_web_interface::test_robot_web_interface_accepts_explicit_host
Stack Traces | 0.009s run time
def test_robot_web_interface_accepts_explicit_host() -> None:
>       interface = RobotWebInterface(host="0.0.0.0", port=8444)


dimos/web/test_robot_web_interface.py:19: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/web/robot_web_interface.py:35: in __init__
    super().__init__(
        __class__  = <class 'dimos.web.robot_web_interface.RobotWebInterface'>
        audio_subject = None
        host       = '0.0.0.0'
        port       = 8444
        self       = <dimos.web.robot_web_interface.RobotWebInterface object at 0xff5ed7204350>
        streams    = {}
        text_streams = None
.../dimos_interface/api/server.py:123: in __init__
    self.setup_routes()
        BASE_DIR   = PosixPath('.../web/dimos_interface/api')
        __class__  = <class 'dimos.web.dimos_interface.api.server.FastAPIServer'>
        audio_subject = None
        dev_name   = 'Robot Web Interface'
        edge_type  = 'Bidirectional'
        host       = '0.0.0.0'
        port       = 8444
        self       = <dimos.web.robot_web_interface.RobotWebInterface object at 0xff5ed7204350>
        streams    = {}
        text_streams = None
.../dimos_interface/api/server.py:268: in setup_routes
    @self.app.post("/submit_query")
        get_streams = <function FastAPIServer.setup_routes.<locals>.get_streams at 0xff5ed4a78d60>
        get_text_streams = <function FastAPIServer.setup_routes.<locals>.get_text_streams at 0xff5ed4a7ade0>
        index      = <function FastAPIServer.setup_routes.<locals>.index at 0xff5ed4a78540>
        self       = <dimos.web.robot_web_interface.RobotWebInterface object at 0xff5ed7204350>
.venv/lib/python3.12........./site-packages/fastapi/routing.py:1125: in decorator
    self.add_api_route(
        callbacks  = None
        dependencies = None
        deprecated = None
        description = None
        func       = <function FastAPIServer.setup_routes.<locals>.submit_query at 0xff5ed4a7aac0>
        generate_unique_id_function = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e38c0>
        include_in_schema = True
        methods    = ['POST']
        name       = None
        openapi_extra = None
        operation_id = None
        path       = '/submit_query'
        response_class = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3890>
        response_description = 'Successful Response'
        response_model = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3800>
        response_model_by_alias = True
        response_model_exclude = None
        response_model_exclude_defaults = False
        response_model_exclude_none = False
        response_model_exclude_unset = False
        response_model_include = None
        responses  = None
        self       = <fastapi.routing.APIRouter object at 0xff5eea25cce0>
        status_code = None
        summary    = None
        tags       = None
.venv/lib/python3.12........./site-packages/fastapi/routing.py:1064: in add_api_route
    route = route_class(
        callbacks  = None
        combined_responses = {}
        current_callbacks = []
        current_dependencies = []
        current_generate_unique_id = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e38c0>
        current_response_class = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3890>
        current_tags = []
        dependencies = None
        deprecated = None
        description = None
        endpoint   = <function FastAPIServer.setup_routes.<locals>.submit_query at 0xff5ed4a7aac0>
        generate_unique_id_function = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e38c0>
        include_in_schema = True
        methods    = ['POST']
        name       = None
        openapi_extra = None
        operation_id = None
        path       = '/submit_query'
        response_class = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3890>
        response_description = 'Successful Response'
        response_model = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3800>
        response_model_by_alias = True
        response_model_exclude = None
        response_model_exclude_defaults = False
        response_model_exclude_none = False
        response_model_exclude_unset = False
        response_model_include = None
        responses  = {}
        route_class = <class 'fastapi.routing.APIRoute'>
        route_class_override = None
        self       = <fastapi.routing.APIRouter object at 0xff5eea25cce0>
        status_code = None
        summary    = None
        tags       = None
.venv/lib/python3.12........./site-packages/fastapi/routing.py:665: in __init__
    self.dependant = get_dependant(
        callbacks  = []
        current_generate_unique_id = <function generate_unique_id at 0xff5f8a9dcb80>
        dependencies = []
        dependency_overrides_provider = <fastapi.applications.FastAPI object at 0xff5ee8cb7bf0>
        deprecated = None
        description = None
        endpoint   = <function FastAPIServer.setup_routes.<locals>.submit_query at 0xff5ed4a7aac0>
        generate_unique_id_function = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e38c0>
        include_in_schema = True
        methods    = ['POST']
        name       = None
        openapi_extra = None
        operation_id = None
        path       = '/submit_query'
        response_class = <fastapi.datastructures.DefaultPlaceholder object at 0xff5f8a9e3890>
        response_description = 'Successful Response'
        response_fields = {}
        response_model = None
        response_model_by_alias = True
        response_model_exclude = None
        response_model_exclude_defaults = False
        response_model_exclude_none = False
        response_model_exclude_unset = False
        response_model_include = None
        responses  = {}
        return_annotation = None
        self       = APIRoute(path='/submit_query', name='submit_query', methods=['POST'])
        status_code = None
        summary    = None
        tags       = []
.venv/lib/python3.12.../fastapi/dependencies/utils.py:279: in get_dependant
    param_details = analyze_param(
        call       = <function FastAPIServer.setup_routes.<locals>.submit_query at 0xff5ed4a7aac0>
        current_scopes = []
        dependant  = Dependant(path_params=[], query_params=[], header_params=[], cookie_params=[], body_params=[], dependencies=[], name=N...ram_name=None, own_oauth_scopes=None, parent_oauth_scopes=None, use_cache=True, path='/submit_query', scope='function')
        endpoint_signature = <Signature (query: str = Form(PydanticUndefined))>
        is_path_param = False
        name       = None
        own_oauth_scopes = None
        param      = <Parameter "query: str = Form(PydanticUndefined)">
        param_name = 'query'
        parent_oauth_scopes = None
        path       = '/submit_query'
        path_param_names = set()
        scope      = 'function'
        signature_params = mappingproxy(OrderedDict({'query': <Parameter "query: str = Form(PydanticUndefined)">}))
        use_cache  = True
.venv/lib/python3.12.../fastapi/dependencies/utils.py:502: in analyze_param
    ensure_multipart_is_installed()
        annotation = <class 'str'>
        depends    = None
        field      = None
        field_info = Form(PydanticUndefined)
        is_path_param = False
        param_name = 'query'
        type_annotation = <class 'str'>
        use_annotation = <class 'str'>
        use_annotation_from_field_info = <class 'str'>
        value      = Form(PydanticUndefined)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def ensure_multipart_is_installed() -> None:
        try:
            from python_multipart import __version__
    
            # Import an attribute that can be mocked/deleted in testing
            assert __version__ > "0.0.12"
        except (ImportError, AssertionError):
            try:
                # __version__ is available in both multiparts, and can be mocked
                from multipart import __version__  # type: ignore[no-redef,import-untyped]
    
                assert __version__
                try:
                    # parse_options_header is only available in the right multipart
                    from multipart.multipart import (  # type: ignore[import-untyped]
                        parse_options_header,
                    )
    
                    assert parse_options_header
                except ImportError:
                    logger.error(multipart_incorrect_install_error)
                    raise RuntimeError(multipart_incorrect_install_error) from None
            except ImportError:
                logger.error(multipart_not_installed_error)
>               raise RuntimeError(multipart_not_installed_error) from None
E               RuntimeError: Form data requires "python-multipart" to be installed. 
E               You can install "python-multipart" with: 
E               
E               pip install python-multipart


.venv/lib/python3.12.../fastapi/dependencies/utils.py:108: RuntimeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@kezaer kezaer force-pushed the fix/go2-lan-web-ui branch from c8c7309 to 24e11bc Compare June 8, 2026 06:40
@kezaer kezaer changed the title Make Go2 teleop web UIs LAN-aware fix(web): make Go2 teleop UIs LAN-aware Jun 8, 2026

kezaer commented Jun 8, 2026

Copy link
Copy Markdown
Author

Follow-up for the Codecov test_robot_web_interface_accepts_explicit_host failure: pushed 77608fd5d, which narrows that unit test so it verifies RobotWebInterface(host=...) forwarding without registering the full FastAPI routes and requiring optional python-multipart.

Local validation after the fix:

  • uv run pytest dimos/web/test_robot_web_interface.py dimos/web/templates/test_rerun_dashboard.py dimos/web/websocket_vis/test_websocket_vis_module.py dimos/teleop/phone/test_phone_teleop_module.py -q -> 6 passed
  • uv run ruff check ... -> passed

The new GitHub Actions runs for this fork PR are currently action_required with no jobs. I tried approving them through the API, but GitHub returned 403 Must have admin rights to Repository, so a maintainer needs to approve/rerun the workflow.

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.

1 participant