feat(desktop): live status in the servers list and a cleaner section header
- Each saved server row shows live reachability (Connected · Nms ping / Unreachable / Auth required) with a status dot, probed once per list change through the shared HTTP/relay probe (relay probing moved to desktopHosts as probeRelayDesktopHost, reused by the host switcher) - Section header: one short description, Import Link promoted to the primary action; the token-storage note moved into the Add Server dialog next to the token field it describes, and the dialog got its own description
This commit is contained in:
@@ -24,13 +24,12 @@ import {
|
||||
getDesktopHostApiUrl,
|
||||
locationMatchesHost,
|
||||
normalizeHostUrl,
|
||||
probeRelayDesktopHost,
|
||||
redactSensitiveUrl,
|
||||
resolveDesktopHostUrl,
|
||||
type DesktopHost,
|
||||
type DesktopHostRelay,
|
||||
type HostProbeResult,
|
||||
} from '@/lib/desktopHosts';
|
||||
import { createRelayTunnelClient } from '@/lib/relay/tunnel-client';
|
||||
import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch';
|
||||
import {
|
||||
desktopSshConnect,
|
||||
@@ -49,26 +48,6 @@ const runtimeKeyForHost = (host: DesktopHost): string => {
|
||||
return `host:${host.id}`;
|
||||
};
|
||||
|
||||
// Quick reachability check for a relay host: open a throwaway E2EE tunnel and
|
||||
// hit /health. Confirms the relay routes to the (still-online) host before we
|
||||
// commit the runtime switch, so an offline host surfaces as an error instead of
|
||||
// a broken runtime. The steady-state tunnel is opened by switchRuntimeEndpoint.
|
||||
const probeRelayHost = async (relay: DesktopHostRelay): Promise<boolean> => {
|
||||
const tunnel = createRelayTunnelClient({
|
||||
relayUrl: relay.relayUrl,
|
||||
serverId: relay.serverId,
|
||||
hostEncPubJwk: relay.hostEncPubJwk,
|
||||
});
|
||||
try {
|
||||
const response = await tunnel.fetch('/health');
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
tunnel.close();
|
||||
}
|
||||
};
|
||||
|
||||
type HostStatus = {
|
||||
status: HostProbeResult['status'];
|
||||
latencyMs: number;
|
||||
@@ -453,9 +432,8 @@ export function DesktopHostSwitcherDialog({
|
||||
// Relay hosts have no HTTP address to probe — check reachability
|
||||
// through a throwaway E2EE tunnel instead.
|
||||
if (h.relay) {
|
||||
const startedAt = performance.now();
|
||||
const ok = await probeRelayHost(h.relay).catch(() => false);
|
||||
return [h.id, { status: ok ? ('ok' as const) : ('unreachable' as const), latencyMs: Math.round(performance.now() - startedAt) } satisfies HostStatus] as const;
|
||||
const res = await probeRelayDesktopHost(h.relay).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
|
||||
return [h.id, { status: res.status, latencyMs: res.latencyMs } satisfies HostStatus] as const;
|
||||
}
|
||||
const url = normalizeHostUrl(isElectronShell() ? getDesktopHostApiUrl(h) : h.url);
|
||||
if (!url) {
|
||||
@@ -527,10 +505,11 @@ export function DesktopHostSwitcherDialog({
|
||||
// fetch/socket layers route through the tunnel from the singleton registry.
|
||||
if (host.relay) {
|
||||
setSwitchingHostId(host.id);
|
||||
const reachable = await probeRelayHost(host.relay).catch(() => false);
|
||||
const probe = await probeRelayDesktopHost(host.relay).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
|
||||
const reachable = probe.status === 'ok';
|
||||
setStatusById((prev) => ({
|
||||
...prev,
|
||||
[host.id]: { status: reachable ? 'ok' : 'unreachable', latencyMs: 0 },
|
||||
[host.id]: { status: probe.status, latencyMs: probe.latencyMs },
|
||||
}));
|
||||
if (!reachable) {
|
||||
toast.error(t('desktopHostSwitcher.toast.instanceUnreachable', { host: redactSensitiveUrl(host.label) }));
|
||||
|
||||
Reference in New Issue
Block a user