From a5b8f26db1d181fb8d251ffe59da9b35114ce105 Mon Sep 17 00:00:00 2001 From: orohi Date: Tue, 28 Jul 2026 09:48:19 +0300 Subject: [PATCH] v2.5.2: redesign Tunnels section in Settings --- README.md | 3 + app.py | 2 +- static/css/style.css | 313 ++++++++++++++++++++++++++++++++++++++++ templates/settings.html | 279 +++++++++++++++++++++-------------- translations/en.json | 7 + translations/fa.json | 9 +- translations/fr.json | 7 + translations/ru.json | 7 + translations/zh.json | 7 + 9 files changed, 522 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 05c4fa1..2018b05 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,9 @@ GitHub Actions workflows in `.github/workflows/`: ## 📋 Fix / changelog (this fork) +### v2.5.2 +* **Tunnels UI redesign** — Settings → Tunnels: grouped layout (public access / outbound VPN), provider cards, status pills, responsive grid. + ### v2.5.1 * **One-click panel update** — Settings → About → **Update panel**: downloads release ZIP from Gitea (or `git checkout` when `.git` exists), runs `pip install`, restarts. diff --git a/app.py b/app.py index 6fbc533..6f601b6 100644 --- a/app.py +++ b/app.py @@ -102,7 +102,7 @@ else: application_path = os.path.dirname(__file__) DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export -CURRENT_VERSION = "v2.5.1" +CURRENT_VERSION = "v2.5.2" RELEASES_REPO_URL = repo_url() RELEASES_API_LATEST = api_latest_url() BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin')) diff --git a/static/css/style.css b/static/css/style.css index a548807..b9bd746 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1623,6 +1623,319 @@ a:hover { gap: var(--space-md); } +/* ===== Tunnels (Settings) ===== */ +.tunnels-card .tunnels-header { + margin-bottom: var(--space-lg); +} + +.tunnels-card .tunnels-header .card-title { + margin: 0; + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.tunnels-card .tunnels-subtitle { + margin: var(--space-xs) 0 0; + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.45; +} + +.tunnels-local { + display: flex; + align-items: center; + gap: var(--space-md); + padding: var(--space-md) var(--space-lg); + margin-bottom: var(--space-lg); + border-radius: var(--radius-md); + border: 1px solid var(--border-focus); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(59, 130, 246, 0.06)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.tunnels-local-icon { + flex-shrink: 0; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + background: var(--accent-gradient); + font-size: 1.25rem; + box-shadow: var(--shadow-glow); +} + +.tunnels-local-body { + flex: 1; + min-width: 0; +} + +.tunnels-local-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); + margin-bottom: var(--space-xs); +} + +.tunnels-local-url { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.tunnels-local-url code { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.9rem; + color: var(--text-primary); + background: transparent; + padding: 0; +} + +.tunnels-section { + margin-bottom: var(--space-lg); +} + +.tunnels-section:last-of-type { + margin-bottom: var(--space-md); +} + +.tunnels-section-title { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin: 0 0 var(--space-sm) var(--space-xs); +} + +.tunnels-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-md); +} + +.tunnel-card { + display: flex; + flex-direction: column; + gap: var(--space-md); + padding: var(--space-md); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + background: var(--bg-primary); + transition: border-color var(--transition-base), box-shadow var(--transition-base); +} + +.tunnel-card:hover { + border-color: var(--border-hover); +} + +.tunnel-card--running { + border-color: var(--success-border); + box-shadow: 0 0 0 1px var(--success-border), inset 0 1px 0 rgba(34, 197, 94, 0.06); +} + +.tunnel-card--connected { + border-color: var(--success-border); + box-shadow: 0 0 0 1px var(--success-border); +} + +.tunnel-card-head { + display: flex; + align-items: flex-start; + gap: var(--space-md); +} + +.tunnel-card-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + font-size: 1.1rem; + font-weight: 700; + color: #fff; +} + +.tunnel-card-icon--cloudflare { background: linear-gradient(135deg, #f38020, #faae40); } +.tunnel-card-icon--ngrok { background: linear-gradient(135deg, #1f2d3d, #3d5a80); } +.tunnel-card-icon--warp { background: linear-gradient(135deg, #f38020, #e85d04); } +.tunnel-card-icon--nordvpn { background: linear-gradient(135deg, #4687ff, #2b4eff); } +.tunnel-card-icon--local { background: var(--accent-gradient); } + +.tunnel-card-meta { + flex: 1; + min-width: 0; +} + +.tunnel-card-title { + font-size: 0.95rem; + font-weight: 600; + margin: 0; + line-height: 1.3; +} + +.tunnel-card-desc { + font-size: 0.78rem; + color: var(--text-muted); + margin: var(--space-xs) 0 0; + line-height: 1.4; +} + +.tunnel-status-pill { + flex-shrink: 0; + font-size: 0.7rem; + font-weight: 600; + padding: 4px 10px; + border-radius: var(--radius-full); + white-space: nowrap; + background: rgba(255, 255, 255, 0.06); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.tunnel-status-pill--ok { + background: var(--success-bg); + color: var(--success); + border-color: var(--success-border); +} + +.tunnel-status-pill--info { + background: var(--info-bg); + color: var(--info); + border-color: rgba(59, 130, 246, 0.25); +} + +.tunnel-card-body { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.tunnel-url-box { + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + background: var(--bg-secondary); + border: 1px dashed var(--border-color); + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.45; + min-height: 2.5rem; +} + +.tunnel-url-box--empty { + font-style: italic; + color: var(--text-muted); +} + +.tunnel-url-row { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-top: var(--space-xs); +} + +.tunnel-url-row:first-child { + margin-top: 0; +} + +.tunnel-url-row code { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.8rem; + color: var(--text-accent); + background: transparent; + padding: 0; +} + +.tunnel-status-line { + font-size: 0.82rem; + color: var(--text-secondary); + line-height: 1.45; +} + +.tunnel-status-line--server { + font-family: ui-monospace, monospace; + font-size: 0.78rem; + color: var(--text-accent); + padding: var(--space-xs) var(--space-sm); + background: var(--bg-secondary); + border-radius: var(--radius-sm); +} + +.tunnel-hint { + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.4; + margin: 0; +} + +.tunnel-card-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-top: auto; +} + +.tunnel-card-actions .btn { + flex: 1; + min-width: 7rem; + justify-content: center; +} + +.tunnel-card-actions--triple .btn { + min-width: 5.5rem; +} + +.tunnel-nordvpn-fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-sm); +} + +.tunnels-footnote { + margin: var(--space-md) 0 0; + padding: var(--space-md); + border-radius: var(--radius-sm); + background: var(--bg-secondary); + border-left: 3px solid var(--accent); + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.5; +} + +@media (max-width: 640px) { + .tunnels-local { + flex-direction: column; + align-items: stretch; + text-align: center; + } + + .tunnels-local-url { + flex-direction: column; + } + + .tunnels-grid { + grid-template-columns: 1fr; + } + + .tunnel-nordvpn-fields { + grid-template-columns: 1fr; + } +} + .mt-md { margin-top: var(--space-md); } diff --git a/templates/settings.html b/templates/settings.html index ca15c54..15d5bfc 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -204,112 +204,147 @@ -
-

