Skip to content

fix(browse): stop the hosted dashboard blaming the coordinator - #113

Merged
jwaldrip merged 1 commit into
mainfrom
fix/dashboard-lna-guidance
Aug 12, 2026
Merged

jwaldrip merged 1 commit into
mainfrom
fix/dashboard-lna-guidance

Conversation

@jwaldrip

Copy link
Copy Markdown
Collaborator

Problem

#112 made han browse serve the dashboard from 127.0.0.1, which works. It left the hosted URL reachable and permanently broken. Opening https://dashboard.local.han.guru still shows:

Connecting to Han Coordinator...

  1. Start a Claude Code session — the coordinator starts automatically with any Claude Code session
  2. Or start manually: han coordinator start
  3. Need to install Han? curl -fsSL https://han.guru/install.sh | bash
    Checking every 3 seconds...

Every line of that is wrong. The coordinator is running and answering:

curl https://coordinator.local.han.guru:41957/health -> HTTP 200

Anyone with that URL bookmarked, or following a link, gets a spinner forever and a list of remedies for a problem they do not have.

Why the page cannot fix itself

Chrome 151 gates a public-origin page reaching a loopback address behind the local-network-access permission, and the page has no way to obtain it. Measured on the live origin:

Attempt Result
plain fetch THREW Failed to fetch
fetch with targetAddressSpace: "loopback" THREW Failed to fetch
same, inside a real click handler (user gesture) THREW Failed to fetch
permissions.query({name:'local-network-access'}) after all of it "prompt"
navigator.requestLocalNetworkAccess undefined

targetAddressSpace is accepted as a Request option, so the hint is understood and still refused. Access-Control-Allow-Private-Network, added in #111, is the superseded PNA mechanism and Chrome 151 does not act on it. There is no server-side or client-side move that makes this origin work.

Change

ConnectionOverlay branches on whether the page can reach loopback at all.

Blocked means page origin is public and the coordinator is a loopback target. Both halves matter: a page already on loopback is unrestricted, and a coordinator on a routable address is not gated, so in either of those cases a failure really does mean the coordinator is unreachable and the existing advice is right.

When blocked it names the cause, quotes the host it was served from, and points at han browse. No spinner and no "checking every 3 seconds", because waiting cannot grant a permission.

coordinator.local.han.guru counts as loopback: it is a public DNS name pointing at 127.0.0.1, so it is a loopback target that does not look like one.

Verification

Real Chrome 151 against the real build, both origins.

Served from 10.4.1.221:4901:

showsRealCause:    true
blamesCoordinator: false
spinnerCopy:       false

"Open the dashboard from your machine
 Your coordinator may well be running. This page is served from 10.4.1.221:4901,
 and browsers block a page on a public address from reaching one on your own
 machine, so it cannot read the coordinator from here.
 1  Run this instead:  han browse"

Served from 127.0.0.1:4902, same bundle:

showsBlockedPanel: false
stillConnecting:   false
stuckPanels:       0
Dashboard -> 76 day streak, 159 active days, +597.0K lines added
  • 16 unit tests pass (11 new), tsc clean, relay clean, build clean
  • Lint findings 10, unchanged from main's baseline of 10; only the four files this branch touches were formatted
  • Both render tests were falsified by neutering the detector, which turns exactly the three origin-dependent assertions red

Note

This makes the hosted origin honest. It does not make it work, and nothing can while Chrome gates it. If the hosted dashboard should genuinely function some day, it needs a coordinator reachable at a routable address, which is a different product.

Opening https://dashboard.local.han.guru showed "Connecting to Han
Coordinator..." forever, told the visitor to run `han coordinator start`, and
offered install instructions. All of it wrong: the coordinator answers /health
with 200 while that page spins.

Chrome 151 gates a public-origin page from reaching a loopback address behind
the local-network-access permission, and the page cannot obtain it. A fetch
carrying `targetAddressSpace: "loopback"` fails with and without a user gesture,
and the permission stays at "prompt" either way. There is no
navigator.requestLocalNetworkAccess. So that origin never connects, whatever the
visitor does to their coordinator.

The overlay now branches. When the page is public and the coordinator is a
loopback target, it names the cause, quotes the host it was served from, and
points at `han browse`, which serves the same dashboard from 127.0.0.1. No
spinner and no "checking every 3 seconds", because waiting cannot grant a
permission. On a loopback origin the coordinator instructions are correct and
are kept.

Verified in Chrome 151 against the real build: served from 10.4.1.221 it renders
the cause and remedy with no coordinator blame; served from 127.0.0.1 it
connects and renders the dashboard with no panels left loading. Both render
tests were falsified by neutering the detector.
@railway-app
railway-app Bot temporarily deployed to han-team-platform / han-pr-113 August 12, 2026 21:52 Destroyed
@railway-app

railway-app Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the han-pr-113 environment in han-team-platform

Service Status Web Updated (UTC)
website ✅ Success (View Logs) Aug 12, 2026 at 9:53 pm
3 services not affected by this PR
  • postgres
  • redis
  • api

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review

Solid, well-scoped fix. The diagnosis (Chrome 151's local-network-access gate has no client-side escape hatch) is verified with real measurements in the PR body, the heuristic (public origin + loopback coordinator target) is the right shape for the problem, and the new copy replaces a permanently-wrong message with an accurate one. ConnectionOverlay is only mounted by ConnectionGate while actually disconnected, so the heuristic never intercepts a connection that would otherwise succeed — good separation of concerns.

No CRITICAL issues found.

MODERATE

Duplicated hostname literal risks silent regression (packages/browse-client/src/helpers/localNetworkAccess.ts:23-28, packages/browse-client/src/config/urls.ts:74-76)
"coordinator.local.han.guru" is hardcoded independently in LOOPBACK_HOSTNAMES (localNetworkAccess.ts) and as the default endpoint host (urls.ts). If the production coordinator hostname is ever changed in one place (rename, new subdomain, per-deployment override baked in at build time) without updating the other, coordinatorIsLoopback() silently stops recognizing it as loopback, isBlockedByLocalNetworkAccess() returns false, and the dashboard regresses to exactly the bug this PR fixes — the permanent "Connecting to Han Coordinator..." spinner — with no test or type error to catch it. Consider deriving the loopback hostname list from the same source getGraphQLEndpoints() uses (or exporting the literal once and importing it in both places) so the two can't drift.

MINOR

  • LOOPBACK_HOSTNAMES includes "[::1]" (localNetworkAccess.ts:27), but window.location.hostname and new URL(...).hostname both strip brackets from IPv6 literals, so this entry can never match through either call path — dead code.
  • The loopback regex /^127(?:\.\d{1,3}){3}$/ (localNetworkAccess.ts:32) doesn't range-check octets, so a string like "127.999.999.999" would be classified as loopback. Harmless for this heuristic's purpose, just noting it's not a real IP validator.
  • Hostname matching is case-sensitive via Set.has / regex; a non-lowercased hostname wouldn't match "coordinator.local.han.guru". Browsers normalize location.hostname to lowercase so this is unlikely to bite in practice.

Nice to have

Test coverage is thorough for the new logic (11 new tests across the helper and the component, including the "falsify by neutering the detector" check described in the PR body). No changes requested there.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jwaldrip
jwaldrip merged commit 7d2d317 into main Aug 12, 2026
9 checks passed
@jwaldrip
jwaldrip deleted the fix/dashboard-lna-guidance branch August 12, 2026 21:59

This branch was successfully deployed

No deployments
han-team-platform / han-pr-113 — 1f33eaf3 Deployed Aug 12, 2026 by railway-app[bot]
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