feat: complete katproxy frontend and backend implementation
This commit is contained in:
parent
7021dfb963
commit
bad0f4dc35
11 changed files with 3180 additions and 0 deletions
63
README.md
Normal file
63
README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# katproxy
|
||||||
|
|
||||||
|
A sleek, minimal, high-performance proxy list stream & active TCP latency prober.
|
||||||
|
|
||||||
|
Features dark glassmorphism aesthetics, live SVG countdown timer, one-click copy suite (IP:Port, cURL, raw lists), multi-protocol filtering (HTTP, SOCKS4, SOCKS5), and live round-trip latency metrics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Architecture
|
||||||
|
|
||||||
|
- **Backend (Android Tablet / Termux)**:
|
||||||
|
- Aggregates thousands of proxies from premier GitHub sources (`monosans/proxy-list`, `TheSpeedX/PROXY-List`, `hookzof/socks5_list`).
|
||||||
|
- Active background TCP socket connection tester measuring real round-trip latency (`ms`).
|
||||||
|
- REST API running on port `5050`, tunneled via Cloudflare Tunnel as `katproxy-api.oiupoyt.space`.
|
||||||
|
- **Frontend (Cloudflare Pages)**:
|
||||||
|
- Fast, zero-dependency static web application located in `public/`.
|
||||||
|
- JetBrains Mono typography, smooth 60fps micro-animations, quick actions, responsive on desktop and mobile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/api/health` | Service health status and uptime |
|
||||||
|
| `GET` | `/api/stats` | Total proxies, verified alive count, protocol counts, avg ping |
|
||||||
|
| `GET` | `/api/proxies` | Paginated list with `protocol`, `alive`, `search`, `sort`, `country` filters |
|
||||||
|
| `POST` | `/api/refresh` | Trigger immediate upstream re-fetch and probe cycle |
|
||||||
|
| `GET` | `/api/raw` | Raw plaintext list (`ip:port` or `format=url`) for curl / bash pipelines |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get 5 fastest SOCKS5 proxies
|
||||||
|
curl "https://katproxy-api.oiupoyt.space/api/proxies?protocol=socks5&alive=true&limit=5"
|
||||||
|
|
||||||
|
# Raw IP:PORT stream for CLI scripts
|
||||||
|
curl -s "https://katproxy-api.oiupoyt.space/api/raw?protocol=socks5&alive=true" | head -n 10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Cloudflare Pages Deployment (Frontend)
|
||||||
|
|
||||||
|
1. Connect this GitHub repository (`oiupoyt/katproxy`) to Cloudflare Pages.
|
||||||
|
2. Build Settings:
|
||||||
|
- **Framework preset**: None
|
||||||
|
- **Build command**: None (leave empty)
|
||||||
|
- **Build output directory**: `public`
|
||||||
|
3. Deploy! The frontend automatically connects to `https://katproxy-api.oiupoyt.space`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Tablet Backend Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the tablet (Termux)
|
||||||
|
cd ~
|
||||||
|
git clone https://github.com/oiupoyt/katproxy.git katproxy
|
||||||
|
cd katproxy/backend
|
||||||
|
npm install
|
||||||
|
node --max-old-space-size=256 server.js
|
||||||
|
```
|
||||||
93
backend/checker.js
Normal file
93
backend/checker.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
const net = require('net');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probe a single proxy host:port via TCP socket to check if open and measure round-trip latency (ms)
|
||||||
|
*/
|
||||||
|
function probeProxy(host, port, timeoutMs = 2000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const socket = new net.Socket();
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
const cleanup = () => {
|
||||||
|
socket.removeAllListeners();
|
||||||
|
socket.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.setTimeout(timeoutMs);
|
||||||
|
|
||||||
|
socket.connect(port, host, () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
const latency = Date.now() - startTime;
|
||||||
|
cleanup();
|
||||||
|
resolve({ alive: true, latency });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('timeout', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
cleanup();
|
||||||
|
resolve({ alive: false, latency: null, reason: 'timeout' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
cleanup();
|
||||||
|
resolve({ alive: false, latency: null, reason: err.code || 'error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a batch of proxies with concurrency limit
|
||||||
|
*/
|
||||||
|
async function batchCheckProxies(proxies, concurrency = 50, timeoutMs = 2000, onProgress = null) {
|
||||||
|
let index = 0;
|
||||||
|
let active = 0;
|
||||||
|
let completed = 0;
|
||||||
|
const total = proxies.length;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (total === 0) return resolve();
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
while (active < concurrency && index < total) {
|
||||||
|
const currentIndex = index++;
|
||||||
|
const proxy = proxies[currentIndex];
|
||||||
|
active++;
|
||||||
|
|
||||||
|
probeProxy(proxy.host, proxy.port, timeoutMs).then((res) => {
|
||||||
|
proxy.alive = res.alive;
|
||||||
|
if (res.alive) {
|
||||||
|
proxy.latency = res.latency;
|
||||||
|
}
|
||||||
|
proxy.lastChecked = Date.now();
|
||||||
|
active--;
|
||||||
|
completed++;
|
||||||
|
|
||||||
|
if (onProgress && completed % 25 === 0) {
|
||||||
|
onProgress(completed, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completed === total) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
probeProxy,
|
||||||
|
batchCheckProxies
|
||||||
|
};
|
||||||
166
backend/fetcher.js
Normal file
166
backend/fetcher.js
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to fetch text or JSON over HTTPS with timeout
|
||||||
|
*/
|
||||||
|
function fetchUrl(url, timeoutMs = 12000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = url.startsWith('https') ? https : http;
|
||||||
|
const req = client.get(url, { headers: { 'User-Agent': 'katproxy/1.0' } }, (res) => {
|
||||||
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||||
|
res.resume();
|
||||||
|
return reject(new Error(`Failed to fetch ${url}, status: ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
let rawData = '';
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
res.on('data', (chunk) => { rawData += chunk; });
|
||||||
|
res.on('end', () => resolve(rawData));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err));
|
||||||
|
req.setTimeout(timeoutMs, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error(`Timeout fetching ${url}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and merge proxies from multiple premier GitHub repositories
|
||||||
|
*/
|
||||||
|
async function fetchAllProxies() {
|
||||||
|
console.log('[Fetcher] Starting proxy list aggregation...');
|
||||||
|
const proxyMap = new Map();
|
||||||
|
|
||||||
|
// Helper to add or merge a proxy
|
||||||
|
function addProxy(host, port, protocol, extra = {}) {
|
||||||
|
if (!host || !port) return;
|
||||||
|
const cleanHost = String(host).trim();
|
||||||
|
const cleanPort = parseInt(port, 10);
|
||||||
|
if (isNaN(cleanPort) || cleanPort <= 0 || cleanPort > 65535) return;
|
||||||
|
// Basic IP validation regex
|
||||||
|
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(cleanHost)) return;
|
||||||
|
|
||||||
|
const key = `${cleanHost}:${cleanPort}`;
|
||||||
|
const existing = proxyMap.get(key);
|
||||||
|
|
||||||
|
const protoClean = (protocol || 'http').toLowerCase();
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
proxyMap.set(key, {
|
||||||
|
id: key,
|
||||||
|
host: cleanHost,
|
||||||
|
port: cleanPort,
|
||||||
|
protocols: [protoClean],
|
||||||
|
country: (extra.country || 'UN').toUpperCase(),
|
||||||
|
countryName: extra.countryName || 'Unknown',
|
||||||
|
city: extra.city || '',
|
||||||
|
org: extra.org || '',
|
||||||
|
latency: extra.latency != null ? Math.round(extra.latency * 1000) : null,
|
||||||
|
alive: extra.alive !== undefined ? extra.alive : null,
|
||||||
|
lastChecked: extra.lastChecked || null,
|
||||||
|
sources: [extra.source || 'upstream']
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!existing.protocols.includes(protoClean)) {
|
||||||
|
existing.protocols.push(protoClean);
|
||||||
|
}
|
||||||
|
if (existing.country === 'UN' && extra.country) {
|
||||||
|
existing.country = extra.country.toUpperCase();
|
||||||
|
existing.countryName = extra.countryName || existing.countryName;
|
||||||
|
}
|
||||||
|
if (!existing.org && extra.org) {
|
||||||
|
existing.org = extra.org;
|
||||||
|
}
|
||||||
|
if (extra.source && !existing.sources.includes(extra.source)) {
|
||||||
|
existing.sources.push(extra.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Fetch monosans structured JSON (provides rich metadata: country, org, timeout)
|
||||||
|
try {
|
||||||
|
console.log('[Fetcher] Fetching monosans/proxy-list JSON...');
|
||||||
|
const monosansData = await fetchUrl('https://raw.githubusercontent.com/monosans/proxy-list/main/proxies.json', 15000);
|
||||||
|
const parsed = JSON.parse(monosansData);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
for (const item of parsed) {
|
||||||
|
const countryCode = item.geolocation?.country?.iso_code || 'UN';
|
||||||
|
const countryName = item.geolocation?.country?.names?.en || 'Unknown';
|
||||||
|
const city = item.geolocation?.city?.names?.en || '';
|
||||||
|
const org = item.asn?.autonomous_system_organization || '';
|
||||||
|
const latencyMs = item.timeout ? Math.round(item.timeout * 1000) : null;
|
||||||
|
|
||||||
|
addProxy(item.host, item.port, item.protocol, {
|
||||||
|
country: countryCode,
|
||||||
|
countryName,
|
||||||
|
city,
|
||||||
|
org,
|
||||||
|
latency: item.timeout != null ? item.timeout : null,
|
||||||
|
alive: true,
|
||||||
|
source: 'monosans'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`[Fetcher] monosans parsed: ${parsed.length} proxies`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Fetcher] Warning: monosans JSON fetch failed:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. TheSpeedX lists (HTTP, SOCKS4, SOCKS5)
|
||||||
|
const speedXTasks = [
|
||||||
|
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt', protocol: 'http' },
|
||||||
|
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt', protocol: 'socks4' },
|
||||||
|
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt', protocol: 'socks5' }
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
speedXTasks.map(async (task) => {
|
||||||
|
try {
|
||||||
|
const text = await fetchUrl(task.url);
|
||||||
|
const lines = text.split('\n');
|
||||||
|
let count = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const [host, port] = trimmed.split(':');
|
||||||
|
if (host && port) {
|
||||||
|
addProxy(host, port, task.protocol, { source: 'thespeedx' });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[Fetcher] TheSpeedX ${task.protocol} parsed: ${count} proxies`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[Fetcher] TheSpeedX ${task.protocol} error:`, err.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. hookzof SOCKS5 list
|
||||||
|
try {
|
||||||
|
const hookzofText = await fetchUrl('https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt');
|
||||||
|
const lines = hookzofText.split('\n');
|
||||||
|
let count = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const [host, port] = trimmed.split(':');
|
||||||
|
if (host && port) {
|
||||||
|
addProxy(host, port, 'socks5', { source: 'hookzof' });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[Fetcher] hookzof SOCKS5 parsed: ${count} proxies`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Fetcher] hookzof SOCKS5 error:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = Array.from(proxyMap.values());
|
||||||
|
console.log(`[Fetcher] Completed aggregation. Total unique proxies: ${results.length}`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchAllProxies
|
||||||
|
};
|
||||||
863
backend/package-lock.json
generated
Normal file
863
backend/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,863 @@
|
||||||
|
{
|
||||||
|
"name": "katproxy-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "katproxy-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "1.20.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
|
||||||
|
"integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"content-type": "~1.0.5",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "~1.2.0",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"qs": "~6.16.0",
|
||||||
|
"raw-body": "~2.5.3",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/cors": {
|
||||||
|
"version": "2.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
|
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/destroy": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "4.22.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
|
||||||
|
"integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.8",
|
||||||
|
"array-flatten": "1.1.1",
|
||||||
|
"body-parser": "~1.20.5",
|
||||||
|
"content-disposition": "~0.5.4",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"cookie": "~0.7.1",
|
||||||
|
"cookie-signature": "~1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"finalhandler": "~1.3.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.0",
|
||||||
|
"merge-descriptors": "1.0.3",
|
||||||
|
"methods": "~1.1.2",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"path-to-regexp": "~0.1.13",
|
||||||
|
"proxy-addr": "~2.0.7",
|
||||||
|
"qs": "~6.16.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"send": "~0.19.0",
|
||||||
|
"serve-static": "~1.16.2",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "~2.0.1",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"utils-merge": "1.0.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
|
||||||
|
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"side-channel": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "0.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||||
|
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "1.2.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"statuses": "~2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"send": "~0.19.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.4",
|
||||||
|
"side-channel-list": "^1.0.1",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/utils-merge": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/package.json
Normal file
19
backend/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "katproxy-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "High-performance minimal proxy aggregator and latency checker backend",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"author": "oiupoyt",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
277
backend/server.js
Normal file
277
backend/server.js
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
const { fetchAllProxies } = require('./fetcher');
|
||||||
|
const { batchCheckProxies } = require('./checker');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 5050;
|
||||||
|
const REFRESH_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||||
|
|
||||||
|
// State
|
||||||
|
let proxyStore = [];
|
||||||
|
let lastUpdated = null;
|
||||||
|
let nextRefreshTime = null;
|
||||||
|
let isRefreshing = false;
|
||||||
|
let isChecking = false;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors({
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Optional: serve static frontend if public directory exists
|
||||||
|
const publicDir = path.join(__dirname, '../public');
|
||||||
|
app.use(express.static(publicDir));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger full refresh cycle
|
||||||
|
*/
|
||||||
|
async function refreshCycle() {
|
||||||
|
if (isRefreshing) return;
|
||||||
|
isRefreshing = true;
|
||||||
|
console.log(`[Server] Starting auto-refresh at ${new Date().toISOString()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawList = await fetchAllProxies();
|
||||||
|
if (rawList && rawList.length > 0) {
|
||||||
|
proxyStore = rawList;
|
||||||
|
lastUpdated = Date.now();
|
||||||
|
nextRefreshTime = Date.now() + REFRESH_INTERVAL_MS;
|
||||||
|
console.log(`[Server] Aggregated ${proxyStore.length} proxies successfully.`);
|
||||||
|
|
||||||
|
// Launch background latency checker on the first batch of proxies
|
||||||
|
// Check high-priority proxies (e.g. up to 300) without hogging CPU/sockets
|
||||||
|
probeTopProxiesInBackground();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Server] Refresh cycle error:', err);
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background latency verification pool
|
||||||
|
*/
|
||||||
|
async function probeTopProxiesInBackground() {
|
||||||
|
if (isChecking || proxyStore.length === 0) return;
|
||||||
|
isChecking = true;
|
||||||
|
console.log('[Server] Starting background TCP latency verification...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Select top 300 proxies to probe (or all if fewer)
|
||||||
|
const targetProxies = proxyStore.slice(0, 350);
|
||||||
|
await batchCheckProxies(targetProxies, 40, 2000, (completed, total) => {
|
||||||
|
if (completed % 100 === 0 || completed === total) {
|
||||||
|
console.log(`[Checker] Latency probe progress: ${completed}/${total}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('[Checker] Latency probing completed.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Checker] Latency probing error:', err);
|
||||||
|
} finally {
|
||||||
|
isChecking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
|
||||||
|
// 1. Health
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'katproxy-backend',
|
||||||
|
uptime: Math.round(process.uptime()),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Stats
|
||||||
|
app.get('/api/stats', (req, res) => {
|
||||||
|
const total = proxyStore.length;
|
||||||
|
const aliveList = proxyStore.filter(p => p.alive === true);
|
||||||
|
|
||||||
|
const protocols = { http: 0, socks4: 0, socks5: 0 };
|
||||||
|
const countryCounts = {};
|
||||||
|
|
||||||
|
for (const p of proxyStore) {
|
||||||
|
for (const proto of p.protocols) {
|
||||||
|
if (protocols[proto] !== undefined) protocols[proto]++;
|
||||||
|
}
|
||||||
|
const c = p.country || 'UN';
|
||||||
|
countryCounts[c] = (countryCounts[c] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top 10 countries
|
||||||
|
const topCountries = Object.entries(countryCounts)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([country, count]) => ({ country, count }));
|
||||||
|
|
||||||
|
// Average latency of alive proxies
|
||||||
|
const latencies = aliveList.filter(p => p.latency != null).map(p => p.latency);
|
||||||
|
const avgLatency = latencies.length > 0
|
||||||
|
? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total,
|
||||||
|
aliveCount: aliveList.length,
|
||||||
|
protocols,
|
||||||
|
topCountries,
|
||||||
|
avgLatency,
|
||||||
|
lastUpdated,
|
||||||
|
nextRefreshTime,
|
||||||
|
remainingSeconds: nextRefreshTime ? Math.max(0, Math.round((nextRefreshTime - Date.now()) / 1000)) : 0,
|
||||||
|
isRefreshing,
|
||||||
|
isChecking
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Proxies list with filter, search, sort, pagination
|
||||||
|
app.get('/api/proxies', (req, res) => {
|
||||||
|
let {
|
||||||
|
protocol,
|
||||||
|
country,
|
||||||
|
alive,
|
||||||
|
maxLatency,
|
||||||
|
search,
|
||||||
|
sort = 'latency_asc',
|
||||||
|
limit = 100,
|
||||||
|
page = 1
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
limit = Math.min(Math.max(parseInt(limit, 10) || 100, 1), 1000);
|
||||||
|
page = Math.max(parseInt(page, 10) || 1, 1);
|
||||||
|
|
||||||
|
let results = proxyStore;
|
||||||
|
|
||||||
|
// Filter: protocol
|
||||||
|
if (protocol && protocol !== 'all') {
|
||||||
|
const protoLower = protocol.toLowerCase();
|
||||||
|
results = results.filter(p => p.protocols.includes(protoLower));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: country
|
||||||
|
if (country && country !== 'all') {
|
||||||
|
const countryUpper = country.toUpperCase();
|
||||||
|
results = results.filter(p => p.country === countryUpper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: alive
|
||||||
|
if (alive === 'true') {
|
||||||
|
results = results.filter(p => p.alive === true);
|
||||||
|
} else if (alive === 'false') {
|
||||||
|
results = results.filter(p => p.alive === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: max latency
|
||||||
|
if (maxLatency) {
|
||||||
|
const maxMs = parseInt(maxLatency, 10);
|
||||||
|
if (!isNaN(maxMs)) {
|
||||||
|
results = results.filter(p => p.latency != null && p.latency <= maxMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: search query (IP, port, country name, org, city)
|
||||||
|
if (search && typeof search === 'string' && search.trim()) {
|
||||||
|
const q = search.trim().toLowerCase();
|
||||||
|
results = results.filter(p =>
|
||||||
|
p.host.includes(q) ||
|
||||||
|
String(p.port).includes(q) ||
|
||||||
|
p.country.toLowerCase().includes(q) ||
|
||||||
|
(p.countryName && p.countryName.toLowerCase().includes(q)) ||
|
||||||
|
(p.org && p.org.toLowerCase().includes(q)) ||
|
||||||
|
(p.city && p.city.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
results = results.slice().sort((a, b) => {
|
||||||
|
if (sort === 'latency_asc') {
|
||||||
|
const latA = a.latency != null ? a.latency : 999999;
|
||||||
|
const latB = b.latency != null ? b.latency : 999999;
|
||||||
|
return latA - latB;
|
||||||
|
}
|
||||||
|
if (sort === 'latency_desc') {
|
||||||
|
const latA = a.latency != null ? a.latency : -1;
|
||||||
|
const latB = b.latency != null ? b.latency : -1;
|
||||||
|
return latB - latA;
|
||||||
|
}
|
||||||
|
if (sort === 'country') {
|
||||||
|
return (a.country || '').localeCompare(b.country || '');
|
||||||
|
}
|
||||||
|
if (sort === 'port') {
|
||||||
|
return a.port - b.port;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalMatches = results.length;
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const paginated = results.slice(startIndex, startIndex + limit);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total: totalMatches,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(totalMatches / limit),
|
||||||
|
proxies: paginated
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Manual Refresh Trigger
|
||||||
|
app.post('/api/refresh', async (req, res) => {
|
||||||
|
if (isRefreshing) {
|
||||||
|
return res.status(429).json({ error: 'Refresh already in progress' });
|
||||||
|
}
|
||||||
|
refreshCycle().catch(console.error);
|
||||||
|
res.json({ success: true, message: 'Proxy aggregation started in background' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Raw Text Export (for curl / bash scripts)
|
||||||
|
app.get('/api/raw', (req, res) => {
|
||||||
|
const { protocol, alive, format } = req.query;
|
||||||
|
let results = proxyStore;
|
||||||
|
|
||||||
|
if (protocol && protocol !== 'all') {
|
||||||
|
results = results.filter(p => p.protocols.includes(protocol.toLowerCase()));
|
||||||
|
}
|
||||||
|
if (alive === 'true') {
|
||||||
|
results = results.filter(p => p.alive === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
if (format === 'url') {
|
||||||
|
const lines = results.map(p => {
|
||||||
|
const proto = p.protocols[0] || 'http';
|
||||||
|
return `${proto}://${p.host}:${p.port}`;
|
||||||
|
});
|
||||||
|
return res.send(lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: IP:PORT
|
||||||
|
const lines = results.map(p => `${p.host}:${p.port}`);
|
||||||
|
res.send(lines.join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback for spa / static index if accessed directly
|
||||||
|
app.get('*', (req, res, next) => {
|
||||||
|
if (req.accepts('html')) {
|
||||||
|
return res.sendFile(path.join(publicDir, 'index.html'), (err) => {
|
||||||
|
if (err) res.status(404).send('KatProxy API Server is running.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server and launch initial fetch
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`[Server] katproxy backend listening on http://0.0.0.0:${PORT}`);
|
||||||
|
refreshCycle();
|
||||||
|
setInterval(refreshCycle, REFRESH_INTERVAL_MS);
|
||||||
|
});
|
||||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "katproxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Minimal, sleek multi-source proxy list stream & active latency tester",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node backend/server.js",
|
||||||
|
"backend": "cd backend && npm start",
|
||||||
|
"dev": "cd backend && npm run dev"
|
||||||
|
},
|
||||||
|
"keywords": ["proxy", "socks5", "socks4", "http", "stream", "minimal"],
|
||||||
|
"author": "oiupoyt",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
513
public/app.js
Normal file
513
public/app.js
Normal file
|
|
@ -0,0 +1,513 @@
|
||||||
|
/**
|
||||||
|
* katproxy — Minimal Frontend Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration & State
|
||||||
|
const DEFAULT_TUNNEL_URL = 'https://katproxy-api.oiupoyt.space';
|
||||||
|
const LOCAL_DEV_URL = 'http://localhost:5050';
|
||||||
|
|
||||||
|
let apiBase = localStorage.getItem('katproxy_api_url') || (
|
||||||
|
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||||
|
? LOCAL_DEV_URL
|
||||||
|
: DEFAULT_TUNNEL_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'),
|
||||||
|
btnSettings: document.getElementById('btn-settings'),
|
||||||
|
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'),
|
||||||
|
settingsModal: document.getElementById('settings-modal'),
|
||||||
|
modalClose: document.getElementById('modal-close'),
|
||||||
|
modalCancel: document.getElementById('modal-cancel'),
|
||||||
|
modalSave: document.getElementById('modal-save'),
|
||||||
|
apiUrlInput: document.getElementById('api-url-input'),
|
||||||
|
toastContainer: document.getElementById('toast-container')
|
||||||
|
};
|
||||||
|
|
||||||
|
// SVG Circle circumference: 2 * Math.PI * 11 ≈ 69.11
|
||||||
|
const CIRCUMFERENCE = 69.11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast Notification Helper
|
||||||
|
*/
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast';
|
||||||
|
toast.innerHTML = `<span class="toast-dot"></span><span>${escapeHtml(message)}</span>`;
|
||||||
|
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 =>
|
||||||
|
`<span class="proto-tag proto-${p.toLowerCase()}">${p.toUpperCase()}</span>`
|
||||||
|
).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 = `
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="endpoint-group">
|
||||||
|
<span class="ip-text">${escapeHtml(proxy.host)}</span>
|
||||||
|
<span class="port-text">:${proxy.port}</span>
|
||||||
|
</div>
|
||||||
|
<button class="copy-hint-btn" title="Click to copy host:port">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-middle">
|
||||||
|
<div class="badge-row">
|
||||||
|
${protoPills}
|
||||||
|
</div>
|
||||||
|
<div class="latency-badge ${latencyClass}" title="TCP connect ping">
|
||||||
|
<span class="latency-dot"></span>
|
||||||
|
<span>${latencyText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-bottom">
|
||||||
|
<div class="geo-info" title="${escapeHtml(countryName)} • ${orgInfo}">
|
||||||
|
<span class="country-flag">${flag}</span>
|
||||||
|
<span>${escapeHtml(countryName)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions-quick">
|
||||||
|
<button class="quick-action-link btn-curl" title="Copy cURL syntax">cURL</button>
|
||||||
|
<button class="quick-action-link btn-proxy-url" title="Copy URL format">URL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Click on card body -> copy IP:Port
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('.quick-action-link')) return;
|
||||||
|
copyToClipboard(`${proxy.host}:${proxy.port}`, `Copied ${proxy.host}:${proxy.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy cURL
|
||||||
|
const btnCurl = card.querySelector('.btn-curl');
|
||||||
|
btnCurl.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const proto = proxy.protocols[0] || 'http';
|
||||||
|
const cmd = `curl -x ${proto}://${proxy.host}:${proxy.port} https://ifconfig.me`;
|
||||||
|
copyToClipboard(cmd, `Copied cURL command for ${proxy.host}:${proxy.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy URL
|
||||||
|
const btnUrl = card.querySelector('.btn-proxy-url');
|
||||||
|
btnUrl.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const proto = proxy.protocols[0] || 'http';
|
||||||
|
const url = `${proto}://${proxy.host}:${proxy.port}`;
|
||||||
|
copyToClipboard(url, `Copied ${url}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
fragment.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.proxyGrid.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard Copy Helper
|
||||||
|
*/
|
||||||
|
async function copyToClipboard(text, successMessage) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast(successMessage);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
showToast(successMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Handlers Setup
|
||||||
|
*/
|
||||||
|
function initEvents() {
|
||||||
|
// Protocol Filter Pills
|
||||||
|
el.protocolFilters.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.pill-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
el.protocolFilters.querySelectorAll('.pill-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
state.protocol = btn.dataset.protocol;
|
||||||
|
state.page = 1;
|
||||||
|
loadProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alive Toggle
|
||||||
|
el.toggleAlive.addEventListener('change', (e) => {
|
||||||
|
state.aliveOnly = e.target.checked;
|
||||||
|
state.page = 1;
|
||||||
|
loadProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debounced Search Input
|
||||||
|
let searchTimer = null;
|
||||||
|
el.searchInput.addEventListener('input', (e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val) {
|
||||||
|
el.searchClear.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
el.searchClear.classList.add('hidden');
|
||||||
|
}
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
state.search = val;
|
||||||
|
state.page = 1;
|
||||||
|
loadProxies();
|
||||||
|
}, 280);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search Clear Button
|
||||||
|
el.searchClear.addEventListener('click', () => {
|
||||||
|
el.searchInput.value = '';
|
||||||
|
el.searchClear.classList.add('hidden');
|
||||||
|
state.search = '';
|
||||||
|
state.page = 1;
|
||||||
|
loadProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort Select
|
||||||
|
el.sortSelect.addEventListener('change', (e) => {
|
||||||
|
state.sort = e.target.value;
|
||||||
|
state.page = 1;
|
||||||
|
loadProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy Visible Proxies
|
||||||
|
el.btnCopyAll.addEventListener('click', () => {
|
||||||
|
if (state.proxies.length === 0) {
|
||||||
|
showToast('No proxies to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = state.proxies.map(p => `${p.host}:${p.port}`).join('\n');
|
||||||
|
copyToClipboard(lines, `Copied ${state.proxies.length} proxies to clipboard`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export TXT
|
||||||
|
el.btnExportTxt.addEventListener('click', () => {
|
||||||
|
if (state.proxies.length === 0) {
|
||||||
|
showToast('No proxies to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = state.proxies.map(p => `${p.host}:${p.port}`).join('\n');
|
||||||
|
const blob = new Blob([lines], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `katproxy-${state.protocol}-${Date.now()}.txt`;
|
||||||
|
link.click();
|
||||||
|
showToast(`Exported ${state.proxies.length} proxies to file`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual Upstream Refresh Trigger
|
||||||
|
el.btnManualRefresh.addEventListener('click', async () => {
|
||||||
|
el.btnManualRefresh.classList.add('spinning');
|
||||||
|
showToast('Triggering upstream proxy refresh...');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase}/api/refresh`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
showToast(data.message || 'Refresh initiated');
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Refresh already running');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
loadStats();
|
||||||
|
loadProxies();
|
||||||
|
el.btnManualRefresh.classList.remove('spinning');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`Refresh error: ${err.message}`);
|
||||||
|
el.btnManualRefresh.classList.remove('spinning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset Filters Button
|
||||||
|
el.btnResetFilters.addEventListener('click', () => {
|
||||||
|
state.protocol = 'all';
|
||||||
|
state.aliveOnly = false;
|
||||||
|
state.search = '';
|
||||||
|
state.page = 1;
|
||||||
|
el.searchInput.value = '';
|
||||||
|
el.toggleAlive.checked = false;
|
||||||
|
el.protocolFilters.querySelectorAll('.pill-btn').forEach(b => {
|
||||||
|
b.classList.toggle('active', b.dataset.protocol === 'all');
|
||||||
|
});
|
||||||
|
loadProxies();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
el.btnPrev.addEventListener('click', () => {
|
||||||
|
if (state.page > 1) {
|
||||||
|
state.page--;
|
||||||
|
loadProxies();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnNext.addEventListener('click', () => {
|
||||||
|
if (state.page < state.totalPages) {
|
||||||
|
state.page++;
|
||||||
|
loadProxies();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings Modal
|
||||||
|
el.btnSettings.addEventListener('click', () => {
|
||||||
|
el.apiUrlInput.value = apiBase;
|
||||||
|
el.settingsModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
el.modalClose.addEventListener('click', () => el.settingsModal.classList.add('hidden'));
|
||||||
|
el.modalCancel.addEventListener('click', () => el.settingsModal.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Quick preset buttons in modal
|
||||||
|
el.settingsModal.querySelectorAll('.quick-endpoints button').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
el.apiUrlInput.value = btn.dataset.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
el.modalSave.addEventListener('click', () => {
|
||||||
|
const newUrl = el.apiUrlInput.value.trim().replace(/\/+$/, '');
|
||||||
|
if (newUrl) {
|
||||||
|
apiBase = newUrl;
|
||||||
|
localStorage.setItem('katproxy_api_url', newUrl);
|
||||||
|
showToast(`API endpoint updated to ${newUrl}`);
|
||||||
|
el.settingsModal.classList.add('hidden');
|
||||||
|
loadStats();
|
||||||
|
loadProxies();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initEvents();
|
||||||
|
loadStats();
|
||||||
|
loadProxies();
|
||||||
|
startTimerLoop();
|
||||||
|
|
||||||
|
// Periodic stats poll every 12 seconds
|
||||||
|
setInterval(loadStats, 12000);
|
||||||
|
});
|
||||||
6
public/favicon.svg
Normal file
6
public/favicon.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect width="32" height="32" rx="8" fill="#09090b"/>
|
||||||
|
<circle cx="16" cy="16" r="11" stroke="#27272a" stroke-width="2"/>
|
||||||
|
<circle cx="16" cy="16" r="6" stroke="#10b981" stroke-width="2" stroke-dasharray="10 4"/>
|
||||||
|
<circle cx="16" cy="16" r="2.5" fill="#10b981"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
204
public/index.html
Normal file
204
public/index.html
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>katproxy — minimal proxy stream</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-glow"></div>
|
||||||
|
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-dot"></span>
|
||||||
|
<span class="brand-name">katproxy</span>
|
||||||
|
<span class="brand-badge">LIVE</span>
|
||||||
|
</div>
|
||||||
|
<p class="brand-subtitle">Curated, multi-source proxy stream with live TCP verification</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- Stats Pills -->
|
||||||
|
<div class="stats-group">
|
||||||
|
<div class="stat-pill" title="Total aggregated proxies">
|
||||||
|
<span class="stat-label">TOTAL</span>
|
||||||
|
<span class="stat-value" id="stat-total">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-pill" title="Alive / verified proxies">
|
||||||
|
<span class="stat-label">ALIVE</span>
|
||||||
|
<span class="stat-value highlight-emerald" id="stat-alive">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-pill" title="Average verified response latency">
|
||||||
|
<span class="stat-label">AVG PING</span>
|
||||||
|
<span class="stat-value highlight-cyan" id="stat-latency">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Refresh Countdown & Trigger -->
|
||||||
|
<div class="refresh-control">
|
||||||
|
<div class="timer-widget" title="Auto-refresh countdown">
|
||||||
|
<svg class="progress-ring" width="28" height="28">
|
||||||
|
<circle class="progress-ring-bg" stroke="#27272a" stroke-width="2.5" fill="transparent" r="11" cx="14" cy="14" />
|
||||||
|
<circle class="progress-ring-circle" id="progress-circle" stroke="#10b981" stroke-width="2.5" stroke-dasharray="69.11" stroke-dashoffset="0" stroke-linecap="round" fill="transparent" r="11" cx="14" cy="14" />
|
||||||
|
</svg>
|
||||||
|
<span class="timer-text" id="timer-text">--s</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-icon" id="btn-manual-refresh" title="Force refresh upstream sources">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-icon" id="btn-settings" title="API Settings">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Controls & Filters Bar -->
|
||||||
|
<section class="controls-bar">
|
||||||
|
<!-- Protocol Filters -->
|
||||||
|
<div class="filter-pills" id="protocol-filters">
|
||||||
|
<button class="pill-btn active" data-protocol="all">ALL</button>
|
||||||
|
<button class="pill-btn" data-protocol="http">HTTP</button>
|
||||||
|
<button class="pill-btn" data-protocol="socks4">SOCKS4</button>
|
||||||
|
<button class="pill-btn" data-protocol="socks5">SOCKS5</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alive Toggle -->
|
||||||
|
<label class="toggle-control" title="Show only verified responsive proxies">
|
||||||
|
<input type="checkbox" id="toggle-alive" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Verified Alive</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="search-box">
|
||||||
|
<svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<input type="text" id="search-input" placeholder="Search IP, port, country, org..." spellcheck="false" autocomplete="off">
|
||||||
|
<button class="search-clear hidden" id="search-clear">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort Selection -->
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select id="sort-select" title="Sort order">
|
||||||
|
<option value="latency_asc">Lowest Ping</option>
|
||||||
|
<option value="latency_desc">Highest Ping</option>
|
||||||
|
<option value="country">Country (A-Z)</option>
|
||||||
|
<option value="port">Port Number</option>
|
||||||
|
</select>
|
||||||
|
<svg class="select-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Action Buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-secondary" id="btn-copy-all" title="Copy currently filtered list to clipboard">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Copy Visible</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" id="btn-export-txt" title="Download raw IP:Port text file">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
<span>Export TXT</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Status Banner (when searching or zero results) -->
|
||||||
|
<div class="list-meta">
|
||||||
|
<span id="results-count">Loading proxies...</span>
|
||||||
|
<span id="checking-indicator" class="checking-indicator hidden">
|
||||||
|
<span class="spin-dot"></span> Checking live latencies...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proxy Cards Grid -->
|
||||||
|
<div class="proxy-grid" id="proxy-grid">
|
||||||
|
<!-- Rendered via JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="empty-state hidden" id="empty-state">
|
||||||
|
<div class="empty-icon">⚲</div>
|
||||||
|
<h3>No matching proxies found</h3>
|
||||||
|
<p>Try easing your filters or searching with a different term.</p>
|
||||||
|
<button class="btn btn-secondary" id="btn-reset-filters">Reset Filters</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<div class="pagination-bar" id="pagination-bar">
|
||||||
|
<button class="btn btn-sm" id="btn-prev" disabled>← Prev</button>
|
||||||
|
<span class="page-indicator" id="page-indicator">Page 1 of 1</span>
|
||||||
|
<button class="btn btn-sm" id="btn-next">Next →</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
<span>katproxy • hosted on tablet via cloudflared</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<a href="https://github.com/oiupoyt/katproxy" target="_blank" rel="noopener noreferrer" class="footer-link">GitHub</a>
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<a id="link-raw-api" href="#" target="_blank" class="footer-link">Raw API</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div class="modal-overlay hidden" id="settings-modal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Backend API Configuration</h3>
|
||||||
|
<button class="modal-close" id="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<label for="api-url-input">API Base URL</label>
|
||||||
|
<input type="text" id="api-url-input" class="modal-input" placeholder="https://katproxy-api.oiupoyt.space">
|
||||||
|
<p class="modal-hint">Cloudflare Tunnel endpoint (public) or direct LAN address (e.g. <code>http://192.168.0.104:5050</code>).</p>
|
||||||
|
|
||||||
|
<div class="quick-endpoints">
|
||||||
|
<button class="btn btn-xs" data-url="https://katproxy-api.oiupoyt.space">Cloudflare Tunnel</button>
|
||||||
|
<button class="btn btn-xs" data-url="http://192.168.0.104:5050">Local LAN</button>
|
||||||
|
<button class="btn btn-xs" data-url="http://localhost:5050">Localhost</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" id="modal-cancel">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="modal-save">Save & Reconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification Container -->
|
||||||
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
959
public/style.css
Normal file
959
public/style.css
Normal file
|
|
@ -0,0 +1,959 @@
|
||||||
|
:root {
|
||||||
|
--bg-primary: #09090b;
|
||||||
|
--bg-surface: #111114;
|
||||||
|
--bg-surface-hover: #18181c;
|
||||||
|
--bg-card: rgba(18, 18, 22, 0.75);
|
||||||
|
--bg-pill: rgba(255, 255, 255, 0.04);
|
||||||
|
--bg-pill-hover: rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
--border-subtle: rgba(255, 255, 255, 0.08);
|
||||||
|
--border-hover: rgba(255, 255, 255, 0.16);
|
||||||
|
--border-focus: rgba(16, 185, 129, 0.5);
|
||||||
|
|
||||||
|
--text-main: #f4f4f5;
|
||||||
|
--text-muted: #a1a1aa;
|
||||||
|
--text-dim: #71717a;
|
||||||
|
|
||||||
|
--accent-emerald: #10b981;
|
||||||
|
--accent-emerald-dim: rgba(16, 185, 129, 0.15);
|
||||||
|
--accent-cyan: #38bdf8;
|
||||||
|
--accent-cyan-dim: rgba(56, 189, 248, 0.15);
|
||||||
|
--accent-amber: #f59e0b;
|
||||||
|
--accent-amber-dim: rgba(245, 158, 11, 0.15);
|
||||||
|
--accent-rose: #f43f5e;
|
||||||
|
|
||||||
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient Subtle Radial Glow */
|
||||||
|
.ambient-glow {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 900px;
|
||||||
|
height: 380px;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(16, 185, 129, 0.05) 0%, rgba(56, 189, 248, 0.02) 40%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
max-width: 1240px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 24px 60px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--accent-emerald);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 10px var(--accent-emerald);
|
||||||
|
animation: pulse-dot 2.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.4; transform: scale(0.85); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-badge {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-emerald);
|
||||||
|
background: var(--accent-emerald-dim);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-pill {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 6px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 78px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-emerald {
|
||||||
|
color: var(--accent-emerald) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-cyan {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh & Timer */
|
||||||
|
.refresh-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-widget {
|
||||||
|
position: relative;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-ring-circle {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transition: stroke-dashoffset 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-text {
|
||||||
|
position: absolute;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-main);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
padding: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon.spinning svg {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent-emerald);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--accent-emerald);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #059669;
|
||||||
|
border-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--bg-pill);
|
||||||
|
border-color: var(--border-subtle);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--bg-pill-hover);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-xs {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls Bar */
|
||||||
|
.controls-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Protocol Pills */
|
||||||
|
.filter-pills {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-btn {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-dim);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-btn:hover {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-btn.active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-control input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
width: 32px;
|
||||||
|
height: 18px;
|
||||||
|
background: #27272a;
|
||||||
|
border-radius: 9999px;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-control input:checked + .toggle-slider {
|
||||||
|
background: var(--accent-emerald);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-control input:checked + .toggle-slider::after {
|
||||||
|
transform: translateX(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Box */
|
||||||
|
.search-box {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 7px 28px 7px 32px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input::placeholder {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select Dropdown */
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper select {
|
||||||
|
appearance: none;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 7px 28px 7px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 9px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons Group */
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content & Metadata */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 0 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checking-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
animation: pulse-dot 1.2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Proxy Grid & Cards */
|
||||||
|
.proxy-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 14px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-card:hover {
|
||||||
|
background: var(--bg-surface-hover);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-text {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-hint-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-card:hover .copy-hint-btn {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-hint-btn:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-middle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-tag {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-tag.proto-http {
|
||||||
|
color: #60a5fa;
|
||||||
|
background: rgba(96, 165, 250, 0.1);
|
||||||
|
border-color: rgba(96, 165, 250, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-tag.proto-socks4 {
|
||||||
|
color: #c084fc;
|
||||||
|
background: rgba(192, 132, 252, 0.1);
|
||||||
|
border-color: rgba(192, 132, 252, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-tag.proto-socks5 {
|
||||||
|
color: #34d399;
|
||||||
|
background: rgba(52, 211, 153, 0.1);
|
||||||
|
border-color: rgba(52, 211, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latency-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latency-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latency-fast {
|
||||||
|
color: var(--accent-emerald);
|
||||||
|
}
|
||||||
|
.latency-fast .latency-dot {
|
||||||
|
background: var(--accent-emerald);
|
||||||
|
box-shadow: 0 0 6px var(--accent-emerald);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latency-med {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
}
|
||||||
|
.latency-med .latency-dot {
|
||||||
|
background: var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latency-slow {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.latency-slow .latency-dot {
|
||||||
|
background: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.geo-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-flag {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions-quick {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-link:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-indicator {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 36px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin: 0 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
background: #121216;
|
||||||
|
border: 1px solid var(--border-hover);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
max-width: 440px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||||||
|
animation: modal-in 0.18s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-in {
|
||||||
|
from { opacity: 0; transform: scale(0.96); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-input {
|
||||||
|
width: 100%;
|
||||||
|
background: #09090b;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-emerald);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-endpoints {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast System */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: rgba(18, 18, 22, 0.95);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--border-hover);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
animation: toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.toast-exit {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-in {
|
||||||
|
from { opacity: 0; transform: translateY(12px) scale(0.96); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-emerald);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.controls-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.proxy-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue