Skip to content

Make Go2 teleop web UIs LAN-aware#2377

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

Make Go2 teleop web UIs LAN-aware#2377
kezaer wants to merge 2 commits into
dimensionalOS:mainfrom
kezaer:fix/go2-lan-web-ui

Conversation

@kezaer
Copy link
Copy Markdown

@kezaer kezaer commented Jun 6, 2026

Summary:

  • Build the Rerun dashboard iframe URLs from the current page host instead of hardcoded localhost/old ports.
  • Let query/config overrides select Rerun host and ports for non-default deployments.
  • Pass GlobalConfig.listen_host into the phone teleop and WebsocketVis web servers so --listen-host 0.0.0.0 works for LAN devices.
  • Add focused tests for dashboard URL generation and server host propagation.

Tests:

  • /Users/kezaer/Git/dimos/.venv/bin/python -m pytest 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
  • /Users/kezaer/Git/dimos/.venv/bin/ruff check dimos/teleop/phone/phone_teleop_module.py dimos/teleop/phone/test_phone_teleop_module.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

No robot motion commands were sent while validating this change.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 6, 2026

Greptile Summary

This PR makes the Go2 teleop and Rerun dashboard web UIs LAN-aware by replacing hardcoded localhost URLs with dynamic host/port resolution. Both the WebsocketVisModule and PhoneTeleopModule now read listen_host from their per-module GlobalConfig rather than a global singleton, and RobotWebInterface gets an explicit host keyword-only parameter.

  • The rerun_dashboard.html iframe URLs are now built in JavaScript from window.location.hostname, with query-param and window.DIMOS_RERUN_CONFIG override layers, IPv6 bracket handling, and protocol normalization that respects HTTPS.
  • WebsocketVisModule._run_uvicorn_server switches from the module-level global_config import to self.config.g.listen_host, and PhoneTeleopModule._create_web_server forwards the same field; RobotWebInterface.__init__ now declares host explicitly as a keyword-only argument and forwards it to FastAPIServer.
  • Focused pytest tests verify the URL-generation logic and server-host propagation for all three components.

Confidence Score: 5/5

Safe to merge — all changes are additive or are replacements of hardcoded values with dynamic equivalents, and each code path is covered by a new focused test.

The diff removes hardcoded localhost URLs, replaces a global singleton read with per-instance config reads, and adds an explicit keyword-only host parameter to RobotWebInterface. Each change is narrowly scoped, the fallback behavior when no host is supplied is preserved via FastAPIServer's existing default, and three new test modules verify the key behaviors. No incorrect logic or broken contracts were found.

No files require special attention.

Important Files Changed

Filename Overview
dimos/web/templates/rerun_dashboard.html Replaces hardcoded localhost iframe URLs with dynamic JavaScript that reads host/port from the current page, query params, or a config object; adds IPv6 bracket wrapping and protocol normalization.
dimos/web/robot_web_interface.py Adds explicit keyword-only host parameter to RobotWebInterface.__init__, forwarding it to FastAPIServer; resolves the previous thread concern about host silently landing in **streams.
dimos/web/websocket_vis/websocket_vis_module.py Removes module-level global_config import; _run_uvicorn_server now binds to self.config.g.listen_host for per-instance configurability.
dimos/teleop/phone/phone_teleop_module.py Extracts _create_web_server() helper that passes host=self.config.g.listen_host to RobotWebInterface, making phone teleop LAN-bindable.
dimos/web/templates/test_rerun_dashboard.py New test file that checks for key strings in the HTML to verify dynamic URL generation and absence of stale hardcoded values.
dimos/teleop/phone/test_phone_teleop_module.py New test that monkeypatches RobotWebInterface and verifies _create_web_server passes host and port correctly from module config.
dimos/web/websocket_vis/test_websocket_vis_module.py New test that patches uvicorn.Config/Server and verifies _run_uvicorn_server uses the module's configured listen_host.
dimos/web/test_robot_web_interface.py New test asserting that explicit host is stored on the interface and not leaked into self.streams.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Dashboard as rerun_dashboard.html
    participant WS as WebsocketVisModule<br/>(host=listen_host)
    participant Phone as PhoneTeleopModule<br/>(_create_web_server)
    participant RWI as RobotWebInterface<br/>(host kwarg)
    participant FastAPI as FastAPIServer

    Browser->>Dashboard: GET /dashboard (LAN IP)
    Dashboard->>Dashboard: window.location.hostname → rerunHost
    Dashboard->>Dashboard: params / DIMOS_RERUN_CONFIG overrides
    Dashboard->>Dashboard: hostForUrl() + normalizeHttpProtocol()
    Dashboard->>WS: "commandCenter.src = origin/command-center"
    Dashboard->>Dashboard: "rerun.src = protocol://host:9878/?url=rerun+grpc://..."

    Note over WS: uvicorn binds to self.config.g.listen_host
    WS->>FastAPI: "uvicorn.Config(host=self.config.g.listen_host)"

    Phone->>RWI: "_create_web_server(host=self.config.g.listen_host)"
    RWI->>FastAPI: "super().__init__(host=host, port=port)"
    FastAPI->>FastAPI: "self.host = host (explicit, not global_config)"
Loading

Reviews (2): Last reviewed commit: "Address LAN web UI review comments" | Re-trigger Greptile

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

codecov Bot commented Jun 6, 2026

❌ 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.

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