1211 lines
54 KiB
HTML
1211 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VPN_CLIENT // SECURE_SHELL</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100..800&display=swap');
|
|
|
|
:root {
|
|
--color-neon: #00ff41;
|
|
--color-bg: #050505;
|
|
}
|
|
|
|
body {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
background-color: var(--color-bg);
|
|
color: var(--color-neon);
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
::selection {
|
|
background-color: var(--color-neon);
|
|
color: black;
|
|
}
|
|
|
|
/* Scrollbar */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(0, 255, 65, 0.2);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(0, 255, 65, 0.5);
|
|
}
|
|
|
|
.matrix-bg {
|
|
background-image: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
|
background-size: 100% 2px, 3px 100%;
|
|
}
|
|
|
|
/* Big Toggle Switch */
|
|
.big-toggle {
|
|
position: relative;
|
|
width: 80px;
|
|
height: 40px;
|
|
}
|
|
|
|
.big-toggle input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.big-toggle .slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
inset: 0;
|
|
background-color: #1a1a1a;
|
|
border: 2px solid rgba(0, 255, 65, 0.3);
|
|
border-radius: 40px;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.big-toggle .slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 28px;
|
|
width: 28px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background-color: rgba(0, 255, 65, 0.4);
|
|
border-radius: 50%;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.big-toggle input:checked+.slider {
|
|
background-color: rgba(0, 255, 65, 0.2);
|
|
border-color: #00ff41;
|
|
box-shadow: 0 0 20px rgba(0, 255, 65, 0.4);
|
|
}
|
|
|
|
.big-toggle input:checked+.slider:before {
|
|
transform: translateX(40px);
|
|
background-color: #00ff41;
|
|
box-shadow: 0 0 10px #00ff41;
|
|
}
|
|
|
|
/* Server Card */
|
|
.server-card {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.server-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 255, 65, 0.15);
|
|
}
|
|
|
|
.server-card.active {
|
|
border-color: #00ff41 !important;
|
|
box-shadow: 0 0 15px rgba(0, 255, 65, 0.3);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="min-h-screen flex flex-col relative selection:bg-[#00ff41] selection:text-black">
|
|
|
|
<!-- CRT Effects -->
|
|
<div class="matrix-bg fixed inset-0 z-0 pointer-events-none"></div>
|
|
<div
|
|
class="fixed inset-0 z-40 pointer-events-none bg-[radial-gradient(circle_at_50%_50%,rgba(0,255,65,0.03)_0%,transparent_100%)]">
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<header class="z-30 border-b border-[#00ff41]/20 bg-black/90 backdrop-blur-md sticky top-0">
|
|
<div class="max-w-[1400px] mx-auto px-4 md:px-6 py-3 flex justify-between items-center">
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative p-1.5 border border-[#00ff41]/50 shadow-[0_0_10px_rgba(0,255,65,0.2)] bg-black">
|
|
<i data-lucide="terminal" class="w-5 h-5 animate-pulse text-[#00ff41]"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-base font-black tracking-[0.2em] uppercase text-[#00ff41]">
|
|
VPN<span class="text-white">_</span>CLIENT
|
|
</h1>
|
|
<p class="text-[10px] opacity-40 uppercase tracking-widest text-[#00ff41]">Secure Shell v4.2</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hidden md:flex items-center gap-6 text-[11px] uppercase">
|
|
<div class="flex flex-col items-end border-r border-[#00ff41]/20 pr-6">
|
|
<span class="opacity-30 text-[#00ff41]">Status</span>
|
|
<span id="headerStatus" class="text-white font-bold text-sm">STANDBY</span>
|
|
</div>
|
|
<div class="flex flex-col items-end">
|
|
<span class="opacity-30 text-[#00ff41]">Traffic_Used</span>
|
|
<span id="trafficValue" class="text-blue-400 font-bold text-sm">-- / --</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main
|
|
class="flex-grow max-w-[1400px] w-full mx-auto px-4 md:px-6 py-4 grid grid-cols-1 lg:grid-cols-12 gap-4 relative z-10">
|
|
|
|
<!-- Left Column: Main Controls -->
|
|
<div class="lg:col-span-7 flex flex-col gap-4">
|
|
|
|
<!-- Master Proxy Toggle + Status -->
|
|
<div class="bg-black border-2 border-[#00ff41]/30 p-5 relative">
|
|
<div class="flex items-center justify-between gap-6">
|
|
<!-- Toggle -->
|
|
<div class="flex items-center gap-5">
|
|
<label class="big-toggle">
|
|
<input type="checkbox" id="masterProxyToggle" checked>
|
|
<span class="slider"></span>
|
|
</label>
|
|
<div>
|
|
<div id="proxyModeLabel" class="text-xl font-black tracking-wider text-[#00ff41]">
|
|
VPN_MODE
|
|
</div>
|
|
<div id="proxyModeSubtitle" class="text-[11px] opacity-50 text-[#00ff41] uppercase">
|
|
Traffic routed via proxy
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Status -->
|
|
<div id="quickStatus" class="text-right hidden md:flex gap-6">
|
|
<div>
|
|
<div class="text-[11px] opacity-40 uppercase text-[#00ff41]">Uptime</div>
|
|
<div id="uptimeDisplay" class="text-lg font-bold text-[#00ff41]">00:00:00</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-[11px] opacity-40 uppercase text-[#00ff41]">Current_IP</div>
|
|
<div id="currentIpDisplay" class="text-lg font-bold text-white">---.---.---.---</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Proxy Chain Visualization (Expanded) -->
|
|
<div id="proxyChainSection" class="bg-black border border-[#00ff41]/30 p-5 font-mono">
|
|
<div
|
|
class="text-[11px] uppercase font-bold tracking-[0.3em] text-[#00ff41] mb-5 flex items-center gap-2">
|
|
<i data-lucide="git-branch" class="w-4 h-4"></i> Proxy_Chain_Visualization
|
|
</div>
|
|
|
|
<div id="proxyChain" class="flex items-stretch gap-4 text-sm justify-center py-4">
|
|
<!-- You -->
|
|
<div class="flex flex-col items-center justify-center gap-2">
|
|
<div
|
|
class="w-14 h-14 rounded-full border-2 border-[#00ff41] flex items-center justify-center bg-[#00ff41]/10">
|
|
<i data-lucide="user" class="w-6 h-6 text-[#00ff41]"></i>
|
|
</div>
|
|
<span class="uppercase opacity-60 text-[#00ff41] text-[10px]">You</span>
|
|
</div>
|
|
|
|
<!-- Arrow to branch -->
|
|
<div class="flex items-center">
|
|
<div class="w-8 h-[3px] bg-[#00ff41]"></div>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-[#00ff41]"></i>
|
|
</div>
|
|
|
|
<!-- Branch: Fallback + VPN -->
|
|
<div id="chainBranch" class="flex flex-col gap-3 py-1">
|
|
<!-- Fallback branch -->
|
|
<div id="chainFallbackRow" class="flex items-center gap-3 transition-all duration-300 hidden">
|
|
<div class="w-5 h-[3px] bg-[#00ff41]/50 rounded-full"></div>
|
|
<div id="chainFallbackBox"
|
|
class="relative w-16 h-12 border-2 border-[#00ff41]/30 flex items-center justify-center bg-[#0a0a0a] transition-all">
|
|
<i data-lucide="server" class="w-5 h-5 text-[#00ff41]/50"></i>
|
|
<div id="chainFallbackX"
|
|
class="absolute inset-0 hidden items-center justify-center bg-black/60">
|
|
<i data-lucide="x" class="w-6 h-6 text-red-500"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span id="chainFallbackLabel"
|
|
class="uppercase text-[10px] opacity-60 text-[#00ff41]">Fallback</span>
|
|
<span id="chainFallbackLatency" class="text-xs text-[#00ff41]/50">--ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- VPN branch -->
|
|
<div id="chainVPNRow" class="flex items-center gap-3 transition-all duration-300">
|
|
<div class="w-5 h-[3px] bg-[#00ff41]/50 rounded-full"></div>
|
|
<div id="chainVPNBox"
|
|
class="relative w-16 h-12 border-2 border-[#00ff41]/30 flex items-center justify-center bg-[#0a0a0a] transition-all">
|
|
<i data-lucide="shield" class="w-5 h-5 text-[#00ff41]/50"></i>
|
|
<div id="chainVPNX"
|
|
class="absolute inset-0 hidden items-center justify-center bg-black/60">
|
|
<i data-lucide="x" class="w-6 h-6 text-red-500"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span id="chainVPNLabel"
|
|
class="uppercase text-[10px] opacity-60 text-[#00ff41]">VPN</span>
|
|
<span id="chainVPNLatency" class="text-xs text-[#00ff41]/50">--ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Direct branch (when proxy disabled) -->
|
|
<div id="chainDirectRow" class="flex items-center gap-3 transition-all duration-300 hidden">
|
|
<div class="w-5 h-[3px] bg-yellow-500/50 rounded-full"></div>
|
|
<div id="chainDirectBox"
|
|
class="relative w-16 h-12 border-2 border-yellow-500/50 flex items-center justify-center bg-yellow-500/5 transition-all">
|
|
<i data-lucide="zap" class="w-5 h-5 text-yellow-500/70"></i>
|
|
</div>
|
|
<span class="uppercase text-[10px] opacity-60 text-yellow-500">DIRECT</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Arrow from branch -->
|
|
<div class="flex items-center">
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-[#00ff41]"></i>
|
|
<div class="w-8 h-[3px] bg-[#00ff41]"></div>
|
|
</div>
|
|
|
|
<!-- Internet -->
|
|
<div class="flex flex-col items-center justify-center gap-2">
|
|
<div
|
|
class="w-14 h-14 rounded-full border-2 border-blue-400 flex items-center justify-center bg-blue-400/10">
|
|
<i data-lucide="globe" class="w-6 h-6 text-blue-400"></i>
|
|
</div>
|
|
<span class="uppercase opacity-60 text-blue-400 text-[10px]">Internet</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chain Status -->
|
|
<div id="chainStatus"
|
|
class="mt-4 text-sm text-center py-2 px-4 bg-[#0a0a0a] border border-[#00ff41]/20 text-[#00ff41] uppercase">
|
|
No proxy configured
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fallback Proxy Configuration (Expanded) -->
|
|
<div id="fallbackSection"
|
|
class="flex flex-col bg-black border border-[#00ff41]/30 overflow-hidden font-mono">
|
|
<div
|
|
class="bg-[#111] px-5 py-3 border-b border-[#00ff41]/10 flex justify-between items-center shrink-0">
|
|
<span
|
|
class="text-[11px] uppercase font-bold tracking-[0.3em] flex items-center gap-2 text-[#00ff41]">
|
|
<i data-lucide="git-branch" class="w-4 h-4"></i> Fallback_Proxy_Settings
|
|
</span>
|
|
<div class="flex items-center gap-4">
|
|
<!-- Enable/Disable Toggle -->
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<span id="fallbackToggleLabel"
|
|
class="text-[11px] opacity-50 uppercase text-[#00ff41]">OFF</span>
|
|
<div class="relative">
|
|
<input type="checkbox" id="fallbackToggle" class="sr-only peer">
|
|
<div
|
|
class="w-10 h-5 bg-[#1a1a1a] border border-[#00ff41]/20 rounded-full peer-checked:bg-[#00ff41]/20 peer-checked:border-[#00ff41]/50 transition-all">
|
|
</div>
|
|
<div
|
|
class="absolute left-0.5 top-0.5 w-4 h-4 bg-[#00ff41]/30 rounded-full peer-checked:translate-x-5 peer-checked:bg-[#00ff41] transition-all">
|
|
</div>
|
|
</div>
|
|
</label>
|
|
<button id="saveFallbackBtn"
|
|
class="text-[11px] opacity-50 hover:opacity-100 hover:text-[#00ff41] transition-opacity uppercase px-3 py-1 border border-[#00ff41]/20 hover:border-[#00ff41]/50">Save</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-4 space-y-4">
|
|
<!-- Host/Port inputs -->
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div class="col-span-2">
|
|
<label class="text-[11px] opacity-50 uppercase text-[#00ff41] block mb-1">Host</label>
|
|
<input type="text" id="fallbackHost" placeholder="192.168.50.111"
|
|
class="w-full bg-[#0a0a0a] border border-[#00ff41]/20 p-3 text-sm text-[#00ff41] focus:outline-none focus:border-[#00ff41]/50 placeholder:text-[#00ff41]/20" />
|
|
</div>
|
|
<div>
|
|
<label class="text-[11px] opacity-50 uppercase text-[#00ff41] block mb-1">Port</label>
|
|
<input type="number" id="fallbackPort" placeholder="8080"
|
|
class="w-full bg-[#0a0a0a] border border-[#00ff41]/20 p-3 text-sm text-[#00ff41] focus:outline-none focus:border-[#00ff41]/50 placeholder:text-[#00ff41]/20" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="text-[11px] opacity-50 text-[#00ff41] flex items-start gap-2">
|
|
<i data-lucide="info" class="w-4 h-4 shrink-0 mt-0.5"></i>
|
|
<span>URLTest auto-selects fastest proxy. Re-apply subscription after changes.</span>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div id="fallbackStatus" class="text-sm text-[#00ff41]/50 uppercase hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connection Settings -->
|
|
<div class="bg-black border border-[#00ff41]/20 p-4 font-mono">
|
|
<div
|
|
class="text-[11px] uppercase font-bold tracking-[0.3em] text-[#00ff41] mb-3 flex items-center gap-2">
|
|
<i data-lucide="plug" class="w-4 h-4"></i> Connection_Settings
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-2">
|
|
<!-- HTTP Proxy -->
|
|
<div class="flex items-center gap-3 p-3 bg-[#0a0a0a] border border-[#00ff41]/10">
|
|
<span class="text-[11px] uppercase opacity-50 text-[#00ff41] w-16">HTTP</span>
|
|
<input type="text" id="httpProxyUrl" readonly value="Loading..."
|
|
class="flex-grow bg-transparent text-sm text-[#00ff41]/80 focus:outline-none cursor-pointer font-mono"
|
|
title="Click to copy" />
|
|
<button onclick="copyToClipboard('httpProxyUrl', this)"
|
|
class="text-[10px] text-[#00ff41]/50 hover:text-[#00ff41] transition-colors uppercase px-2 py-1 border border-[#00ff41]/20">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
|
|
<!-- SOCKS5 Proxy -->
|
|
<div class="flex items-center gap-3 p-3 bg-[#0a0a0a] border border-[#00ff41]/10">
|
|
<span class="text-[11px] uppercase opacity-50 text-[#00ff41] w-16">SOCKS5</span>
|
|
<input type="text" id="socks5ProxyUrl" readonly value="Loading..."
|
|
class="flex-grow bg-transparent text-sm text-[#00ff41]/80 focus:outline-none cursor-pointer font-mono"
|
|
title="Click to copy" />
|
|
<button onclick="copyToClipboard('socks5ProxyUrl', this)"
|
|
class="text-[10px] text-[#00ff41]/50 hover:text-[#00ff41] transition-colors uppercase px-2 py-1 border border-[#00ff41]/20">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-2 text-[11px] opacity-40 text-[#00ff41] flex items-center gap-2">
|
|
<i data-lucide="info" class="w-3 h-3"></i>
|
|
Use these URLs in browser/app proxy settings
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Subscription & Servers -->
|
|
<div class="lg:col-span-5 flex flex-col gap-4">
|
|
|
|
<!-- Subscription Input -->
|
|
<div class="flex flex-col gap-2 p-4 bg-[#0a0a0a] border border-[#00ff41]/30 relative">
|
|
<label class="text-[11px] uppercase tracking-widest opacity-50 text-[#00ff41]">Subscription_URL</label>
|
|
<div class="flex gap-2">
|
|
<div class="relative flex-grow">
|
|
<i data-lucide="link"
|
|
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#00ff41]/40"></i>
|
|
<!-- Hidden full URL input -->
|
|
<input type="hidden" id="subUrlFull" />
|
|
<!-- Masked display input -->
|
|
<input type="text" id="subUrlInput" placeholder="https://provider.com/..."
|
|
class="w-full bg-black border border-[#00ff41]/20 py-2.5 pl-10 pr-10 text-sm tracking-wider focus:outline-none focus:border-[#00ff41] transition-all placeholder:text-[#00ff41]/20 text-[#00ff41]" />
|
|
<!-- Toggle visibility -->
|
|
<button id="toggleUrlVisibility" type="button"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-[#00ff41]/40 hover:text-[#00ff41] transition-colors"
|
|
title="Show/Hide full URL">
|
|
<i data-lucide="eye-off" class="w-4 h-4" id="urlEyeIcon"></i>
|
|
</button>
|
|
</div>
|
|
<button id="fetchServersBtn"
|
|
class="flex items-center justify-center gap-2 bg-[#00ff41] text-black px-4 py-2.5 text-xs font-black uppercase tracking-widest hover:bg-white hover:shadow-[0_0_15px_rgba(0,255,65,0.4)] transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<i data-lucide="download" class="w-4 h-4" id="fetchIcon"></i>
|
|
<span id="fetchText">Sync</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server List as Cards -->
|
|
<div class="flex flex-col bg-[#0a0a0a]/50 border border-[#00ff41]/10 overflow-hidden">
|
|
<div class="px-4 py-2 border-b border-[#00ff41]/10 bg-black flex justify-between items-center shrink-0">
|
|
<span class="text-[11px] uppercase tracking-[0.2em] font-bold text-[#00ff41]">Servers</span>
|
|
<span id="serverCount" class="text-[10px] opacity-40 text-[#00ff41]">0 endpoints</span>
|
|
</div>
|
|
|
|
<div id="serverListContainer"
|
|
class="overflow-y-auto custom-scrollbar p-3 grid grid-cols-3 gap-2 max-h-[280px]">
|
|
<!-- Cards populated by JS -->
|
|
<div class="col-span-3 text-center py-6 text-[#00ff41]/30 text-xs uppercase">
|
|
No_Data // Awaiting_Sync
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Terminal Logs -->
|
|
<div
|
|
class="flex-grow flex flex-col bg-black border border-[#00ff41]/20 overflow-hidden font-mono min-h-[180px]">
|
|
<div
|
|
class="bg-[#111] px-4 py-2 border-b border-[#00ff41]/10 flex justify-between items-center shrink-0">
|
|
<span
|
|
class="text-[11px] uppercase font-bold tracking-[0.3em] flex items-center gap-2 text-[#00ff41]">
|
|
<div class="w-1.5 h-1.5 bg-[#00ff41] rounded-full animate-ping"></div> Logs
|
|
</span>
|
|
<button id="clearLogs"
|
|
class="text-[10px] opacity-30 hover:opacity-100 hover:text-[#00ff41] transition-opacity uppercase">Clear</button>
|
|
</div>
|
|
|
|
<div id="logsContainer"
|
|
class="flex-grow p-3 overflow-y-auto custom-scrollbar text-[11px] space-y-1 opacity-80 font-mono">
|
|
<div class="flex gap-2">
|
|
<span class="opacity-20 text-[#00ff41]">[SYSTEM]</span>
|
|
<span class="text-[#00ff41] animate-pulse">_</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="z-30 bg-[#0d0d0d] border-t border-[#00ff41]/10 py-2 mt-auto">
|
|
<div
|
|
class="max-w-[1400px] mx-auto px-6 flex justify-between items-center text-[10px] uppercase tracking-[0.2em] opacity-40 text-[#00ff41]">
|
|
<div class="flex gap-6">
|
|
<span>Core: 4.1.0-Release</span>
|
|
<span>Proxy: HTTP/8080</span>
|
|
</div>
|
|
<div class="hidden md:flex gap-6">
|
|
<span>AES-256-GCM</span>
|
|
<span class="text-[#00ff41] opacity-100 font-bold tracking-normal">SESSION: <span
|
|
id="sessionId">...</span></span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
// --- Icons Initialization ---
|
|
lucide.createIcons();
|
|
|
|
// --- State ---
|
|
const state = {
|
|
nodes: [],
|
|
logs: [],
|
|
activeNode: null,
|
|
isFetching: false,
|
|
isConnecting: false,
|
|
subscriptionUrl: '',
|
|
sessionId: Math.random().toString(16).substr(2, 8).toUpperCase(),
|
|
userInfo: null,
|
|
proxyEnabled: true,
|
|
urlVisible: false,
|
|
serverStartTime: 0,
|
|
uptimeInterval: null
|
|
};
|
|
|
|
// --- DOM Elements ---
|
|
const els = {
|
|
subUrlInput: document.getElementById('subUrlInput'),
|
|
subUrlFull: document.getElementById('subUrlFull'),
|
|
toggleUrlVisibility: document.getElementById('toggleUrlVisibility'),
|
|
urlEyeIcon: document.getElementById('urlEyeIcon'),
|
|
fetchServersBtn: document.getElementById('fetchServersBtn'),
|
|
fetchIcon: document.getElementById('fetchIcon'),
|
|
fetchText: document.getElementById('fetchText'),
|
|
serverListContainer: document.getElementById('serverListContainer'),
|
|
serverCount: document.getElementById('serverCount'),
|
|
logsContainer: document.getElementById('logsContainer'),
|
|
clearLogs: document.getElementById('clearLogs'),
|
|
headerStatus: document.getElementById('headerStatus'),
|
|
sessionId: document.getElementById('sessionId'),
|
|
trafficValue: document.getElementById('trafficValue'),
|
|
// Master toggle
|
|
masterProxyToggle: document.getElementById('masterProxyToggle'),
|
|
proxyModeLabel: document.getElementById('proxyModeLabel'),
|
|
proxyModeSubtitle: document.getElementById('proxyModeSubtitle'),
|
|
currentIpDisplay: document.getElementById('currentIpDisplay'),
|
|
// Fallback Proxy elements
|
|
fallbackToggle: document.getElementById('fallbackToggle'),
|
|
fallbackToggleLabel: document.getElementById('fallbackToggleLabel'),
|
|
fallbackHost: document.getElementById('fallbackHost'),
|
|
fallbackPort: document.getElementById('fallbackPort'),
|
|
saveFallbackBtn: document.getElementById('saveFallbackBtn'),
|
|
fallbackStatus: document.getElementById('fallbackStatus'),
|
|
// Proxy Chain visualization
|
|
chainFallbackRow: document.getElementById('chainFallbackRow'),
|
|
chainFallbackBox: document.getElementById('chainFallbackBox'),
|
|
chainFallbackLabel: document.getElementById('chainFallbackLabel'),
|
|
chainFallbackLatency: document.getElementById('chainFallbackLatency'),
|
|
chainFallbackX: document.getElementById('chainFallbackX'),
|
|
chainVPNRow: document.getElementById('chainVPNRow'),
|
|
chainVPNBox: document.getElementById('chainVPNBox'),
|
|
chainVPNLabel: document.getElementById('chainVPNLabel'),
|
|
chainVPNLatency: document.getElementById('chainVPNLatency'),
|
|
chainVPNX: document.getElementById('chainVPNX'),
|
|
chainDirectRow: document.getElementById('chainDirectRow'),
|
|
chainStatus: document.getElementById('chainStatus'),
|
|
// Connection settings
|
|
httpProxyUrl: document.getElementById('httpProxyUrl'),
|
|
socks5ProxyUrl: document.getElementById('socks5ProxyUrl'),
|
|
// Uptime
|
|
uptimeDisplay: document.getElementById('uptimeDisplay')
|
|
};
|
|
|
|
els.sessionId.textContent = state.sessionId;
|
|
|
|
// --- Helpers ---
|
|
function formatBytes(bytes, decimals = 1) {
|
|
if (!+bytes) return '0 B';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
}
|
|
|
|
function maskUrl(url) {
|
|
if (!url) return '';
|
|
try {
|
|
const parsed = new URL(url);
|
|
return `${parsed.hostname}/...`;
|
|
} catch {
|
|
return url.length > 30 ? url.substring(0, 30) + '...' : url;
|
|
}
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function startUptimeTimer() {
|
|
if (state.uptimeInterval) clearInterval(state.uptimeInterval);
|
|
if (!state.serverStartTime || state.serverStartTime <= 0) {
|
|
els.uptimeDisplay.textContent = '00:00:00';
|
|
return;
|
|
}
|
|
|
|
// Adjust local offset if needed, but simple diff is usually enough
|
|
const update = () => {
|
|
const now = Date.now() / 1000;
|
|
const elapsed = Math.floor(now - state.serverStartTime);
|
|
if (elapsed >= 0) {
|
|
els.uptimeDisplay.textContent = formatUptime(elapsed);
|
|
}
|
|
};
|
|
|
|
update();
|
|
state.uptimeInterval = setInterval(update, 1000);
|
|
}
|
|
|
|
function stopUptimeTimer() {
|
|
if (state.uptimeInterval) {
|
|
clearInterval(state.uptimeInterval);
|
|
state.uptimeInterval = null;
|
|
}
|
|
els.uptimeDisplay.textContent = '00:00:00';
|
|
}
|
|
|
|
// --- URL Visibility Toggle ---
|
|
els.toggleUrlVisibility.addEventListener('click', () => {
|
|
state.urlVisible = !state.urlVisible;
|
|
updateUrlDisplay();
|
|
});
|
|
|
|
function updateUrlDisplay() {
|
|
const fullUrl = els.subUrlFull.value || state.subscriptionUrl;
|
|
if (state.urlVisible) {
|
|
els.subUrlInput.value = fullUrl;
|
|
els.urlEyeIcon.setAttribute('data-lucide', 'eye');
|
|
} else {
|
|
els.subUrlInput.value = maskUrl(fullUrl);
|
|
els.urlEyeIcon.setAttribute('data-lucide', 'eye-off');
|
|
}
|
|
lucide.createIcons();
|
|
}
|
|
|
|
els.subUrlInput.addEventListener('input', () => {
|
|
// When user types, store full URL
|
|
els.subUrlFull.value = els.subUrlInput.value;
|
|
state.subscriptionUrl = els.subUrlInput.value;
|
|
});
|
|
|
|
els.subUrlInput.addEventListener('blur', () => {
|
|
// On blur, mask if not visible
|
|
if (!state.urlVisible && els.subUrlFull.value) {
|
|
els.subUrlInput.value = maskUrl(els.subUrlFull.value);
|
|
}
|
|
});
|
|
|
|
els.subUrlInput.addEventListener('focus', () => {
|
|
// On focus, show full for editing
|
|
if (els.subUrlFull.value) {
|
|
els.subUrlInput.value = els.subUrlFull.value;
|
|
}
|
|
});
|
|
|
|
// --- Logger ---
|
|
function addLog(msg, type = 'info') {
|
|
const time = new Date().toLocaleTimeString('ru-RU', { hour12: false });
|
|
|
|
const logEl = document.createElement('div');
|
|
logEl.className = 'flex gap-2 items-start leading-tight animate-in fade-in slide-in-from-left-1 duration-300';
|
|
|
|
let colorClass = 'text-[#00ff41]/70';
|
|
let prefix = '>>';
|
|
if (type === 'success') { colorClass = 'text-blue-400'; prefix = 'OK.'; }
|
|
if (type === 'warning') { colorClass = 'text-yellow-500'; prefix = '!!'; }
|
|
if (type === 'error') { colorClass = 'text-red-500'; prefix = 'ERR'; }
|
|
|
|
logEl.innerHTML = `
|
|
<span class="opacity-20 shrink-0 tracking-tighter text-[#00ff41]">[${time}]</span>
|
|
<span class="${colorClass}">${prefix} ${msg}</span>
|
|
`;
|
|
|
|
els.logsContainer.insertBefore(logEl, els.logsContainer.lastElementChild);
|
|
els.logsContainer.scrollTop = els.logsContainer.scrollHeight;
|
|
|
|
// Limit logs
|
|
while (els.logsContainer.children.length > 50) {
|
|
els.logsContainer.removeChild(els.logsContainer.firstElementChild);
|
|
}
|
|
}
|
|
|
|
// Global copy function for proxy URLs
|
|
function copyToClipboard(inputId, btn) {
|
|
const input = document.getElementById(inputId);
|
|
navigator.clipboard.writeText(input.value).then(() => {
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Copied!';
|
|
btn.classList.add('text-blue-400');
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.classList.remove('text-blue-400');
|
|
}, 1500);
|
|
addLog(`COPIED: ${input.value}`, 'success');
|
|
});
|
|
}
|
|
|
|
els.clearLogs.addEventListener('click', () => {
|
|
const lastChild = els.logsContainer.lastElementChild;
|
|
els.logsContainer.innerHTML = '';
|
|
els.logsContainer.appendChild(lastChild);
|
|
addLog('LOGS_CLEARED', 'info');
|
|
});
|
|
|
|
// --- UI Rendering ---
|
|
function renderNodes() {
|
|
els.serverListContainer.innerHTML = '';
|
|
|
|
if (state.nodes.length === 0) {
|
|
els.serverListContainer.innerHTML = `
|
|
<div class="col-span-3 text-center py-6 text-[#00ff41]/30 text-xs uppercase">
|
|
No_Data // Awaiting_Sync
|
|
</div>`;
|
|
els.serverCount.textContent = '0 endpoints';
|
|
return;
|
|
}
|
|
|
|
els.serverCount.textContent = `${state.nodes.length} endpoints`;
|
|
|
|
state.nodes.forEach((node, index) => {
|
|
const isActive = state.activeNode && state.activeNode.tag === node.tag;
|
|
const card = document.createElement('div');
|
|
card.className = `server-card cursor-pointer p-2 bg-black border border-[#00ff41]/20 hover:border-[#00ff41]/50 ${isActive ? 'active' : ''}`;
|
|
card.onclick = () => handleConnect(node);
|
|
|
|
card.innerHTML = `
|
|
<div class="flex items-center gap-1.5 mb-1">
|
|
<div class="w-1.5 h-1.5 rounded-full ${isActive ? 'bg-[#00ff41] animate-pulse' : 'bg-[#00ff41]/30'}"></div>
|
|
<span class="text-[10px] font-bold text-white uppercase truncate">${node.tag}</span>
|
|
</div>
|
|
<div class="text-[9px] opacity-40 text-[#00ff41] truncate">${node.type}</div>
|
|
<div id="ping-${index}" class="text-[10px] font-mono text-[#00ff41]/70 mt-1">--</div>
|
|
`;
|
|
els.serverListContainer.appendChild(card);
|
|
});
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateMasterToggleUI() {
|
|
if (state.proxyEnabled) {
|
|
els.proxyModeLabel.textContent = 'VPN_MODE';
|
|
els.proxyModeLabel.classList.remove('text-yellow-500');
|
|
els.proxyModeLabel.classList.add('text-[#00ff41]');
|
|
els.proxyModeSubtitle.textContent = 'Traffic routed via proxy';
|
|
els.headerStatus.textContent = state.activeNode ? 'TUNNEL_UP' : 'STANDBY';
|
|
|
|
// Show VPN row, hide direct
|
|
els.chainVPNRow.classList.remove('hidden');
|
|
els.chainDirectRow.classList.add('hidden');
|
|
|
|
// Start uptime if connected
|
|
if (state.activeNode && !state.uptimeInterval) {
|
|
startUptimeTimer();
|
|
}
|
|
} else {
|
|
els.proxyModeLabel.textContent = 'DIRECT_MODE';
|
|
els.proxyModeLabel.classList.remove('text-[#00ff41]');
|
|
els.proxyModeLabel.classList.add('text-yellow-500');
|
|
els.proxyModeSubtitle.textContent = 'Bypass proxy — direct connection';
|
|
els.headerStatus.textContent = 'DIRECT';
|
|
|
|
// Hide VPN/Fallback, show direct
|
|
els.chainVPNRow.classList.add('hidden');
|
|
els.chainFallbackRow.classList.add('hidden');
|
|
els.chainDirectRow.classList.remove('hidden');
|
|
|
|
els.chainStatus.innerHTML = `<span class="text-yellow-400">●</span> Direct connection active`;
|
|
|
|
// Stop uptime timer
|
|
stopUptimeTimer();
|
|
}
|
|
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateTrafficUI(info) {
|
|
if (!info) return;
|
|
const used = formatBytes((info.download || 0) + (info.upload || 0));
|
|
const total = info.total ? formatBytes(info.total) : '∞';
|
|
els.trafficValue.textContent = `${used} / ${total}`;
|
|
}
|
|
|
|
async function checkServerLatencies(nodes) {
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
const pingEl = document.getElementById(`ping-${i}`);
|
|
if (pingEl) pingEl.textContent = '...';
|
|
|
|
try {
|
|
const res = await fetch('/ping-target', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ server: node.server, port: node.server_port || 443 })
|
|
});
|
|
const data = await res.json();
|
|
if (pingEl) {
|
|
if (data.latency && data.latency !== -1) {
|
|
pingEl.textContent = data.latency + 'ms';
|
|
if (data.latency > 300) pingEl.style.color = 'rgb(239, 68, 68)';
|
|
else if (data.latency < 100) pingEl.style.color = '#00ff41';
|
|
else pingEl.style.color = 'rgb(234, 179, 8)';
|
|
} else {
|
|
pingEl.textContent = 'Timeout';
|
|
pingEl.style.color = 'rgb(239, 68, 68)';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (pingEl) pingEl.textContent = 'Err';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkConnectionSpeed(fullTest = false) {
|
|
els.currentIpDisplay.textContent = '...';
|
|
|
|
try {
|
|
const res = await fetch(`/test-connection?speed=${fullTest}`);
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
els.currentIpDisplay.textContent = 'ERROR';
|
|
} else {
|
|
els.currentIpDisplay.textContent = data.ip || '---.---.---.---';
|
|
}
|
|
} catch (e) {
|
|
els.currentIpDisplay.textContent = 'NET_ERR';
|
|
}
|
|
}
|
|
|
|
// --- Master Proxy Toggle ---
|
|
els.masterProxyToggle.addEventListener('change', async () => {
|
|
state.proxyEnabled = els.masterProxyToggle.checked;
|
|
updateMasterToggleUI();
|
|
|
|
addLog(state.proxyEnabled ? 'PROXY_ENABLED' : 'PROXY_DISABLED_DIRECT_MODE', state.proxyEnabled ? 'success' : 'warning');
|
|
|
|
try {
|
|
const res = await fetch('/proxy-enabled', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled: state.proxyEnabled })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
addLog('CONFIG_APPLIED', 'success');
|
|
|
|
// Force fetch new status to get correct fallback/uptime state
|
|
setTimeout(async () => {
|
|
await fetchStatus();
|
|
checkConnectionSpeed(false);
|
|
}, 500);
|
|
}
|
|
} catch (e) {
|
|
addLog('TOGGLE_FAILED: ' + e.message, 'error');
|
|
}
|
|
|
|
updateProxyChain();
|
|
});
|
|
|
|
// --- Actions ---
|
|
async function handleFetchNodes() {
|
|
// Get the actual URL (from hidden field or input)
|
|
let url = els.subUrlFull.value || els.subUrlInput.value;
|
|
url = url.trim();
|
|
|
|
if (!url || url.endsWith('/...')) {
|
|
addLog('ERROR_MISSING_URL', 'error');
|
|
return;
|
|
}
|
|
|
|
state.subscriptionUrl = url;
|
|
els.subUrlFull.value = url;
|
|
|
|
state.isFetching = true;
|
|
els.fetchIcon.classList.add('hidden');
|
|
els.fetchText.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 animate-spin"></i>';
|
|
lucide.createIcons();
|
|
|
|
addLog(`FETCHING: ${maskUrl(url)}`, 'info');
|
|
|
|
try {
|
|
const res = await fetch('/fetch-subscription', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success && data.servers) {
|
|
state.nodes = data.servers;
|
|
state.config = data.config;
|
|
state.userInfo = data.userInfo;
|
|
|
|
renderNodes();
|
|
updateTrafficUI(state.userInfo);
|
|
updateUrlDisplay();
|
|
addLog(`SYNC_OK: ${state.nodes.length} endpoints`, 'success');
|
|
|
|
addLog('CHECKING_LATENCY...', 'info');
|
|
checkServerLatencies(state.nodes);
|
|
|
|
} else {
|
|
throw new Error(data.error || 'Unknown Error');
|
|
}
|
|
} catch (e) {
|
|
addLog(`SYNC_FAILED: ${e.message}`, 'error');
|
|
} finally {
|
|
state.isFetching = false;
|
|
els.fetchIcon.classList.remove('hidden');
|
|
els.fetchText.textContent = 'Sync';
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
async function handleConnect(node) {
|
|
if (!state.proxyEnabled) {
|
|
addLog('ENABLE_PROXY_FIRST', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (state.activeNode && state.activeNode.tag === node.tag && !state.isConnecting) {
|
|
return;
|
|
}
|
|
|
|
state.activeNode = node;
|
|
state.isConnecting = true;
|
|
renderNodes();
|
|
|
|
addLog(`CONNECTING: ${node.tag}`, 'warning');
|
|
|
|
try {
|
|
const res = await fetch('/apply-subscription', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
config: state.config,
|
|
selectedServer: node.tag,
|
|
subUrl: state.subscriptionUrl,
|
|
userInfo: state.userInfo
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
setTimeout(async () => {
|
|
state.isConnecting = false;
|
|
els.headerStatus.textContent = 'TUNNEL_UP';
|
|
renderNodes();
|
|
addLog(`CONNECTED: ${node.tag}`, 'success');
|
|
|
|
// Wait for next status update to sync start time properly
|
|
await fetchStatus();
|
|
|
|
// Fallback: start timer optimistically if status didn't catch it yet
|
|
if (!state.uptimeInterval) {
|
|
state.serverStartTime = Date.now() / 1000;
|
|
startUptimeTimer();
|
|
}
|
|
|
|
checkConnectionSpeed(false);
|
|
updateProxyChain();
|
|
|
|
}, 800);
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (e) {
|
|
state.isConnecting = false;
|
|
state.activeNode = null;
|
|
renderNodes();
|
|
addLog(`CONNECT_FAILED: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function fetchStatus() {
|
|
try {
|
|
const res = await fetch('/status');
|
|
const data = await res.json();
|
|
|
|
if (data.proxyPort) {
|
|
els.httpProxyUrl.value = `http://127.0.0.1:${data.proxyPort}`;
|
|
els.socks5ProxyUrl.value = `socks5://127.0.0.1:${data.proxyPort}`;
|
|
}
|
|
|
|
if (data.proxyEnabled !== undefined) {
|
|
state.proxyEnabled = data.proxyEnabled;
|
|
els.masterProxyToggle.checked = state.proxyEnabled;
|
|
updateMasterToggleUI();
|
|
}
|
|
|
|
if (data.startTime) {
|
|
state.serverStartTime = data.startTime;
|
|
}
|
|
|
|
if (data.active && data.tag && state.proxyEnabled) {
|
|
const currentTag = state.activeNode ? state.activeNode.tag : null;
|
|
|
|
if (currentTag !== data.tag) {
|
|
const fullNode = state.nodes.find(n => n.tag === data.tag);
|
|
|
|
if (fullNode) {
|
|
state.activeNode = fullNode;
|
|
} else {
|
|
state.activeNode = { tag: data.tag, server: data.server, port: '?', type: 'UNKNOWN' };
|
|
}
|
|
|
|
renderNodes();
|
|
checkConnectionSpeed(false);
|
|
|
|
// Restart timer with new server start time
|
|
startUptimeTimer();
|
|
}
|
|
|
|
// Ensure timer is running if active
|
|
if (!state.uptimeInterval) startUptimeTimer();
|
|
} else if (!data.active && state.proxyEnabled) {
|
|
// Proxy enabled but backend says not active/configured
|
|
state.activeNode = null;
|
|
renderNodes();
|
|
stopUptimeTimer();
|
|
}
|
|
|
|
// Always update chain visualization
|
|
updateProxyChain();
|
|
|
|
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function loadSaved() {
|
|
try {
|
|
addLog('SYSTEM_BOOT...', 'info');
|
|
const res = await fetch('/subscription');
|
|
const data = await res.json();
|
|
|
|
if (data.saved && data.url) {
|
|
state.subscriptionUrl = data.url;
|
|
els.subUrlFull.value = data.url;
|
|
els.subUrlInput.value = maskUrl(data.url);
|
|
state.userInfo = data.userInfo;
|
|
updateTrafficUI(state.userInfo);
|
|
|
|
await handleFetchNodes();
|
|
} else {
|
|
addLog('NO_SAVED_CONFIG', 'warning');
|
|
}
|
|
|
|
await fetchStatus();
|
|
|
|
} catch (e) {
|
|
addLog('BOOT_ERROR', 'error');
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
els.fetchServersBtn.addEventListener('click', handleFetchNodes);
|
|
|
|
// --- Fallback Proxy Functions ---
|
|
async function loadFallbackConfig() {
|
|
try {
|
|
const res = await fetch('/fallback-config');
|
|
const data = await res.json();
|
|
|
|
els.fallbackToggle.checked = data.enabled || false;
|
|
els.fallbackHost.value = data.host || '192.168.50.111';
|
|
els.fallbackPort.value = data.port || 8080;
|
|
|
|
updateFallbackUI(data.enabled || false);
|
|
} catch (e) {
|
|
addLog('FALLBACK_LOAD_FAILED', 'error');
|
|
}
|
|
}
|
|
|
|
function updateFallbackUI(enabled) {
|
|
els.fallbackToggleLabel.textContent = enabled ? 'ON' : 'OFF';
|
|
els.fallbackToggleLabel.classList.toggle('opacity-100', enabled);
|
|
els.fallbackToggleLabel.classList.toggle('opacity-50', !enabled);
|
|
}
|
|
|
|
async function saveFallbackConfig() {
|
|
const enabled = els.fallbackToggle.checked;
|
|
const host = els.fallbackHost.value.trim();
|
|
const port = parseInt(els.fallbackPort.value) || 8080;
|
|
|
|
if (enabled && !host) {
|
|
addLog('FALLBACK_HOST_REQUIRED', 'error');
|
|
showFallbackStatus('Host required', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/fallback-config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled, host, port })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
addLog(enabled ? 'FALLBACK_ON' : 'FALLBACK_OFF', 'success');
|
|
showFallbackStatus('Applied!', 'success');
|
|
updateFallbackUI(enabled);
|
|
updateProxyChain();
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (e) {
|
|
addLog(`FALLBACK_FAILED: ${e.message}`, 'error');
|
|
showFallbackStatus('Failed', 'error');
|
|
}
|
|
}
|
|
|
|
function showFallbackStatus(msg, type = 'info') {
|
|
els.fallbackStatus.textContent = msg;
|
|
els.fallbackStatus.classList.remove('hidden', 'text-[#00ff41]/50', 'text-red-500', 'text-blue-400');
|
|
|
|
if (type === 'success') els.fallbackStatus.classList.add('text-blue-400');
|
|
else if (type === 'error') els.fallbackStatus.classList.add('text-red-500');
|
|
else els.fallbackStatus.classList.add('text-[#00ff41]/50');
|
|
|
|
setTimeout(() => {
|
|
els.fallbackStatus.classList.add('hidden');
|
|
}, 3000);
|
|
}
|
|
|
|
// --- Proxy Chain Visualization ---
|
|
async function updateProxyChain() {
|
|
// Always update UI based on current state first for immediate feedback
|
|
if (!state.proxyEnabled) {
|
|
// Direct mode is handled by updateMasterToggleUI generally,
|
|
// but we ensure clean slate here too if needed
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// If we are in VPN mode but no node selected (Standby)
|
|
if (!state.activeNode) {
|
|
els.chainVPNLabel.textContent = 'VPN (Standby)';
|
|
els.chainVPNLatency.textContent = '--ms';
|
|
els.chainVPNBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainVPNBox.classList.add('border-[#00ff41]/30', 'border-dashed');
|
|
els.chainStatus.innerHTML = '<span class="text-orange-400">●</span> VPN Standby - Select Server';
|
|
// Show X to indicate no connection through VPN yet
|
|
els.chainVPNX.classList.remove('hidden');
|
|
els.chainVPNX.classList.add('flex');
|
|
} else {
|
|
els.chainVPNBox.classList.remove('border-dashed');
|
|
}
|
|
|
|
const res = await fetch('/active-proxy');
|
|
const data = await res.json();
|
|
|
|
|
|
// Reset all states
|
|
els.chainFallbackBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainFallbackBox.classList.add('border-[#00ff41]/30');
|
|
els.chainVPNBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainVPNBox.classList.add('border-[#00ff41]/30');
|
|
els.chainFallbackX.classList.add('hidden');
|
|
els.chainFallbackX.classList.remove('flex');
|
|
els.chainVPNX.classList.add('hidden');
|
|
els.chainVPNX.classList.remove('flex');
|
|
|
|
if (!data.configured) {
|
|
els.chainStatus.innerHTML = '<span class="text-orange-400">●</span> VPN Standby';
|
|
els.chainFallbackRow.classList.add('hidden');
|
|
els.chainVPNLabel.textContent = 'Select Server';
|
|
// Visual cue for disconnected VPN
|
|
els.chainVPNX.classList.remove('hidden');
|
|
els.chainVPNX.classList.add('flex');
|
|
return;
|
|
}
|
|
|
|
// Update VPN label
|
|
els.chainVPNLabel.textContent = data.vpnTag || 'VPN';
|
|
els.chainVPNLatency.textContent = data.vpnLatency ? `${data.vpnLatency}ms` : '--ms';
|
|
|
|
if (data.fallbackEnabled) {
|
|
els.chainFallbackRow.classList.remove('hidden');
|
|
els.chainFallbackLabel.textContent = data.fallbackHost || 'Fallback';
|
|
els.chainFallbackLatency.textContent = data.fallbackLatency ? `${data.fallbackLatency}ms` : '--ms';
|
|
|
|
if (data.fallbackReachable) {
|
|
els.chainFallbackBox.classList.remove('border-[#00ff41]/30');
|
|
els.chainFallbackBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainStatus.innerHTML = `<span class="text-[#00ff41]">●</span> Fallback active (${data.fallbackLatency}ms)`;
|
|
} else {
|
|
els.chainFallbackX.classList.remove('hidden');
|
|
els.chainFallbackX.classList.add('flex');
|
|
els.chainVPNBox.classList.remove('border-[#00ff41]/30');
|
|
els.chainVPNBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainStatus.innerHTML = `<span class="text-yellow-400">●</span> VPN active (fallback down)`;
|
|
}
|
|
} else {
|
|
els.chainFallbackRow.classList.add('hidden');
|
|
els.chainVPNBox.classList.remove('border-[#00ff41]/30');
|
|
els.chainVPNBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20');
|
|
els.chainStatus.innerHTML = `<span class="text-[#00ff41]">●</span> VPN direct`;
|
|
}
|
|
|
|
lucide.createIcons();
|
|
|
|
} catch (e) {
|
|
els.chainStatus.textContent = 'Failed to load';
|
|
}
|
|
}
|
|
|
|
// Fallback Event Listeners
|
|
els.saveFallbackBtn.addEventListener('click', saveFallbackConfig);
|
|
els.fallbackToggle.addEventListener('change', saveFallbackConfig);
|
|
|
|
// --- Init ---
|
|
addLog('TERMINAL_READY', 'info');
|
|
loadSaved();
|
|
loadFallbackConfig();
|
|
updateProxyChain();
|
|
|
|
// Periodically update proxy chain
|
|
setInterval(updateProxyChain, 10000);
|
|
|
|
// Blink animation style
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.blink-1 { animation: blink 1s infinite; }
|
|
.blink-2 { animation: blink 1s infinite 0.2s; }
|
|
.blink-3 { animation: blink 1s infinite 0.4s; }
|
|
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |