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
+22
View File
@@ -1,4 +1,5 @@
import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop';
import { createRelayTunnelClient } from '@/lib/relay/tunnel-client';
type DesktopInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
@@ -287,6 +288,27 @@ export const desktopInstallIdGet = async (): Promise<string> => {
return typeof raw === 'string' ? raw.trim() : '';
};
/**
* Reachability check for a relay host: open a throwaway E2EE tunnel and hit
* /health. Relay hosts have no HTTP address for `desktopHostProbe`.
*/
export const probeRelayDesktopHost = async (relay: DesktopHostRelay): Promise<HostProbeResult> => {
const tunnel = createRelayTunnelClient({
relayUrl: relay.relayUrl,
serverId: relay.serverId,
hostEncPubJwk: relay.hostEncPubJwk,
});
const startedAt = Date.now();
try {
const response = await tunnel.fetch('/health');
return { status: response.ok ? 'ok' : 'unreachable', latencyMs: Math.max(0, Date.now() - startedAt) };
} catch {
return { status: 'unreachable', latencyMs: 0 };
} finally {
tunnel.close();
}
};
export const desktopHostProbe = async (url: string, options?: { clientToken?: string | null; requestHeaders?: Record<string, string> | null }): Promise<HostProbeResult> => {
const invoke = getInvoke();
if (!invoke) {
@@ -253,7 +253,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': 'Server links',
'settings.remoteInstances.direct.sidebarDescription': 'Connect with a link or token',
'settings.remoteInstances.direct.title': 'Other OpenChamber servers',
'settings.remoteInstances.direct.description': 'Add another OpenChamber server by URL. Use this when the server is already running and you have a connection token.',
'settings.remoteInstances.direct.description': 'Servers this app can switch to. Import a pairing link from the other server, or add one by address.',
'settings.remoteInstances.direct.addDialog.description': 'Add another OpenChamber server by URL. Use this when the server is already running and you have a connection token.',
'settings.remoteInstances.direct.field.labelPlaceholder': 'Label (optional)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://host:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': 'Connection token (optional for trusted local servers)',
@@ -220,7 +220,8 @@ export const settingsDict = {
"settings.remoteInstances.direct.sidebarTitle": "Enlaces a servidores",
"settings.remoteInstances.direct.sidebarDescription": "Conecta con un enlace o token",
"settings.remoteInstances.direct.title": "Otros servidores de OpenChamber",
"settings.remoteInstances.direct.description": "Añade otro servidor de OpenChamber por URL. Úsalo cuando el servidor ya esté en marcha y tengas un token de conexión.",
"settings.remoteInstances.direct.description": "Servidores a los que esta aplicación puede cambiar. Importa un enlace de conexión del otro servidor o añádelo por dirección.",
"settings.remoteInstances.direct.addDialog.description": "Añade otro servidor de OpenChamber por URL. Úsalo cuando el servidor ya esté en marcha y tengas un token de conexión.",
"settings.remoteInstances.direct.field.labelPlaceholder": "Etiqueta (opcional)",
"settings.remoteInstances.direct.field.urlPlaceholder": "https://host:port",
"settings.remoteInstances.direct.field.tokenPlaceholder": "Token de conexión (opcional para servidores locales de confianza)",
@@ -1761,7 +1761,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': 'Liens de serveur',
'settings.remoteInstances.direct.sidebarDescription': 'Se connecter avec un lien ou un token',
'settings.remoteInstances.direct.title': 'Autres serveurs OpenChamber',
'settings.remoteInstances.direct.description': 'Ajoutez un autre serveur OpenChamber par URL. Utilisez ceci lorsque le serveur est déjà lancé et que vous disposez d’un token de connexion.',
'settings.remoteInstances.direct.description': 'Serveurs vers lesquels cette application peut basculer. Importez un lien de connexion depuis un autre serveur ou ajoutez-le par adresse.',
'settings.remoteInstances.direct.addDialog.description': 'Ajoutez un autre serveur OpenChamber par URL. Utilisez ceci lorsque le serveur est déjà lancé et que vous disposez d’un token de connexion.',
'settings.remoteInstances.direct.field.labelPlaceholder': 'Libellé (facultatif)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://hôte:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': 'Token de connexion (facultatif pour les serveurs locaux de confiance)',
@@ -253,7 +253,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': 'サーバーリンク',
'settings.remoteInstances.direct.sidebarDescription': 'リンクまたは Token で接続',
'settings.remoteInstances.direct.title': 'その他の OpenChamber サーバー',
'settings.remoteInstances.direct.description': 'URL で別の OpenChamber サーバーを追加します。サーバーが既に実行中で接続 Token がある場合に使用します。',
'settings.remoteInstances.direct.description': 'このアプリが切り替えられるサーバーです。相手サーバーからペアリングリンクを取り込むか、アドレスで追加します。',
'settings.remoteInstances.direct.addDialog.description': 'URL で別の OpenChamber サーバーを追加します。サーバーが既に実行中で接続 Token がある場合に使用します。',
'settings.remoteInstances.direct.field.labelPlaceholder': 'ラベル(任意)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://host:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': '接続 Token(信頼できるローカルサーバーでは任意)',
@@ -220,7 +220,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': '서버 링크',
'settings.remoteInstances.direct.sidebarDescription': '링크나 토큰으로 연결',
'settings.remoteInstances.direct.title': '다른 OpenChamber 서버',
'settings.remoteInstances.direct.description': 'URL로 다른 OpenChamber 서버를 추가합니다. 서버가 이미 실행 중이고 연결 토큰이 있을 때 사용하세요.',
'settings.remoteInstances.direct.description': '이 앱이 전환할 수 있는 서버입니다. 다른 서버의 페어링 링크를 가져오거나 주소로 추가하세요.',
'settings.remoteInstances.direct.addDialog.description': 'URL로 다른 OpenChamber 서버를 추가합니다. 서버가 이미 실행 중이고 연결 토큰이 있을 때 사용하세요.',
'settings.remoteInstances.direct.field.labelPlaceholder': '라벨(선택 사항)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://host:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': '연결 토큰(신뢰할 수 있는 로컬 서버는 선택 사항)',
@@ -1449,7 +1449,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': 'Linki do serwerów',
'settings.remoteInstances.direct.sidebarDescription': 'Połącz przez link lub token',
'settings.remoteInstances.direct.title': 'Inne serwery OpenChamber',
'settings.remoteInstances.direct.description': 'Dodaj inny serwer OpenChamber przez URL. Użyj tego, gdy serwer już działa i masz token połączenia.',
'settings.remoteInstances.direct.description': 'Serwery, na które ta aplikacja może się przełączać. Zaimportuj link parowania z innego serwera lub dodaj przez adres.',
'settings.remoteInstances.direct.addDialog.description': 'Dodaj inny serwer OpenChamber przez URL. Użyj tego, gdy serwer już działa i masz token połączenia.',
'settings.remoteInstances.direct.field.labelPlaceholder': 'Etykieta (opcjonalnie)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://host:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': 'Token połączenia (opcjonalny dla zaufanych serwerów lokalnych)',
@@ -220,7 +220,8 @@ export const settingsDict = {
"settings.remoteInstances.direct.sidebarTitle": "Links de servidores",
"settings.remoteInstances.direct.sidebarDescription": "Conecte com um link ou token",
"settings.remoteInstances.direct.title": "Outros servidores OpenChamber",
"settings.remoteInstances.direct.description": "Adicione outro servidor OpenChamber por URL. Use isto quando o servidor já estiver em execução e você tiver um token de conexão.",
"settings.remoteInstances.direct.description": "Servidores para os quais este aplicativo pode alternar. Importe um link de pareamento do outro servidor ou adicione pelo endereço.",
"settings.remoteInstances.direct.addDialog.description": "Adicione outro servidor OpenChamber por URL. Use isto quando o servidor já estiver em execução e você tiver um token de conexão.",
"settings.remoteInstances.direct.field.labelPlaceholder": "Rótulo (opcional)",
"settings.remoteInstances.direct.field.urlPlaceholder": "https://host:port",
"settings.remoteInstances.direct.field.tokenPlaceholder": "Token de conexão (opcional para servidores locais confiáveis)",
@@ -220,7 +220,8 @@ export const settingsDict = {
"settings.remoteInstances.direct.sidebarTitle": "Посилання на сервери",
"settings.remoteInstances.direct.sidebarDescription": "Підключення через посилання або токен",
"settings.remoteInstances.direct.title": "Інші сервери OpenChamber",
"settings.remoteInstances.direct.description": "Додайте інший сервер OpenChamber за URL. Використовуйте це, коли сервер уже запущений і у вас є токен підключення.",
"settings.remoteInstances.direct.description": "Сервери, на які може перемикатися цей застосунок. Імпортуйте лінк підключення з іншого сервера або додайте за адресою.",
"settings.remoteInstances.direct.addDialog.description": "Додайте інший сервер OpenChamber за URL. Використовуйте це, коли сервер уже запущений і у вас є токен підключення.",
"settings.remoteInstances.direct.field.labelPlaceholder": "Назва (необов’язково)",
"settings.remoteInstances.direct.field.urlPlaceholder": "https://host:port",
"settings.remoteInstances.direct.field.tokenPlaceholder": "Токен підключення (необов’язково для довірених локальних серверів)",
@@ -220,7 +220,8 @@ export const settingsDict = {
'settings.remoteInstances.direct.sidebarTitle': '服务器链接',
'settings.remoteInstances.direct.sidebarDescription': '使用链接或令牌连接',
'settings.remoteInstances.direct.title': '其他 OpenChamber 服务器',
'settings.remoteInstances.direct.description': '通过 URL 添加另一个 OpenChamber 服务器。适用于服务器已在运行且你拥有连接令牌的情况。',
'settings.remoteInstances.direct.description': '此应用可切换到的服务器。从另一台服务器导入配对链接,或通过地址添加。',
'settings.remoteInstances.direct.addDialog.description': '通过 URL 添加另一个 OpenChamber 服务器。适用于服务器已在运行且你拥有连接令牌的情况。',
'settings.remoteInstances.direct.field.labelPlaceholder': '标签(可选)',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://host:port',
'settings.remoteInstances.direct.field.tokenPlaceholder': '连接令牌(受信任的本地服务器可选)',
@@ -226,7 +226,8 @@
'settings.remoteInstances.direct.sidebarTitle': '直接連線',
'settings.remoteInstances.direct.sidebarDescription': '連線到已在執行的 OpenChamber 伺服器。',
'settings.remoteInstances.direct.title': '直接遠端執行個體',
'settings.remoteInstances.direct.description': '儲存可從桌面切換器使用的遠端伺服器 URL。',
'settings.remoteInstances.direct.description': '此應用可切換到的伺服器。從另一台伺服器匯入配對連結,或透過位址新增。',
'settings.remoteInstances.direct.addDialog.description': '儲存可從桌面切換器使用的遠端伺服器 URL。',
'settings.remoteInstances.direct.field.labelPlaceholder': '我的遠端伺服器',
'settings.remoteInstances.direct.field.urlPlaceholder': 'https://openchamber.example.com',
'settings.remoteInstances.direct.field.tokenPlaceholder': '用戶端 token(可選)',