From 6666f3c4aed633df7b53df147a6dfcb0fa6ba125 Mon Sep 17 00:00:00 2001 From: bifrost0x Date: Thu, 23 Jul 2026 07:10:57 +0200 Subject: [PATCH 1/2] Add saved profile launcher --- README.md | 1 + app/key_manager.py | 42 +++++++ app/socket_events.py | 16 ++- app/tailscale_ssh.py | 11 ++ docs/screenshots/02_main_empty.png | Bin 39813 -> 45218 bytes static/css/style.css | 154 +++++++++++++++++------- static/js/app.js | 104 +++++++++++++--- static/js/i18n.js | 36 ++++++ static/js/jump-host-manager.js | 3 + static/js/profile-launcher-utils.js | 67 +++++++++++ static/js/profile-manager.js | 110 ++++++++++++++++- static/js/session-manager.js | 43 +++---- templates/index.html | 18 +-- tests/js/profile-launcher-utils.test.js | 101 ++++++++++++++++ tests/test_key_manager.py | 53 ++++++++ tests/test_profile_launcher_ui.py | 115 ++++++++++++++++++ tests/test_tailscale_ssh.py | 80 ++++++++++++ 17 files changed, 856 insertions(+), 98 deletions(-) create mode 100644 static/js/profile-launcher-utils.js create mode 100644 tests/js/profile-launcher-utils.test.js create mode 100644 tests/test_profile_launcher_ui.py diff --git a/README.md b/README.md index 9039790..8fb2948 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Web SSH Terminal is a self-hosted web application that provides secure SSH acces - **Session Restoration** - Restore live sessions after a page refresh without injecting terminal input - **Persistent tmux Sessions** - Keep remote shells and running commands alive across browser closes and WebSSH restarts, then reattach later - **Manual Reconnect** - Reconnect from a session tab; SSH-key and Tailscale sessions can reconnect directly, while password sessions reopen the pre-filled connection form +- **Profile Launcher** - Empty terminal panes show saved profiles; key and Tailscale profiles connect immediately when no password is needed, while password-dependent profiles open pre-filled at the required field - **Post-Connect Command Sets** - Build named, ordered command sequences and assign one to a connection or saved profile - **Persistent Session Names** - Custom tab names are retained for persistent sessions across browsers - **Configurable Scrollback** - Set 50 to 10,000 terminal lines and navigate them with the custom scrollbar diff --git a/app/key_manager.py b/app/key_manager.py index 17b00dc..6fcf0b0 100644 --- a/app/key_manager.py +++ b/app/key_manager.py @@ -148,6 +148,48 @@ def read_key_content(user_id, key_id): log_error(f"Error reading key content", user_id=user_id, key_id=key_id, error=str(e)) return None, f"Failed to read key: {str(e)}" + +def load_key_summaries(user_id): + """Return key metadata with a transient usability status for clients.""" + keys = load_keys(user_id) + keys_dir = get_user_keys_dir(user_id) + if not keys_dir: + return [{**key, 'usable': False} for key in keys] + + fernet = None + summaries = [] + for key in keys: + if not isinstance(key, dict): + continue + usable = False + try: + filename = key.get('filename') + if not filename or Path(filename).name != filename: + raise ValueError('invalid key filename') + + raw = (keys_dir / filename).read_bytes() + if key_encryption.is_encrypted(raw): + if fernet is None: + fernet = key_encryption.get_user_fernet(str(user_id)) + raw = fernet.decrypt(raw) + + identify_private_key(raw.decode('utf-8')) + usable = True + except ( + OSError, + UnicodeDecodeError, + key_encryption.InvalidToken, + paramiko.PasswordRequiredException, + paramiko.SSHException, + UnsupportedPrivateKeyError, + ValueError, + TypeError, + ): + pass + summaries.append({**key, 'usable': usable}) + return summaries + + def delete_key(user_id, key_id): """Delete an SSH key and its metadata for a specific user.""" try: diff --git a/app/socket_events.py b/app/socket_events.py index 9c6e884..8cade2d 100644 --- a/app/socket_events.py +++ b/app/socket_events.py @@ -11,7 +11,10 @@ log_file_upload, log_file_download, log_key_upload, log_key_delete, log_tailscale_ssh_usage) -from .tailscale_ssh import validate_tailscale_ssh_access +from .tailscale_ssh import ( + profile_is_authorized_for_launch, + validate_tailscale_ssh_access, +) from .startup_commands import normalize_startup_commands from .storage_utils import storage_lock from . import binary_transfer, connection_pool @@ -577,7 +580,14 @@ def handle_ssh_disconnect(data, current_user=None): def handle_list_profiles(current_user=None): """Return list of saved connection profiles for this user.""" try: - profiles = profile_manager.load_profiles(current_user.id) + profiles = [] + for stored_profile in profile_manager.load_profiles(current_user.id): + profile = dict(stored_profile) + if profile.get('auth_type') == 'tailscale': + profile['tailscale_authorized'] = ( + profile_is_authorized_for_launch(current_user, profile) + ) + profiles.append(profile) emit('profiles_list', {'profiles': profiles}) except Exception as e: log_error("Failed to load profiles", error=str(e)) @@ -704,7 +714,7 @@ def handle_delete_jump_host(data, current_user=None): def handle_list_keys(current_user=None): """Return list of stored SSH keys for this user.""" try: - keys = key_manager.load_keys(current_user.id) + keys = key_manager.load_key_summaries(current_user.id) emit('keys_list', {'keys': keys}) except Exception as e: log_error("Failed to load keys", error=str(e)) diff --git a/app/tailscale_ssh.py b/app/tailscale_ssh.py index 100e4a3..32e2430 100644 --- a/app/tailscale_ssh.py +++ b/app/tailscale_ssh.py @@ -27,3 +27,14 @@ def validate_tailscale_ssh_access(user, host, remote_username): return 'Tailscale SSH remote username is not allowed' return None + + +def profile_is_authorized_for_launch(user, profile): + """Return whether a saved profile is still allowed by current policy.""" + if not isinstance(profile, dict) or profile.get('auth_type') != 'tailscale': + return True + return validate_tailscale_ssh_access( + user, + profile.get('host'), + profile.get('username'), + ) is None diff --git a/docs/screenshots/02_main_empty.png b/docs/screenshots/02_main_empty.png index 6f887132acc7b812f4b2dbe03aa322b834bf597a..3eabe7a753bac56f75c221f7fd16c8d7290ecf89 100644 GIT binary patch literal 45218 zcmeFZ1yo#FwlG`?Q9=ZF=wN}uf)pB{lOVxUK;f>T(BMuC0u&OYa7pm0!kxn1AxPow z?yi4zx~J#Oy#Mvrv)-EdzV+VRTC2{z_v~}F+`W(Nv(LF2zM2L+l97~_1YElY09?cV z09Rvxmw;P0Zr-|i-g7j@ULAp0;sS=UdOri3%!52Zrr?%bL;lCJJ?+1M}TWM*Kw}jdU)f`O&pvX z*Kq*XuH)Rmzj=#*@G%!yIkcenHW3LO4~Xx%ox_KSZ~6TY1H&+4ZY33?x9?}@U!>Ia zk@AM$VR$KSU%1Vf%~AstsPxuNY#t9jR%+L;;oiie`i&?oq1SH^5^;kK z-@Xs6d(6f2{3U^s!8hXVoB6#nbURn006ZKl5BNCv05QP%Ab{a7eEhdkZs@h0M`x)k z1-dGTPE2?369(R14bvE%?3ey?1#oagGC1b_OnGLKbFYCBsLf@_p}}b<801N0Q(&`FP0u%0a|;GX-V@F@1J!) z{!xmyap>GpEG*^3r8_kBQ1R6Kqt}j~_q^HpL)h@pt&7>3y+SO~*egJA#xAd}AeJ)W!&^{_l+U57MDU9Cls8teY}Y^R{({l-jl3X0AaCNk1IfifYcW8 zR6^1YZIc=nUug0!HNEs&&Hmb*i`fb+3MSU!t>13xkC8vH5$Q@}v6;Km%!pO0c{zDE zo?HPG^AdG`0cY?g)!V^{wJN!CgwK9kV;w%byRhbsMFlq14jJXOK{l9#nM%ecQTfG& z3iK*}N>kjy`a%2v7&BeiB zB9Cyvp{!q}_OE>GB`js-^;||>&N~;YA6S&V#`~EYj$cqk_?{RHXvA^XFQ9AP09G5z zW>Q?EH=<>}0<>K;H;aO=0O4ovsR;PEc0V{?p(>1;O( zzNP+XjYK3-*HWAI9>^h6Q!@g>s&AoPl7lFk96a}<#3!l`FF*M|#pA0eki-W|%PK^J zAWy8>%qf+4C5ypet-6NvD?sPgcdnj@vqs0#kWH#sU0RpJn3avwsBoO}P0jE}L2m

