/** * katproxy — Minimal Frontend Client */ // Configuration & State const DEFAULT_API_URL = 'https://katproxy-api.oiupoyt.space'; const LOCAL_DEV_URL = 'http://localhost:5050'; const apiBase = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') ? LOCAL_DEV_URL : DEFAULT_API_URL; const state = { protocol: 'all', aliveOnly: true, search: '', sort: 'latency_asc', page: 1, limit: 60, totalPages: 1, totalProxies: 0, proxies: [], timerInterval: null, nextRefreshSeconds: 0, refreshTotalDuration: 900 // 15 mins default }; // DOM Elements const el = { statTotal: document.getElementById('stat-total'), statAlive: document.getElementById('stat-alive'), statLatency: document.getElementById('stat-latency'), timerText: document.getElementById('timer-text'), progressCircle: document.getElementById('progress-circle'), btnManualRefresh: document.getElementById('btn-manual-refresh'), protocolFilters: document.getElementById('protocol-filters'), toggleAlive: document.getElementById('toggle-alive'), searchInput: document.getElementById('search-input'), searchClear: document.getElementById('search-clear'), sortSelect: document.getElementById('sort-select'), btnCopyAll: document.getElementById('btn-copy-all'), btnExportTxt: document.getElementById('btn-export-txt'), resultsCount: document.getElementById('results-count'), checkingIndicator: document.getElementById('checking-indicator'), proxyGrid: document.getElementById('proxy-grid'), emptyState: document.getElementById('empty-state'), btnResetFilters: document.getElementById('btn-reset-filters'), paginationBar: document.getElementById('pagination-bar'), btnPrev: document.getElementById('btn-prev'), btnNext: document.getElementById('btn-next'), pageIndicator: document.getElementById('page-indicator'), linkRawApi: document.getElementById('link-raw-api'), toastContainer: document.getElementById('toast-container') }; // SVG Circle circumference: 2 * Math.PI * 6 ≈ 37.7 const CIRCUMFERENCE = 37.7; /** * Toast Notification Helper */ function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = 'toast'; toast.innerHTML = `${escapeHtml(message)}`; el.toastContainer.appendChild(toast); setTimeout(() => { toast.classList.add('toast-exit'); setTimeout(() => toast.remove(), 250); }, 2400); } /** * HTML Escaper */ function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>"']/g, (m) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]); } /** * Convert 2-letter ISO country code to Emoji Flag */ function getCountryFlag(countryCode) { if (!countryCode || countryCode === 'UN' || countryCode.length !== 2) return '🌐'; const codePoints = countryCode .toUpperCase() .split('') .map(c => 127397 + c.charCodeAt(0)); return String.fromCodePoint(...codePoints); } /** * Fetch System Statistics */ async function loadStats() { try { const res = await fetch(`${apiBase}/api/stats`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); el.statTotal.textContent = data.total?.toLocaleString() || '0'; el.statAlive.textContent = data.aliveCount?.toLocaleString() || '0'; el.statLatency.textContent = data.avgLatency ? `${data.avgLatency}ms` : '—'; if (data.isChecking) { el.checkingIndicator.classList.remove('hidden'); } else { el.checkingIndicator.classList.add('hidden'); } if (data.remainingSeconds != null) { state.nextRefreshSeconds = data.remainingSeconds; updateTimerDisplay(); } // Update Raw API link el.linkRawApi.href = `${apiBase}/api/raw`; } catch (err) { console.warn('[katproxy] Could not fetch stats:', err.message); el.statTotal.textContent = 'Err'; el.statAlive.textContent = 'Err'; } } /** * Update Timer UI */ function updateTimerDisplay() { const s = Math.max(0, state.nextRefreshSeconds); const minutes = Math.floor(s / 60); const seconds = s % 60; el.timerText.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; const ratio = Math.min(1, Math.max(0, s / state.refreshTotalDuration)); const offset = CIRCUMFERENCE - (ratio * CIRCUMFERENCE); el.progressCircle.style.strokeDashoffset = offset; } /** * Start Local Timer Countdown */ function startTimerLoop() { if (state.timerInterval) clearInterval(state.timerInterval); state.timerInterval = setInterval(() => { if (state.nextRefreshSeconds > 0) { state.nextRefreshSeconds--; updateTimerDisplay(); } else { loadStats(); loadProxies(); } }, 1000); } /** * Load Proxies from Backend */ async function loadProxies() { el.resultsCount.textContent = 'Fetching proxies...'; const params = new URLSearchParams({ page: state.page, limit: state.limit, sort: state.sort }); if (state.protocol !== 'all') { params.set('protocol', state.protocol); } if (state.aliveOnly) { params.set('alive', 'true'); } if (state.search.trim()) { params.set('search', state.search.trim()); } try { const res = await fetch(`${apiBase}/api/proxies?${params.toString()}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); state.proxies = data.proxies || []; state.totalProxies = data.total || 0; state.totalPages = data.totalPages || 1; renderProxies(); } catch (err) { console.error('[katproxy] Load proxies failed:', err); el.resultsCount.textContent = `Backend connection error (${err.message})`; el.proxyGrid.innerHTML = ''; el.emptyState.classList.remove('hidden'); el.paginationBar.classList.add('hidden'); } } /** * Render Proxies Grid */ function renderProxies() { el.proxyGrid.innerHTML = ''; if (state.proxies.length === 0) { el.emptyState.classList.remove('hidden'); el.paginationBar.classList.add('hidden'); el.resultsCount.textContent = '0 matching proxies'; return; } el.emptyState.classList.add('hidden'); el.paginationBar.classList.remove('hidden'); el.resultsCount.textContent = `Showing ${(state.page - 1) * state.limit + 1}–${Math.min(state.page * state.limit, state.totalProxies)} of ${state.totalProxies.toLocaleString()} proxies`; el.pageIndicator.textContent = `Page ${state.page} of ${state.totalPages}`; el.btnPrev.disabled = state.page <= 1; el.btnNext.disabled = state.page >= state.totalPages; const fragment = document.createDocumentFragment(); for (const proxy of state.proxies) { const card = document.createElement('div'); card.className = 'proxy-card'; // Latency badge class & display let latencyClass = 'latency-slow'; let latencyText = 'untested'; if (proxy.latency != null) { if (proxy.latency < 200) latencyClass = 'latency-fast'; else if (proxy.latency < 600) latencyClass = 'latency-med'; latencyText = `${proxy.latency} ms`; } // Protocol pills const protoPills = proxy.protocols.map(p => `${p.toUpperCase()}` ).join(' '); const flag = getCountryFlag(proxy.country); const countryName = proxy.countryName && proxy.countryName !== 'Unknown' ? proxy.countryName : (proxy.country || 'Global'); const orgInfo = proxy.org ? escapeHtml(proxy.org) : (proxy.city ? escapeHtml(proxy.city) : 'Public Node'); card.innerHTML = `