Update VPN proxy client behavior
Build and Deploy Gateway / build-and-push (push) Successful in 18s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-09 11:55:02 +03:00
parent 71ede44be0
commit 90433d7cd8
27 changed files with 770 additions and 254 deletions
+40 -10
View File
@@ -119,6 +119,9 @@ interface ClientOverviewPageProps {
}
type CopyKind = 'gateway' | 'socks5' | 'http';
type CopyFeedback = { failed: boolean; cycle: number };
type CopyFeedbackMap = Partial<Record<CopyKind, CopyFeedback>>;
type CopyAnnouncement = { text: string; cycle: number };
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
@@ -263,8 +266,9 @@ function InlineProgress({ operations, context }: {
);
}
function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSetGatewayAuto }: {
function HarborBrand({ isGateway, connected, gatewayAvailable, gatewayDirect, blocked, onSetGatewayAuto }: {
isGateway: boolean;
connected: boolean;
gatewayAvailable: boolean;
gatewayDirect: boolean;
blocked: boolean;
@@ -333,7 +337,7 @@ function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSe
</div>;
return (
<div className={`harbor-brand is-${product.toLowerCase()}${switchable ? ` is-switchable${gatewayDirect ? ' is-gateway-active' : ''}` : ''}`}>
<div className={`harbor-brand is-${product.toLowerCase()}${connected ? ' is-connected' : ''}${switchable ? ` is-switchable${gatewayDirect ? ' is-gateway-active' : ''}` : ''}`}>
{switchable ? <button
className={`harbor-brand-control${modeAnimating ? ' is-mode-animating' : ''}`}
type="button"
@@ -392,8 +396,10 @@ export function ClientOverviewPage({
const showPower = isGateway || (hasSubscription && Boolean(selectedServerId));
const [now, setNow] = useState(Date.now());
const [showIntro, setShowIntro] = useState(true);
const [copyFeedback, setCopyFeedback] = useState<{ kind: CopyKind; failed: boolean } | null>(null);
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [copyFeedback, setCopyFeedback] = useState<CopyFeedbackMap>({});
const [copyAnnouncement, setCopyAnnouncement] = useState<CopyAnnouncement>({ text: '', cycle: 0 });
const copyTimersRef = useRef<Partial<Record<CopyKind, ReturnType<typeof setTimeout>>>>({});
const copyAttemptsRef = useRef<Partial<Record<CopyKind, object>>>({});
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
const controlHost = window.location.host || `${gatewayAddress}:3456`;
const proxyUrls = localProxyUrls(state?.clientRuntime?.proxyPort, gatewayAddress);
@@ -472,7 +478,8 @@ export function ClientOverviewPage({
}, [diagnosticsAvailable]);
useEffect(() => () => {
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
for (const timer of Object.values(copyTimersRef.current)) clearTimeout(timer);
copyAttemptsRef.current = {};
}, []);
function selectServer(serverId: string) {
@@ -482,14 +489,36 @@ export function ClientOverviewPage({
async function copyProxy(kind: CopyKind) {
const value = kind === 'gateway' ? gatewayAddress : proxyUrls[kind];
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
const activeTimer = copyTimersRef.current[kind];
if (activeTimer) clearTimeout(activeTimer);
const attempt = {};
copyAttemptsRef.current[kind] = attempt;
let failed = false;
try {
await copyText(value);
setCopyFeedback({ kind, failed: false });
} catch {
setCopyFeedback({ kind, failed: true });
failed = true;
}
copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
if (copyAttemptsRef.current[kind] !== attempt) return;
const pendingTimer = copyTimersRef.current[kind];
if (pendingTimer) clearTimeout(pendingTimer);
setCopyFeedback((current) => ({
...current,
[kind]: { failed, cycle: (current[kind]?.cycle || 0) + 1 },
}));
setCopyAnnouncement((current) => ({
text: failed ? 'Не удалось скопировать' : 'Скопировано',
cycle: current.cycle + 1,
}));
copyTimersRef.current[kind] = setTimeout(() => {
setCopyFeedback((current) => {
const next = { ...current };
delete next[kind];
return next;
});
delete copyTimersRef.current[kind];
delete copyAttemptsRef.current[kind];
}, 800);
}
function openRouting() {
@@ -506,10 +535,11 @@ export function ClientOverviewPage({
>
<VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
<span key={copyAnnouncement.cycle}>{copyAnnouncement.text}</span>
</div>
<HarborBrand
isGateway={isGateway}
connected={connected}
gatewayAvailable={gatewayAvailable}
gatewayDirect={gatewayDirect}
blocked={gatewayAutoBlocked}