TorProxy / dashboard.html
Jishnupg
Deploy VLESS fix sing-box startup crash
b0d7f94
Raw
History Blame Contribute Delete
24.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>TorProxy VPN Console</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
:root {
--bg-base: #030303;
--panel-bg: rgba(9, 9, 12, 0.7);
--panel-border: rgba(255, 90, 31, 0.08);
--primary: #FF5A1F;
--primary-glow: rgba(255, 90, 31, 0.2);
--accent: #FF9E00;
--success: #00E676;
--danger: #FF1744;
--text-primary: #F4F4F5;
--text-secondary: #A1A1AA;
--text-muted: #52525B;
--font-mono: 'JetBrains Mono', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-base);
background-image:
radial-gradient(circle at 50% 0%, rgba(255, 90, 31, 0.12) 0%, transparent 60%),
radial-gradient(circle at 0% 100%, rgba(255, 158, 0, 0.04) 0%, transparent 40%);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px 20px 40px;
}
.container {
width: 100%;
max-width: 520px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Glassmorphic Panel */
.glass-panel {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 24px;
padding: 24px;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.6);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.glass-panel:hover {
border-color: rgba(255, 90, 31, 0.16);
box-shadow: 0 14px 45px 0 rgba(255, 90, 31, 0.05);
}
/* Header Style */
.header {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 12px 0 4px;
}
.brand-pill {
background: rgba(255, 90, 31, 0.08);
border: 1px solid rgba(255, 90, 31, 0.2);
padding: 6px 14px;
border-radius: 100px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
color: var(--primary);
text-transform: uppercase;
margin-bottom: 12px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.brand-pill .pulse-dot {
width: 8px;
height: 8px;
background-color: var(--success);
border-radius: 50%;
animation: pulse-glow 1.5s infinite alternate;
}
@keyframes pulse-glow {
from { box-shadow: 0 0 2px var(--success), 0 0 4px var(--success); }
to { box-shadow: 0 0 8px var(--success), 0 0 12px var(--success); }
}
.header-title {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.8px;
background: linear-gradient(135deg, #ffffff 40%, var(--text-secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-subtitle {
font-size: 13px;
color: var(--text-secondary);
margin-top: 6px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.stat-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: 800;
color: #fff;
}
/* VLESS Config Card */
.config-card {
display: flex;
flex-direction: column;
gap: 16px;
}
.section-title {
font-size: 14px;
font-weight: 700;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 8px;
margin-bottom: 4px;
}
.link-container {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 90, 31, 0.15);
border-radius: 16px;
padding: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.vless-link-text {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
}
.copy-btn {
background: var(--primary);
border: none;
color: #fff;
padding: 8px 16px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.copy-btn:hover {
opacity: 0.9;
box-shadow: 0 0 12px rgba(255, 90, 31, 0.3);
}
.actions-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #fff;
padding: 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.15);
}
/* QR Code Modal/Display */
.qr-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0;
}
#qrcode {
background: #fff;
padding: 16px;
border-radius: 16px;
display: inline-block;
}
/* Console/Logs */
.console-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.console-screen {
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 16px;
height: 180px;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 11px;
color: #a3e635; /* Terminal green */
white-space: pre-wrap;
line-height: 1.5;
}
/* Setup Guides */
.guide-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 8px;
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
cursor: pointer;
padding: 4px 8px;
transition: color 0.2s;
}
.tab-btn.active {
color: var(--primary);
border-bottom: 2px solid var(--primary);
}
.tab-content {
display: none;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.tab-content.active {
display: block;
}
.tab-content ol {
padding-left: 20px;
margin-top: 8px;
}
.tab-content li {
margin-bottom: 6px;
}
/* Config Form */
.config-form {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.input-group {
display: flex;
gap: 8px;
}
.input-group input {
flex-grow: 1;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 10px;
color: #fff;
font-size: 13px;
}
.input-group input:focus {
outline: none;
border-color: var(--primary);
}
.alert-popup {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: rgba(255, 90, 31, 0.95);
color: #fff;
padding: 12px 24px;
border-radius: 100px;
font-size: 13px;
font-weight: 700;
box-shadow: 0 10px 30px rgba(255, 90, 31, 0.3);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1000;
}
.alert-popup.show {
transform: translateX(-50%) translateY(0);
}
/* Login Overlay */
#login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #030303;
background-image: radial-gradient(circle at 50% 30%, rgba(255, 90, 31, 0.1) 0%, transparent 60%);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.login-card {
background: rgba(9, 9, 12, 0.8);
border: 1px solid rgba(255, 90, 31, 0.1);
border-radius: 24px;
padding: 40px;
width: 100%;
max-width: 360px;
box-shadow: 0 20px 40px rgba(0,0,0,0.5);
backdrop-filter: blur(20px);
text-align: center;
}
.login-card h2 {
margin-bottom: 8px;
font-weight: 800;
color: #fff;
letter-spacing: -0.5px;
}
.login-card p {
font-size: 13px;
color: #a1a1aa;
margin-bottom: 24px;
}
.login-card input[type="password"] {
width: 100%;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 90, 31, 0.2);
border-radius: 12px;
padding: 14px;
color: #fff;
box-sizing: border-box;
margin-bottom: 16px;
font-size: 14px;
transition: all 0.2s;
}
.login-card input[type="password"]:focus {
outline: none;
border-color: #FF5A1F;
box-shadow: 0 0 10px rgba(255, 90, 31, 0.2);
}
.login-card button {
width: 100%;
background: #FF5A1F;
border: none;
border-radius: 12px;
padding: 14px;
color: #fff;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: opacity 0.2s;
}
.login-card button:hover {
opacity: 0.9;
}
.login-card .error {
color: #ff1744;
font-size: 12px;
margin-bottom: 16px;
font-weight: 600;
}
</style>
</head>
<body>
<!-- Login Lockout Layer -->
<div id="login-overlay" style="display: none;">
<div class="login-card">
<h2>TorProxy VPN</h2>
<p>Enter your administration password</p>
<div class="error" id="login-error"></div>
<form onsubmit="handleLogin(event)">
<input type="password" id="login-password" placeholder="Password" required autofocus>
<button type="submit">Unlock Console</button>
</form>
</div>
</div>
<div class="container">
<!-- Brand Header -->
<div class="header">
<div class="brand-pill">
<span class="pulse-dot"></span>
VLESS VPN Service
</div>
<div class="header-title">TorProxy Console</div>
<div class="header-subtitle">Secure, fast, and permanent WebSockets-over-TLS</div>
</div>
<!-- Health / Stats -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">CPU Usage</span>
<span class="stat-value" id="cpu-val">0.0%</span>
</div>
<div class="stat-card">
<span class="stat-label">RAM Usage</span>
<span class="stat-value" id="ram-val">0.0%</span>
</div>
</div>
<!-- Connection Config Panel -->
<div class="glass-panel config-card">
<h3 class="section-title">VLESS Connection Profile</h3>
<div class="link-container">
<div class="vless-link-text" id="vless-link">vless://loading...</div>
<button class="copy-btn" onclick="copyLink()">Copy Link</button>
</div>
<div class="actions-row">
<button class="btn-secondary" onclick="toggleQRCode()">Show QR Code</button>
<button class="btn-secondary" onclick="resetUUID()">Regen VLESS UUID</button>
</div>
<div class="qr-wrapper" id="qr-wrapper" style="display: none;">
<div id="qrcode"></div>
</div>
</div>
<!-- Configuration Settings Panel -->
<div class="glass-panel">
<h3 class="section-title">Server Routing Settings</h3>
<p style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;">
Specify a custom SNI host if you are using a custom domain. Leave blank to use default.
</p>
<form class="config-form" onsubmit="saveConfig(event)">
<div class="input-group">
<input type="text" id="custom-sni" placeholder="e.g. yourdomain.com">
<button type="submit" class="copy-btn">Save SNI</button>
</div>
</form>
</div>
<!-- Client Setup Guide -->
<div class="glass-panel">
<h3 class="section-title">Client Setup Guides</h3>
<div class="guide-tabs">
<button class="tab-btn active" onclick="switchTab('hiddify')">Hiddify</button>
<button class="tab-btn" onclick="switchTab('v2rayng')">v2rayNG</button>
<button class="tab-btn" onclick="switchTab('shadowrocket')">Shadowrocket</button>
</div>
<div class="tab-content active" id="guide-hiddify">
<p><strong>Universal (Android, iOS, Windows, Mac):</strong></p>
<ol>
<li>Install the <strong>Hiddify</strong> app from the Google Play Store or App Store.</li>
<li>Copy the VLESS link above to your clipboard.</li>
<li>Open Hiddify and click <strong>Add Profile</strong> or the <strong>+</strong> button.</li>
<li>Select <strong>Import from Clipboard</strong>.</li>
<li>Tap the big circle button to connect!</li>
</ol>
</div>
<div class="tab-content" id="guide-v2rayng">
<p><strong>Android Client:</strong></p>
<ol>
<li>Install <strong>v2rayNG</strong> from the Play Store.</li>
<li>Copy the VLESS link above.</li>
<li>Open the app, tap the <strong>+</strong> icon at the top, and select <strong>Import config from Clipboard</strong>.</li>
<li>Tap the imported config to select it, then tap the V button at the bottom right to connect.</li>
</ol>
</div>
<div class="tab-content" id="guide-shadowrocket">
<p><strong>iOS/macOS Client:</strong></p>
<ol>
<li>Open <strong>Shadowrocket</strong> on your iOS device.</li>
<li>Copy the VLESS link or tap "Show QR Code" and scan it using the scanner icon at the top left.</li>
<li>The app will automatically import the profile.</li>
<li>Turn on the connection switch at the top.</li>
</ol>
</div>
</div>
<!-- Live Server Console -->
<div class="glass-panel console-panel">
<h3 class="section-title">Live Sing-box Engine Logs</h3>
<div class="console-screen" id="log-console">Initializing log stream...</div>
</div>
</div>
<!-- Alert Popup Notification -->
<div class="alert-popup" id="alert-popup">Copied to Clipboard!</div>
<script>
let currentLink = "";
function getAuthToken() {
return localStorage.getItem("torproxy_token") || "";
}
function setAuthToken(token) {
localStorage.setItem("torproxy_token", token);
}
function clearAuthToken() {
localStorage.removeItem("torproxy_token");
}
function checkAuth() {
const token = getAuthToken();
const overlay = document.getElementById("login-overlay");
if (!token) {
overlay.style.display = "flex";
} else {
overlay.style.display = "none";
updateStats();
}
}
function handleLogin(e) {
e.preventDefault();
const password = document.getElementById("login-password").value;
const errorDiv = document.getElementById("login-error");
errorDiv.innerText = "";
fetch("/login", {
method: "POST",
body: "password=" + encodeURIComponent(password),
headers: { "Content-Type": "application/x-www-form-urlencoded" }
})
.then(r => r.json())
.then(data => {
if (data.success && data.token) {
setAuthToken(data.token);
checkAuth();
} else {
errorDiv.innerText = data.error || "Login failed";
}
})
.catch(() => {
errorDiv.innerText = "Connection error";
});
}
function showAlert(msg) {
const popup = document.getElementById("alert-popup");
popup.innerText = msg;
popup.classList.add("show");
setTimeout(() => {
popup.classList.remove("show");
}, 2000);
}
function copyLink() {
navigator.clipboard.writeText(currentLink).then(() => {
showAlert("VLESS Link Copied!");
}).catch(() => {
showAlert("Failed to copy link");
});
}
function switchTab(client) {
document.querySelectorAll(".tab-btn").forEach(btn => btn.classList.remove("active"));
document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
event.target.classList.add("active");
document.getElementById("guide-" + client).classList.add("active");
}
function toggleQRCode() {
const wrapper = document.getElementById("qr-wrapper");
if (wrapper.style.display === "none") {
wrapper.style.display = "flex";
generateQR();
} else {
wrapper.style.display = "none";
}
}
function generateQR() {
const qrContainer = document.getElementById("qrcode");
qrContainer.innerHTML = "";
if (currentLink) {
new QRCode(qrContainer, {
text: currentLink,
width: 200,
height: 200,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
}
}
function resetUUID() {
if (!confirm("Are you sure you want to regenerate the VLESS UUID? This will disconnect all current users.")) return;
fetch("/api/uuid/reset?token=" + encodeURIComponent(getAuthToken()), { method: "POST" })
.then(r => {
if (r.status === 401) { clearAuthToken(); checkAuth(); return; }
return r.json();
})
.then(data => {
if (data && data.success) {
showAlert("UUID Regenerated successfully!");
updateStats();
}
});
}
function saveConfig(e) {
e.preventDefault();
const customSni = document.getElementById("custom-sni").value;
fetch("/api/config/update?token=" + encodeURIComponent(getAuthToken()), {
method: "POST",
body: "custom_sni=" + encodeURIComponent(customSni),
headers: { "Content-Type": "application/x-www-form-urlencoded" }
})
.then(r => {
if (r.status === 401) { clearAuthToken(); checkAuth(); return; }
return r.json();
})
.then(data => {
if (data && data.success) {
showAlert("SNI Config Saved!");
updateStats();
}
});
}
function updateStats() {
const token = getAuthToken();
if (!token) return;
fetch("/api/status?token=" + encodeURIComponent(token))
.then(r => {
if (r.status === 401) {
clearAuthToken();
checkAuth();
throw new Error("Unauthorized");
}
return r.json();
})
.then(data => {
document.getElementById("cpu-val").innerText = data.cpu + "%";
document.getElementById("ram-val").innerText = data.ram + "%";
document.getElementById("vless-link").innerText = data.vless_link;
document.getElementById("custom-sni").value = data.custom_sni || "";
currentLink = data.vless_link;
const consoleScreen = document.getElementById("log-console");
const isScrolledToBottom = consoleScreen.scrollHeight - consoleScreen.clientHeight <= consoleScreen.scrollTop + 20;
consoleScreen.innerText = data.logs || "No logs available.";
if (isScrolledToBottom) {
consoleScreen.scrollTop = consoleScreen.scrollHeight;
}
const wrapper = document.getElementById("qr-wrapper");
if (wrapper.style.display !== "none") {
generateQR();
}
})
.catch(err => console.log("Fetch error:", err.message));
}
// Initialize auth check on load
checkAuth();
// Poll stats and logs every 3 seconds
setInterval(updateStats, 3000);
</script>
</body>
</html>