709 lines
31 KiB
HTML
709 lines
31 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);
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes scanline {
|
|
0% {
|
|
transform: translateY(-100%);
|
|
}
|
|
|
|
100% {
|
|
transform: translateY(100%);
|
|
}
|
|
}
|
|
|
|
.scanline {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(to bottom, transparent 50%, rgba(0, 255, 65, 0.02) 50%);
|
|
background-size: 100% 4px;
|
|
pointer-events: none;
|
|
z-index: 50;
|
|
}
|
|
|
|
.crt-flicker {
|
|
animation: flicker 0.15s infinite;
|
|
opacity: 0.1;
|
|
position: fixed;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
background: radial-gradient(circle at center, transparent 80%, black 100%);
|
|
z-index: 40;
|
|
}
|
|
|
|
.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%;
|
|
}
|
|
|
|
/* Utilities */
|
|
.frame-corner {
|
|
position: absolute;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-color: var(--color-neon);
|
|
border-style: solid;
|
|
}
|
|
</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-4 flex justify-between items-center">
|
|
<div class="flex items-center gap-4">
|
|
<div class="relative p-1 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-lg font-black tracking-[0.2em] uppercase text-[#00ff41]">
|
|
VPN<span class="text-white">_</span>CLIENT
|
|
</h1>
|
|
<p class="text-[9px] opacity-40 uppercase tracking-widest text-[#00ff41]">Secure Shell v4.2</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hidden md:flex items-center gap-6 text-[10px] 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">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">-- / --</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main
|
|
class="flex-grow max-w-[1400px] w-full mx-auto px-4 md:px-6 py-6 grid grid-cols-1 lg:grid-cols-12 gap-6 relative z-10">
|
|
|
|
<!-- Left Column -->
|
|
<div class="lg:col-span-8 flex flex-col gap-6">
|
|
|
|
<!-- Subscription Input -->
|
|
<div class="flex flex-col gap-2 p-4 bg-[#0a0a0a] border border-[#00ff41]/30 relative group">
|
|
<!-- Frame Corners -->
|
|
<div class="absolute top-0 left-0 w-2 h-2 border-t border-l border-[#00ff41]"></div>
|
|
<div class="absolute top-0 right-0 w-2 h-2 border-t border-r border-[#00ff41]"></div>
|
|
<div class="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-[#00ff41]"></div>
|
|
<div class="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-[#00ff41]"></div>
|
|
|
|
<label
|
|
class="text-[10px] uppercase tracking-widest opacity-50 text-[#00ff41]">Subscription_URL_Input</label>
|
|
<div class="flex flex-col md:flex-row gap-3">
|
|
<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>
|
|
<input type="text" id="subUrlInput" placeholder="https://provider.com/api/v1/config?key=..."
|
|
class="w-full bg-black border border-[#00ff41]/20 py-2.5 pl-10 pr-4 text-xs tracking-wider focus:outline-none focus:border-[#00ff41] transition-all placeholder:text-[#00ff41]/20 text-[#00ff41]" />
|
|
</div>
|
|
<button id="fetchServersBtn"
|
|
class="flex items-center justify-center gap-2 bg-[#00ff41] text-black px-6 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 -->
|
|
<div
|
|
class="flex-grow bg-[#0a0a0a]/50 border border-[#00ff41]/10 overflow-hidden flex flex-col relative h-[500px] lg:h-auto">
|
|
<div class="px-5 py-3 border-b border-[#00ff41]/10 bg-black flex justify-between items-center shrink-0">
|
|
<span
|
|
class="text-[10px] uppercase tracking-[0.2em] font-bold text-[#00ff41]">Available_Endpoints</span>
|
|
<span class="text-[9px] opacity-40 italic text-[#00ff41]">Click row to initiate link</span>
|
|
</div>
|
|
|
|
<div class="overflow-y-auto custom-scrollbar flex-grow p-0">
|
|
<table class="w-full text-left border-collapse">
|
|
<tbody id="serverListBody" class="divide-y divide-[#00ff41]/5">
|
|
<!-- Populated by JS -->
|
|
<tr class="text-[#00ff41]/30">
|
|
<td colspan="4" class="p-8 text-center text-xs uppercase tracking-widest">
|
|
No_Data_Stream // Awaiting_Sync
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="lg:col-span-4 flex flex-col gap-6">
|
|
|
|
<!-- Tunnel Intelligence (Status) -->
|
|
<div id="statusBox"
|
|
class="border-2 border-[#00ff41]/10 bg-black/40 p-6 relative transition-all duration-500 min-h-[200px]">
|
|
<div class="absolute top-2 right-2 flex gap-1">
|
|
<div class="w-1 h-3 border border-[#00ff41]/30 bg-[#00ff41]/10 blink-1"></div>
|
|
<div class="w-1 h-3 border border-[#00ff41]/30 bg-[#00ff41]/10 blink-2"></div>
|
|
<div class="w-1 h-3 border border-[#00ff41]/30 bg-[#00ff41]/10 blink-3"></div>
|
|
</div>
|
|
|
|
<p class="text-[9px] uppercase tracking-widest mb-4 text-[#00ff41]">Tunnel_Intelligence</p>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div id="statusIconContainer"
|
|
class="w-14 h-14 flex items-center justify-center border border-[#00ff41]/20 rounded-sm">
|
|
<i data-lucide="shield-alert" class="w-6 h-6 text-[#00ff41]/50"></i>
|
|
</div>
|
|
<div>
|
|
<div id="statusTitle" class="text-2xl font-black italic tracking-tighter text-[#00ff41]/50">
|
|
NO_SIGNAL
|
|
</div>
|
|
<div id="statusSubtitle"
|
|
class="text-[10px] opacity-50 uppercase tracking-widest text-[#00ff41]">
|
|
Link not initialized
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="connectionDetails"
|
|
class="grid grid-cols-2 gap-2 opacity-0 transition-opacity duration-300">
|
|
<div class="p-2 border border-[#00ff41]/20 bg-black">
|
|
<div class="text-[8px] opacity-40 uppercase flex items-center gap-1 text-[#00ff41]">
|
|
<i data-lucide="zap" class="w-2 h-2"></i> Speed
|
|
<button id="testSpeedBtn" class="ml-auto hover:text-white transition-colors"
|
|
title="Run Speed Test">
|
|
<i data-lucide="play" class="w-3 h-3"></i>
|
|
</button>
|
|
</div>
|
|
<div id="speedValue" class="text-xs font-bold tracking-widest text-[#00ff41]">-- Mb/s</div>
|
|
</div>
|
|
<div class="p-2 border border-[#00ff41]/20 bg-black">
|
|
<div class="text-[8px] opacity-40 uppercase flex items-center gap-1 text-[#00ff41]">
|
|
<i data-lucide="globe" class="w-2 h-2"></i> IP_ADDR
|
|
</div>
|
|
<div id="ipValue" class="text-xs font-bold tracking-widest text-[#00ff41]">HIDDEN</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Terminal Logs -->
|
|
<div
|
|
class="flex-grow flex flex-col bg-black border border-[#00ff41]/20 overflow-hidden font-mono h-[300px] lg:h-[400px]">
|
|
<div
|
|
class="bg-[#111] px-4 py-2 border-b border-[#00ff41]/10 flex justify-between items-center shrink-0">
|
|
<span
|
|
class="text-[9px] uppercase font-bold tracking-[0.3em] flex items-center gap-2 text-[#00ff41]">
|
|
<div class="w-1 h-1 bg-[#00ff41] rounded-full animate-ping"></div> Connection_Logs
|
|
</span>
|
|
<button id="clearLogs"
|
|
class="text-[8px] opacity-30 hover:opacity-100 hover:text-[#00ff41] transition-opacity uppercase">Clear</button>
|
|
</div>
|
|
|
|
<div id="logsContainer"
|
|
class="flex-grow p-4 overflow-y-auto custom-scrollbar text-[10px] space-y-1.5 opacity-80 font-mono">
|
|
<!-- Logs populated by JS -->
|
|
<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-[9px] uppercase tracking-[0.2em] opacity-40 text-[#00ff41]">
|
|
<div class="flex gap-6">
|
|
<span>Core: 4.1.0-Release</span>
|
|
<span>Proxy: HTTP/8082</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
|
|
};
|
|
|
|
// --- DOM Elements ---
|
|
const els = {
|
|
subUrlInput: document.getElementById('subUrlInput'),
|
|
fetchServersBtn: document.getElementById('fetchServersBtn'),
|
|
fetchIcon: document.getElementById('fetchIcon'),
|
|
fetchText: document.getElementById('fetchText'),
|
|
serverListBody: document.getElementById('serverListBody'),
|
|
statusBox: document.getElementById('statusBox'),
|
|
statusIconContainer: document.getElementById('statusIconContainer'),
|
|
statusTitle: document.getElementById('statusTitle'),
|
|
statusSubtitle: document.getElementById('statusSubtitle'),
|
|
connectionDetails: document.getElementById('connectionDetails'),
|
|
speedValue: document.getElementById('speedValue'),
|
|
ipValue: document.getElementById('ipValue'),
|
|
logsContainer: document.getElementById('logsContainer'),
|
|
clearLogs: document.getElementById('clearLogs'),
|
|
headerStatus: document.getElementById('headerStatus'),
|
|
sessionId: document.getElementById('sessionId'),
|
|
trafficValue: document.getElementById('trafficValue'),
|
|
testSpeedBtn: document.getElementById('testSpeedBtn')
|
|
};
|
|
|
|
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]}`;
|
|
}
|
|
|
|
// --- 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);
|
|
}
|
|
}
|
|
|
|
els.clearLogs.addEventListener('click', () => {
|
|
const lastChild = els.logsContainer.lastElementChild;
|
|
els.logsContainer.innerHTML = '';
|
|
els.logsContainer.appendChild(lastChild);
|
|
addLog('LOGS_CLEARED_BY_USER', 'info');
|
|
});
|
|
|
|
|
|
// --- UI Rendering ---
|
|
function renderNodes() {
|
|
els.serverListBody.innerHTML = '';
|
|
if (state.nodes.length === 0) {
|
|
els.serverListBody.innerHTML = `
|
|
<tr class="text-[#00ff41]/30">
|
|
<td colspan="4" class="p-8 text-center text-xs uppercase tracking-widest">
|
|
No_Nodes_Found // Sync_Required
|
|
</td>
|
|
</tr>`;
|
|
return;
|
|
}
|
|
|
|
state.nodes.forEach((node, index) => {
|
|
const isActive = state.activeNode && state.activeNode.tag === node.tag;
|
|
const tr = document.createElement('tr');
|
|
tr.className = `group cursor-pointer transition-all border-b border-[#00ff41]/5 hover:bg-[#00ff41]/5 ${isActive ? 'bg-[#00ff41]/10' : ''}`;
|
|
tr.onclick = () => handleConnect(node);
|
|
|
|
tr.innerHTML = `
|
|
<td class="p-4 w-12">
|
|
<div class="w-2 h-2 rounded-full bg-[#00ff41] ${isActive ? 'animate-ping' : ''}"></div>
|
|
</td>
|
|
<td class="p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex flex-col">
|
|
<span class="text-xs font-bold text-white uppercase tracking-wider">${node.tag}</span>
|
|
<span class="text-[9px] opacity-40 uppercase text-[#00ff41]">${node.type} // ${node.server}:${node.port}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="p-4">
|
|
<div class="flex flex-col items-start">
|
|
<span class="text-[9px] opacity-40 uppercase text-[#00ff41]/50">Ping</span>
|
|
<span id="ping-${index}" class="text-xs font-mono text-[#00ff41]">--</span>
|
|
</div>
|
|
</td>
|
|
<td class="p-4 text-right">
|
|
${isActive
|
|
? '<span class="text-[10px] font-black animate-pulse text-white">[ CONNECTED ]</span>'
|
|
: '<i data-lucide="chevron-right" class="w-4 h-4 text-[#00ff41] opacity-10 group-hover:opacity-100 group-hover:translate-x-1 transition-all inline-block"></i>'
|
|
}
|
|
</td>
|
|
`;
|
|
els.serverListBody.appendChild(tr);
|
|
});
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateStatusUI() {
|
|
if (state.activeNode) {
|
|
// Active Styling
|
|
els.statusBox.classList.remove('border-[#00ff41]/10', 'bg-black/40');
|
|
els.statusBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/5', 'shadow-[inset_0_0_20px_rgba(0,255,65,0.05)]');
|
|
|
|
// Icon
|
|
if (state.isConnecting) {
|
|
els.statusIconContainer.innerHTML = '<i data-lucide="refresh-cw" class="w-6 h-6 text-[#00ff41] animate-spin"></i>';
|
|
els.statusTitle.textContent = 'HANDSHAKE';
|
|
els.statusSubtitle.textContent = 'Negotiating keys...';
|
|
els.headerStatus.textContent = 'NEGOTIATING';
|
|
} else {
|
|
els.statusIconContainer.innerHTML = '<i data-lucide="shield-check" class="w-6 h-6 text-[#00ff41]"></i>';
|
|
els.statusIconContainer.classList.add('shadow-[0_0_15px_#00ff4166]', 'border-[#00ff41]');
|
|
els.statusTitle.textContent = 'ENCRYPTED';
|
|
els.statusTitle.classList.remove('text-[#00ff41]/50');
|
|
els.statusTitle.classList.add('text-[#00ff41]');
|
|
els.statusSubtitle.textContent = `${state.activeNode.tag} // SECURE`;
|
|
els.connectionDetails.classList.remove('opacity-0');
|
|
els.headerStatus.textContent = 'TUNNEL_UP';
|
|
els.headerStatus.classList.add('text-[#00ff41]');
|
|
}
|
|
} else {
|
|
// Inactive Styling
|
|
els.statusBox.classList.add('border-[#00ff41]/10', 'bg-black/40');
|
|
els.statusBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/5', 'shadow-[inset_0_0_20px_rgba(0,255,65,0.05)]');
|
|
|
|
els.statusIconContainer.innerHTML = '<i data-lucide="shield-alert" class="w-6 h-6 text-[#00ff41]/50"></i>';
|
|
els.statusIconContainer.classList.remove('shadow-[0_0_15px_#00ff4166]', 'border-[#00ff41]');
|
|
els.statusTitle.textContent = 'NO_SIGNAL';
|
|
els.statusTitle.classList.add('text-[#00ff41]/50');
|
|
els.statusTitle.classList.remove('text-[#00ff41]');
|
|
els.statusSubtitle.textContent = 'Link not initialized';
|
|
els.connectionDetails.classList.add('opacity-0');
|
|
els.headerStatus.textContent = 'STANDBY';
|
|
els.headerStatus.classList.remove('text-[#00ff41]');
|
|
|
|
// Reset stats
|
|
els.speedValue.textContent = '-- Mb/s';
|
|
els.ipValue.textContent = 'HIDDEN';
|
|
}
|
|
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)'; // red
|
|
else if (data.latency < 100) pingEl.style.color = '#00ff41'; // green
|
|
else pingEl.style.color = 'rgb(234, 179, 8)'; // yellow
|
|
} else {
|
|
pingEl.textContent = 'Timeout';
|
|
pingEl.style.color = 'rgb(239, 68, 68)';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (pingEl) pingEl.textContent = 'Err';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkConnectionSpeed(fullTest = false) {
|
|
if (fullTest) els.speedValue.textContent = 'TESTING...';
|
|
else els.speedValue.textContent = 'PAUSED';
|
|
|
|
els.ipValue.textContent = '...';
|
|
|
|
try {
|
|
const res = await fetch(`/test-connection?speed=${fullTest}`);
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
els.ipValue.textContent = 'ERROR';
|
|
if (fullTest) els.speedValue.textContent = 'ERROR';
|
|
} else {
|
|
els.ipValue.textContent = data.ip;
|
|
if (data.speed) {
|
|
els.speedValue.textContent = data.speed;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (fullTest) els.speedValue.textContent = 'NET_ERR';
|
|
}
|
|
}
|
|
|
|
|
|
// --- Actions ---
|
|
|
|
async function handleFetchNodes() {
|
|
const url = els.subUrlInput.value.trim();
|
|
if (!url) {
|
|
addLog('ERROR_MISSING_URL_INPUT', 'error');
|
|
return;
|
|
}
|
|
|
|
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_CONFIG: ${url.substring(0, 25)}...`, '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.subscriptionUrl = url;
|
|
state.userInfo = data.userInfo;
|
|
|
|
renderNodes();
|
|
updateTrafficUI(state.userInfo);
|
|
addLog(`SYNC_COMPLETE: ${state.nodes.length} Endpoints Retrieved`, 'success');
|
|
|
|
// Trigger Ping Check
|
|
addLog('INITIATING_LATENCY_CHECK...', '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.activeNode && state.activeNode.tag === node.tag && !state.isConnecting) {
|
|
return;
|
|
}
|
|
|
|
state.activeNode = node;
|
|
state.isConnecting = true;
|
|
updateStatusUI();
|
|
renderNodes(); // to update active styling
|
|
|
|
addLog(`INITIATING_HANDSHAKE: ${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 // save user info if needed
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
setTimeout(() => {
|
|
state.isConnecting = false;
|
|
updateStatusUI();
|
|
addLog(`ENCRYPTED_SESSION_ESTABLISHED_${node.tag.toUpperCase()}`, 'success');
|
|
|
|
// Check IP but skip Speed Test
|
|
addLog('VERIFYING_TUNNEL_IP...', 'info');
|
|
checkConnectionSpeed(false);
|
|
|
|
}, 1000);
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (e) {
|
|
state.isConnecting = false;
|
|
state.activeNode = null;
|
|
updateStatusUI();
|
|
renderNodes();
|
|
addLog(`HANDSHAKE_FAILED: ${e.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function fetchStatus() {
|
|
try {
|
|
const res = await fetch('/status');
|
|
const data = await res.json();
|
|
|
|
if (data.active && data.tag) {
|
|
const currentTag = state.activeNode ? state.activeNode.tag : null;
|
|
|
|
if (currentTag !== data.tag) {
|
|
// Find full node info if available
|
|
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' };
|
|
}
|
|
|
|
updateStatusUI();
|
|
renderNodes(); // Update list highlighting
|
|
|
|
if (els.speedValue.textContent === '-- Mb/s' || els.ipValue.textContent === 'HIDDEN') {
|
|
checkConnectionSpeed(false);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function loadSaved() {
|
|
try {
|
|
addLog('SYSTEM_BOOT_SEQUENCE_INITIATED...', 'info');
|
|
const res = await fetch('/subscription');
|
|
const data = await res.json();
|
|
|
|
if (data.saved && data.url) {
|
|
els.subUrlInput.value = data.url;
|
|
state.userInfo = data.userInfo;
|
|
updateTrafficUI(state.userInfo);
|
|
|
|
// Auto-sync
|
|
await handleFetchNodes();
|
|
|
|
if (data.selectedServer) {
|
|
const savedNode = state.nodes.find(n => n.tag === data.selectedServer);
|
|
if (savedNode) {
|
|
// Will be handled by fetchStatus
|
|
}
|
|
}
|
|
} else {
|
|
addLog('NO_SAVED_CONFIG_FOUND', 'warning');
|
|
}
|
|
|
|
await fetchStatus();
|
|
|
|
} catch (e) {
|
|
addLog('SYSTEM_BOOT_ERROR', 'error');
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
els.fetchServersBtn.addEventListener('click', handleFetchNodes);
|
|
|
|
els.testSpeedBtn.addEventListener('click', () => {
|
|
addLog('MANUAL_SPEED_TEST_INITIATED', 'info');
|
|
checkConnectionSpeed(true);
|
|
});
|
|
|
|
// --- Init ---
|
|
addLog('TERMINAL_READY', 'info');
|
|
loadSaved();
|
|
|
|
// 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> |