#224

Move the web connection stack into environment-client so the phone can share it

Opencalledforthwants to merge intofrom

Summary

The environment connection model (about 1.9k lines that were already DOM-free) lived in apps/web/src/lib, where nothing outside the web app could import it. It now lives in packages/environment-client/src/connection/ and is exported as @openmanager/environment-client/connection, so the React Native app can use the same code.

What moved (with git mv, so history follows)

bootstrap, pairing, route-fallback, route-health, connection-state, environment-store and their tests. The tests now live in packages/environment-client/tests/connection/ (the package's test home) and run in a node environment, which also shows they need no DOM. The subpath is a source-only exports entry declared exactly like the package root (types + default pointing at ./src/connection/index.ts). Web resolves it the same way it already resolves the root.

Seams

  • Registry storage. readEnvironmentRegistry, writeEnvironmentRegistry and clearEnvironmentRegistry take a RegistryStorage (getItem, setItem, removeItem). They used to fall back to window.localStorage themselves. Web now passes browserRegistryStorage (apps/web/src/lib/registry-storage.ts), which looks up window.localStorage on each call. It does this because reading window.localStorage itself throws where storage is blocked, and that throw has to happen inside the registry functions, which already read it as an empty or unsaved registry.
  • Loopback permission. fetchBootstrap, probeRouteHealth and searchRoutes take a loopbackAccessDenied check. It is only asked when a fetch to this device's loopback address fails, so a refused browser permission shows as "blocked" rather than "nothing listening". The check is required, not defaulted. If it were optional, a caller that forgot it would quietly lose the "blocked" answer, and the types would not complain. A host with no such gate passes noLoopbackGate explicitly. searchRoutes only asks for the check when no custom probe is supplied. Web passes its existing loopbackAccessDenied at every call site.

What stayed browser-only, and why

  • pairingAppUrl reads window.location and import.meta.env.BASE_URL, which only make sense for the web app. It is now in apps/web/src/lib/pairing-app-url.ts.
  • loopbackAccessDenied reads navigator.permissions. It stays in apps/web/src/lib/local-access.ts and is injected as described above.
  • local-owner.ts, environment-cache.ts, browser-runtime.ts and providers/connection-provider.tsx stay in apps/web unchanged, apart from import paths and passing the two seams.

Keeping the folder host-neutral

The root eslint.config.mjs adds no-restricted-globals for window, document, localStorage, navigator, indexedDB and location on packages/environment-client/src/connection/**. It also adds no-restricted-properties for the globalThis. / self. forms of the same names. I checked the rule by planting a file that uses each one: lint reported all six.

Web behaviour unchanged

  • No protocol change. Error messages, timeouts, registry versioning and the legacy-key migration are the same.
  • The story-fixture checks that ran deriveConnectionUi over the web stories now sit next to those stories (apps/web/src/stories/connection-states.test.ts), so the package does not depend on web fixtures.
  • environment-client-provider.test.tsx still drives a real loopback-network: denied permission through ConnectionProvider to local_access_blocked. That covers the injected check end to end.
  • New tests cover the storage seam on web (registry-storage.test.ts, including a throwing localStorage getter) and the loopback seam in the package (the check is asked for loopback routes and never for remote ones, for fetchBootstrap, probeRouteHealth and searchRoutes).

Verification

  • pnpm --filter @openmanager/environment-client typecheck, lint, test: 16 files, 374 tests pass
  • pnpm --filter @openmanager/web typecheck, lint: clean
  • Web vitest on Node 24.14.1: 25 files, 236 tests pass
  • Prettier is clean on every file I touched. Four files were already unformatted on main (the three docs and connection-state.ts, which moved unchanged), and I left those as they were.

🤖 Generated with Claude Code

Activity

6
greptile-apps[bot]10d ago


[Medium impact] The PR appears safe to merge; the documentation update matches the storage code.

Summary

Moves the shared connection code into @openmanager/environment-client/connection.

  • Web and phone clients share the environment connection flow.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Web[Web app] --> Shared[environment-client/connection]
  Phone[Phone app] -. Can reuse .-> Shared
  Web --> Storage[Browser storage adapter]
  Web --> Permission[Loopback permission check]
  Storage --> Shared
  Permission --> Shared

Reviews (2) · Last reviewed commit: "Describe web's registry storage adapter ..." · Reviewed by Greptile

cloudflare-workers-and-pages[bot]10d ago

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

StatusDeployment URLCommitUpdated (UTC)See this deployment's details
  • Build: Failed ❌

View logs ↗
09761b02026-10-20T17:19:38.195ZView logs ↗
  • Build: Failed ❌

View logs ↗
1df8b592026-10-20T16:33:10.687ZView logs ↗