Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion dimos/teleop/phone/phone_teleop_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def __init__(self, **kwargs: Any) -> None:
self._stop_event = threading.Event()

# Embedded web server — RobotWebInterface provides FastAPI app + run()/shutdown()
self._web_server = RobotWebInterface(port=self.config.server_port)
self._web_server = self._create_web_server()
self._web_server_thread: threading.Thread | None = None

# Fingerprint-based message dispatch table
Expand All @@ -96,6 +96,12 @@ def __init__(self, **kwargs: Any) -> None:

self._setup_routes()

def _create_web_server(self) -> RobotWebInterface:
return RobotWebInterface(
host=self.config.g.listen_host,
port=self.config.server_port,
)
Comment thread
kezaer marked this conversation as resolved.

def _setup_routes(self) -> None:
"""Register teleop routes on the embedded web server."""

Expand Down
39 changes: 39 additions & 0 deletions dimos/teleop/phone/test_phone_teleop_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any

from dimos.core.global_config import GlobalConfig
from dimos.teleop.phone import phone_teleop_module
from dimos.teleop.phone.phone_teleop_module import PhoneTeleopConfig, PhoneTeleopModule


def test_phone_teleop_web_server_uses_module_global_config_listen_host(monkeypatch) -> None:
captured: dict[str, Any] = {}

class FakeRobotWebInterface:
def __init__(self, **kwargs: Any) -> None:
captured.update(kwargs)

monkeypatch.setattr(phone_teleop_module, "RobotWebInterface", FakeRobotWebInterface)

module = PhoneTeleopModule.__new__(PhoneTeleopModule)
module.config = PhoneTeleopConfig(
server_port=8444,
g=GlobalConfig(listen_host="0.0.0.0"),
)

module._create_web_server()

assert captured == {"host": "0.0.0.0", "port": 8444}
26 changes: 23 additions & 3 deletions dimos/web/templates/rerun_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,35 @@
</head>
<body>
<div class="container">
<iframe class="command-center" src="http://localhost:7779/command-center"></iframe>
<iframe class="command-center"></iframe>
<div class="divider" role="separator" aria-label="Resize panels"></div>
<iframe class="rerun" src="http://localhost:9090/?url=rerun%2Bhttp%3A%2F%2Flocalhost%3A9876%2Fproxy"></iframe>
<iframe class="rerun"></iframe>
</div>
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const config = window.DIMOS_RERUN_CONFIG || {};
const defaultHost = window.location.hostname || '127.0.0.1';
const rerunHost = params.get('rerunHost') || config.host || defaultHost;
const rerunGrpcPort = params.get('rerunGrpcPort') || config.grpcPort || '9877';
const rerunWebPort = params.get('rerunWebPort') || config.webPort || '9878';
const commandCenter = document.querySelector('.command-center');
const rerun = document.querySelector('.rerun');

function hostForUrl(hostname) {
return hostname.includes(':') && !hostname.startsWith('[')
? `[${hostname}]`
: hostname;
}

const rerunUrlHost = hostForUrl(rerunHost);
const rerunGrpcUrl = `rerun+http://${rerunUrlHost}:${rerunGrpcPort}/proxy`;

commandCenter.src = `${window.location.origin}/command-center`;
rerun.src = `http://${rerunUrlHost}:${rerunWebPort}/?url=${encodeURIComponent(rerunGrpcUrl)}`;
Comment thread
kezaer marked this conversation as resolved.
Outdated

const container = document.querySelector('.container');
const divider = document.querySelector('.divider');
const commandCenter = document.querySelector('.command-center');
const body = document.body;
const minWidth = 0; // 16rem baseline for small screens

Expand Down
35 changes: 35 additions & 0 deletions dimos/web/templates/test_rerun_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pathlib import Path


def test_rerun_dashboard_uses_current_host_and_rerun_ports() -> None:
html = Path(__file__).with_name("rerun_dashboard.html").read_text()

assert "window.location.origin}/command-center" in html
assert "window.location.hostname" in html
assert "rerunGrpcPort" in html
assert "rerunWebPort" in html
assert "9877" in html
assert "9878" in html


def test_rerun_dashboard_does_not_use_stale_rerun_defaults() -> None:
html = Path(__file__).with_name("rerun_dashboard.html").read_text()

assert "localhost:9090" not in html
assert "localhost:9876" not in html
assert ":9090" not in html
assert ":9876" not in html
51 changes: 51 additions & 0 deletions dimos/web/websocket_vis/test_websocket_vis_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any

from dimos.core.global_config import GlobalConfig
from dimos.web.websocket_vis import websocket_vis_module
from dimos.web.websocket_vis.websocket_vis_module import WebsocketConfig, WebsocketVisModule


def test_websocket_vis_uses_module_global_config_listen_host(monkeypatch) -> None:
captured: dict[str, Any] = {}

class FakeServer:
def __init__(self, config: Any) -> None:
captured["config"] = config

def run(self) -> None:
captured["ran"] = True

def fake_config(app: Any, **kwargs: Any) -> dict[str, Any]:
captured["app"] = app
captured["kwargs"] = kwargs
return kwargs

monkeypatch.setattr(websocket_vis_module.uvicorn, "Config", fake_config)
monkeypatch.setattr(websocket_vis_module.uvicorn, "Server", FakeServer)

module = WebsocketVisModule.__new__(WebsocketVisModule)
module.app = object()
module.config = WebsocketConfig(
port=7779,
g=GlobalConfig(listen_host="0.0.0.0"),
)

module._run_uvicorn_server()

assert captured["kwargs"]["host"] == "0.0.0.0"
assert captured["kwargs"]["port"] == 7779
assert captured["ran"] is True
3 changes: 1 addition & 2 deletions dimos/web/websocket_vis/websocket_vis_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@

from dimos.constants import DEFAULT_THREAD_JOIN_TIMEOUT
from dimos.core.core import rpc
from dimos.core.global_config import global_config
from dimos.core.module import Module, ModuleConfig
from dimos.core.stream import In, Out
from dimos.mapping.models import LatLon
Expand Down Expand Up @@ -355,7 +354,7 @@ async def move_command(sid: str, data: dict[str, Any]) -> None:
def _run_uvicorn_server(self) -> None:
config = uvicorn.Config(
self.app, # type: ignore[arg-type]
host=global_config.listen_host,
host=self.config.g.listen_host,
port=self.config.port,
log_level="error", # Reduce verbosity
)
Expand Down
Loading