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:
Bohdan Triapitsyn
2026-07-10 03:36:10 +03:00
parent 26e88355e1
commit ba32518b88
13 changed files with 132 additions and 58 deletions
@@ -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) }));