Add direct gateway forwarding when VPN is off
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 11:37:08 +03:00
parent 9d4f312595
commit 41922ad30b
14 changed files with 264 additions and 46 deletions

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { api } from '../api.js';
import {
connectionAction,
copyText,
formatConnectionDuration,
localProxyUrls,
subscriptionDomain,
@@ -35,13 +36,15 @@ export function ClientOverviewPage({
const [now, setNow] = useState(Date.now());
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
const [pings, setPings] = useState({});
const [copiedProxy, setCopiedProxy] = useState('');
const [accessTab, setAccessTab] = useState('gateway');
const [copyFeedback, setCopyFeedback] = useState(null);
const [refreshingInfo, setRefreshingInfo] = useState(false);
const [usageUpdated, setUsageUpdated] = useState(false);
const [serverRevealVersion, setServerRevealVersion] = useState(0);
const [serversLeaving, setServersLeaving] = useState(false);
const subscriptionInputRef = useRef(null);
const subscriptionRef = useRef(null);
const copyTimerRef = useRef(null);
const serverKey = servers.map((server) => `${server.tag}:${server.server}:${server.server_port}`).join('|');
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
@@ -133,6 +136,8 @@ export function ClientOverviewPage({
return () => cancelAnimationFrame(frame);
}, [usage.used]);
useEffect(() => () => clearTimeout(copyTimerRef.current), []);
async function toggleConnection() {
const action = connectionAction({ connected, selectedTag, configExists: state?.configExists });
if (action?.type === 'stop') return onStop();
@@ -154,13 +159,14 @@ export function ClientOverviewPage({
}
async function copyProxy(kind) {
const value = kind === 'gateway' ? gatewayAddress : proxyUrls[kind];
clearTimeout(copyTimerRef.current);
setCopyFeedback({ kind, failed: false });
copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
try {
await navigator.clipboard.writeText(kind === 'gateway' ? gatewayAddress : proxyUrls[kind]);
setCopiedProxy(kind);
setTimeout(() => setCopiedProxy(''), 800);
await copyText(value);
} catch {
setCopiedProxy('error');
setTimeout(() => setCopiedProxy(''), 800);
setCopyFeedback({ kind, failed: true });
}
}
@@ -194,7 +200,7 @@ export function ClientOverviewPage({
type="button"
role="switch"
aria-checked={connected}
aria-label={connected ? `Выключить ${isGateway ? 'Gateway' : 'VPN'}` : `Включить ${isGateway ? 'Gateway' : 'VPN'}`}
aria-label={connected ? 'Выключить VPN' : 'Включить VPN'}
disabled={busy || (!connected && !canStart)}
onClick={toggleConnection}
>
@@ -204,9 +210,7 @@ export function ClientOverviewPage({
</button>
<div className="client-state-copy" aria-live="polite">
<h2 key={connected ? 'connected' : 'disconnected'} id="connection-title">
{isGateway
? connected ? 'Gateway включён' : 'Gateway выключен'
: connected ? 'VPN включён' : 'VPN выключен'}
{connected ? 'VPN включён' : 'VPN выключен'}
</h2>
<div className="client-state-detail">
{connected ? (
@@ -221,24 +225,42 @@ export function ClientOverviewPage({
</div>
</div>
<section className="client-proxies" aria-label={isGateway ? 'Адреса Gateway и Proxy' : 'Локальный прокси'}>
<section className={`client-proxies${isGateway ? ' has-tabs' : ''}`} aria-label={isGateway ? 'Gateway и Local Proxy' : 'Локальный прокси'}>
{isGateway && (
<div className="client-access-point">
<span className="client-proxy-label">Gateway</span>
<div className="client-access-tabs" role="tablist" aria-label="Способ подключения">
{[
['gateway', 'Gateway'],
['proxy', 'Local Proxy'],
].map(([tab, label]) => (
<button
className={`client-access-tab${accessTab === tab ? ' is-active' : ''}`}
type="button"
role="tab"
key={tab}
aria-selected={accessTab === tab}
onClick={() => setAccessTab(tab)}
>
{label}
</button>
))}
</div>
)}
{(isGateway ? accessTab === 'gateway' : false) ? (
<div className="client-access-point" role="tabpanel" key="gateway">
<strong className="client-proxy-address">{gatewayAddress}</strong>
<button
className={copiedProxy === 'gateway' ? 'is-copied' : ''}
className={`client-copy-button${copyFeedback?.kind === 'gateway' ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
type="button"
aria-label={`Скопировать Gateway: ${gatewayAddress}`}
onClick={() => copyProxy('gateway')}
>
<span className="client-copy-label">КОПИРОВАТЬ</span>
{copiedProxy === 'gateway' && <span className="client-copy-feedback">Copied</span>}
{copyFeedback?.kind === 'gateway' && <span className="client-copy-feedback">{copyFeedback.failed ? 'Error' : 'Copied'}</span>}
</button>
</div>
)}
<div className="client-access-point">
<span className="client-proxy-label">{isGateway ? 'Proxy' : 'Адрес'}</span>
) : (
<div className="client-access-point" role={isGateway ? 'tabpanel' : undefined} key="proxy">
{!isGateway && <span className="client-proxy-label">Адрес</span>}
<strong className="client-proxy-address">
{proxyUrls.http.replace(/^https?:\/\//, '')}
</strong>
@@ -248,18 +270,19 @@ export function ClientOverviewPage({
['http', 'HTTP'],
].map(([kind, label]) => (
<button
className={copiedProxy === kind ? 'is-copied' : ''}
className={`client-copy-button${copyFeedback?.kind === kind ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
type="button"
key={kind}
aria-label={`Скопировать ${label}: ${proxyUrls[kind]}`}
onClick={() => copyProxy(kind)}
>
<span className="client-copy-label">{label}</span>
{copiedProxy === kind && <span className="client-copy-feedback">Copied</span>}
{copyFeedback?.kind === kind && <span className="client-copy-feedback">{copyFeedback.failed ? 'Error' : 'Copied'}</span>}
</button>
))}
</div>
</div>
)}
</section>
</section>
)}