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
@@ -1548,17 +1548,15 @@ export const settingsDict = {
'settings.remoteInstances.page.empty.noExtraForwards': 'Nie skonfigurowano dodatkowych przekierowań portów.',
'settings.remoteInstances.page.empty.selectInstance': 'Wybierz instancję, aby wyświetlić i edytować jej ustawienia.',
'settings.remoteInstances.page.field.auto': 'Auto',
'settings.remoteInstances.page.field.bindHost': 'Host powiązania',
'settings.remoteInstances.page.field.bindHostHint': 'Miejsce nasłuchiwania lokalnego połączenia. Użyj 127.0.0.1 lub localhost, chyba że potrzebujesz dostępu z sieci lokalnej.',
'settings.remoteInstances.page.field.bindHost': 'Dostępne dla',
'settings.remoteInstances.page.field.bindHostHint': 'Kto może otworzyć przekierowany adres na tym komputerze. Sama zdalna maszyna i tak pozostaje dostępna tylko przez tunel SSH.',
'settings.remoteInstances.page.field.connectionTimeoutSeconds': 'Limit czasu połączenia (sekundy)',
'settings.remoteInstances.page.field.forwardType': 'Typ przekierowania',
'settings.remoteInstances.page.field.forwardTypeHint': 'Wybierz, jaki dostęp do portów ma zapewniać to połączenie SSH.',
'settings.remoteInstances.page.field.installMethod': 'Metoda instalacji',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'Pobierz wydanie',
'settings.remoteInstances.page.field.installMethodHint': 'Jak OpenChamber ma zostać umieszczony na zdalnej maszynie, gdy aplikacja uruchamia go za Ciebie.',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'Prześlij paczkę',
'settings.remoteInstances.page.field.keepServerRunning': 'Pozostaw serwer uruchomiony',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Pozostaw OpenChamber uruchomiony na zdalnej maszynie po rozłączeniu.',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Pozostaw zdalny serwer uruchomiony po rozłączeniu. Wyłączone: zatrzymuje się przy rozłączeniu i startuje ponownie przy kolejnym połączeniu.',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.mode': 'Tryb',
'settings.remoteInstances.page.field.modeExternal': 'Już działa',
@@ -1567,10 +1565,10 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'Wybierz tryb',
'settings.remoteInstances.page.field.nickname': 'Pseudonim',
'settings.remoteInstances.page.field.nicknamePlaceholder': 'Laptop służbowy',
'settings.remoteInstances.page.field.preferredLocalPort': 'Preferowany port lokalny',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Lokalny port dla tego połączenia. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Preferowany port zdalny',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port używany na zdalnej maszynie. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port na tym komputerze',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port otwierany na tym komputerze dla tunelu. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port na zdalnej maszynie',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port, którego OpenChamber używa na zdalnej maszynie. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'Wybierz host powiązania',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'Wybierz metodę instalacji',
@@ -1587,11 +1585,48 @@ export const settingsDict = {
'settings.remoteInstances.page.forwardTypeDescription.dynamic': 'Otwórz lokalny proxy SOCKS przez połączenie SSH.',
'settings.remoteInstances.page.forwardTypeDescription.local': 'Otwórz lokalny port łączący się z usługą na zdalnej maszynie.',
'settings.remoteInstances.page.forwardTypeDescription.remote': 'Otwórz port na zdalnej maszynie, który połączy się z powrotem z Twoim komputerem.',
'settings.remoteInstances.page.addDialog.description': 'Wybierz host z konfiguracji SSH albo wpisz połączenie samodzielnie.',
'settings.remoteInstances.page.addDialog.sourceLabel': 'Skąd pochodzi połączenie',
'settings.remoteInstances.page.addDialog.tab.saved': 'Z konfiguracji SSH',
'settings.remoteInstances.page.addDialog.tab.manual': 'Wpiszę sam',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'Szukaj hostów',
'settings.remoteInstances.page.addDialog.emptySaved': 'Nie znaleziono hostów w konfiguracji SSH. Wpisz połączenie samodzielnie.',
'settings.remoteInstances.page.addDialog.searchEmpty': 'Żaden host nie pasuje do tego wyszukiwania.',
'settings.remoteInstances.page.addDialog.use': 'Użyj',
'settings.remoteInstances.page.state.notConnected': 'Brak połączenia',
'settings.remoteInstances.page.state.connecting': 'Łączenie',
'settings.remoteInstances.page.state.ready': 'Połączono',
'settings.remoteInstances.page.state.problem': 'Wymaga uwagi',
'settings.remoteInstances.page.section.advanced': 'Ustawienia zaawansowane',
'settings.remoteInstances.page.section.advancedHint': 'Porty, metoda instalacji, hasła i dodatkowe przekierowania. Domyślne wartości wystarczą dla większości połączeń.',
'settings.remoteInstances.page.field.installMethodAuto': 'Automatycznie',
'settings.remoteInstances.page.error.hint.noRuntime': 'Na zdalnej maszynie nie ma ani bun, ani npm. Zainstaluj tam jedno z nich albo przełącz to połączenie na „Już uruchomiony”.',
'settings.remoteInstances.page.error.hint.noOpencode': 'Na zdalnej maszynie nie ma zainstalowanego opencode CLI. Zainstaluj je tam (zobacz opencode.ai) i połącz się ponownie.',
'settings.remoteInstances.page.error.action.setUiPassword': 'Ustaw hasło interfejsu',
'settings.remoteInstances.page.error.action.pickRandomPort': 'Użyj innego portu lokalnego',
'settings.remoteInstances.page.error.action.setRemotePort': 'Ustaw port zdalny',
'settings.remoteInstances.page.validation.externalPortRequired': 'Najpierw podaj port zdalny. W trybie „Już uruchomiony” OpenChamber musi wiedzieć, na którym porcie nasłuchuje serwer.',
'settings.remoteInstances.page.empty.noInstances': 'Brak połączeń SSH.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'Hasło interfejsu (wymagane)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'Wymagane, dopóki zdalny serwer jest dostępny w swojej sieci.',
'settings.remoteInstances.page.field.remoteLanAccess': 'Dostępne w sieci zdalnej maszyny',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'Pozwól innym urządzeniom w sieci zdalnej maszyny otwierać ten OpenChamber bezpośrednio, bez tunelu SSH. Wymagane jest hasło interfejsu.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'Każdy w tej sieci dotrze do zdalnego OpenChamber. Chroni go tylko hasło interfejsu poniżej.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': 'Najpierw ustaw hasło interfejsu. Bez niego zdalny OpenChamber byłby otwarty dla każdego urządzenia w tej sieci.',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'Tylko ten komputer (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'Tylko ten komputer (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'Dowolne urządzenie w mojej sieci (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'Potrzebne tylko wtedy, gdy host prosi o hasło zamiast przyjąć klucz SSH.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'Hasło chroniące zdalny interfejs OpenChamber. OpenChamber ustawia je na serwerze, który uruchamia za Ciebie.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'Hasło serwera OpenChamber już działającego na zdalnej maszynie, używane do zalogowania się.',
'settings.remoteInstances.page.tunnelPreview.caption': 'To połączenie przekierowuje:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'Brak połączeń SSH. Z konfiguracji SSH można zaimportować 1 host.',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'Brak połączeń SSH. Z konfiguracji SSH można zaimportować {count} hostów.',
'settings.remoteInstances.page.state.loadingInstances': 'Wczytywanie połączeń...',
'settings.remoteInstances.page.import.loading': 'Ładowanie hostów SSH...',
'settings.remoteInstances.page.import.noneAvailable': 'Brak hostów SSH dostępnych do importu.',
'settings.remoteInstances.page.import.noneFound': 'Nie znaleziono hostów SSH.',
'settings.remoteInstances.page.import.patternSuffix': '(wzorzec)',
'settings.remoteInstances.page.import.sectionTitle': 'Zapisane hosty SSH',
'settings.remoteInstances.page.logsDialog.empty': 'Brak logów SSH.',
'settings.remoteInstances.page.logsDialog.loading': 'Ładowanie logów...',
'settings.remoteInstances.page.logsDialog.selectedInstanceFallback': 'Wybrana instancja',
@@ -1619,8 +1654,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.authenticationDescription': 'Opcjonalne dane logowania dla SSH i zdalnego interfejsu OpenChamber.',
'settings.remoteInstances.page.section.instance': 'Instancja',
'settings.remoteInstances.page.section.instanceDescription': 'Wybierz polecenie SSH i nazwę wyświetlaną dla tego połączenia.',
'settings.remoteInstances.page.section.mainTunnel': 'Dostęp lokalny',
'settings.remoteInstances.page.section.mainTunnelDescription': 'Wybierz lokalny adres używany do otwierania tego zdalnego serwera OpenChamber.',
'settings.remoteInstances.page.section.mainTunnel': 'Dostęp z tego komputera',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber działa na zdalnej maszynie. Te ustawienia dotyczą wyłącznie adresu na tym komputerze, który prowadzi do niej przez tunel SSH.',
'settings.remoteInstances.page.section.portForwards': 'Przekierowania portów',
'settings.remoteInstances.page.section.portForwardsDescription': 'Opcjonalne dodatkowe porty udostępniane przez to połączenie SSH.',
'settings.remoteInstances.page.section.remoteServer': 'OpenChamber na zdalnej maszynie',