Files
vpn-proxy/web/index.html

517 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPN Proxy Control</title>
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-glass: rgba(255, 255, 255, 0.03);
--border-color: rgba(255, 255, 255, 0.08);
--text-primary: #e8e8ec;
--text-secondary: #8b8b9e;
--accent: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.4);
--success: #22c55e;
--error: #ef4444;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
background-image:
radial-gradient(ellipse at top, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
radial-gradient(ellipse at bottom right, rgba(168, 85, 247, 0.08) 0%, transparent 50%);
}
.container {
width: 100%;
max-width: 520px;
}
.card {
background: var(--bg-glass);
backdrop-filter: blur(20px);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 2.5rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -2px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.logo {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--accent), #a855f7);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.25rem;
font-size: 1.75rem;
box-shadow: 0 8px 32px var(--accent-glow);
}
h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
}
.status-bar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
background: var(--bg-secondary);
border-radius: 12px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-secondary);
transition: all 0.3s ease;
}
.status-indicator.active {
background: var(--success);
box-shadow: 0 0 12px rgba(34, 197, 94, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-text {
flex: 1;
}
.status-label {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.125rem;
}
.status-value {
font-size: 0.9rem;
font-weight: 500;
}
.form-group {
margin-bottom: 1.25rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.input-wrapper {
position: relative;
}
input[type="text"] {
width: 100%;
padding: 1rem 1.25rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-primary);
font-size: 0.9rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
transition: all 0.2s ease;
}
input[type="text"]::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
input[type="text"]:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.btn {
width: 100%;
padding: 1rem;
border: none;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #8b5cf6);
color: white;
box-shadow: 0 4px 20px var(--accent-glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px var(--accent-glow);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-icon {
font-size: 1.1rem;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.message {
margin-top: 1.25rem;
padding: 1rem;
border-radius: 12px;
font-size: 0.875rem;
display: none;
align-items: flex-start;
gap: 0.75rem;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.show {
display: flex;
}
.message.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: var(--success);
}
.message.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--error);
}
.message-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: var(--text-secondary);
font-size: 0.8rem;
}
.footer a {
color: var(--accent);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.hint {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
.connection-info {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.connection-info h3 {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 1rem;
font-weight: 500;
}
.info-item {
background: var(--bg-secondary);
padding: 0.75rem 1rem;
border-radius: 12px;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.85rem;
}
.info-item .label {
color: var(--text-secondary);
}
.info-item .value {
font-family: 'JetBrains Mono', monospace;
color: var(--accent);
background: rgba(99, 102, 241, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 6px;
user-select: all;
cursor: pointer;
}
/* Responsive */
@media (max-width: 480px) {
body {
padding: 1rem;
}
.card {
padding: 1.5rem;
border-radius: 20px;
}
h1 {
font-size: 1.25rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="header">
<div class="logo">🔐</div>
<h1>VPN Proxy Control</h1>
<p class="subtitle">Управление подключением sing-box</p>
</div>
<div class="status-bar" id="statusBar">
<div class="status-indicator" id="statusIndicator"></div>
<div class="status-text">
<div class="status-label">Статус</div>
<div class="status-value" id="statusValue">Загрузка...</div>
</div>
</div>
<form id="proxyForm">
<div class="form-group">
<label for="urlInput">VLESS / Subscription URL</label>
<div class="input-wrapper">
<input
type="text"
id="urlInput"
placeholder="vless://... или https://subscription.link"
autocomplete="off"
spellcheck="false"
>
</div>
<p class="hint">Вставьте VLESS ссылку или URL подписки</p>
</div>
<button type="submit" class="btn btn-primary" id="submitBtn">
<span class="btn-icon"></span>
<span id="btnText">Применить</span>
</button>
</form>
<div class="message" id="message">
<span class="message-icon" id="messageIcon"></span>
<span id="messageText"></span>
</div>
<div class="connection-info">
<h3>Данные для подключения</h3>
<div class="info-item">
<span class="label">HTTP / HTTPS</span>
<code class="value" id="httpLink" title="Нажмите, чтобы скопировать">...</code>
</div>
<div class="info-item">
<span class="label">SOCKS5</span>
<code class="value" id="socksLink" title="Нажмите, чтобы скопировать">...</code>
</div>
</div>
</div>
<div class="footer">
Proxy работает на порту <strong>8082</strong>
<a href="https://github.com/SagerNet/sing-box" target="_blank">sing-box</a>
</div>
</div>
<script>
const form = document.getElementById('proxyForm');
const urlInput = document.getElementById('urlInput');
const submitBtn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const message = document.getElementById('message');
const messageIcon = document.getElementById('messageIcon');
const messageText = document.getElementById('messageText');
const statusIndicator = document.getElementById('statusIndicator');
const statusValue = document.getElementById('statusValue');
// Fetch initial status
async function fetchStatus() {
try {
const res = await fetch('/status');
const data = await res.json();
if (data.active) {
statusIndicator.classList.add('active');
statusValue.textContent = data.tag
? `${data.tag} (${data.server})`
: 'Активен';
} else {
statusIndicator.classList.remove('active');
statusValue.textContent = 'Не настроен';
}
} catch (e) {
statusValue.textContent = 'Ошибка загрузки';
}
}
function showMessage(type, text) {
message.className = `message show ${type}`;
messageIcon.textContent = type === 'success' ? '✓' : '✕';
messageText.textContent = text;
}
function setLoading(loading) {
submitBtn.disabled = loading;
if (loading) {
btnText.innerHTML = '<div class="spinner"></div>';
} else {
btnText.textContent = 'Применить';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value.trim();
if (!url) {
showMessage('error', 'Введите URL');
return;
}
message.className = 'message';
setLoading(true);
try {
const res = await fetch('/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await res.json();
if (data.success) {
showMessage('success', data.message || 'Конфигурация применена!');
urlInput.value = '';
fetchStatus();
} else {
showMessage('error', data.error || 'Произошла ошибка');
}
} catch (e) {
showMessage('error', `Ошибка сети: ${e.message}`);
} finally {
setLoading(false);
}
});
// Initial load
fetchStatus();
// Refresh status every 30 seconds
setInterval(fetchStatus, 30000);
// Update connection links
const hostname = window.location.hostname;
const httpLink = document.getElementById('httpLink');
const socksLink = document.getElementById('socksLink');
httpLink.textContent = `http://${hostname}:8082`;
socksLink.textContent = `socks5://${hostname}:8082`;
// Copy to clipboard on click
[httpLink, socksLink].forEach(el => {
el.addEventListener('click', () => {
navigator.clipboard.writeText(el.textContent);
const original = el.style.color;
el.style.color = 'var(--success)';
setTimeout(() => el.style.color = original, 500);
});
});
</script>
</body>
</html>