Fix textual-serve web: robust IP detection for WebSocket URL

The landing page wasn't interactive because public_url resolved to
localhost, making WebSocket connections fail from remote browsers.

- Added multiple IP detection methods (socket, hostname -I, gethostbyname)
- Support --port= and --host= flag formats
- Print actual serving URL on startup
- Switch web start back to textual-serve (TUI in browser)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 06:12:29 +02:00
parent 133ae1e7a4
commit fa7eb14369
3 changed files with 55 additions and 22 deletions

View File

@@ -402,10 +402,10 @@ if [[ "$SUBCOMMAND" == "web" ]]; then
_web_port="" _web_host="" _web_port="" _web_host=""
_web_port=$(_parse_flag "--port" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true _web_port=$(_parse_flag "--port" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
_web_host=$(_parse_flag "--host" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true _web_host=$(_parse_flag "--host" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
_web_args=() _web_args=(--web)
[[ -n "$_web_port" ]] && _web_args+=(--port="$_web_port") [[ -n "$_web_port" ]] && _web_args+=(--port="$_web_port")
[[ -n "$_web_host" ]] && _web_args+=(--host="$_web_host") [[ -n "$_web_host" ]] && _web_args+=(--host="$_web_host")
PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m web "${_web_args[@]}" PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "${_web_args[@]}"
;; ;;
install-service) install-service)
_service_src="$GNIZA_DIR/etc/gniza-web.service" _service_src="$GNIZA_DIR/etc/gniza-web.service"

View File

@@ -4,11 +4,9 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/python3 -m web ExecStart=/usr/bin/python3 -m tui --web --host 0.0.0.0 --port 8080
WorkingDirectory=/usr/local/gniza WorkingDirectory=/usr/local/gniza
Environment=GNIZA_DIR=/usr/local/gniza Environment=GNIZA_DIR=/usr/local/gniza
Environment=GNIZA_CONFIG_DIR=/etc/gniza
Environment=LOG_DIR=/var/log/gniza
Environment=PYTHONPATH=/usr/local/gniza Environment=PYTHONPATH=/usr/local/gniza
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5

View File

@@ -1,5 +1,6 @@
import os import os
import socket import socket
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -11,40 +12,74 @@ _ROOT = os.environ.get("GNIZA_DIR", str(Path(__file__).resolve().parent.parent))
def _get_local_ip() -> str: def _get_local_ip() -> str:
"""Get the machine's LAN IP for public_url.""" """Get the machine's LAN IP for public_url."""
# Method 1: UDP socket trick
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.connect(("8.8.8.8", 80)) s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0] ip = s.getsockname()[0]
s.close() s.close()
return ip if ip and ip != "0.0.0.0":
return ip
except Exception: except Exception:
return "localhost" pass
# Method 2: hostname -I
try:
result = subprocess.run(
["hostname", "-I"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
ip = result.stdout.strip().split()[0]
if ip:
return ip
except Exception:
pass
# Method 3: hostname resolution
try:
return socket.gethostbyname(socket.gethostname())
except Exception:
return "127.0.0.1"
def _parse_web_args():
"""Parse --port and --host from sys.argv."""
port = 8080
host = "0.0.0.0"
for i, arg in enumerate(sys.argv):
if arg.startswith("--port="):
port = int(arg.split("=", 1)[1])
elif arg == "--port" and i + 1 < len(sys.argv):
port = int(sys.argv[i + 1])
elif arg.startswith("--host="):
host = arg.split("=", 1)[1]
elif arg == "--host" and i + 1 < len(sys.argv):
host = sys.argv[i + 1]
return host, port
def main(): def main():
if "--web" in sys.argv: if "--web" in sys.argv:
from textual_serve.server import Server from textual_serve.server import Server
port = 8080
host = "0.0.0.0" host, port = _parse_web_args()
public_host = None
for i, arg in enumerate(sys.argv):
if arg == "--port" and i + 1 < len(sys.argv):
port = int(sys.argv[i + 1])
elif arg == "--host" and i + 1 < len(sys.argv):
host = sys.argv[i + 1]
public_host = host
os.environ["PYTHONPATH"] = f"{_ROOT}:{os.environ.get('PYTHONPATH', '')}" os.environ["PYTHONPATH"] = f"{_ROOT}:{os.environ.get('PYTHONPATH', '')}"
os.environ["GNIZA_DIR"] = _ROOT os.environ["GNIZA_DIR"] = _ROOT
# textual-serve uses public_url to build WebSocket URLs.
# If binding to 0.0.0.0, detect the real IP for the browser. # Determine public URL for WebSocket connections
if public_host is None: if host == "0.0.0.0":
public_host = _get_local_ip() if host == "0.0.0.0" else host public_host = _get_local_ip()
else:
public_host = host
public_url = f"http://{public_host}:{port}" public_url = f"http://{public_host}:{port}"
print(f"GNIZA web: serving TUI at {public_url}")
server = Server( server = Server(
"python3 -m tui", f"python3 -m tui",
host=host, host=host,
port=port, port=port,
title="gniza", title="GNIZA Backup",
public_url=public_url, public_url=public_url,
) )
server.serve() server.serve()