DQ?3a<-Q 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 MPgg&ebxsLQ0Q2_!_5c6? diff --git a/static/css/style.css b/static/css/style.css index 418d819..47017d8 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1737,31 +1737,134 @@ body.broadcast-active .terminal-wrapper:not(.unassigned) { .pane-empty { flex: 1; + min-width: 0; + min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 12px; + gap: 10px; color: var(--text-muted); - font-size: 14px; padding: 24px; text-align: center; } .pane-empty-icon { - font-size: 48px; + flex: 0 0 auto; + font-size: 42px; + line-height: 1; opacity: 0.4; - margin-bottom: 8px; } -.pane-empty-text { +.profile-launcher-title { + color: var(--text-primary); + font-size: 18px; + font-weight: 700; +} + +.profile-launcher-hint { color: var(--text-secondary); - font-weight: 500; + font-size: 13px; } -.pane-empty button { - padding: 10px 18px; - margin-top: 8px; +.profile-launcher-list { + width: min(760px, 100%); + max-height: min(48vh, 420px); + min-height: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px; + padding: 4px; + overflow-y: auto; + scrollbar-gutter: stable; +} + +.profile-launcher-card { + min-width: 0; + min-height: var(--touch-target-min); + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 10px; + padding: 12px 14px; + border: 1px solid var(--border-color); + border-radius: 9px; + background: var(--bg-secondary); + color: var(--text-primary); + font: inherit; + text-align: left; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; +} + +.profile-launcher-card:hover { + border-color: var(--accent-primary); + background: var(--bg-hover); + transform: translateY(-1px); +} + +.profile-launcher-card:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +.profile-launcher-name, +.profile-launcher-endpoint { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profile-launcher-name { + font-weight: 700; +} + +.profile-launcher-endpoint { + grid-column: 1; + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 11px; +} + +.profile-launcher-action { + grid-column: 2; + grid-row: 1 / span 2; + align-self: center; + color: var(--text-secondary); + font-size: 11px; + white-space: nowrap; +} + +.profile-launcher-action.mode-connect { + color: var(--success-color); +} + +.profile-launcher-action.mode-password, +.profile-launcher-action.mode-jump-host-password { + color: var(--warning-color); +} + +.profile-launcher-new { + flex: 0 0 auto; + margin-top: 2px; +} + +@media (max-width: 767px) { + .profile-launcher { + justify-content: flex-start; + padding: 18px 12px; + overflow: hidden; + } + + .profile-launcher-list { + width: 100%; + max-height: none; + grid-template-columns: 1fr; + } + + .profile-launcher-card { + padding: 12px; + } } .terminal-wrapper { @@ -1802,39 +1905,6 @@ body.broadcast-active .terminal-wrapper:not(.unassigned) { } } -.no-sessions { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: var(--text-secondary); -} - -.no-sessions.hidden { - display: none; -} - -.no-sessions-content { - text-align: center; - animation: fadeIn 0.5s ease; -} - -.no-sessions-content h2 { - font-size: 28px; - font-weight: 700; - margin-bottom: 12px; - color: var(--text-primary); - background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.no-sessions-content p { - font-size: 15px; - color: var(--text-secondary); -} - .btn { padding: 10px 20px; border: none; diff --git a/static/js/app.js b/static/js/app.js index 6a863a6..640ed11 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1095,6 +1095,91 @@ setConnectLoading(false); } + function selectConnectionProfile(profileId) { + const profileSelect = document.getElementById('profileSelect'); + const deleteBtn = document.getElementById('deleteProfileBtn'); + if (profileSelect) { + profileSelect.value = profileId || ''; + } + + if (!profileId) { + ProfileManager.clearLegacyCommands(); + CommandSetManager.selectForConnection(''); + deleteBtn.style.display = 'none'; + delete deleteBtn.dataset.profileId; + return null; + } + + const profile = ProfileManager.getProfile(profileId); + if (!profile) { + if (profileSelect) { + profileSelect.value = ''; + } + deleteBtn.style.display = 'none'; + delete deleteBtn.dataset.profileId; + return null; + } + ProfileManager.selectProfile(profileId); + deleteBtn.style.display = 'block'; + deleteBtn.dataset.profileId = profileId; + return profile; + } + + window.selectConnectionProfile = selectConnectionProfile; + + function isSelectedProfileReady(profile) { + const authTypeSelect = document.getElementById('authTypeSelect'); + const keySelect = document.getElementById('keySelect'); + const jumpHostSelect = document.getElementById('jumpHostSelect'); + if (!profile || authTypeSelect.value !== profile.auth_type) { + return false; + } + if (profile.auth_type === 'key' && keySelect.value !== profile.key_id) { + return false; + } + if (jumpHostSelect.value !== (profile.jump_host_id || '')) { + return false; + } + return true; + } + + function launchProfileForPane(profileId, paneIndex) { + const profile = ProfileManager.getProfile(profileId); + if (!profile) { + showNotification( + window.i18n + ? i18n.t('connection.profileUnavailable') + : 'This profile is no longer available.', + 'warning', + ); + ProfileManager.loadProfiles(); + return; + } + + openConnectionModalForPane(paneIndex); + const selected = selectConnectionProfile(profileId); + if (!selected) { + return; + } + + const mode = ProfileManager.getLaunchMode(selected); + const form = document.getElementById('connectionForm'); + if (mode === 'connect' && isSelectedProfileReady(selected)) { + form.requestSubmit(); + return; + } + + let focusTarget = document.getElementById('connectBtn'); + if (mode === 'password') { + focusTarget = document.getElementById('passwordInput'); + } else if (mode === 'jump-host-password') { + focusTarget = document.getElementById('jumpHostPasswordInput'); + } + window.requestAnimationFrame(() => focusTarget?.focus()); + } + + window.launchProfileForPane = launchProfileForPane; + function queuePaneConnection(paneIndex) { if (paneIndex === null || paneIndex === undefined) { return; @@ -1859,7 +1944,6 @@ ? '' : ProfileManager.getLegacyStartupCommands(); const targetPane = pendingPaneIndex; - pendingPaneIndex = null; if (!host || !username) { showNotification('Host and username are required', 'error'); @@ -1868,6 +1952,7 @@ if (authType === 'password' && !password) { showNotification('Password is required', 'error'); + document.getElementById('passwordInput').focus(); return; } @@ -1895,6 +1980,7 @@ const jhPass = document.getElementById('jumpHostPasswordInput').value; if (!jhPass) { showNotification('Jump host password is required', 'error'); + document.getElementById('jumpHostPasswordInput').focus(); return; } proxyJump.password = jhPass; @@ -1903,6 +1989,8 @@ } } + pendingPaneIndex = null; + if (saveProfile && profileName) { const profilePayload = { name: profileName, @@ -1993,19 +2081,7 @@ }); document.getElementById('profileSelect').addEventListener('change', (e) => { - const profileId = e.target.value; - const deleteBtn = document.getElementById('deleteProfileBtn'); - - if (profileId) { - ProfileManager.selectProfile(profileId); - deleteBtn.style.display = 'block'; - deleteBtn.dataset.profileId = profileId; - } else { - ProfileManager.clearLegacyCommands(); - CommandSetManager.selectForConnection(''); - deleteBtn.style.display = 'none'; - delete deleteBtn.dataset.profileId; - } + selectConnectionProfile(e.target.value); }); document.getElementById('deleteProfileBtn').addEventListener('click', (e) => { diff --git a/static/js/i18n.js b/static/js/i18n.js index 47383c0..fbc3a71 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -38,6 +38,12 @@ const translations = { 'connection.newSSHConnection': 'New SSH Connection', 'connection.noActiveSessions': 'No Active Sessions', 'connection.clickToStart': 'Click "New Connection" to start an SSH session', + 'connection.savedProfiles': 'Saved Profiles', + 'connection.savedProfilesHint': 'Choose a profile to connect', + 'connection.connectNow': 'Connect now', + 'connection.passwordRequired': 'Password required', + 'connection.reviewConnection': 'Review connection', + 'connection.profileUnavailable': 'This profile is no longer available.', 'connection.loadProfile': 'Load Profile (Optional)', 'connection.selectProfile': '-- Select Profile --', 'connection.host': 'Host *', @@ -390,6 +396,12 @@ const translations = { 'connection.newSSHConnection': 'Kết nối SSH mới', 'connection.noActiveSessions': 'Không có phiên hoạt động', 'connection.clickToStart': 'Nhấp vào "Kết nối mới" để bắt đầu một phiên SSH', + 'connection.savedProfiles': 'Cấu hình đã lưu', + 'connection.savedProfilesHint': 'Chọn một cấu hình để kết nối', + 'connection.connectNow': 'Kết nối ngay', + 'connection.passwordRequired': 'Yêu cầu mật khẩu', + 'connection.reviewConnection': 'Kiểm tra kết nối', + 'connection.profileUnavailable': 'Cấu hình này không còn khả dụng.', 'connection.loadProfile': 'Tải cấu hình (Tùy chọn)', 'connection.selectProfile': '-- Chọn cấu hình --', 'connection.host': 'Máy chủ *', @@ -740,6 +752,12 @@ const translations = { 'connection.newSSHConnection': 'Neue SSH-Verbindung', 'connection.noActiveSessions': 'Keine aktiven Sitzungen', 'connection.clickToStart': 'Klicken Sie auf "Neue Verbindung", um eine SSH-Sitzung zu starten', + 'connection.savedProfiles': 'Gespeicherte Profile', + 'connection.savedProfilesHint': 'Profil auswählen und verbinden', + 'connection.connectNow': 'Jetzt verbinden', + 'connection.passwordRequired': 'Kennwort erforderlich', + 'connection.reviewConnection': 'Verbindung prüfen', + 'connection.profileUnavailable': 'Dieses Profil ist nicht mehr verfügbar.', 'connection.loadProfile': 'Profil laden (Optional)', 'connection.selectProfile': '-- Profil auswählen --', 'connection.host': 'Host *', @@ -1072,6 +1090,12 @@ const translations = { 'connection.newSSHConnection': 'Nouvelle connexion SSH', 'connection.noActiveSessions': 'Aucune session active', 'connection.clickToStart': 'Cliquez sur "Nouvelle connexion" pour démarrer une session SSH', + 'connection.savedProfiles': 'Profils enregistrés', + 'connection.savedProfilesHint': 'Choisissez un profil pour vous connecter', + 'connection.connectNow': 'Se connecter', + 'connection.passwordRequired': 'Mot de passe requis', + 'connection.reviewConnection': 'Vérifier la connexion', + 'connection.profileUnavailable': 'Ce profil n’est plus disponible.', 'connection.loadProfile': 'Charger le profil (Optionnel)', 'connection.selectProfile': '-- Sélectionner un profil --', 'connection.host': 'Hôte *', @@ -1391,6 +1415,12 @@ const translations = { 'connection.newSSHConnection': 'Nueva conexión SSH', 'connection.noActiveSessions': 'Sin sesiones activas', 'connection.clickToStart': 'Haz clic en "Nueva conexión" para iniciar una sesión SSH', + 'connection.savedProfiles': 'Perfiles guardados', + 'connection.savedProfilesHint': 'Elige un perfil para conectarte', + 'connection.connectNow': 'Conectar ahora', + 'connection.passwordRequired': 'Se requiere contraseña', + 'connection.reviewConnection': 'Revisar conexión', + 'connection.profileUnavailable': 'Este perfil ya no está disponible.', 'connection.loadProfile': 'Cargar perfil (Opcional)', 'connection.selectProfile': '-- Seleccionar perfil --', 'connection.host': 'Host *', @@ -1710,6 +1740,12 @@ const translations = { 'connection.newSSHConnection': '新建 SSH 连接', 'connection.noActiveSessions': '当前没有活动会话', 'connection.clickToStart': '点击“新建连接”开始一个 SSH 会话', + 'connection.savedProfiles': '已保存的配置', + 'connection.savedProfilesHint': '选择配置以连接', + 'connection.connectNow': '立即连接', + 'connection.passwordRequired': '需要密码', + 'connection.reviewConnection': '检查连接', + 'connection.profileUnavailable': '此配置已不可用。', 'connection.loadProfile': '加载配置(可选)', 'connection.selectProfile': '-- 选择配置 --', 'connection.host': '主机 *', diff --git a/static/js/jump-host-manager.js b/static/js/jump-host-manager.js index 3d3a832..9765885 100644 --- a/static/js/jump-host-manager.js +++ b/static/js/jump-host-manager.js @@ -16,6 +16,9 @@ window.JumpHostManager = { this.jumpHosts = Array.isArray(list) ? list : []; this.renderSelect(); this.renderList(); + if (typeof SessionManager !== 'undefined') { + SessionManager.refreshEmptyPanes(); + } }, getById(id) { diff --git a/static/js/profile-launcher-utils.js b/static/js/profile-launcher-utils.js new file mode 100644 index 0000000..e775be1 --- /dev/null +++ b/static/js/profile-launcher-utils.js @@ -0,0 +1,67 @@ +(function (root, factory) { + const api = factory(); + if (typeof module === 'object' && module.exports) { + module.exports = api; + } + if (root) { + root.ProfileLauncherUtils = api; + } +}(typeof globalThis !== 'undefined' ? globalThis : this, function () { + 'use strict'; + + function hasKey(keys, keyId) { + return Boolean(keyId) && (Array.isArray(keys) ? keys : []) + .some(key => key && key.id === keyId && key.usable === true); + } + + function determineLaunchMode(profile, context = {}) { + if (!profile || typeof profile !== 'object') { + return 'review'; + } + + const keys = Array.isArray(context.keys) ? context.keys : []; + const jumpHosts = Array.isArray(context.jumpHosts) ? context.jumpHosts : []; + const needsTargetPassword = profile.auth_type === 'password'; + + if (profile.auth_type === 'key') { + if (!hasKey(keys, profile.key_id)) { + return 'review'; + } + } else if ( + profile.auth_type === 'tailscale' + && profile.tailscale_authorized !== true + ) { + return 'review'; + } else if (!needsTargetPassword && profile.auth_type !== 'tailscale') { + return 'review'; + } + + if (!profile.jump_host_id) { + return needsTargetPassword ? 'password' : 'connect'; + } + + const jumpHost = jumpHosts.find(item => ( + item && item.id === profile.jump_host_id + )); + if (!jumpHost) { + return 'review'; + } + if (jumpHost.auth_type === 'password') { + return needsTargetPassword ? 'password' : 'jump-host-password'; + } + if (jumpHost.auth_type !== 'key' || !hasKey(keys, jumpHost.key_id)) { + return 'review'; + } + return needsTargetPassword ? 'password' : 'connect'; + } + + function formatEndpoint(profile) { + const value = profile && typeof profile === 'object' ? profile : {}; + const username = String(value.username || ''); + const host = String(value.host || ''); + const port = Number(value.port) || 22; + return `${username}@${host}:${port}`; + } + + return { determineLaunchMode, formatEndpoint }; +})); diff --git a/static/js/profile-manager.js b/static/js/profile-manager.js index b10f9c7..bc9dc7c 100644 --- a/static/js/profile-manager.js +++ b/static/js/profile-manager.js @@ -1,6 +1,7 @@ const ProfileManager = { profiles: [], keys: [], + profilesLoaded: false, selectedLegacyStartupCommands: '', loadProfiles() { @@ -16,14 +17,119 @@ const ProfileManager = { }, setProfiles(profiles) { - this.profiles = profiles; + this.profiles = Array.isArray(profiles) ? profiles : []; + this.profilesLoaded = true; this.renderProfileSelect(); + this.refreshEmptyPanes(); }, setKeys(keys) { - this.keys = keys; + this.keys = Array.isArray(keys) ? keys : []; this.renderKeySelect(); this.renderKeysList(); + this.refreshEmptyPanes(); + }, + + refreshEmptyPanes() { + if (typeof SessionManager !== 'undefined') { + SessionManager.refreshEmptyPanes(); + } + }, + + getProfile(profileId) { + return this.profiles.find(profile => profile && profile.id === profileId) || null; + }, + + getLaunchMode(profile) { + return ProfileLauncherUtils.determineLaunchMode(profile, { + keys: this.keys, + jumpHosts: window.JumpHostManager?.jumpHosts || [], + }); + }, + + createEmptyPaneContent(paneIndex) { + const empty = document.createElement('div'); + empty.className = 'pane-empty profile-launcher'; + + const icon = document.createElement('div'); + icon.className = 'pane-empty-icon'; + icon.setAttribute('aria-hidden', 'true'); + icon.textContent = '💻'; + empty.appendChild(icon); + + const profiles = this.profilesLoaded ? this.profiles : []; + const title = document.createElement('div'); + title.className = 'profile-launcher-title'; + title.textContent = profiles.length + ? (window.i18n ? i18n.t('connection.savedProfiles') : 'Saved Profiles') + : (window.i18n ? i18n.t('panes.emptyPane') : 'Empty pane'); + empty.appendChild(title); + + const hint = document.createElement('div'); + hint.className = 'profile-launcher-hint'; + hint.textContent = profiles.length + ? (window.i18n ? i18n.t('connection.savedProfilesHint') : 'Choose a profile to connect') + : (window.i18n ? i18n.t('panes.selectSession') : 'Select a session or open a connection'); + empty.appendChild(hint); + + if (profiles.length) { + const list = document.createElement('div'); + list.className = 'profile-launcher-list'; + profiles.forEach(profile => { + if (!profile || !profile.id) return; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'profile-launcher-card'; + button.dataset.profileId = profile.id; + + const name = document.createElement('span'); + name.className = 'profile-launcher-name'; + name.textContent = profile.name; + + const endpoint = document.createElement('span'); + endpoint.className = 'profile-launcher-endpoint'; + endpoint.textContent = ProfileLauncherUtils.formatEndpoint(profile); + + const mode = this.getLaunchMode(profile); + const action = document.createElement('span'); + action.className = `profile-launcher-action mode-${mode}`; + action.textContent = mode === 'connect' + ? (window.i18n ? i18n.t('connection.connectNow') : 'Connect now') + : (mode === 'password' || mode === 'jump-host-password' + ? (window.i18n ? i18n.t('connection.passwordRequired') : 'Password required') + : (window.i18n ? i18n.t('connection.reviewConnection') : 'Review connection')); + + button.setAttribute( + 'aria-label', + `${String(profile.name || '')}, ${ProfileLauncherUtils.formatEndpoint(profile)}, ${action.textContent}`, + ); + button.append(name, endpoint, action); + button.addEventListener('click', event => { + event.stopPropagation(); + if (window.launchProfileForPane) { + window.launchProfileForPane(profile.id, paneIndex); + } + }); + list.appendChild(button); + }); + empty.appendChild(list); + } + + const newConnection = document.createElement('button'); + newConnection.type = 'button'; + newConnection.className = profiles.length + ? 'btn btn-secondary profile-launcher-new' + : 'btn btn-primary profile-launcher-new'; + newConnection.textContent = window.i18n + ? i18n.t('connection.newConnection') + : 'New Connection'; + newConnection.addEventListener('click', event => { + event.stopPropagation(); + window.openConnectionModalForPane?.(paneIndex); + }); + empty.appendChild(newConnection); + return empty; }, renderProfileSelect() { diff --git a/static/js/session-manager.js b/static/js/session-manager.js index af5f17f..8232c98 100644 --- a/static/js/session-manager.js +++ b/static/js/session-manager.js @@ -17,6 +17,9 @@ const SessionManager = { } this.ensureTerminalGrid(); this.setSplitLayout(1); + window.addEventListener('languageChanged', () => { + this.refreshEmptyPanes(); + }); const sessionBar = document.getElementById('sessionBar'); if (sessionBar) { sessionBar.classList.remove('hidden'); @@ -77,7 +80,6 @@ const SessionManager = { terminalContainer.className = 'terminal-wrapper unassigned'; document.getElementById('terminalsContainer').appendChild(terminalContainer); - document.getElementById('noSessions').classList.add('hidden'); const sessionBar = document.getElementById('sessionBar'); if (sessionBar) { sessionBar.classList.remove('hidden'); @@ -145,7 +147,6 @@ const SessionManager = { } }); - document.getElementById('noSessions').classList.add('hidden'); const sessionBar = document.getElementById('sessionBar'); if (sessionBar) { sessionBar.classList.remove('hidden'); @@ -339,7 +340,6 @@ const SessionManager = { } } else { this.activeSessionId = null; - document.getElementById('noSessions').classList.remove('hidden'); this.updateSessionMeta(null); } }, @@ -759,6 +759,14 @@ const SessionManager = { this.updateSplitControls(); }, + refreshEmptyPanes() { + this.paneAssignments.forEach((sessionId, index) => { + if (!this.paneAssignments[index]) { + this.renderPane(index); + } + }); + }, + renderPane(paneIndex) { const grid = this.ensureTerminalGrid(); if (!grid) { @@ -785,28 +793,13 @@ const SessionManager = { return; } - const empty = document.createElement('div'); - empty.className = 'pane-empty'; - - const icon = document.createElement('div'); - icon.className = 'pane-empty-icon'; - icon.textContent = '💻'; - empty.appendChild(icon); - - const emptyText = document.createElement('div'); - emptyText.className = 'pane-empty-text'; - emptyText.textContent = window.i18n ? window.i18n.t('panes.emptyPane') : 'Empty pane'; - empty.appendChild(emptyText); - - const button = document.createElement('button'); - button.className = 'btn btn-primary'; - button.textContent = window.i18n ? window.i18n.t('connection.newConnection') : 'New connection'; - button.addEventListener('click', () => { - if (window.openConnectionModalForPane) { - window.openConnectionModalForPane(paneIndex); - } - }); - empty.appendChild(button); + const empty = typeof ProfileManager !== 'undefined' + ? ProfileManager.createEmptyPaneContent(paneIndex) + : document.createElement('div'); + if (!empty.className) { + empty.className = 'pane-empty'; + empty.textContent = window.i18n ? i18n.t('panes.emptyPane') : 'Empty pane'; + } pane.appendChild(empty); }, diff --git a/templates/index.html b/templates/index.html index 33d6008..c2d83e0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -17,7 +17,7 @@ - + @@ -142,13 +142,6 @@

Web SSH Terminal

Release to choose destination path
-
-
-

No Active Sessions

-

Click "New Connection" to start an SSH session

-
-
-