Files
openchamber/packages/web/src/main.tsx
T
Bohdan Triapitsyn 51e6ae7e3f feat(desktop): multi-transport hosts with relay fallback, card-style services dropdown
- A saved host now keeps every transport its pairing link carried: direct URL
  plus the relay descriptor, with one token for both (the mobile connection
  model). Switching tries the direct leg and falls back to the E2EE tunnel;
  list probes report Connected · Relay when only the tunnel reaches the host;
  relaunch restore picks direct first
- Host switching trusts the dropdown's fresh probe instead of re-probing on
  click (no doubled latency, no transient Unreachable flashes); statuses are
  written once with the final outcome, survive the dropdown closing via a
  last-known cache, and an unprobed host reads Checking — never Unknown
- Open-in-new-window works for relay hosts: a new IPC command boots the local
  UI with the host id injected and the renderer picks the transport; the app
  render holds on the relay restore so the splash shows instead of a transient
  auth screen (10s safety valve)
- Relay host control socket gained protocol-level keepalive: a missed pong
  window terminates and reconnects, so the relay can no longer hold a ghost
  registration that leaves every client tunnel hanging; the desktop relay
  probe also hard-times-out at 8s instead of hanging status flows
- Services dropdown restyled with mobile-style cards: per-provider usage
  cards, per-host instance cards with a selected highlight and a toned
  status line, MCP servers grouped in a card
2026-07-10 12:24:50 +03:00

125 lines
4.0 KiB
TypeScript

import { createConfiguredWebAPIs, getDesktopRelayRestoreReady } from './runtimeConfig';
import { registerSW } from 'virtual:pwa-register';
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
import { getStoredMobileLayoutPreference } from '@openchamber/ui/lib/mobileLayoutPreference';
import type { HostedSurface } from '@openchamber/ui/lib/runtimeSurface';
import '@openchamber/ui/index.css';
import '@openchamber/ui/styles/fonts';
declare global {
interface Window {
__OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
__OPENCHAMBER_SURFACE__?: HostedSurface;
}
}
window.__OPENCHAMBER_RUNTIME_APIS__ = createConfiguredWebAPIs();
const isCoarsePointer = (): boolean => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return false;
}
return window.matchMedia('(pointer: coarse)').matches;
};
const detectHostedSurface = (): HostedSurface => {
const params = new URLSearchParams(window.location.search);
const override = params.get('surface');
if (override === 'mobile') return 'mobile';
if (override === 'desktop') return 'desktop';
const width = Math.min(window.innerWidth || 0, window.screen?.width || window.innerWidth || 0);
const touchPoints = navigator.maxTouchPoints || 0;
const likelyPhone = width > 0 && width <= 760 && (touchPoints > 0 || isCoarsePointer());
return likelyPhone && getStoredMobileLayoutPreference() === 'new' ? 'mobile' : 'desktop';
};
const hostedSurface = detectHostedSurface();
window.__OPENCHAMBER_SURFACE__ = hostedSurface;
type PrerenderingDocument = Document & {
prerendering?: boolean;
};
const canUseServiceWorker = (): boolean => {
if (!('serviceWorker' in navigator)) return false;
if (!window.isSecureContext) return false;
if (window.location.protocol !== 'http:' && window.location.protocol !== 'https:') return false;
const documentState = document as PrerenderingDocument;
if (documentState.prerendering || String(document.visibilityState) === 'prerender') {
return false;
}
return true;
};
const runWhenDocumentCanRegisterServiceWorker = (task: () => void): void => {
let completed = false;
const run = () => {
if (completed) return;
if (canUseServiceWorker()) {
completed = true;
task();
}
};
const afterLoad = () => {
setTimeout(run, 0);
};
if (document.readyState === 'complete') {
afterLoad();
} else {
window.addEventListener('load', afterLoad, { once: true });
}
const documentState = document as PrerenderingDocument;
if (documentState.prerendering || String(document.visibilityState) === 'prerender') {
document.addEventListener('visibilitychange', run, { once: true });
}
};
const registerPwaServiceWorker = (): void => {
runWhenDocumentCanRegisterServiceWorker(() => {
try {
registerSW({
onRegisterError(error: unknown) {
console.warn('[PWA] service worker registration skipped:', error);
},
});
} catch (error) {
console.warn('[PWA] service worker registration skipped:', error);
}
});
};
const unregisterDevelopmentServiceWorkers = (): void => {
runWhenDocumentCanRegisterServiceWorker(() => {
void navigator.serviceWorker.getRegistrations()
.then((registrations) => Promise.all(registrations.map((registration) => registration.unregister())))
.catch(() => {});
});
};
if (hostedSurface === 'mobile') {
void import('@openchamber/ui/apps/renderMobileApp')
.then(({ renderMobileApp }) => {
renderMobileApp(window.__OPENCHAMBER_RUNTIME_APIS__ ?? createConfiguredWebAPIs());
});
} else {
// Hold the render (HTML splash stays up) until a desktop relay-host restore
// has picked its transport — otherwise the app boots against a not-yet-chosen
// endpoint and flashes the auth screen before the tunnel connects. Resolves
// immediately when no relay host is involved.
void getDesktopRelayRestoreReady().then(() => import('@openchamber/ui/main'));
}
if (import.meta.env.PROD) {
registerPwaServiceWorker();
} else {
unregisterDevelopmentServiceWorkers();
}