From 61f06fcce8732ad69874ce5c34bbbde4361cce1c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Sep 2026 23:00:33 +0100 Subject: [PATCH] fix: docsgpt open points at the address a native install answers on `open` built its address from stack.url, which honours DOCSGPT_BIND, while the native units always bind 127.0.0.1: with a LAN bind it handed the browser an address nothing was listening on. status had the same mismatch and was fixed with it; both now go through one helper so they cannot drift apart again. --- docsgpt/deploy/commands.py | 22 +++++++++++++++++----- tests/deploy/test_native.py | 12 ++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docsgpt/deploy/commands.py b/docsgpt/deploy/commands.py index 45b9c461..aad99560 100644 --- a/docsgpt/deploy/commands.py +++ b/docsgpt/deploy/commands.py @@ -240,6 +240,16 @@ def _docsgpt_launcher() -> list[str]: ) +def _native_port(env: Mapping[str, str]) -> str: + """The port a native install listens on.""" + return str(env.get("DOCSGPT_PORT") or stack.DEFAULT_PORT) + + +def _native_address(env: Mapping[str, str]) -> str: + """Where a native install answers: its units bind 127.0.0.1, whatever DOCSGPT_BIND says.""" + return f"http://localhost:{_native_port(env)}" + + def _native_up(args, context: Context, directory: Path) -> int: """Run the API and the worker as services on this machine, against an existing Postgres and Redis.""" services = context.service_manager() @@ -445,12 +455,11 @@ def status(args, context: Optional[Context] = None) -> int: return 1 if _mode(directory) == "native": services = context.service_manager() - # The units bind 127.0.0.1 whatever DOCSGPT_BIND says, so that is what gets printed and checked: - # against a LAN bind, stack.url and stack.health_url would advertise and poll an address + # Against a LAN bind, stack.url and stack.health_url would advertise and poll an address # nothing listens on, and status would call a healthy install dead. - port = env.get("DOCSGPT_PORT") or stack.DEFAULT_PORT + port = _native_port(env) print(f"DocsGPT {_record(directory).get('version', 'unknown')} in {directory} (native, {services.name})") - print(f"Address: http://localhost:{port}") + print(f"Address: {_native_address(env)}") for name in native.SERVICES: print(f" {name}: {'running' if services.is_running(name) else 'stopped'}") healthy = context.wait(f"http://127.0.0.1:{port}/api/health", 0) @@ -510,7 +519,10 @@ def open_ui(args, context: Optional[Context] = None) -> int: env = _installed(directory) if env is None: return 1 - address = stack.url(env, context.lan_ip()) + # A native install answers on loopback only, so stack.url would hand the browser a LAN address + # or a domain that nothing behind this command is serving. + native_install = _mode(directory) == "native" + address = _native_address(env) if native_install else stack.url(env, context.lan_ip()) print(address) context.open_browser(address) return 0 diff --git a/tests/deploy/test_native.py b/tests/deploy/test_native.py index 54cb681d..39a8938c 100644 --- a/tests/deploy/test_native.py +++ b/tests/deploy/test_native.py @@ -209,6 +209,18 @@ class TestNativeLifecycle: assert _run(["status", "--dir", str(tmp_path)], context) == 0 assert checked == ["http://127.0.0.1:7091/api/health"] + def test_open_hands_the_browser_the_address_that_answers(self, tmp_path, capsys): + """stack.url would offer a LAN address, but the native units listen on loopback only.""" + services = FakeServices() + self._installed(tmp_path, services) + envfile.update(tmp_path / ".env", {"DOCSGPT_BIND": "192.168.1.50"}) + opened = [] + context = _native_context(services) + context.open_browser = lambda url: opened.append(url) + assert _run(["open", "--dir", str(tmp_path)], context) == 0 + assert opened == ["http://localhost:7091"] + assert "192.168.1.50" not in capsys.readouterr().out + def test_down_stops_them_and_leaves_the_settings(self, tmp_path): services = FakeServices() self._installed(tmp_path, services)