+ class="max-w-[1400px] mx-auto px-6 flex justify-between items-center text-[10px] uppercase tracking-[0.2em] opacity-40 text-[#00ff41]">
Core: 4.1.0-Release
Proxy: HTTP/8080
@@ -467,29 +479,34 @@
isConnecting: false,
subscriptionUrl: '',
sessionId: Math.random().toString(16).substr(2, 8).toUpperCase(),
- userInfo: null
+ 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'),
- 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'),
+ 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'),
- testSpeedBtn: document.getElementById('testSpeedBtn'),
+ // 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'),
@@ -501,14 +518,20 @@
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')
+ socks5ProxyUrl: document.getElementById('socks5ProxyUrl'),
+ // Uptime
+ uptimeDisplay: document.getElementById('uptimeDisplay')
};
els.sessionId.textContent = state.sessionId;
@@ -523,6 +546,89 @@
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 });
@@ -569,101 +675,77 @@
const lastChild = els.logsContainer.lastElementChild;
els.logsContainer.innerHTML = '';
els.logsContainer.appendChild(lastChild);
- addLog('LOGS_CLEARED_BY_USER', 'info');
+ addLog('LOGS_CLEARED', 'info');
});
-
// --- UI Rendering ---
function renderNodes() {
- els.serverListBody.innerHTML = '';
+ els.serverListContainer.innerHTML = '';
+
if (state.nodes.length === 0) {
- els.serverListBody.innerHTML = `
-
- |
- No_Nodes_Found // Sync_Required
- |
-
`;
+ els.serverListContainer.innerHTML = `
+
+ No_Data // Awaiting_Sync
+
`;
+ 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 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);
+ 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);
- tr.innerHTML = `
-
-
- |
-
-
-
- ${node.tag}
- ${node.type} // ${node.server}:${node.port}
-
-
- |
-
-
- Ping
- --
-
- |
-
- ${isActive
- ? '[ CONNECTED ]'
- : ''
- }
- |
+ card.innerHTML = `
+
+
${node.type}
+
--
`;
- els.serverListBody.appendChild(tr);
+ els.serverListContainer.appendChild(card);
});
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)]');
+ 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';
- // Icon
- if (state.isConnecting) {
- els.statusIconContainer.innerHTML = '
';
- els.statusTitle.textContent = 'HANDSHAKE';
- els.statusSubtitle.textContent = 'Negotiating keys...';
- els.headerStatus.textContent = 'NEGOTIATING';
- } else {
- els.statusIconContainer.innerHTML = '
';
- 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]');
+ // 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 {
- // 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.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';
- els.statusIconContainer.innerHTML = '
';
- 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]');
+ // Hide VPN/Fallback, show direct
+ els.chainVPNRow.classList.add('hidden');
+ els.chainFallbackRow.classList.add('hidden');
+ els.chainDirectRow.classList.remove('hidden');
- // Reset stats
- els.speedValue.textContent = '-- Mb/s';
- els.ipValue.textContent = 'HIDDEN';
+ els.chainStatus.innerHTML = `
● Direct connection active`;
+
+ // Stop uptime timer
+ stopUptimeTimer();
}
+
lucide.createIcons();
}
@@ -689,9 +771,9 @@
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
+ 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)';
@@ -704,45 +786,72 @@
}
async function checkConnectionSpeed(fullTest = false) {
- if (fullTest) els.speedValue.textContent = 'TESTING...';
- else els.speedValue.textContent = 'PAUSED';
-
- els.ipValue.textContent = '...';
+ els.currentIpDisplay.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';
+ els.currentIpDisplay.textContent = 'ERROR';
} else {
- els.ipValue.textContent = data.ip;
- if (data.speed) {
- els.speedValue.textContent = data.speed;
- }
+ els.currentIpDisplay.textContent = data.ip || '---.---.---.---';
}
} catch (e) {
- if (fullTest) els.speedValue.textContent = 'NET_ERR';
+ 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() {
- const url = els.subUrlInput.value.trim();
- if (!url) {
- addLog('ERROR_MISSING_URL_INPUT', 'error');
+ // 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 = '
';
lucide.createIcons();
- addLog(`FETCHING_CONFIG: ${url.substring(0, 25)}...`, 'info');
+ addLog(`FETCHING: ${maskUrl(url)}`, 'info');
try {
const res = await fetch('/fetch-subscription', {
@@ -755,15 +864,14 @@
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');
+ updateUrlDisplay();
+ addLog(`SYNC_OK: ${state.nodes.length} endpoints`, 'success');
- // Trigger Ping Check
- addLog('INITIATING_LATENCY_CHECK...', 'info');
+ addLog('CHECKING_LATENCY...', 'info');
checkServerLatencies(state.nodes);
} else {
@@ -780,16 +888,20 @@
}
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;
- updateStatusUI();
- renderNodes(); // to update active styling
+ renderNodes();
- addLog(`INITIATING_HANDSHAKE: ${node.tag}`, 'warning');
+ addLog(`CONNECTING: ${node.tag}`, 'warning');
try {
const res = await fetch('/apply-subscription', {
@@ -799,31 +911,39 @@
config: state.config,
selectedServer: node.tag,
subUrl: state.subscriptionUrl,
- userInfo: state.userInfo // save user info if needed
+ userInfo: state.userInfo
})
});
const data = await res.json();
if (data.success) {
- setTimeout(() => {
+ setTimeout(async () => {
state.isConnecting = false;
- updateStatusUI();
- addLog(`ENCRYPTED_SESSION_ESTABLISHED_${node.tag.toUpperCase()}`, 'success');
+ 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();
+ }
- // Check IP but skip Speed Test
- addLog('VERIFYING_TUNNEL_IP...', 'info');
checkConnectionSpeed(false);
+ updateProxyChain();
- }, 1000);
+ }, 800);
} else {
throw new Error(data.error);
}
} catch (e) {
state.isConnecting = false;
state.activeNode = null;
- updateStatusUI();
renderNodes();
- addLog(`HANDSHAKE_FAILED: ${e.message}`, 'error');
+ addLog(`CONNECT_FAILED: ${e.message}`, 'error');
}
}
@@ -832,17 +952,25 @@
const res = await fetch('/status');
const data = await res.json();
- // Update proxy URLs with actual port
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.active && data.tag) {
+ 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) {
- // Find full node info if available
const fullNode = state.nodes.find(n => n.tag === data.tag);
if (fullNode) {
@@ -851,14 +979,26 @@
state.activeNode = { tag: data.tag, server: data.server, port: '?', type: 'UNKNOWN' };
}
- updateStatusUI();
- renderNodes(); // Update list highlighting
+ renderNodes();
+ checkConnectionSpeed(false);
- if (els.speedValue.textContent === '-- Mb/s' || els.ipValue.textContent === 'HIDDEN') {
- 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
}
@@ -866,45 +1006,33 @@
async function loadSaved() {
try {
- addLog('SYSTEM_BOOT_SEQUENCE_INITIATED...', 'info');
+ addLog('SYSTEM_BOOT...', 'info');
const res = await fetch('/subscription');
const data = await res.json();
if (data.saved && data.url) {
- els.subUrlInput.value = data.url;
+ state.subscriptionUrl = data.url;
+ els.subUrlFull.value = data.url;
+ els.subUrlInput.value = maskUrl(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');
+ addLog('NO_SAVED_CONFIG', 'warning');
}
await fetchStatus();
} catch (e) {
- addLog('SYSTEM_BOOT_ERROR', 'error');
+ addLog('BOOT_ERROR', 'error');
}
}
// --- Event Listeners ---
els.fetchServersBtn.addEventListener('click', handleFetchNodes);
- els.testSpeedBtn.addEventListener('click', () => {
- addLog('MANUAL_SPEED_TEST_INITIATED', 'info');
- checkConnectionSpeed(true);
- });
-
// --- Fallback Proxy Functions ---
-
async function loadFallbackConfig() {
try {
const res = await fetch('/fallback-config');
@@ -915,9 +1043,8 @@
els.fallbackPort.value = data.port || 8080;
updateFallbackUI(data.enabled || false);
- addLog('FALLBACK_CONFIG_LOADED', 'info');
} catch (e) {
- addLog('FALLBACK_CONFIG_LOAD_FAILED', 'error');
+ addLog('FALLBACK_LOAD_FAILED', 'error');
}
}
@@ -934,7 +1061,7 @@
if (enabled && !host) {
addLog('FALLBACK_HOST_REQUIRED', 'error');
- showFallbackStatus('Host is required', 'error');
+ showFallbackStatus('Host required', 'error');
return;
}
@@ -947,18 +1074,16 @@
const data = await res.json();
if (data.success) {
- addLog(enabled ? 'FALLBACK_ENABLED' : 'FALLBACK_DISABLED', 'success');
- const msg = data.regenerated ? 'Applied!' : 'Saved!';
- showFallbackStatus(msg, 'success');
+ addLog(enabled ? 'FALLBACK_ON' : 'FALLBACK_OFF', 'success');
+ showFallbackStatus('Applied!', 'success');
updateFallbackUI(enabled);
- // Refresh proxy chain visualization
updateProxyChain();
} else {
throw new Error(data.error);
}
} catch (e) {
- addLog(`FALLBACK_SAVE_FAILED: ${e.message}`, 'error');
- showFallbackStatus('Save failed', 'error');
+ addLog(`FALLBACK_FAILED: ${e.message}`, 'error');
+ showFallbackStatus('Failed', 'error');
}
}
@@ -977,10 +1102,32 @@
// --- 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 = '
● 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');
@@ -992,27 +1139,29 @@
els.chainVPNX.classList.remove('flex');
if (!data.configured) {
- els.chainStatus.textContent = 'No proxy configured';
+ els.chainStatus.innerHTML = '
● VPN Standby';
els.chainFallbackRow.classList.add('hidden');
- els.chainVPNLabel.textContent = 'VPN';
+ 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) {
- // Show fallback branch
els.chainFallbackRow.classList.remove('hidden');
els.chainFallbackLabel.textContent = data.fallbackHost || 'Fallback';
+ els.chainFallbackLatency.textContent = data.fallbackLatency ? `${data.fallbackLatency}ms` : '--ms';
if (data.fallbackReachable) {
- // Fallback is active (green border, no X)
els.chainFallbackBox.classList.remove('border-[#00ff41]/30');
els.chainFallbackBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20');
els.chainStatus.innerHTML = `
● Fallback active (${data.fallbackLatency}ms)`;
} else {
- // Fallback unreachable - show X, VPN is active
els.chainFallbackX.classList.remove('hidden');
els.chainFallbackX.classList.add('flex');
els.chainVPNBox.classList.remove('border-[#00ff41]/30');
@@ -1020,14 +1169,12 @@
els.chainStatus.innerHTML = `
● VPN active (fallback down)`;
}
} else {
- // No fallback - hide fallback row, VPN is active
els.chainFallbackRow.classList.add('hidden');
els.chainVPNBox.classList.remove('border-[#00ff41]/30');
els.chainVPNBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20');
els.chainStatus.innerHTML = `
● VPN direct`;
}
- // Reinitialize lucide icons for new X elements
lucide.createIcons();
} catch (e) {
@@ -1037,7 +1184,6 @@
// Fallback Event Listeners
els.saveFallbackBtn.addEventListener('click', saveFallbackConfig);
-
els.fallbackToggle.addEventListener('change', saveFallbackConfig);
// --- Init ---
diff --git a/web/server.py b/web/server.py
index 560e3bc..e385991 100644
--- a/web/server.py
+++ b/web/server.py
@@ -14,6 +14,8 @@ import urllib.error
import uuid
import socket
import time
+import time
+from datetime import datetime, timezone
from urllib.parse import parse_qs, unquote
from pathlib import Path
@@ -27,7 +29,10 @@ DATA_DIR = BASE_DIR / "data"
CONFIG_FILE = DATA_DIR / "client.json"
HWID_FILE = DATA_DIR / "hwid"
SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
+SUBSCRIPTION_FILE = DATA_DIR / "subscription.json"
FALLBACK_FILE = DATA_DIR / "fallback.json"
+PROXY_ENABLED_FILE = DATA_DIR / "proxy_enabled.json"
+START_TIME_FILE = DATA_DIR / "start_time.json"
# Default fallback proxy settings
DEFAULT_FALLBACK = {
@@ -103,6 +108,40 @@ def load_fallback_config() -> dict:
return DEFAULT_FALLBACK.copy()
+def save_proxy_enabled(enabled: bool):
+ """Save proxy enabled state to file"""
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
+ PROXY_ENABLED_FILE.write_text(json.dumps({"enabled": enabled}))
+
+
+def load_proxy_enabled() -> bool:
+ """Load proxy enabled state from file"""
+ if PROXY_ENABLED_FILE.exists():
+ try:
+ data = json.loads(PROXY_ENABLED_FILE.read_text())
+ return data.get("enabled", True)
+ except json.JSONDecodeError:
+ pass
+ return True # Default: proxy enabled
+
+
+def save_start_time(start_time: float):
+ """Save VPN start time to file"""
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
+ START_TIME_FILE.write_text(json.dumps({"startTime": start_time}))
+
+
+def load_start_time() -> float:
+ """Load VPN start time from file"""
+ if START_TIME_FILE.exists():
+ try:
+ data = json.loads(START_TIME_FILE.read_text())
+ return data.get("startTime", 0.0)
+ except json.JSONDecodeError:
+ pass
+ return 0.0
+
+
def measure_tcp_latency(host: str, port: int, timeout: float = 2.0) -> int:
"""Measure TCP latency to a host:port in milliseconds"""
start_time = time.time()
@@ -372,6 +411,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
self.get_fallback_config()
elif self.path == "/active-proxy":
self.get_active_proxy()
+ elif self.path == "/proxy-enabled":
+ self.get_proxy_enabled()
else:
self.send_error(404)
@@ -387,6 +428,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
self.ping_target()
elif self.path == "/fallback-config":
self.save_fallback_config_endpoint()
+ elif self.path == "/proxy-enabled":
+ self.set_proxy_enabled()
else:
self.send_error(404)
@@ -584,6 +627,7 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
config_exists = CONFIG_FILE.exists()
current_tag = None
current_server = None
+ proxy_enabled = load_proxy_enabled()
if config_exists:
try:
@@ -597,12 +641,102 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
pass
self.send_json({
- "active": config_exists,
+ "active": config_exists and proxy_enabled,
"tag": current_tag,
"server": current_server,
- "proxyPort": PROXY_PORT
+ "proxyPort": PROXY_PORT,
+ "proxyEnabled": proxy_enabled,
+ "startTime": load_start_time() if config_exists and proxy_enabled else 0
})
+ def get_proxy_enabled(self):
+ """Get proxy enabled state"""
+ enabled = load_proxy_enabled()
+ self.send_json({"enabled": enabled})
+
+ def set_proxy_enabled(self):
+ """Set proxy enabled state and regenerate config"""
+ try:
+ content_length = int(self.headers.get("Content-Length", 0))
+ body = self.rfile.read(content_length).decode("utf-8")
+ data = json.loads(body)
+
+ enabled = data.get("enabled", True)
+ save_proxy_enabled(enabled)
+
+ # Regenerate config based on state
+ if enabled:
+ # Restore normal VPN config
+ regenerated = self.regenerate_current_config()
+ if regenerated:
+ # Only update start time if actually enabling VPN
+ save_start_time(datetime.now(timezone.utc).timestamp())
+ else:
+ # Generate direct config (bypass proxy)
+ regenerated = self.generate_direct_config()
+ save_start_time(0)
+
+ self.send_json({
+ "success": True,
+ "enabled": enabled,
+ "regenerated": regenerated
+ })
+
+ except json.JSONDecodeError:
+ self.send_json({"success": False, "error": "Invalid JSON"}, 400)
+ except Exception as e:
+ self.send_json({"success": False, "error": str(e)}, 500)
+
+ def generate_direct_config(self) -> bool:
+ """Generate a direct connection config (bypass all proxies)"""
+ try:
+ config = {
+ "dns": {
+ "independent_cache": True
+ },
+ "log": {
+ "level": "debug",
+ "disabled": True,
+ "timestamp": True
+ },
+ "route": {
+ "final": "direct",
+ "auto_detect_interface": True
+ },
+ "inbounds": [
+ {
+ "tag": "mixed-in",
+ "type": "mixed",
+ "sniff": True,
+ "users": [],
+ "listen": "0.0.0.0",
+ "listen_port": PROXY_PORT,
+ "set_system_proxy": False
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "direct",
+ "type": "direct"
+ }
+ ]
+ }
+
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
+ CONFIG_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False))
+
+ # Reload sing-box
+ try:
+ urllib.request.urlopen("http://127.0.0.1:9090/reload", timeout=3)
+ except Exception:
+ pass
+
+ return True
+
+ except Exception as e:
+ print(f"[WebUI] Failed to generate direct config: {e}")
+ return False
+
def get_subscription(self):
"""Get saved subscription info"""
sub = load_subscription()
@@ -690,6 +824,9 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler):
"success": True,
"message": f"Конфигурация '{vless_params['tag']}' успешно применена!"
})
+
+ # Save new start time as connection is reset
+ save_start_time(datetime.now(timezone.utc).timestamp())
except json.JSONDecodeError:
self.send_json({"success": False, "error": "Неверный JSON"}, 400)