🌍 {{ _('tunnels_title') }}

-
-
-
- {{ _('local_server') }} - {{ _('active') }} -
-
- {{ request.url.scheme }}://{{ request.url.netloc }} +
+
+

🌍 {{ _('tunnels_title') }}

+

{{ _('tunnels_subtitle') }}

+
+ +
+ +
+
{{ _('local_server') }}
+
+ {{ request.url.scheme }}://{{ request.url.netloc }}
+ {{ _('active') }} +
-
-
- Cloudflare Quick Tunnel - {{ _('not_installed') }} -
-
{{ _('tunnel_no_public_url') }}
-
- - - -
-
+
+

{{ _('tunnels_section_inbound') }}

+
+
+
+ +
+
Cloudflare Quick Tunnel
+

{{ _('tunnel_cloudflare_desc') }}

+
+ {{ _('not_installed') }} +
+
+
{{ _('tunnel_no_public_url') }}
+
+
+ + + +
+
-
-
- ngrok Tunnel + Authtoken - {{ _('not_installed') }} -
-
- -
-
{{ _('tunnel_no_public_url') }}
-
- - - -
-
- - -
-
- Cloudflare WARP - {{ _('not_installed') }} -
-
{{ _('warp_status_unknown') }}
-
{{ _('warp_hint') }}
-
- - -
-
- -
-
- NordVPN - {{ _('not_installed') }} -
-
{{ _('nordvpn_status_unknown') }}
- -
{{ _('nordvpn_hint') }}
-
- - -
-
- - -
+
+
+ +
+
ngrok
+

