From be24dbc53d9b095eb9f0a05cee8f8b3f207a601b Mon Sep 17 00:00:00 2001 From: bifrost0x Date: Fri, 13 Nov 2026 18:19:55 +0100 Subject: [PATCH 1/3] Show project support links in Settings and the account menu The Settings side panel lists GitHub Sponsors, Patreon and Buy Me a Coffee below the account and version facts. The side panel only appears on wide screens, so the account menu also links to the repository's sponsor dialog, which lists the same profiles. The links open in a new tab without access to the WebSSH window and load nothing from external sites until a user follows one. Administrators can hide the links for all users under Administration -> Integrations. The setting is shown by default, stored in app_settings.json and changed through the existing step-up protected settings endpoint. Older versions ignore the additional key. --- app/__init__.py | 14 +++++- app/app_settings.py | 18 ++++++++ docs/wiki/Configuration-Reference.md | 5 +++ static/css/admin.css | 25 +++++++++++ static/css/style.css | 6 +++ static/js/admin.js | 4 +- static/js/i18n.js | 24 +++++++++++ templates/admin.html | 4 ++ templates/index.html | 3 ++ templates/security.html | 11 +++++ tests/e2e/account-menu.spec.js | 64 ++++++++++++++++++++++++++++ tests/test_admin_routes.py | 34 +++++++++++++++ tests/test_security_ui.py | 26 +++++++++++ 13 files changed, 235 insertions(+), 3 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 66e2e997..af980156 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -27,7 +27,8 @@ save_user_settings, ) from .app_settings import (is_registration_enabled, set_registration_enabled, - is_ssh_gateway_enabled, set_ssh_gateway_enabled) + is_ssh_gateway_enabled, set_ssh_gateway_enabled, + is_support_links_enabled, set_support_links_enabled) from .storage_errors import StorageCorruptionError from .tailscale_ssh import user_can_use_tailscale_ssh from .version import display_version @@ -641,6 +642,9 @@ def inject_url_prefix(): 'webssh_version': ( display_version() if current_user.is_authenticated else '' ), + 'support_links_enabled': ( + current_user.is_authenticated and is_support_links_enabled() + ), } @app.before_request @@ -1735,6 +1739,7 @@ def admin_get_settings(): policies = {name: runtime_feature_status(name) for name in RUNTIME_FEATURES} return jsonify({'setting_sources': native_setting_sources(), 'runtime_features': policies, **{name: status['enabled'] for name, status in policies.items()}, 'registration_enabled': is_registration_enabled(), 'ssh_gateway_enabled': is_ssh_gateway_enabled(), + 'support_links_enabled': is_support_links_enabled(), 'credentials_enabled': is_credentials_enabled(), 'automation_enabled': is_automation_enabled()}) @@ -1764,6 +1769,9 @@ def admin_set_settings(): if ('ssh_gateway_enabled' in data and type(data['ssh_gateway_enabled']) is not bool): return jsonify({'error': 'ssh_gateway_enabled must be a boolean'}), 400 + if ('support_links_enabled' in data + and type(data['support_links_enabled']) is not bool): + return jsonify({'error': 'support_links_enabled must be a boolean'}), 400 if 'registration_enabled' in data: if type(data['registration_enabled']) is not bool: return jsonify({ @@ -1786,6 +1794,10 @@ def admin_set_settings(): val = set_ssh_gateway_enabled(data['ssh_gateway_enabled']) log_info("Admin changed SSH gateway setting", admin=current_user.username, ssh_gateway_enabled=val) + if 'support_links_enabled' in data: + val = set_support_links_enabled(data['support_links_enabled']) + log_info("Admin changed support links setting", + admin=current_user.username, support_links_enabled=val) from .app_settings import set_credentials_enabled, set_automation_enabled for feature, setter in (('credentials_enabled', set_credentials_enabled), ('automation_enabled', set_automation_enabled)): diff --git a/app/app_settings.py b/app/app_settings.py index a581d2df..cc909d57 100644 --- a/app/app_settings.py +++ b/app/app_settings.py @@ -22,6 +22,7 @@ 'server_operations_enabled', 'tmux_enabled', 'tmux_default', + 'support_links_enabled', ) @@ -124,6 +125,23 @@ def set_ssh_gateway_enabled(value): return value +def is_support_links_enabled(): + """Project support links are shown unless an administrator hid them.""" + return _load().get('support_links_enabled') is not False + + +def set_support_links_enabled(value): + if type(value) is not bool: + return False + with storage_lock(f'app-settings:{_SETTINGS_FILE}'): + data = _load_with_lock_held() + data['support_links_enabled'] = value + if not _valid_settings(data): + return False + _save(data) + return value + + def set_audit_backup_count(value): if type(value) is not int or not 1 <= value <= 90: return False diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md index 629ba857..3fe58623 100644 --- a/docs/wiki/Configuration-Reference.md +++ b/docs/wiki/Configuration-Reference.md @@ -305,6 +305,11 @@ or bypass provider, HTTPS, origin, RP-ID or recovery checks. Networking, identit provider trust, secrets, resource ceilings, mounts and backup recovery paths remain deployment configuration. Audit retention is already managed in Administration. +Signed-in users see links to support WebSSH development in the account menu +and the Settings side panel. **Project support links** in Administration → +Integrations hides them for all users; the links load nothing from external +sites until a user follows one. + TOTP capability is available by default in 3.0; Admin activation remains off until explicitly enabled. `TOTP_ENABLED=false` is the optional operator kill switch. Existing enrolled users and saved activation choices are retained. diff --git a/static/css/admin.css b/static/css/admin.css index 08c0cb62..a953e9cf 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -580,6 +580,31 @@ font-size: 17px; } +.settings-support-hint { + margin: 0 0 8px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.5; +} + +.settings-support-links { + display: grid; + gap: 2px; + margin: 0; + padding: 0; + list-style: none; +} + +.settings-support-links .settings-quick-link { + font-size: 13px; + text-decoration: none; +} + +.settings-support-links .settings-quick-link:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 1px; +} + .settings-context-note { display: flex; align-items: flex-start; diff --git a/static/css/style.css b/static/css/style.css index 2159e3f9..e2c7470f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -4157,6 +4157,12 @@ body.keyboard-open.notepad-focused .notepad-panel { display: none; } +.account-item .account-item-external { + margin-left: auto; + color: var(--text-secondary); + font-size: 15px; +} + .account-version { margin: 6px 4px 0; padding: 8px 10px 4px; diff --git a/static/js/admin.js b/static/js/admin.js index b611748c..af7827ae 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1378,7 +1378,7 @@ gateway.checked = data.ssh_gateway_enabled === true; gateway.disabled = false; } - for (const [id, key] of [['settingCredentials', 'credentials_enabled'], ['settingAutomation', 'automation_enabled']]) { + for (const [id, key] of [['settingCredentials', 'credentials_enabled'], ['settingAutomation', 'automation_enabled'], ['settingSupportLinks', 'support_links_enabled']]) { const input = document.getElementById(id); if (input && !settingsWrites.has(key)) { input.checked = data[key] === true; @@ -1551,7 +1551,7 @@ target.disabled = false; } }); - for (const [id, key] of [['settingCredentials', 'credentials_enabled'], ['settingAutomation', 'automation_enabled']]) { + for (const [id, key] of [['settingCredentials', 'credentials_enabled'], ['settingAutomation', 'automation_enabled'], ['settingSupportLinks', 'support_links_enabled']]) { document.getElementById(id)?.addEventListener('change', async (event) => { const input = event.target; const enabled = input.checked; diff --git a/static/js/i18n.js b/static/js/i18n.js index 4544ce31..ff82f289 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -955,6 +955,8 @@ const translations = { 'gateway.target': "Waiting for target access. Review gateway prompts below.", 'admin.integrations': "Integrations", 'admin.sshGateway': "Warpgate / SSH gateway", + 'admin.supportLinks': "Project support links", + 'admin.supportLinksHint': "Show links to support WebSSH development in the account menu and the Settings side panel for all users.", 'admin.sshGatewayHint': "Allow interactive gateway connections using user:target for all users. Disabled by default. Disabling blocks new connections and unfinished gateway authentication; established sessions remain connected. Reload open workspaces after changing this setting.", 'gateway.cancel': "Cancel connection", 'gateway.passwordHint': "Leave the password empty for interactive gateway authentication.", @@ -2021,6 +2023,8 @@ const translations = { 'settings.webSshVersion': 'WebSSH version', 'settings.personal': 'Personal', 'settings.personalAndAdministration': 'Personal + administration', + 'settings.supportProject': 'Support WebSSH', + 'settings.supportProjectHint': 'WebSSH is free and open source. Support ongoing development:', 'settings.quickAccess': 'Quick access', 'settings.userManagement': 'User management', 'settings.authenticationPolicy': 'Authentication policy', @@ -3383,6 +3387,8 @@ const translations = { 'workspace.directorySync.prompt': "Đồng bộ thư mục tạm dừng: chưa xác nhận được dòng lệnh trống. Hoàn tất nhập lệnh hoặc tắt đồng bộ để duyệt tệp độc lập.", 'workspace.directorySync.setup': "Thiết lập nhận diện dấu nhắc Bash", 'workspace.directorySync.setupHint': "Tự chạy khối lệnh đã sao chép tại dấu nhắc Bash trống trong ô này. Không thay đổi tệp cấu hình. Chờ nhận diện rồi nhấn Enter. Tiện ích tồn tại đến khi thoát shell, kể cả khi tắt đồng bộ.", + 'admin.supportLinks': "Liên kết ủng hộ dự án", + 'admin.supportLinksHint': "Hiển thị liên kết ủng hộ việc phát triển WebSSH trong menu tài khoản và bảng bên của Cài đặt cho mọi người dùng.", 'workspace.directorySync.copy': "Sao chép lệnh kích hoạt", 'workspace.directorySync.copyFailed': "Không sao chép được. Hãy chọn văn bản hiển thị hoặc thử lại.", 'workspace.directorySync.ready': "Đồng bộ thư mục sẵn sàng.", @@ -4449,6 +4455,8 @@ const translations = { 'settings.centerSummary': 'Tùy chọn cá nhân, bảo mật tài khoản và các điều khiển quản trị nay dùng chung một Trung tâm cài đặt.', 'settings.languageUpdated': 'Đã cập nhật ngôn ngữ.', 'settings.savingTheme': 'Đang lưu chủ đề…', + 'settings.supportProject': 'Ủng hộ WebSSH', + 'settings.supportProjectHint': 'WebSSH miễn phí và mã nguồn mở. Hãy ủng hộ việc phát triển tiếp theo:', 'settings.themeSaved': 'Đã lưu chủ đề.', 'settings.scrollbackSaved': 'Đã lưu cuộn lùi trong trình duyệt này.', 'settings.scrollbackFailed': 'Bộ nhớ trình duyệt không khả dụng hoặc đã đầy. Cài đặt cuộn lùi trước đó không thay đổi.', @@ -5815,6 +5823,8 @@ const translations = { 'workspace.directorySync.waiting': "Ordner-Sync pausiert. Beende oder verwirf deine Eingabe. Drücke am leeren Prompt Enter; bleibt der Sync pausiert, schalte ihn aus und wieder an und drücke Enter.", 'workspace.directorySync.setupRequired': "Die Bash-Prompt-Erkennung ist für diesen Bereich noch nicht eingerichtet.", 'workspace.directorySync.unsupported': "Für diese Shell ist keine automatische Prompt-Erkennung verfügbar.", + 'admin.supportLinks': "Links zur Projektunterstützung", + 'admin.supportLinksHint': "Zeigt allen Benutzern im Kontomenü und in der Seitenleiste der Einstellungen Links, mit denen sie die Entwicklung von WebSSH unterstützen können.", 'workspace.directorySync.unavailable': "Der Terminal-Ordner konnte nicht geprüft werden. Verlasse den tmux-Kopiermodus oder das laufende Programm und versuche es erneut.", 'workspace.directorySync.pending': "Der vorherige Ordnerwechsel wird noch geprüft. Versuche es gleich erneut.", 'profiles.saveConnect': "Speichern & verbinden", @@ -6899,6 +6909,8 @@ const translations = { 'settings.saving': 'Einstellung wird gespeichert…', 'settings.saved': 'Einstellung gespeichert.', 'security.passkeysAndMfa': 'Passkeys & MFA', + 'settings.supportProject': 'WebSSH unterstützen', + 'settings.supportProjectHint': 'WebSSH ist kostenlos und Open Source. Unterstütze die Weiterentwicklung:', 'security.personalSshTrust': 'Persönliches SSH-Vertrauen', 'security.personalSshTrustHint': 'Die von diesem Konto als vertrauenswürdig eingestuften SSH-Host-Schlüssel prüfen.', 'security.factorsHint': 'Starke Authentifizierungsfaktoren dieses Kontos hinzufügen, prüfen und entfernen.', @@ -8242,6 +8254,8 @@ const translations = { 'profiles.compact': "Vue compacte", 'settings.directorySyncDefault': "Synchroniser les dossiers avec le terminal par défaut", 'settings.directorySyncDefaultHint': "S’applique à l’ouverture ou au rechargement de l’espace de travail.", + 'admin.supportLinks': "Liens de soutien au projet", + 'admin.supportLinksHint': "Affiche à tous les utilisateurs, dans le menu du compte et le panneau latéral des paramètres, des liens pour soutenir le développement de WebSSH.", 'workspace.directorySync.label': "Synchroniser les dossiers avec le terminal", 'workspace.directorySync.hint': "Lier la navigation à ce terminal. Commence dans le dossier du terminal.", 'sessionCommands.editParameters': "Paramètres pour cette insertion", @@ -9337,6 +9351,8 @@ const translations = { 'security.loadingLinkedAccount': 'Chargement du compte lié…', 'security.managePasskeysHint': 'Gérez les identifiants de connexion sans mot de passe.', 'security.manageAuthenticatorHint': 'Gérez les codes à usage unique basés sur le temps.', + 'settings.supportProject': 'Soutenir WebSSH', + 'settings.supportProjectHint': 'WebSSH est gratuit et open source. Soutenez son développement :', 'security.manageGithubHint': 'Connectez ou déconnectez l’identité GitHub utilisée avec ce compte WebSSH.', 'security.manageOidcHint': 'Associez une identité en vous connectant au fournisseur configuré. WebSSH ne conserve que son émetteur et son sujet vérifiés.', 'security.manageRecoveryCodesHint': 'Générez et conservez les codes de connexion d’urgence.', @@ -10669,6 +10685,8 @@ const translations = { 'sessionCommands.resetParameters': "Restaurar los parámetros guardados", 'workspace.toolsReconnecting': "Reconectando — los datos mostrados pueden estar desactualizados.", 'workspace.toolsRefreshing': "Actualizando — los datos mostrados pueden estar desactualizados.", + 'admin.supportLinks': "Enlaces de apoyo al proyecto", + 'admin.supportLinksHint': "Muestra a todos los usuarios, en el menú de la cuenta y el panel lateral de ajustes, enlaces para apoyar el desarrollo de WebSSH.", 'workspace.toolsRefreshFailed': "No se pudo actualizar. Los datos mostrados están desactualizados.", 'workspace.toolsRetry': "Volver a cargar", 'commands.added': "Comando añadido", @@ -11764,6 +11782,8 @@ const translations = { 'security.githubManagedTitle': 'Inicio de sesión con GitHub', 'security.githubManagedHint': 'GitHub controla este inicio de sesión principal. WebSSH descarta el token temporal de GitHub tras comprobar la identidad y la organización.', 'security.recoveryCodesPageHint': 'Genera y guarda de forma segura códigos de un solo uso para inicios de sesión de emergencia.', + 'settings.supportProject': 'Apoyar WebSSH', + 'settings.supportProjectHint': 'WebSSH es gratuito y de código abierto. Apoya su desarrollo:', 'security.accountIdentityUnavailable': 'La identidad de la cuenta no está disponible', 'admin.authentication': 'Autenticación', 'admin.authenticationHint': 'Gestiona los métodos de inicio de sesión disponibles para los usuarios de WebSSH.', @@ -13095,6 +13115,8 @@ const translations = { 'commands.updated': "命令已更新", 'commands.deleted': "命令已删除", 'commands.deleteConfirm': "删除命令“{name}”?", + 'admin.supportLinks': "项目支持链接", + 'admin.supportLinksHint': "在账户菜单和设置侧栏中向所有用户显示支持 WebSSH 开发的链接。", 'commands.copyToMine': "添加到我的命令", 'commands.requiredFields': "名称、命令和描述不能为空", 'commands.osRequired': "请至少选择一个操作系统", @@ -14179,6 +14201,8 @@ const translations = { 'admin.registration': '注册', 'admin.registrationHint': '控制谁可以创建本地 WebSSH 账户。', 'admin.globalSshTrust': '全局 SSH 信任', + 'settings.supportProject': '支持 WebSSH', + 'settings.supportProjectHint': 'WebSSH 免费且开源。支持后续开发:', 'admin.auditRetention': '审计保留', 'admin.auditRetentionPageHint': '定义 WebSSH 保留的轮转审计归档数量。', 'admin.usersHint': '管理账户、角色、访问状态和恢复操作。', diff --git a/templates/admin.html b/templates/admin.html index 7ef1d644..9db74ebb 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -303,6 +303,10 @@

