feat(ssh): rework remote instance setup and fix remote installs

Adding an SSH connection now starts from the hosts in the SSH config
instead of a blank command field, ports and install options sit behind
Advanced settings, and each connection reports one of three states with
the failure text and an action that resolves it.

Remote installs no longer touch the root-owned global npm prefix: npm is
pinned to a prefix under $HOME and bun is resolved at its known location,
because an SSH login shell exposes neither on PATH. The opencode CLI is
resolved the same way and handed to the remote server through
OPENCODE_BINARY, and the server is started and stopped through the
resolved binary rather than PATH — the HTTP shutdown route sits behind UI
authentication and never stopped anything.

A managed remote server can also be published to the remote machine's own
network. That requires a UI password, enforced in the form and again in
the SSH manager.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 16:45:56 +03:00
parent eed317f18d
commit f7a0d1f0f6
16 changed files with 1234 additions and 302 deletions
@@ -374,14 +374,14 @@ export const settingsDict = {
"settings.remoteInstances.page.field.modePlaceholder": "Seleccionar modo",
"settings.remoteInstances.page.field.modeManaged": "Iniciarlo por mí",
"settings.remoteInstances.page.field.modeExternal": "Ya está en marcha",
"settings.remoteInstances.page.field.preferredRemotePort": "Puerto remoto preferido",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Port to use on the remote machine. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.preferredRemotePort": "Puerto en la máquina remota",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Puerto que OpenChamber usa en la máquina remota. Déjalo vacío para elegir uno automáticamente.",
"settings.remoteInstances.page.field.keepServerRunning": "Mantener servidor en ejecución",
"settings.remoteInstances.page.field.keepServerRunningHint": "Keep OpenChamber running on the remote machine after you disconnect.",
"settings.remoteInstances.page.field.bindHost": "Host de enlace",
"settings.remoteInstances.page.field.bindHostHint": "Where the local connection should listen. Use 127.0.0.1 or localhost unless you need LAN access.",
"settings.remoteInstances.page.field.preferredLocalPort": "Puerto local preferido",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Local port to open for this connection. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.keepServerRunningHint": "Dejar el servidor remoto en marcha tras desconectar. Si está desactivado, se detiene al desconectar y se inicia de nuevo la próxima vez que conectes.",
"settings.remoteInstances.page.field.bindHost": "Quién puede acceder",
"settings.remoteInstances.page.field.bindHostHint": "Quién puede abrir la dirección reenviada en este equipo. La máquina remota sigue siendo accesible únicamente por el túnel SSH en cualquier caso.",
"settings.remoteInstances.page.field.preferredLocalPort": "Puerto en este equipo",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Puerto que se abre en este equipo para el túnel. Déjalo vacío para elegir uno automáticamente.",
"settings.remoteInstances.page.field.forwardType": "Tipo de redirección",
"settings.remoteInstances.page.field.localHostPlaceholder": "127.0.0.1",
"settings.remoteInstances.page.field.remoteHostPlaceholder": "127.0.0.1",
@@ -1203,8 +1203,8 @@ export const settingsDict = {
"settings.remoteInstances.page.section.actionsDescription": "Conecta, reconecta, revisa registros o elimina esta conexión.",
"settings.remoteInstances.page.section.remoteServer": "OpenChamber en la máquina remota",
"settings.remoteInstances.page.section.remoteServerDescription": "Elige cómo debe ejecutarse OpenChamber después de conectar por SSH.",
"settings.remoteInstances.page.section.mainTunnel": "Acceso local",
"settings.remoteInstances.page.section.mainTunnelDescription": "Elige la dirección local que se usará para abrir este servidor remoto de OpenChamber.",
"settings.remoteInstances.page.section.mainTunnel": "Acceso desde este equipo",
"settings.remoteInstances.page.section.mainTunnelDescription": "OpenChamber se ejecuta en la máquina remota. Estos ajustes solo controlan la dirección de este equipo que lleva hasta ella por el túnel SSH.",
"settings.remoteInstances.page.section.authentication": "Autenticación",
"settings.remoteInstances.page.section.authenticationDescription": "Credenciales opcionales para SSH y la interfaz de usuario de OpenChamber remoto.",
"settings.remoteInstances.page.section.portForwards": "Redirecciones de puerto",
@@ -1217,8 +1217,6 @@ export const settingsDict = {
"settings.remoteInstances.page.field.installMethod": "Método de instalación",
"settings.remoteInstances.page.field.installMethodHint": "Cómo debe colocarse OpenChamber en la máquina remota cuando esta app lo inicia por ti.",
"settings.remoteInstances.page.field.selectInstallMethodPlaceholder": "Seleccionar método de instalación",
"settings.remoteInstances.page.field.installMethodDownloadRelease": "Descargar versión",
"settings.remoteInstances.page.field.installMethodUploadBundle": "Subir paquete",
"settings.remoteInstances.page.field.selectBindHostPlaceholder": "Seleccionar host de enlace",
"settings.remoteInstances.page.field.sshPasswordOptional": "Contraseña SSH (opcional)",
"settings.remoteInstances.page.field.sshPasswordPlaceholder": "Introducir contraseña SSH",
@@ -1242,7 +1240,44 @@ export const settingsDict = {
"settings.remoteInstances.page.actions.enableForwardAria": "Habilitar redirección",
"settings.remoteInstances.page.actions.openLocal": "Abrir local",
"settings.remoteInstances.page.actions.addForward": "Añadir redirección",
"settings.remoteInstances.page.import.sectionTitle": "Hosts SSH guardados",
"settings.remoteInstances.page.addDialog.description": "Elige un host de tu configuración SSH o escribe la conexión tú mismo.",
"settings.remoteInstances.page.addDialog.sourceLabel": "De dónde viene la conexión",
"settings.remoteInstances.page.addDialog.tab.saved": "Desde la configuración SSH",
"settings.remoteInstances.page.addDialog.tab.manual": "Escribirla yo",
"settings.remoteInstances.page.addDialog.searchPlaceholder": "Buscar hosts",
"settings.remoteInstances.page.addDialog.emptySaved": "No se encontraron hosts en tu configuración SSH. Escribe la conexión tú mismo.",
"settings.remoteInstances.page.addDialog.searchEmpty": "Ningún host coincide con esta búsqueda.",
"settings.remoteInstances.page.addDialog.use": "Usar",
"settings.remoteInstances.page.state.notConnected": "Sin conexión",
"settings.remoteInstances.page.state.connecting": "Conectando",
"settings.remoteInstances.page.state.ready": "Conectado",
"settings.remoteInstances.page.state.problem": "Requiere atención",
"settings.remoteInstances.page.section.advanced": "Ajustes avanzados",
"settings.remoteInstances.page.section.advancedHint": "Puertos, método de instalación, contraseñas y reenvíos adicionales. Los valores predeterminados sirven para casi todas las conexiones.",
"settings.remoteInstances.page.field.installMethodAuto": "Automático",
"settings.remoteInstances.page.error.hint.noRuntime": "La máquina remota no tiene ni bun ni npm. Instala uno de ellos allí o cambia esta conexión a «Ya está en ejecución».",
"settings.remoteInstances.page.error.hint.noOpencode": "La CLI de opencode no está instalada en la máquina remota. Instálala allí (consulta opencode.ai) y vuelve a conectar.",
"settings.remoteInstances.page.error.action.setUiPassword": "Definir contraseña de la interfaz",
"settings.remoteInstances.page.error.action.pickRandomPort": "Usar otro puerto local",
"settings.remoteInstances.page.error.action.setRemotePort": "Definir el puerto remoto",
"settings.remoteInstances.page.validation.externalPortRequired": "Primero indica un puerto remoto. En el modo «Ya está en ejecución», OpenChamber necesita saber en qué puerto escucha el servidor.",
"settings.remoteInstances.page.empty.noInstances": "Aún no hay conexiones SSH.",
"settings.remoteInstances.page.field.uiPasswordRequired": "Contraseña de la interfaz (obligatoria)",
"settings.remoteInstances.page.field.uiPasswordMissingForLan": "Obligatoria mientras el servidor remoto sea accesible en su red.",
"settings.remoteInstances.page.field.remoteLanAccess": "Accesible en la red remota",
"settings.remoteInstances.page.field.remoteLanAccessHint": "Permitir también que otros dispositivos de la red de la máquina remota abran este OpenChamber directamente, sin el túnel SSH. Requiere contraseña de la interfaz.",
"settings.remoteInstances.page.field.remoteLanAccessWarning": "Cualquiera en esa red puede llegar al OpenChamber remoto. Solo lo protege la contraseña de la interfaz de abajo.",
"settings.remoteInstances.page.validation.remoteLanNeedsPassword": "Define primero una contraseña de la interfaz. Sin ella, el OpenChamber remoto quedaría abierto a todos los dispositivos de esa red.",
"settings.remoteInstances.page.field.bindHostOption.loopback": "Solo este equipo (127.0.0.1)",
"settings.remoteInstances.page.field.bindHostOption.localhost": "Solo este equipo (localhost)",
"settings.remoteInstances.page.field.bindHostOption.lan": "Cualquier dispositivo de mi red (0.0.0.0)",
"settings.remoteInstances.page.field.sshPasswordHint": "Solo hace falta cuando este host pide contraseña en lugar de aceptar una clave SSH.",
"settings.remoteInstances.page.field.uiPasswordHintManaged": "Contraseña con la que se protegerá la interfaz remota de OpenChamber. OpenChamber la aplica al servidor que inicia por ti.",
"settings.remoteInstances.page.field.uiPasswordHintExternal": "Contraseña del servidor OpenChamber que ya se ejecuta en la máquina remota, usada para iniciar sesión.",
"settings.remoteInstances.page.tunnelPreview.caption": "Esta conexión reenvía:",
"settings.remoteInstances.page.empty.noInstancesWithOneImport": "Aún no hay conexiones SSH. Hay 1 host disponible para importar desde tu configuración SSH.",
"settings.remoteInstances.page.empty.noInstancesWithImports": "Aún no hay conexiones SSH. Hay {count} hosts disponibles para importar desde tu configuración SSH.",
"settings.remoteInstances.page.state.loadingInstances": "Cargando conexiones...",
"settings.remoteInstances.page.import.loading": "Cargando hosts SSH...",
"settings.remoteInstances.page.import.noneFound": "No se encontraron hosts SSH.",
"settings.remoteInstances.page.import.noneAvailable": "No hay hosts SSH disponibles para importar.",