--lan startup: lead with the default-route address, demote the rest

A multi-homed machine (VM bridges, Ethernet + Wi-Fi) has several
addresses and the server cannot know which network the phone is on —
but the OS's default route is the right answer nearly always. The
banner now prints one "on your phone" URL from the route probe, with
the other interfaces on an if-that-doesn't-answer line; when the probe
fails, the settings hint plus candidates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 16:21:26 -04:00
co-authored by Claude Fable 5
parent eb2e841d27
commit 95fd18ca5c
5 changed files with 119 additions and 29 deletions
+43 -22
View File
@@ -247,6 +247,21 @@ NAV_PAGES = (
PHOTO_SUFFIXES = {".jpg", ".jpeg", ".png", ".heic"}
def _route_ip() -> str | None:
"""The IPv4 address of this machine's default outbound interface — the
single best guess for the address a phone on the same network dials.
Multi-homed machines (VM bridges, Ethernet + Wi-Fi) have several
addresses; this is the one the OS actually routes through."""
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
probe.connect(("192.0.2.1", 80)) # never sent; just picks the route
address = probe.getsockname()[0]
probe.close()
return address
except OSError:
return None
def lan_hosts() -> set[str]:
"""This machine's names and addresses on the local network — what a
phone's browser will put in the Host header. An allowlist (never a
@@ -256,13 +271,8 @@ def lan_hosts() -> set[str]:
hostname = socket.gethostname()
hosts.add(hostname.lower())
hosts.add(hostname.split(".")[0].lower() + ".local")
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
probe.connect(("192.0.2.1", 80)) # never sent; just picks the route
hosts.add(probe.getsockname()[0])
probe.close()
except OSError:
pass
if route := _route_ip():
hosts.add(route)
try:
for info in socket.getaddrinfo(hostname, None):
address = info[4][0]
@@ -1091,27 +1101,38 @@ def run_web_review(
# type it once; the cookie remembers it from then on.
token = secrets.token_urlsafe(16) if lan else None
if lan:
hosts = lan_hosts()
ips = sorted(
h
for h in hosts
if h.replace(".", "").isdigit() and not h.startswith("127.")
primary = _route_ip()
spares = sorted(
h for h in lan_hosts() if h != primary and not h.startswith("127.")
)
names = sorted(h for h in hosts if not h.replace(".", "").isdigit())
addresses = ", ".join(f"http://{h}:{port}/?k={token}" for h in ips + names)
typer.echo(f"bggpipe web UI: {url}?k={token}")
typer.echo(f" from your phone, open: {addresses}")
typer.echo(f"bggpipe web UI: {url} (this machine needs no key)")
if primary:
typer.echo(f" on your phone, open: http://{primary}:{port}/?k={token}")
if spares:
typer.echo(
" (several network interfaces here — if that address "
"doesn't answer, try: "
+ ", ".join(f"http://{h}:{port}/?k={token}" for h in spares)
+ ")"
)
else:
typer.echo(
" couldn't determine this machine's network address — find "
"it in your network settings and open "
f"http://<that-ip>:{port}/?k={token}"
+ (
" (or try: "
+ ", ".join(f"http://{h}:{port}/?k={token}" for h in spares)
+ ")"
if spares
else ""
)
)
typer.echo(
" --lan: no login beyond that key — anyone who has it can run "
"stages, change your data, and (once unlocked) drive uploads "
"to your BGG account. Use only on a network you trust."
)
if not ips:
typer.echo(
" couldn't determine this machine's network address — find "
"it in your network settings and open "
f"http://<that-ip>:{port}/?k={token}"
)
else:
typer.echo(
f"bggpipe web UI: {url} (localhost only; dashboard at /, review "