GitHub authentication

Allow interactive gateway connections using user:target for all users. Disabled by default. Disabling blocks new connections and unfinished gateway authentication; established sessions remain connected. Reload open workspaces after changing this setting.

+
+

Show links to support WebSSH development in the account menu and the Settings side panel for all users.

+ +
diff --git a/templates/index.html b/templates/index.html index f89f691b..8e640ac4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -143,6 +143,9 @@ Settings + {% if support_links_enabled %} + Support WebSSH + {% endif %} diff --git a/templates/security.html b/templates/security.html index 442d4187..f1093d26 100644 --- a/templates/security.html +++ b/templates/security.html @@ -423,6 +423,17 @@

SSH host trust

Settings scope
{% if admin_panel_enabled and is_admin %}Personal + administration{% else %}Personal{% endif %}
+ {% if support_links_enabled %} +
+ Support WebSSH +

WebSSH is free and open source. Support ongoing development:

+ +
+ {% endif %} {% endif %} diff --git a/tests/e2e/account-menu.spec.js b/tests/e2e/account-menu.spec.js index d27a0762..8b2d4662 100644 --- a/tests/e2e/account-menu.spec.js +++ b/tests/e2e/account-menu.spec.js @@ -126,6 +126,11 @@ test('account menu and settings stay inside a mobile viewport', async ({ page }) await version.scrollIntoViewIfNeeded(); await expect(version).toBeVisible(); await expect(version).toHaveText(/^\d+\.\d+\.\d+( \([0-9a-f]{7}\))?$/); + const support = page.locator('#accountSupportLink'); + await expect(support).toBeVisible(); + await expect(support).toHaveAttribute('href', 'https://github.com/bifrost0x/webssh?sponsor=1'); + await expect(support).toHaveAttribute('target', '_blank'); + await expect(support).toHaveAttribute('rel', 'noopener noreferrer'); const versionBounds = await version.boundingBox(); expect(versionBounds.x + versionBounds.width).toBeLessThanOrEqual(390); @@ -141,3 +146,62 @@ test('account menu and settings stay inside a mobile viewport', async ({ page }) })); expect(layout.document).toBeLessThanOrEqual(layout.viewport); }); + +test('settings side panel lists the support links below the account facts', async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 1000 }); + await login(page, 'e2e_user', { workspace: false }); + await page.goto('/settings'); + + const rail = page.locator('.settings-context-rail'); + const support = rail.locator('.settings-support-card'); + await expect(support).toBeVisible(); + const links = support.getByRole('link'); + await expect(links).toHaveCount(3); + for (const [index, name] of ['GitHub Sponsors', 'Patreon', 'Buy Me a Coffee'].entries()) { + await expect(links.nth(index)).toHaveAccessibleName(name); + } + const accountBounds = await rail.locator('.settings-account-card').boundingBox(); + const supportBounds = await support.boundingBox(); + expect(supportBounds.y).toBeGreaterThan(accountBounds.y + accountBounds.height); + + // Answer the new tab locally; the test must not reach the real site. + await page.context().route('https://github.com/**', route => route.fulfill({ body: 'sponsors' })); + const opened = page.context().waitForEvent('page'); + await support.getByRole('link', { name: 'GitHub Sponsors' }).click(); + const popup = await opened; + expect(popup.url()).toBe('https://github.com/sponsors/bifrost0x'); + expect(await popup.evaluate(() => window.opener)).toBeNull(); + await popup.close(); + await expect(page).toHaveURL(/\/settings/); +}); + +test('administrators can hide the support links for all users', async ({ page }) => { + async function setSupportLinks(enabled) { + await page.goto('/settings#integrations'); + const toggle = page.locator('#settingSupportLinks'); + await expect(toggle).toBeEnabled(); + if ((await toggle.isChecked()) === enabled) return; + await toggle.setChecked(enabled); + await expect(page.locator('#stepUpModal')).toHaveClass(/show/); + await page.locator('#stepUpPassword').fill('browser-password'); + await page.locator('#stepUpSubmit').click(); + await expect(toggle).toBeEnabled(); + await expect(toggle).toBeChecked({ checked: enabled }); + } + + await page.setViewportSize({ width: 1600, height: 1000 }); + await login(page); + await expect(page.locator('#settingSupportLinks')).toHaveCount(0); + try { + await setSupportLinks(false); + await page.reload(); + await expect(page.locator('#settingSupportLinks')).not.toBeChecked(); + await expect(page.locator('.settings-support-card')).toHaveCount(0); + await page.goto('/'); + await expect(page.locator('#accountSupportLink')).toHaveCount(0); + } finally { + await setSupportLinks(true); + } + await page.reload(); + await expect(page.locator('.settings-support-card')).toBeVisible(); +}); diff --git a/tests/test_admin_routes.py b/tests/test_admin_routes.py index cde89ae5..c54b4406 100644 --- a/tests/test_admin_routes.py +++ b/tests/test_admin_routes.py @@ -420,6 +420,40 @@ def test_invalid_gateway_setting_does_not_change_registration(app, client): assert app_settings.is_ssh_gateway_enabled() is False +def test_support_links_setting_requires_step_up_and_hides_the_links(app, client): + from app import app_settings + _prepare_role(app, client, 'admin') + sponsor = b'href="https://github.com/bifrost0x/webssh?sponsor=1"' + assert client.get('/admin/api/settings').get_json()['support_links_enabled'] is True + assert sponsor in client.get('/').data + assert b'settings-support-card' in client.get('/settings').data + + response = client.post('/admin/api/settings', json={'support_links_enabled': False}) + assert response.status_code == 403 + assert app_settings.is_support_links_enabled() is True + + for enabled in (False, True): + response = client.post('/admin/api/settings', json={'support_links_enabled': enabled}, + headers=password_step_up_headers(client, 'settings.update', 'global')[0]) + assert response.status_code == 200 + assert response.get_json()['support_links_enabled'] is enabled + assert app_settings.is_support_links_enabled() is enabled + assert (sponsor in client.get('/').data) is enabled + assert (b'settings-support-card' in client.get('/settings').data) is enabled + + +def test_invalid_support_links_setting_is_rejected(app, client): + from app import app_settings + _prepare_role(app, client, 'admin') + response = client.post('/admin/api/settings', + json={'support_links_enabled': 'false', 'ssh_gateway_enabled': True}, + headers=password_step_up_headers(client, 'settings.update', 'global')[0]) + assert response.status_code == 400 + assert app_settings.is_support_links_enabled() is True + assert app_settings.is_ssh_gateway_enabled() is False + assert app_settings.set_support_links_enabled('false') is False + + def test_security_feature_status_exposes_all_supported_gates(app, client): _prepare_role(app, client, 'admin') diff --git a/tests/test_security_ui.py b/tests/test_security_ui.py index 527dc687..8854e507 100644 --- a/tests/test_security_ui.py +++ b/tests/test_security_ui.py @@ -476,3 +476,29 @@ def test_start_page_persists_without_changing_other_preferences(app, client): assert b'data-saved="workspaces"' in client.get('/settings').data assert client.post('/api/account/preferences', json={'start_page': 'overview'}).status_code == 200 assert b'name="start-page" content="overview"' in client.get('/').data + + +def test_settings_rail_links_to_the_funding_profiles(app, client): + import re + + _create_user(app, "settings_supporter") + _login(client, "settings_supporter") + + page = client.get("/settings").data.decode() + + assert 'data-i18n="settings.supportProject"' in page + funding = (ROOT / ".github/FUNDING.yml").read_text(encoding="utf-8") + profiles = dict(re.findall(r"^(\w+):\s*(\S+)$", funding, re.MULTILINE)) + expected = { + "github": "https://github.com/sponsors/{}", + "patreon": "https://www.patreon.com/{}", + "buy_me_a_coffee": "https://buymeacoffee.com/{}", + } + assert set(profiles) == set(expected) + for platform, url in expected.items(): + href = url.format(profiles[platform]) + link = re.search(r']*href="' + re.escape(href) + r'"[^>]*>', page) + assert link, href + # External links must not get a handle on the WebSSH window. + assert 'target="_blank"' in link.group(0) + assert 'rel="noopener noreferrer"' in link.group(0) From 6b2296a2de5018b1990e254d51c40d4229b5f49e Mon Sep 17 00:00:00 2001 From: bifrost0x Date: Fri, 13 Nov 2026 18:19:56 +0100 Subject: [PATCH 2/3] Translate remaining hardcoded workspace strings Several labels stayed English regardless of the selected language: the truncated file preview notice, the image preview alt text, one connection resources navigation label, the Bash activation command field, the cancel button on a connecting tab, the tmux badge tooltip, the SSH key validation message, the drop hint without a session, and the file manager quick connect password field. They now use existing keys where one fits and new keys in all six languages otherwise. The quick connect dialog also translates placeholders now. Decorative icons in the touched markup are hidden from screen readers. --- static/js/drag-drop-manager.js | 2 +- static/js/i18n.js | 80 ++++++++++++++++++++++++---------- static/js/profile-manager.js | 2 +- static/js/session-manager.js | 9 ++-- static/js/sftp-file-manager.js | 11 +++-- templates/index.html | 10 ++--- tests/test_security_ui.py | 25 +++++++++++ 7 files changed, 104 insertions(+), 35 deletions(-) diff --git a/static/js/drag-drop-manager.js b/static/js/drag-drop-manager.js index 6cc8dbc0..4f623630 100644 --- a/static/js/drag-drop-manager.js +++ b/static/js/drag-drop-manager.js @@ -331,7 +331,7 @@ class DragDropManager { if (session) { hint.textContent = `${session.username}@${session.host} — ${this.t('dropUpload.chooseFolder', 'Choose a destination folder after dropping.')}`; } else { - hint.textContent = 'Connect to a server first'; + hint.textContent = this.t('dropUpload.noSession', 'Connect to a server first'); } } } diff --git a/static/js/i18n.js b/static/js/i18n.js index ff82f289..0538db4f 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -964,6 +964,7 @@ const translations = { 'workspace.directorySync.setup': "Set up Bash prompt detection", 'workspace.directorySync.setupHint': "Run the copied block yourself at an empty Bash prompt in this pane. No configuration files are changed. Wait for detection, then press Enter. The helper stays until this shell exits, even with sync off.", 'workspace.directorySync.copy': "Copy activation command", + 'workspace.directorySync.commandLabel': 'Bash activation command', 'workspace.directorySync.copyFailed': "Could not copy the command. Select the displayed text or try again.", 'workspace.directorySync.ready': "Folder sync ready.", 'workspace.directorySync.waiting': "Folder sync paused. Finish or cancel your input. At an empty prompt, press Enter; if still paused, switch sync off and on, then press Enter.", @@ -1111,6 +1112,7 @@ const translations = { 'dropUpload.folderHint': "Relative paths start in the SFTP home directory, not the terminal working directory.", 'dropUpload.timeout': "Creating the destination folder timed out.", 'dropUpload.chooseFolder': "Choose a destination folder after dropping.", + 'dropUpload.noSession': 'Connect to a server first', 'app.title': 'Web SSH Terminal', 'app.subtitle': 'Modern Terminal Access', @@ -2021,10 +2023,10 @@ const translations = { 'settings.primarySignIn': 'Primary sign-in', 'settings.scope': 'Settings scope', 'settings.webSshVersion': 'WebSSH version', - 'settings.personal': 'Personal', - 'settings.personalAndAdministration': 'Personal + administration', 'settings.supportProject': 'Support WebSSH', 'settings.supportProjectHint': 'WebSSH is free and open source. Support ongoing development:', + 'settings.personal': 'Personal', + 'settings.personalAndAdministration': 'Personal + administration', 'settings.quickAccess': 'Quick access', 'settings.userManagement': 'User management', 'settings.authenticationPolicy': 'Authentication policy', @@ -2180,6 +2182,7 @@ const translations = { 'fm.qc.username': 'Username', 'fm.qc.authentication': 'Authentication', 'fm.qc.password': 'Password', + 'fm.qc.passwordPlaceholder': 'Enter password', 'fm.qc.sshKey': 'SSH Key', 'fm.qc.selectKey': '-- Select Key --', 'fm.qc.connect': 'Connect', @@ -2299,6 +2302,9 @@ const translations = { 'preview.loading': 'Loading preview...', 'preview.binaryUnavailable': 'Binary file - cannot display preview', 'preview.downloadFile': 'Download File', + 'preview.truncatedPrefix': 'File truncated - showing first', + 'preview.truncatedOf': 'of', + 'preview.imageAlt': 'Image preview', 'terminal.saveTranscript': 'Save Transcript', 'terminal.copySelection': 'Copy Selection', 'terminal.pasteClipboard': 'Paste Clipboard', @@ -3381,15 +3387,16 @@ const translations = { 'gateway.target': "Đang chờ quyền truy cập đích. Kiểm tra yêu cầu bên dưới.", 'admin.integrations': "Tích hợp", 'admin.sshGateway': "Warpgate / cổng SSH", + 'admin.supportLinks': "Liên kết ủng hộ dự án", + 'admin.supportLinksHint': "Hiển thị liên kết ủng hộ việc phát triển WebSSH trong menu tài khoản và bảng bên của Cài đặt cho mọi người dùng.", 'admin.sshGatewayHint': "Cho phép mọi người dùng kết nối cổng tương tác bằng user:target. Mặc định tắt. Khi tắt, kết nối mới và xác thực chưa hoàn tất bị chặn; phiên đã kết nối được giữ lại. Tải lại không gian làm việc sau khi thay đổi.", 'gateway.cancel': "Hủy kết nối", 'gateway.passwordHint': "Để trống mật khẩu để xác thực tương tác.", 'workspace.directorySync.prompt': "Đồng bộ thư mục tạm dừng: chưa xác nhận được dòng lệnh trống. Hoàn tất nhập lệnh hoặc tắt đồng bộ để duyệt tệp độc lập.", 'workspace.directorySync.setup': "Thiết lập nhận diện dấu nhắc Bash", 'workspace.directorySync.setupHint': "Tự chạy khối lệnh đã sao chép tại dấu nhắc Bash trống trong ô này. Không thay đổi tệp cấu hình. Chờ nhận diện rồi nhấn Enter. Tiện ích tồn tại đến khi thoát shell, kể cả khi tắt đồng bộ.", - 'admin.supportLinks': "Liên kết ủng hộ dự án", - 'admin.supportLinksHint': "Hiển thị liên kết ủng hộ việc phát triển WebSSH trong menu tài khoản và bảng bên của Cài đặt cho mọi người dùng.", 'workspace.directorySync.copy': "Sao chép lệnh kích hoạt", + 'workspace.directorySync.commandLabel': 'Lệnh kích hoạt Bash', 'workspace.directorySync.copyFailed': "Không sao chép được. Hãy chọn văn bản hiển thị hoặc thử lại.", 'workspace.directorySync.ready': "Đồng bộ thư mục sẵn sàng.", 'workspace.directorySync.waiting': "Đồng bộ tạm dừng. Hoàn tất hoặc hủy nhập lệnh. Nhấn Enter tại dấu nhắc trống; nếu vẫn tạm dừng, tắt rồi bật đồng bộ và nhấn Enter.", @@ -3537,6 +3544,7 @@ const translations = { 'dropUpload.folderHint': "Đường dẫn tương đối bắt đầu tại thư mục nhà SFTP, không phải thư mục làm việc của terminal.", 'dropUpload.timeout': "Hết thời gian tạo thư mục đích.", 'dropUpload.chooseFolder': "Chọn thư mục đích sau khi thả.", + 'dropUpload.noSession': 'Hãy kết nối tới máy chủ trước', 'app.title': 'Web SSH Terminal', 'app.subtitle': 'Truy cập terminal hiện đại', @@ -4447,6 +4455,8 @@ const translations = { 'settings.primarySignIn': 'Đăng nhập chính', 'settings.scope': 'Phạm vi cài đặt', 'settings.webSshVersion': 'Phiên bản WebSSH', + 'settings.supportProject': 'Ủng hộ WebSSH', + 'settings.supportProjectHint': 'WebSSH miễn phí và mã nguồn mở. Hãy ủng hộ việc phát triển tiếp theo:', 'settings.personal': 'Cá nhân', 'settings.personalAndAdministration': 'Cá nhân + quản trị', 'settings.quickAccess': 'Truy cập nhanh', @@ -4455,8 +4465,6 @@ const translations = { 'settings.centerSummary': 'Tùy chọn cá nhân, bảo mật tài khoản và các điều khiển quản trị nay dùng chung một Trung tâm cài đặt.', 'settings.languageUpdated': 'Đã cập nhật ngôn ngữ.', 'settings.savingTheme': 'Đang lưu chủ đề…', - 'settings.supportProject': 'Ủng hộ WebSSH', - 'settings.supportProjectHint': 'WebSSH miễn phí và mã nguồn mở. Hãy ủng hộ việc phát triển tiếp theo:', 'settings.themeSaved': 'Đã lưu chủ đề.', 'settings.scrollbackSaved': 'Đã lưu cuộn lùi trong trình duyệt này.', 'settings.scrollbackFailed': 'Bộ nhớ trình duyệt không khả dụng hoặc đã đầy. Cài đặt cuộn lùi trước đó không thay đổi.', @@ -4606,6 +4614,7 @@ const translations = { 'fm.qc.username': 'Tên đăng nhập', 'fm.qc.authentication': 'Xác thực', 'fm.qc.password': 'Mật khẩu', + 'fm.qc.passwordPlaceholder': 'Nhập mật khẩu', 'fm.qc.sshKey': 'Khóa SSH', 'fm.qc.selectKey': '-- Chọn khóa --', 'fm.qc.connect': 'Kết nối', @@ -4725,6 +4734,9 @@ const translations = { 'preview.loading': 'Đang tải bản xem trước...', 'preview.binaryUnavailable': 'Tệp nhị phân - không thể hiển thị bản xem trước', 'preview.downloadFile': 'Tải tệp xuống', + 'preview.truncatedPrefix': 'Tệp đã bị cắt bớt - chỉ hiển thị', + 'preview.truncatedOf': 'trên tổng', + 'preview.imageAlt': 'Xem trước hình ảnh', 'terminal.saveTranscript': 'Lưu bản ghi', 'terminal.copySelection': 'Sao chép vùng chọn', 'terminal.pasteClipboard': 'Dán từ bộ nhớ tạm', @@ -5811,6 +5823,8 @@ const translations = { 'gateway.target': "Warten auf den Zielzugriff. Bitte die Gateway-Abfragen unten prüfen.", 'admin.integrations': "Integrationen", 'admin.sshGateway': "Warpgate / SSH-Gateway", + 'admin.supportLinks': "Links zur Projektunterstützung", + 'admin.supportLinksHint': "Zeigt allen Benutzern im Kontomenü und in der Seitenleiste der Einstellungen Links, mit denen sie die Entwicklung von WebSSH unterstützen können.", 'admin.sshGatewayHint': "Interaktive Gateway-Verbindungen mit user:target für alle Benutzer erlauben. Standardmäßig deaktiviert. Abschalten sperrt neue Verbindungen und laufende Gateway-Anmeldungen; aufgebaute Sitzungen bleiben verbunden. Offene Arbeitsbereiche nach einer Änderung neu laden.", 'gateway.cancel': "Verbindung abbrechen", 'gateway.passwordHint': "Passwort für die interaktive Gateway-Anmeldung leer lassen.", @@ -5818,13 +5832,12 @@ const translations = { 'workspace.directorySync.setup': "Bash-Prompt-Erkennung einrichten", 'workspace.directorySync.setupHint': "Führe den kopierten Block selbst am leeren Bash-Prompt dieses Bereichs aus. Konfigurationsdateien bleiben unverändert. Warte auf die Erkennung und drücke dann Enter. Die Ergänzung bleibt bis zum Ende dieser Shell aktiv, auch bei ausgeschaltetem Sync.", 'workspace.directorySync.copy': "Aktivierungsbefehl kopieren", + 'workspace.directorySync.commandLabel': 'Bash-Aktivierungsbefehl', 'workspace.directorySync.copyFailed': "Kopieren fehlgeschlagen. Markiere den angezeigten Text oder versuche es erneut.", 'workspace.directorySync.ready': "Ordner-Sync bereit.", 'workspace.directorySync.waiting': "Ordner-Sync pausiert. Beende oder verwirf deine Eingabe. Drücke am leeren Prompt Enter; bleibt der Sync pausiert, schalte ihn aus und wieder an und drücke Enter.", 'workspace.directorySync.setupRequired': "Die Bash-Prompt-Erkennung ist für diesen Bereich noch nicht eingerichtet.", 'workspace.directorySync.unsupported': "Für diese Shell ist keine automatische Prompt-Erkennung verfügbar.", - 'admin.supportLinks': "Links zur Projektunterstützung", - 'admin.supportLinksHint': "Zeigt allen Benutzern im Kontomenü und in der Seitenleiste der Einstellungen Links, mit denen sie die Entwicklung von WebSSH unterstützen können.", 'workspace.directorySync.unavailable': "Der Terminal-Ordner konnte nicht geprüft werden. Verlasse den tmux-Kopiermodus oder das laufende Programm und versuche es erneut.", 'workspace.directorySync.pending': "Der vorherige Ordnerwechsel wird noch geprüft. Versuche es gleich erneut.", 'profiles.saveConnect': "Speichern & verbinden", @@ -5967,6 +5980,7 @@ const translations = { 'dropUpload.folderHint': "Relative Pfade beginnen im SFTP-Heimatverzeichnis, nicht im Arbeitsverzeichnis des Terminals.", 'dropUpload.timeout': "Zeitüberschreitung beim Erstellen des Zielordners.", 'dropUpload.chooseFolder': "Nach dem Ablegen einen Zielordner auswählen.", + 'dropUpload.noSession': 'Zuerst mit einem Server verbinden', 'app.title': 'Web SSH Terminal', 'app.subtitle': 'Moderner Terminal-Zugang', @@ -6895,6 +6909,8 @@ const translations = { 'settings.primarySignIn': 'Primäre Anmeldung', 'settings.scope': 'Einstellungsumfang', 'settings.webSshVersion': 'WebSSH-Version', + 'settings.supportProject': 'WebSSH unterstützen', + 'settings.supportProjectHint': 'WebSSH ist kostenlos und Open Source. Unterstütze die Weiterentwicklung:', 'settings.personal': 'Persönlich', 'settings.personalAndAdministration': 'Persönlich + Administration', 'settings.quickAccess': 'Schnellzugriff', @@ -6909,8 +6925,6 @@ const translations = { 'settings.saving': 'Einstellung wird gespeichert…', 'settings.saved': 'Einstellung gespeichert.', 'security.passkeysAndMfa': 'Passkeys & MFA', - 'settings.supportProject': 'WebSSH unterstützen', - 'settings.supportProjectHint': 'WebSSH ist kostenlos und Open Source. Unterstütze die Weiterentwicklung:', 'security.personalSshTrust': 'Persönliches SSH-Vertrauen', 'security.personalSshTrustHint': 'Die von diesem Konto als vertrauenswürdig eingestuften SSH-Host-Schlüssel prüfen.', 'security.factorsHint': 'Starke Authentifizierungsfaktoren dieses Kontos hinzufügen, prüfen und entfernen.', @@ -7054,6 +7068,7 @@ const translations = { 'fm.qc.username': 'Benutzername', 'fm.qc.authentication': 'Authentifizierung', 'fm.qc.password': 'Passwort', + 'fm.qc.passwordPlaceholder': 'Passwort eingeben', 'fm.qc.sshKey': 'SSH-Schlüssel', 'fm.qc.selectKey': '-- Schlüssel auswählen --', 'fm.qc.connect': 'Verbinden', @@ -7168,6 +7183,9 @@ const translations = { 'preview.loading': 'Vorschau wird geladen...', 'preview.binaryUnavailable': 'Binärdatei - Vorschau nicht möglich', 'preview.downloadFile': 'Datei herunterladen', + 'preview.truncatedPrefix': 'Datei gekürzt – angezeigt werden die ersten', + 'preview.truncatedOf': 'von', + 'preview.imageAlt': 'Bildvorschau', 'terminal.saveTranscript': 'Terminalprotokoll speichern', 'terminal.copySelection': 'Auswahl kopieren', 'terminal.pasteClipboard': 'Zwischenablage einfügen', @@ -8236,6 +8254,8 @@ const translations = { 'gateway.target': "En attente de l'accès à la cible. Vérifiez les demandes ci-dessous.", 'admin.integrations': "Intégrations", 'admin.sshGateway': "Warpgate / passerelle SSH", + 'admin.supportLinks': "Liens de soutien au projet", + 'admin.supportLinksHint': "Affiche à tous les utilisateurs, dans le menu du compte et le panneau latéral des paramètres, des liens pour soutenir le développement de WebSSH.", 'admin.sshGatewayHint': "Autoriser les connexions interactives via user:target pour tous les utilisateurs. Désactivé par défaut. La désactivation bloque les nouvelles connexions et les authentifications en cours ; les sessions établies restent connectées. Rechargez les espaces de travail après modification.", 'gateway.cancel': "Annuler la connexion", 'gateway.passwordHint': "Laissez le mot de passe vide pour l'authentification interactive.", @@ -8243,6 +8263,7 @@ const translations = { 'workspace.directorySync.setup': "Configurer la détection Bash", 'workspace.directorySync.setupHint': "Exécutez vous-même le bloc copié à une invite Bash vide dans ce panneau. Aucun fichier de configuration ne change. Attendez la détection, puis appuyez sur Entrée. Le module reste actif jusqu’à la fermeture du shell, même sans synchronisation.", 'workspace.directorySync.copy': "Copier la commande d’activation", + 'workspace.directorySync.commandLabel': 'Commande d’activation Bash', 'workspace.directorySync.copyFailed': "Copie impossible. Sélectionnez le texte affiché ou réessayez.", 'workspace.directorySync.ready': "Synchronisation prête.", 'workspace.directorySync.waiting': "Synchronisation en pause. Terminez ou annulez la saisie, puis appuyez sur Entrée à une invite vide. Si nécessaire, désactivez et réactivez la synchronisation, puis appuyez sur Entrée.", @@ -8254,8 +8275,6 @@ const translations = { 'profiles.compact': "Vue compacte", 'settings.directorySyncDefault': "Synchroniser les dossiers avec le terminal par défaut", 'settings.directorySyncDefaultHint': "S’applique à l’ouverture ou au rechargement de l’espace de travail.", - 'admin.supportLinks': "Liens de soutien au projet", - 'admin.supportLinksHint': "Affiche à tous les utilisateurs, dans le menu du compte et le panneau latéral des paramètres, des liens pour soutenir le développement de WebSSH.", 'workspace.directorySync.label': "Synchroniser les dossiers avec le terminal", 'workspace.directorySync.hint': "Lier la navigation à ce terminal. Commence dans le dossier du terminal.", 'sessionCommands.editParameters': "Paramètres pour cette insertion", @@ -8392,6 +8411,7 @@ const translations = { 'dropUpload.folderHint': "Les chemins relatifs partent du dossier personnel SFTP, pas du répertoire de travail du terminal.", 'dropUpload.timeout': "Délai dépassé pour la création du dossier de destination.", 'dropUpload.chooseFolder': "Choisissez un dossier de destination après le dépôt.", + 'dropUpload.noSession': 'Connectez-vous d’abord à un serveur', 'app.title': 'Terminal SSH Web', 'app.subtitle': 'Accès Terminal Moderne', @@ -9331,6 +9351,8 @@ const translations = { 'settings.primarySignIn': 'Connexion principale', 'settings.scope': 'Portée des paramètres', 'settings.webSshVersion': 'Version de WebSSH', + 'settings.supportProject': 'Soutenir WebSSH', + 'settings.supportProjectHint': 'WebSSH est gratuit et open source. Soutenez son développement :', 'settings.personal': 'Personnel', 'settings.personalAndAdministration': 'Personnel + administration', 'settings.quickAccess': 'Accès rapide', @@ -9351,8 +9373,6 @@ const translations = { 'security.loadingLinkedAccount': 'Chargement du compte lié…', 'security.managePasskeysHint': 'Gérez les identifiants de connexion sans mot de passe.', 'security.manageAuthenticatorHint': 'Gérez les codes à usage unique basés sur le temps.', - 'settings.supportProject': 'Soutenir WebSSH', - 'settings.supportProjectHint': 'WebSSH est gratuit et open source. Soutenez son développement :', 'security.manageGithubHint': 'Connectez ou déconnectez l’identité GitHub utilisée avec ce compte WebSSH.', 'security.manageOidcHint': 'Associez une identité en vous connectant au fournisseur configuré. WebSSH ne conserve que son émetteur et son sujet vérifiés.', 'security.manageRecoveryCodesHint': 'Générez et conservez les codes de connexion d’urgence.', @@ -9490,6 +9510,7 @@ const translations = { 'fm.qc.username': "Nom d'utilisateur", 'fm.qc.authentication': 'Authentification', 'fm.qc.password': 'Mot de passe', + 'fm.qc.passwordPlaceholder': 'Saisir le mot de passe', 'fm.qc.sshKey': 'Clé SSH', 'fm.qc.selectKey': '-- Sélectionner une clé --', 'fm.qc.connect': 'Connecter', @@ -9604,6 +9625,9 @@ const translations = { 'preview.loading': 'Chargement de l’aperçu...', 'preview.binaryUnavailable': 'Fichier binaire - aperçu indisponible', 'preview.downloadFile': 'Télécharger le fichier', + 'preview.truncatedPrefix': 'Fichier tronqué – affichage des premiers', + 'preview.truncatedOf': 'sur', + 'preview.imageAlt': 'Aperçu de l’image', 'terminal.saveTranscript': 'Enregistrer la transcription', 'terminal.copySelection': 'Copier la sélection', 'terminal.pasteClipboard': 'Coller le presse-papiers', @@ -10661,6 +10685,8 @@ const translations = { 'gateway.target': "Esperando acceso al destino. Revisa las solicitudes siguientes.", 'admin.integrations': "Integraciones", 'admin.sshGateway': "Warpgate / puerta de enlace SSH", + 'admin.supportLinks': "Enlaces de apoyo al proyecto", + 'admin.supportLinksHint': "Muestra a todos los usuarios, en el menú de la cuenta y el panel lateral de ajustes, enlaces para apoyar el desarrollo de WebSSH.", 'admin.sshGatewayHint': "Permitir conexiones interactivas con user:target para todos los usuarios. Desactivado por defecto. Al desactivar se bloquean nuevas conexiones y autenticaciones pendientes; las sesiones establecidas siguen conectadas. Recarga los espacios de trabajo tras el cambio.", 'gateway.cancel': "Cancelar conexión", 'gateway.passwordHint': "Deja la contraseña vacía para la autenticación interactiva.", @@ -10668,6 +10694,7 @@ const translations = { 'workspace.directorySync.setup': "Configurar detección de Bash", 'workspace.directorySync.setupHint': "Ejecuta tú mismo el bloque copiado en un prompt Bash vacío de este panel. No se modifican archivos de configuración. Espera la detección y pulsa Intro. La integración permanece hasta cerrar la shell, incluso con la sincronización desactivada.", 'workspace.directorySync.copy': "Copiar comando de activación", + 'workspace.directorySync.commandLabel': 'Comando de activación de Bash', 'workspace.directorySync.copyFailed': "No se pudo copiar. Selecciona el texto mostrado o inténtalo de nuevo.", 'workspace.directorySync.ready': "Sincronización lista.", 'workspace.directorySync.waiting': "Sincronización pausada. Termina o cancela la entrada y pulsa Intro en un prompt vacío. Si sigue pausada, desactiva y activa la sincronización y pulsa Intro.", @@ -10685,8 +10712,6 @@ const translations = { 'sessionCommands.resetParameters': "Restaurar los parámetros guardados", 'workspace.toolsReconnecting': "Reconectando — los datos mostrados pueden estar desactualizados.", 'workspace.toolsRefreshing': "Actualizando — los datos mostrados pueden estar desactualizados.", - 'admin.supportLinks': "Enlaces de apoyo al proyecto", - 'admin.supportLinksHint': "Muestra a todos los usuarios, en el menú de la cuenta y el panel lateral de ajustes, enlaces para apoyar el desarrollo de WebSSH.", 'workspace.toolsRefreshFailed': "No se pudo actualizar. Los datos mostrados están desactualizados.", 'workspace.toolsRetry': "Volver a cargar", 'commands.added': "Comando añadido", @@ -10817,6 +10842,7 @@ const translations = { 'dropUpload.folderHint': "Las rutas relativas parten del directorio personal SFTP, no del directorio de trabajo del terminal.", 'dropUpload.timeout': "Se agotó el tiempo para crear la carpeta de destino.", 'dropUpload.chooseFolder': "Elige una carpeta de destino después de soltar.", + 'dropUpload.noSession': 'Conéctate primero a un servidor', 'app.title': 'Terminal SSH Web', 'app.subtitle': 'Acceso Terminal Moderno', @@ -11756,6 +11782,8 @@ const translations = { 'settings.primarySignIn': 'Inicio de sesión principal', 'settings.scope': 'Ámbito de los ajustes', 'settings.webSshVersion': 'Versión de WebSSH', + 'settings.supportProject': 'Apoyar WebSSH', + 'settings.supportProjectHint': 'WebSSH es gratuito y de código abierto. Apoya su desarrollo:', 'settings.personal': 'Personal', 'settings.personalAndAdministration': 'Personal + administración', 'settings.quickAccess': 'Acceso rápido', @@ -11782,8 +11810,6 @@ const translations = { 'security.githubManagedTitle': 'Inicio de sesión con GitHub', 'security.githubManagedHint': 'GitHub controla este inicio de sesión principal. WebSSH descarta el token temporal de GitHub tras comprobar la identidad y la organización.', 'security.recoveryCodesPageHint': 'Genera y guarda de forma segura códigos de un solo uso para inicios de sesión de emergencia.', - 'settings.supportProject': 'Apoyar WebSSH', - 'settings.supportProjectHint': 'WebSSH es gratuito y de código abierto. Apoya su desarrollo:', 'security.accountIdentityUnavailable': 'La identidad de la cuenta no está disponible', 'admin.authentication': 'Autenticación', 'admin.authenticationHint': 'Gestiona los métodos de inicio de sesión disponibles para los usuarios de WebSSH.', @@ -11915,6 +11941,7 @@ const translations = { 'fm.qc.username': 'Usuario', 'fm.qc.authentication': 'Autenticación', 'fm.qc.password': 'Contraseña', + 'fm.qc.passwordPlaceholder': 'Introduce la contraseña', 'fm.qc.sshKey': 'Clave SSH', 'fm.qc.selectKey': '-- Seleccionar clave --', 'fm.qc.connect': 'Conectar', @@ -12029,6 +12056,9 @@ const translations = { 'preview.loading': 'Cargando vista previa...', 'preview.binaryUnavailable': 'Archivo binario - no se puede mostrar la vista previa', 'preview.downloadFile': 'Descargar archivo', + 'preview.truncatedPrefix': 'Archivo truncado: se muestran los primeros', + 'preview.truncatedOf': 'de', + 'preview.imageAlt': 'Vista previa de la imagen', 'terminal.saveTranscript': 'Guardar transcripción', 'terminal.copySelection': 'Copiar selección', 'terminal.pasteClipboard': 'Pegar portapapeles', @@ -13085,6 +13115,8 @@ const translations = { 'gateway.target': "正在等待目标访问。请检查下方的网关提示。", 'admin.integrations': "集成", 'admin.sshGateway': "Warpgate / SSH 网关", + 'admin.supportLinks': "项目支持链接", + 'admin.supportLinksHint': "在账户菜单和设置侧栏中向所有用户显示支持 WebSSH 开发的链接。", 'admin.sshGatewayHint': "允许所有用户使用 user:target 建立交互式网关连接。默认禁用。禁用后会阻止新连接和未完成的网关认证;已建立的会话保持连接。更改后请重新加载工作区。", 'gateway.cancel': "取消连接", 'gateway.passwordHint': "将密码留空以使用交互式网关身份验证。", @@ -13092,6 +13124,7 @@ const translations = { 'workspace.directorySync.setup': "设置 Bash 提示符检测", 'workspace.directorySync.setupHint': "请自行在此窗格的空白 Bash 提示符处运行复制的代码。不会修改配置文件。等待检测完成后按 Enter。即使关闭同步,辅助功能也会保留到此 shell 退出。", 'workspace.directorySync.copy': "复制激活命令", + 'workspace.directorySync.commandLabel': 'Bash 激活命令', 'workspace.directorySync.copyFailed': "无法复制。请选择显示的文本或重试。", 'workspace.directorySync.ready': "文件夹同步已就绪。", 'workspace.directorySync.waiting': "同步已暂停。完成或取消输入后,在空白提示符处按 Enter。如仍暂停,请关闭再开启同步,然后按 Enter。", @@ -13115,8 +13148,6 @@ const translations = { 'commands.updated': "命令已更新", 'commands.deleted': "命令已删除", 'commands.deleteConfirm': "删除命令“{name}”?", - 'admin.supportLinks': "项目支持链接", - 'admin.supportLinksHint': "在账户菜单和设置侧栏中向所有用户显示支持 WebSSH 开发的链接。", 'commands.copyToMine': "添加到我的命令", 'commands.requiredFields': "名称、命令和描述不能为空", 'commands.osRequired': "请至少选择一个操作系统", @@ -13241,6 +13272,7 @@ const translations = { 'dropUpload.folderHint': "相对路径从 SFTP 主目录开始,而不是终端的工作目录。", 'dropUpload.timeout': "创建目标文件夹超时。", 'dropUpload.chooseFolder': "拖放后选择目标文件夹。", + 'dropUpload.noSession': '请先连接到服务器', 'app.title': 'Web SSH 终端', 'app.subtitle': '现代化终端访问', @@ -14169,6 +14201,8 @@ const translations = { 'settings.primarySignIn': '主要登录方式', 'settings.scope': '设置范围', 'settings.webSshVersion': 'WebSSH 版本', + 'settings.supportProject': '支持 WebSSH', + 'settings.supportProjectHint': 'WebSSH 免费且开源。支持后续开发:', 'settings.personal': '个人', 'settings.personalAndAdministration': '个人 + 管理', 'settings.quickAccess': '快速访问', @@ -14201,8 +14235,6 @@ const translations = { 'admin.registration': '注册', 'admin.registrationHint': '控制谁可以创建本地 WebSSH 账户。', 'admin.globalSshTrust': '全局 SSH 信任', - 'settings.supportProject': '支持 WebSSH', - 'settings.supportProjectHint': 'WebSSH 免费且开源。支持后续开发:', 'admin.auditRetention': '审计保留', 'admin.auditRetentionPageHint': '定义 WebSSH 保留的轮转审计归档数量。', 'admin.usersHint': '管理账户、角色、访问状态和恢复操作。', @@ -14328,6 +14360,7 @@ const translations = { 'fm.qc.username': '用户名', 'fm.qc.authentication': '认证方式', 'fm.qc.password': '密码', + 'fm.qc.passwordPlaceholder': '输入密码', 'fm.qc.sshKey': 'SSH 密钥', 'fm.qc.selectKey': '-- 选择密钥 --', 'fm.qc.connect': '连接', @@ -14442,6 +14475,9 @@ const translations = { 'preview.loading': '正在加载预览…', 'preview.binaryUnavailable': '二进制文件 - 无法显示预览', 'preview.downloadFile': '下载文件', + 'preview.truncatedPrefix': '文件已截断 - 仅显示前', + 'preview.truncatedOf': ',总大小', + 'preview.imageAlt': '图片预览', 'terminal.saveTranscript': '保存终端记录', 'terminal.copySelection': '复制选中内容', 'terminal.pasteClipboard': '粘贴剪贴板', diff --git a/static/js/profile-manager.js b/static/js/profile-manager.js index c24dea02..ffbaa686 100644 --- a/static/js/profile-manager.js +++ b/static/js/profile-manager.js @@ -1879,7 +1879,7 @@ const ProfileManager = { const keyContent = contentInput?.value || ''; if (!name || !keyContent) { if (status) { - status.textContent = 'Key name and content are required'; + status.textContent = this.t('toast.keyNameContentRequired', 'Key name and content are required'); status.classList.add('error'); } return; diff --git a/static/js/session-manager.js b/static/js/session-manager.js index 454a0b64..0f9f01a0 100644 --- a/static/js/session-manager.js +++ b/static/js/session-manager.js @@ -329,7 +329,7 @@ const SessionManager = { const tmuxBadge = document.createElement('span'); tmuxBadge.className = 'tab-tmux-badge material-icons'; tmuxBadge.textContent = 'push_pin'; - tmuxBadge.title = 'Persistent tmux session' + (sess.tmuxSessionName ? ': ' + sess.tmuxSessionName : ''); + tmuxBadge.title = (window.i18n ? i18n.t('session.persistentTmux') : 'Persistent session (tmux)') + (sess.tmuxSessionName ? ': ' + sess.tmuxSessionName : ''); tab.appendChild(tmuxBadge); } const tabReconnect = document.createElement('span'); @@ -676,8 +676,11 @@ const SessionManager = { tabClose.dataset.pendingId = requestId; tabClose.classList.add('material-icons'); tabClose.textContent = 'close'; - tabClose.setAttribute('aria-label', 'Cancel connection'); - tabClose.setAttribute('title', 'Cancel connection'); + const cancelLabel = window.i18n ? i18n.t('connection.authBannerCancel') : 'Cancel connection'; + tabClose.setAttribute('aria-label', cancelLabel); + tabClose.setAttribute('title', cancelLabel); + tabClose.dataset.i18nAriaLabel = 'connection.authBannerCancel'; + tabClose.dataset.i18nTitle = 'connection.authBannerCancel'; tab.appendChild(statusDot); tab.appendChild(tabLabel); diff --git a/static/js/sftp-file-manager.js b/static/js/sftp-file-manager.js index 6f44315a..e4f7d21d 100644 --- a/static/js/sftp-file-manager.js +++ b/static/js/sftp-file-manager.js @@ -1528,9 +1528,9 @@ class SFTPFileManager extends window.FileDirectoryRequests {
- -
@@ -6321,6 +6321,11 @@ class SFTPFileManager extends window.FileDirectoryRequests { const translation = window.i18n.t(key); if (translation) el.setAttribute('aria-label', translation); }); + this.qcModal.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + const key = el.getAttribute('data-i18n-placeholder'); + const translation = window.i18n.t(key); + if (translation) el.setAttribute('placeholder', translation); + }); } } diff --git a/templates/index.html b/templates/index.html index 8e640ac4..b22302bf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -300,7 +300,7 @@ Set up Bash prompt detection

Run the copied block yourself at an empty Bash prompt in this pane. No configuration files are changed. Wait for detection, then press Enter. The helper stays until this shell exits, even with sync off.

- +
@@ -1064,7 +1064,7 @@

Rem