{{ _('tunnel_ngrok_desc') }}

+
+ {{ _('not_installed') }} +
+
+ +
{{ _('tunnel_no_public_url') }}
+
+
+ + + +
+
-

{{ _('tunnels_hint') }}

+ +
+

{{ _('tunnels_section_outbound') }}

+
+
+
+ +
+
Cloudflare WARP
+

{{ _('tunnel_warp_desc') }}

+
+ {{ _('not_installed') }} +
+
+

{{ _('warp_status_unknown') }}

+

{{ _('warp_hint') }}

+
+
+ + +
+
+ +
+
+ +
+
NordVPN
+

{{ _('tunnel_nordvpn_desc') }}

+
+ {{ _('not_installed') }} +
+
+

{{ _('nordvpn_status_unknown') }}

+ +

{{ _('nordvpn_hint') }}

+
+ + +
+
+
+ + +
+
+
+
+ +

{{ _('tunnels_hint') }}

@@ -949,31 +984,45 @@ const el = document.getElementById(`${provider}PublicUrls`); if (!el) return; if (!urls || !urls.length) { + el.className = 'tunnel-url-box tunnel-url-box--empty'; el.textContent = _('tunnel_no_public_url'); return; } + el.className = 'tunnel-url-box'; el.innerHTML = urls.map((url, idx) => ` -
- ${escapeHTML(url)} +
+ ${escapeHTML(url)}
`).join(''); } + function setTunnelStatusPill(badge, { ok, info, text }) { + if (!badge) return; + badge.className = 'tunnel-status-pill' + (ok ? ' tunnel-status-pill--ok' : (info ? ' tunnel-status-pill--info' : '')); + badge.textContent = text; + } + function updateTunnelProvider(provider, status) { const badge = document.getElementById(`${provider}InstallBadge`); const text = document.getElementById(`${provider}TunnelBtnText`); const startBtn = document.getElementById(`${provider}TunnelBtn`); const stopBtn = document.getElementById(`${provider}StopBtn`); const deleteBtn = document.getElementById(`${provider}DeleteBtn`); + const card = document.getElementById(`tunnelCard-${provider}`); if (!badge || !text) return; - badge.className = status.installed ? 'badge badge-success' : 'badge badge-secondary'; - badge.textContent = status.installed ? _('installed') : _('not_installed'); - text.textContent = status.running ? _('tunnel_running') : (status.installed ? _('tunnel_enable') : _('tunnel_install_enable')); - if (startBtn) startBtn.disabled = !!status.running; - if (stopBtn) stopBtn.classList.toggle('hidden', !status.running); + const running = !!status.running; + setTunnelStatusPill(badge, { + ok: running, + info: !running && status.installed, + text: running ? _('tunnel_running') : (status.installed ? _('installed') : _('not_installed')), + }); + text.textContent = running ? _('tunnel_running') : (status.installed ? _('tunnel_enable') : _('tunnel_install_enable')); + if (startBtn) startBtn.disabled = running; + if (stopBtn) stopBtn.classList.toggle('hidden', !running); if (deleteBtn) deleteBtn.classList.toggle('hidden', !status.installed); + if (card) card.classList.toggle('tunnel-card--running', running); renderTunnelUrls(provider, status.public_urls || (status.public_url ? [status.public_url] : [])); } @@ -997,12 +1046,16 @@ const connectBtn = document.getElementById('warpConnectBtn'); const disconnectBtn = document.getElementById('warpDisconnectBtn'); const connectText = document.getElementById('warpConnectBtnText'); + const card = document.getElementById('tunnelCard-warp'); if (!badge || !statusText || !connectBtn || !disconnectBtn || !connectText) return; const installed = !!status.installed; const connected = !!status.connected || status.status === 'connected'; - badge.className = connected ? 'badge badge-success' : (installed ? 'badge badge-info' : 'badge badge-secondary'); - badge.textContent = connected ? _('warp_connected') : (installed ? _('installed') : _('not_installed')); + setTunnelStatusPill(badge, { + ok: connected, + info: !connected && installed, + text: connected ? _('warp_connected') : (installed ? _('installed') : _('not_installed')), + }); const statusKey = `warp_status_${status.status || 'unknown'}`; statusText.textContent = _(statusKey) || status.status || _('warp_status_unknown'); @@ -1010,6 +1063,7 @@ connectText.textContent = installed ? _('warp_connect') : _('warp_install_required'); connectBtn.disabled = connected; disconnectBtn.classList.toggle('hidden', !connected); + if (card) card.classList.toggle('tunnel-card--connected', connected); } async function connectWarp() { @@ -1062,12 +1116,16 @@ const connectBtn = document.getElementById('nordvpnConnectBtn'); const disconnectBtn = document.getElementById('nordvpnDisconnectBtn'); const connectText = document.getElementById('nordvpnConnectBtnText'); + const card = document.getElementById('tunnelCard-nordvpn'); if (!badge || !statusText || !connectBtn || !disconnectBtn || !connectText) return; const installed = !!status.installed; const connected = !!status.connected || status.status === 'connected'; - badge.className = connected ? 'badge badge-success' : (installed ? 'badge badge-info' : 'badge badge-secondary'); - badge.textContent = connected ? _('nordvpn_connected') : (installed ? _('installed') : _('not_installed')); + setTunnelStatusPill(badge, { + ok: connected, + info: !connected && installed, + text: connected ? _('nordvpn_connected') : (installed ? _('installed') : _('not_installed')), + }); const statusKey = `nordvpn_status_${status.status || 'unknown'}`; statusText.textContent = _(statusKey) || status.status || _('nordvpn_status_unknown'); @@ -1080,6 +1138,7 @@ connectText.textContent = installed ? _('nordvpn_connect') : _('nordvpn_install_required'); connectBtn.disabled = connected; disconnectBtn.classList.toggle('hidden', !connected); + if (card) card.classList.toggle('tunnel-card--connected', connected); } async function connectNordvpn() { diff --git a/translations/en.json b/translations/en.json index c330dd9..185b401 100644 --- a/translations/en.json +++ b/translations/en.json @@ -267,6 +267,13 @@ "api_docs_title": "🔌 API Documentation", "api_docs_hint": "Use these interfaces to explore the panel\u0027s API capabilities", "tunnels_title": "Tunnels", + "tunnels_subtitle": "Expose the panel to the internet or route outbound traffic through a VPN.", + "tunnels_section_inbound": "Public access", + "tunnels_section_outbound": "Outbound VPN", + "tunnel_cloudflare_desc": "Free HTTPS URL via Cloudflare — no account required.", + "tunnel_ngrok_desc": "Stable tunnel with your ngrok authtoken.", + "tunnel_warp_desc": "Route server traffic through Cloudflare WARP.", + "tunnel_nordvpn_desc": "Connect this host to NordVPN servers.", "local_server": "Local Server", "tunnel_install_enable": "Install \u0026 Enable", "tunnel_enable": "Enable Tunnel", diff --git a/translations/fa.json b/translations/fa.json index ed75df0..f69db6a 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -256,7 +256,14 @@ "bot_hint": "پس از تغییر توکن تنظیمات را ذخیره کنید تا ربات بازراه‌اندازی شود.", "api_docs_title": "🔌 مستندات API", "api_docs_hint": "از این رابط‌ها برای بررسی قابلیت‌های API استفاده کنید", - "tunnels_title": "Tunnels", + "tunnels_title": "تونل‌ها", + "tunnels_subtitle": "پنل را در اینترنت منتشر کنید یا ترافیک خروجی را از طریق VPN هدایت کنید.", + "tunnels_section_inbound": "دسترسی عمومی", + "tunnels_section_outbound": "VPN خروجی", + "tunnel_cloudflare_desc": "آدرس HTTPS رایگان از Cloudflare — بدون ثبت‌نام.", + "tunnel_ngrok_desc": "تونل پایدار با authtoken ngrok شما.", + "tunnel_warp_desc": "هدایت ترافیک سرور از طریق Cloudflare WARP.", + "tunnel_nordvpn_desc": "اتصال این میزبان به سرورهای NordVPN.", "local_server": "سرور محلی", "tunnel_install_enable": "نصب و فعال‌سازی", "tunnel_enable": "فعال‌سازی تونل", diff --git a/translations/fr.json b/translations/fr.json index 4ee5e2f..95ebf20 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -257,6 +257,13 @@ "api_docs_title": "🔌 Documentation API", "api_docs_hint": "Utilisez ces interfaces pour explorer l\u0027API", "tunnels_title": "Tunnels", + "tunnels_subtitle": "Exposez le panneau sur Internet ou routez le trafic sortant via un VPN.", + "tunnels_section_inbound": "Accès public", + "tunnels_section_outbound": "VPN sortant", + "tunnel_cloudflare_desc": "URL HTTPS gratuite via Cloudflare — sans compte.", + "tunnel_ngrok_desc": "Tunnel stable avec votre authtoken ngrok.", + "tunnel_warp_desc": "Routez le trafic du serveur via Cloudflare WARP.", + "tunnel_nordvpn_desc": "Connectez cet hôte aux serveurs NordVPN.", "local_server": "Serveur local", "tunnel_install_enable": "Installer et activer", "tunnel_enable": "Activer le tunnel", diff --git a/translations/ru.json b/translations/ru.json index 251294c..889b99b 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -267,6 +267,13 @@ "api_docs_title": "🔌 API Документация", "api_docs_hint": "Используйте эти интерфейсы для изучения возможностей API панели", "tunnels_title": "Туннели", + "tunnels_subtitle": "Публикация панели в интернете или исходящий трафик через VPN.", + "tunnels_section_inbound": "Публичный доступ", + "tunnels_section_outbound": "Исходящий VPN", + "tunnel_cloudflare_desc": "Бесплатный HTTPS-адрес через Cloudflare — без регистрации.", + "tunnel_ngrok_desc": "Стабильный туннель с вашим authtoken ngrok.", + "tunnel_warp_desc": "Маршрутизация трафика сервера через Cloudflare WARP.", + "tunnel_nordvpn_desc": "Подключение хоста к серверам NordVPN.", "local_server": "Локальный сервер", "tunnel_install_enable": "Установить и включить", "tunnel_enable": "Включить туннель", diff --git a/translations/zh.json b/translations/zh.json index ed80917..0be5fa9 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -257,6 +257,13 @@ "api_docs_title": "🔌 API 文档", "api_docs_hint": "使用这些接口了解面板功能", "tunnels_title": "Tunnels", + "tunnels_subtitle": "将面板暴露到互联网,或通过 VPN 路由出站流量。", + "tunnels_section_inbound": "公网访问", + "tunnels_section_outbound": "出站 VPN", + "tunnel_cloudflare_desc": "通过 Cloudflare 获取免费 HTTPS 地址,无需注册。", + "tunnel_ngrok_desc": "使用 ngrok authtoken 的稳定隧道。", + "tunnel_warp_desc": "通过 Cloudflare WARP 路由服务器流量。", + "tunnel_nordvpn_desc": "将此主机连接到 NordVPN 服务器。", "local_server": "本地服务器", "tunnel_install_enable": "安装并启用", "tunnel_enable": "启用隧道",