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": "Виберіть режим",
"settings.remoteInstances.page.field.modeManaged": "Запустити для мене",
"settings.remoteInstances.page.field.modeExternal": "Уже запущено",
"settings.remoteInstances.page.field.preferredRemotePort": "Бажаний віддалений порт",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Порт на віддаленій машині. Залиште порожнім, щоб вибрати автоматично.",
"settings.remoteInstances.page.field.preferredRemotePort": "Порт на віддаленій машині",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Порт, який OpenChamber займе на віддаленій машині. Лишіть порожнім, щоб вибрався автоматично.",
"settings.remoteInstances.page.field.keepServerRunning": "Залишати сервер запущеним",
"settings.remoteInstances.page.field.keepServerRunningHint": "Залишати OpenChamber запущеним на віддаленій машині після відключення.",
"settings.remoteInstances.page.field.bindHost": "Прив’язати хост",
"settings.remoteInstances.page.field.bindHostHint": "Де має слухати локальне підключення. Використовуйте 127.0.0.1 або localhost, якщо вам не потрібен доступ з локальної мережі.",
"settings.remoteInstances.page.field.preferredLocalPort": "Бажаний локальний порт",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Локальний порт для цього підключення. Залиште порожнім, щоб вибрати автоматично.",
"settings.remoteInstances.page.field.keepServerRunningHint": "Лишати віддалений сервер запущеним після відключення. Якщо вимкнено, він зупиняється при відключенні і запускається знову при наступному підключенні.",
"settings.remoteInstances.page.field.bindHost": "Хто має доступ",
"settings.remoteInstances.page.field.bindHostHint": "Хто може відкрити прокинуту адресу на цьому комп’ютері. Сама віддалена машина в будь-якому разі лишається доступною тільки через SSH-тунель.",
"settings.remoteInstances.page.field.preferredLocalPort": "Порт на цьому комп’ютері",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Порт, який відкриється на цьому комп’ютері для тунелю. Лишіть порожнім, щоб вибрався автоматично.",
"settings.remoteInstances.page.field.forwardType": "Тип переадресації",
"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": "Підключіться, перепідключіться, перегляньте журнали або видаліть це підключення.",
"settings.remoteInstances.page.section.remoteServer": "OpenChamber на віддаленій машині",
"settings.remoteInstances.page.section.remoteServerDescription": "Виберіть, як OpenChamber має працювати після SSH-підключення.",
"settings.remoteInstances.page.section.mainTunnel": "Локальний доступ",
"settings.remoteInstances.page.section.mainTunnelDescription": "Виберіть локальну адресу, через яку відкриватиметься цей віддалений сервер OpenChamber.",
"settings.remoteInstances.page.section.mainTunnel": "Доступ із цього комп’ютера",
"settings.remoteInstances.page.section.mainTunnelDescription": "OpenChamber працює на віддаленій машині. Ці налаштування керують лише адресою на цьому комп’ютері, яка веде до неї через SSH-тунель.",
"settings.remoteInstances.page.section.authentication": "Аутентифікація",
"settings.remoteInstances.page.section.authenticationDescription": "Додаткові облікові дані для SSH та віддаленого інтерфейсу користувача OpenChamber.",
"settings.remoteInstances.page.section.portForwards": "Перенаправлення портів",
@@ -1217,8 +1217,6 @@ export const settingsDict = {
"settings.remoteInstances.page.field.installMethod": "Спосіб встановлення",
"settings.remoteInstances.page.field.installMethodHint": "Як розмістити OpenChamber на віддаленій машині, коли цей застосунок запускає його для вас.",
"settings.remoteInstances.page.field.selectInstallMethodPlaceholder": "Вибрати метод встановлення",
"settings.remoteInstances.page.field.installMethodDownloadRelease": "Завантажити випуск",
"settings.remoteInstances.page.field.installMethodUploadBundle": "Завантажити пакет",
"settings.remoteInstances.page.field.selectBindHostPlaceholder": "Вибрати bind host",
"settings.remoteInstances.page.field.sshPasswordOptional": "Пароль SSH (необов'язково)",
"settings.remoteInstances.page.field.sshPasswordPlaceholder": "Введіть пароль SSH",
@@ -1242,7 +1240,44 @@ export const settingsDict = {
"settings.remoteInstances.page.actions.enableForwardAria": "Увімкнути пересилання",
"settings.remoteInstances.page.actions.openLocal": "Відкрити локально",
"settings.remoteInstances.page.actions.addForward": "Додати переадресацію",
"settings.remoteInstances.page.import.sectionTitle": "Збережені SSH-хости",
"settings.remoteInstances.page.addDialog.description": "Виберіть хост зі свого SSH-конфігу або впишіть підключення вручну.",
"settings.remoteInstances.page.addDialog.sourceLabel": "Звідки береться підключення",
"settings.remoteInstances.page.addDialog.tab.saved": "З SSH-конфігу",
"settings.remoteInstances.page.addDialog.tab.manual": "Ввести вручну",
"settings.remoteInstances.page.addDialog.searchPlaceholder": "Пошук хостів",
"settings.remoteInstances.page.addDialog.emptySaved": "У вашому SSH-конфізі немає хостів. Впишіть підключення вручну.",
"settings.remoteInstances.page.addDialog.searchEmpty": "Жоден хост не збігається з пошуком.",
"settings.remoteInstances.page.addDialog.use": "Обрати",
"settings.remoteInstances.page.state.notConnected": "Не підключено",
"settings.remoteInstances.page.state.connecting": "Підключення",
"settings.remoteInstances.page.state.ready": "Підключено",
"settings.remoteInstances.page.state.problem": "Потрібна дія",
"settings.remoteInstances.page.section.advanced": "Додаткові налаштування",
"settings.remoteInstances.page.section.advancedHint": "Порти, спосіб встановлення, паролі та додаткові прокидання. Для більшості підключень достатньо значень за замовчуванням.",
"settings.remoteInstances.page.field.installMethodAuto": "Автоматично",
"settings.remoteInstances.page.error.hint.noRuntime": "На віддаленій машині немає ні bun, ні npm. Встановіть щось із них там або переведіть це підключення в режим «Вже запущено».",
"settings.remoteInstances.page.error.hint.noOpencode": "На віддаленій машині не встановлено opencode CLI. Встановіть його там (див. opencode.ai) і підключіться знову.",
"settings.remoteInstances.page.error.action.setUiPassword": "Задати пароль UI",
"settings.remoteInstances.page.error.action.pickRandomPort": "Взяти інший локальний порт",
"settings.remoteInstances.page.error.action.setRemotePort": "Задати віддалений порт",
"settings.remoteInstances.page.validation.externalPortRequired": "Спершу вкажіть віддалений порт. У режимі «Вже запущено» OpenChamber має знати, на якому порту слухає сервер.",
"settings.remoteInstances.page.empty.noInstances": "SSH-підключень ще немає.",
"settings.remoteInstances.page.field.uiPasswordRequired": "Пароль UI (обов’язковий)",
"settings.remoteInstances.page.field.uiPasswordMissingForLan": "Обов’язковий, поки віддалений сервер доступний у своїй мережі.",
"settings.remoteInstances.page.field.remoteLanAccess": "Доступ у мережі віддаленої машини",
"settings.remoteInstances.page.field.remoteLanAccessHint": "Дозволити іншим пристроям у мережі віддаленої машини відкривати цей OpenChamber напряму, без SSH-тунелю. Потрібен пароль UI.",
"settings.remoteInstances.page.field.remoteLanAccessWarning": "Будь-хто в тій мережі зможе дістатись віддаленого OpenChamber. Його захищає лише пароль UI нижче.",
"settings.remoteInstances.page.validation.remoteLanNeedsPassword": "Спершу задайте пароль UI. Без нього віддалений OpenChamber буде відкритий для всіх пристроїв у тій мережі.",
"settings.remoteInstances.page.field.bindHostOption.loopback": "Лише цей комп’ютер (127.0.0.1)",
"settings.remoteInstances.page.field.bindHostOption.localhost": "Лише цей комп’ютер (localhost)",
"settings.remoteInstances.page.field.bindHostOption.lan": "Будь-який пристрій у моїй мережі (0.0.0.0)",
"settings.remoteInstances.page.field.sshPasswordHint": "Потрібен лише тоді, коли цей хост питає пароль замість того, щоб приймати SSH-ключ.",
"settings.remoteInstances.page.field.uiPasswordHintManaged": "Пароль, яким буде захищено віддалений інтерфейс OpenChamber. OpenChamber задасть його серверу, який запускає для вас.",
"settings.remoteInstances.page.field.uiPasswordHintExternal": "Пароль сервера OpenChamber, який уже працює на віддаленій машині, для входу в нього.",
"settings.remoteInstances.page.tunnelPreview.caption": "Це підключення прокидає:",
"settings.remoteInstances.page.empty.noInstancesWithOneImport": "SSH-підключень ще немає. З вашого SSH-конфігу можна імпортувати 1 хост.",
"settings.remoteInstances.page.empty.noInstancesWithImports": "SSH-підключень ще немає. З вашого SSH-конфігу можна імпортувати {count} хостів.",
"settings.remoteInstances.page.state.loadingInstances": "Завантаження підключень...",
"settings.remoteInstances.page.import.loading": "Завантаження хостів SSH...",
"settings.remoteInstances.page.import.noneFound": "Не знайдено хостів SSH.",
"settings.remoteInstances.page.import.noneAvailable": "Немає доступних для імпорту хостів SSH.",