mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 09:19:09 +00:00
1314 lines
48 KiB
HTML
1314 lines
48 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Xray Admin Panel</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
background: #f5f5f7;
|
|
color: #1d1d1f;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.layout {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 260px;
|
|
background: white;
|
|
border-right: 1px solid #d2d2d7;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 0 20px 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.sidebar-header h1 {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #1d1d1f;
|
|
}
|
|
|
|
.nav-menu {
|
|
list-style: none;
|
|
}
|
|
|
|
.nav-item {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.nav-link {
|
|
display: block;
|
|
padding: 12px 20px;
|
|
color: #424245;
|
|
text-decoration: none;
|
|
font-weight: 400;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.nav-link:hover,
|
|
.nav-link.active {
|
|
background: #f0f0f0;
|
|
color: #0071e3;
|
|
}
|
|
|
|
.main-content {
|
|
flex: 1;
|
|
padding: 30px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: #6e6e73;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card-header {
|
|
margin-bottom: 20px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card-subtitle {
|
|
color: #6e6e73;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
text-align: center;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #0071e3;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #0077ed;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f5f5f7;
|
|
color: #1d1d1f;
|
|
border: 1px solid #d2d2d7;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #e8e8ed;
|
|
}
|
|
|
|
.btn-success {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: #218838;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #c82333;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
color: #0071e3;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #6e6e73;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.table th,
|
|
.table td {
|
|
text-align: left;
|
|
padding: 12px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.table th {
|
|
font-weight: 600;
|
|
color: #6e6e73;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.status-online {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.status-offline {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.status-unknown {
|
|
background: #e2e3e5;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.status-success {
|
|
color: #28a745;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-error {
|
|
color: #dc3545;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-running {
|
|
color: #007bff;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-idle {
|
|
color: #6c757d;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.task-stats {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.success-rate {
|
|
margin-top: 2px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.success-rate-good {
|
|
color: #28a745;
|
|
}
|
|
|
|
.success-rate-ok {
|
|
color: #ffc107;
|
|
}
|
|
|
|
.success-rate-bad {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.page-section {
|
|
display: none;
|
|
}
|
|
|
|
.page-section.active {
|
|
display: block;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
color: #1d1d1f;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #d2d2d7;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: #0071e3;
|
|
}
|
|
|
|
.form-select {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #d2d2d7;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
background: white;
|
|
}
|
|
|
|
.alert {
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.alert.show {
|
|
display: block;
|
|
}
|
|
|
|
.alert-success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.alert-error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #6e6e73;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
margin-bottom: 8px;
|
|
color: #1d1d1f;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #6e6e73;
|
|
}
|
|
|
|
/* Modal styles */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 12px;
|
|
max-width: 800px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
width: 90%;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-header h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 20px;
|
|
}
|
|
|
|
.server-section {
|
|
margin-bottom: 30px;
|
|
padding: 15px;
|
|
border: 1px solid #f0f0f0;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.server-section h3 {
|
|
margin: 0 0 15px 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #1d1d1f;
|
|
}
|
|
|
|
.inbound-item {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.inbound-item label {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.inbound-item label:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.inbound-item input[type="checkbox"] {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.user-details {
|
|
margin-top: 30px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.user-details label {
|
|
display: block;
|
|
margin-bottom: 15px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.user-details input {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #d2d2d7;
|
|
border-radius: 4px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.input-group input {
|
|
margin-top: 0;
|
|
border-radius: 4px 0 0 4px;
|
|
border-right: none;
|
|
}
|
|
|
|
.refresh-btn {
|
|
background: #f8f9fa;
|
|
border: 1px solid #d2d2d7;
|
|
border-radius: 0 4px 4px 0;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.modal-actions {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>Xray Admin</h1>
|
|
</div>
|
|
<nav>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="#dashboard" class="nav-link active" onclick="showPage('dashboard')">Dashboard</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="#servers" class="nav-link" onclick="showPage('servers')">Servers</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="#templates" class="nav-link" onclick="showPage('templates')">Templates</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="#certificates" class="nav-link" onclick="showPage('certificates')">Certificates</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="#users" class="nav-link" onclick="showPage('users')">Users</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="#tasks" class="nav-link" onclick="showPage('tasks')">Tasks</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<div id="alert" class="alert"></div>
|
|
|
|
<!-- Dashboard -->
|
|
<section id="dashboard" class="page-section active">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Dashboard</h1>
|
|
<p class="page-subtitle">Overview of your Xray infrastructure</p>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="totalServers">-</div>
|
|
<div class="stat-label">Total Servers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="onlineServers">-</div>
|
|
<div class="stat-label">Online Servers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="totalCertificates">-</div>
|
|
<div class="stat-label">Certificates</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="totalUsers">-</div>
|
|
<div class="stat-label">Users</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Recent Activity</h2>
|
|
<p class="card-subtitle">Latest server status and activity</p>
|
|
</div>
|
|
<div id="recentActivity" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Servers -->
|
|
<section id="servers" class="page-section">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Servers</h1>
|
|
<p class="page-subtitle">Manage your Xray server instances</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Add New Server</h2>
|
|
<p class="card-subtitle">Register a new Xray server instance</p>
|
|
</div>
|
|
<form id="serverForm" class="form-grid">
|
|
<div class="form-group">
|
|
<label class="form-label" for="serverName">Server Name *</label>
|
|
<input type="text" id="serverName" class="form-input" placeholder="Enter server name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="serverHostname">Hostname *</label>
|
|
<input type="text" id="serverHostname" class="form-input" placeholder="server.example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="serverPort">gRPC Port</label>
|
|
<input type="number" id="serverPort" class="form-input" placeholder="2053">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="serverCredentials">API Credentials</label>
|
|
<input type="text" id="serverCredentials" class="form-input" placeholder="Optional credentials">
|
|
</div>
|
|
<div class="form-group">
|
|
<button type="submit" class="btn btn-primary">Add Server</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Servers List</h2>
|
|
</div>
|
|
<div id="serversTable" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Templates -->
|
|
<section id="templates" class="page-section">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Inbound Templates</h1>
|
|
<p class="page-subtitle">Manage reusable inbound configurations</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Templates List</h2>
|
|
</div>
|
|
<div id="templatesTable" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Certificates -->
|
|
<section id="certificates" class="page-section">
|
|
<div class="page-header">
|
|
<h1 class="page-title">SSL Certificates</h1>
|
|
<p class="page-subtitle">Manage SSL/TLS certificates for your servers</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Certificates List</h2>
|
|
</div>
|
|
<div id="certificatesTable" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Users -->
|
|
<section id="users" class="page-section">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Users</h1>
|
|
<p class="page-subtitle">Manage user accounts and access</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Users List</h2>
|
|
</div>
|
|
<div id="usersTable" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="tasks" class="page-section">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Background Tasks</h1>
|
|
<p class="page-subtitle">Monitor scheduled tasks and background jobs</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Active Tasks</h2>
|
|
<button class="button button-outline" onclick="refreshTasks()">
|
|
<span class="icon">🔄</span>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
<div id="tasksTable" class="loading">Loading...</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let currentPage = 'dashboard';
|
|
|
|
// Navigation
|
|
function showPage(page) {
|
|
// Hide all pages
|
|
document.querySelectorAll('.page-section').forEach(section => {
|
|
section.classList.remove('active');
|
|
});
|
|
|
|
// Remove active class from all nav links
|
|
document.querySelectorAll('.nav-link').forEach(link => {
|
|
link.classList.remove('active');
|
|
});
|
|
|
|
// Show selected page
|
|
document.getElementById(page).classList.add('active');
|
|
document.querySelector(`[onclick="showPage('${page}')"]`).classList.add('active');
|
|
|
|
currentPage = page;
|
|
|
|
// Load page data
|
|
loadPageData(page);
|
|
}
|
|
|
|
function loadPageData(page) {
|
|
switch(page) {
|
|
case 'dashboard':
|
|
loadDashboard();
|
|
break;
|
|
case 'servers':
|
|
loadServers();
|
|
break;
|
|
case 'templates':
|
|
loadTemplates();
|
|
break;
|
|
case 'certificates':
|
|
loadCertificates();
|
|
break;
|
|
case 'users':
|
|
loadUsers();
|
|
break;
|
|
case 'tasks':
|
|
loadTasks();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Dashboard
|
|
async function loadDashboard() {
|
|
try {
|
|
// Load statistics
|
|
const [servers, certificates, users] = await Promise.all([
|
|
fetch(`${API_BASE}/servers`).then(r => r.json()),
|
|
fetch(`${API_BASE}/certificates`).then(r => r.json()),
|
|
fetch(`${API_BASE}/users`).then(r => r.json())
|
|
]);
|
|
|
|
document.getElementById('totalServers').textContent = servers.length;
|
|
document.getElementById('onlineServers').textContent = servers.filter(s => s.status === 'online').length;
|
|
document.getElementById('totalCertificates').textContent = certificates.length;
|
|
document.getElementById('totalUsers').textContent = users.users ? users.users.length : users.length;
|
|
|
|
// Recent activity
|
|
const activity = servers.slice(0, 5).map(server =>
|
|
`<p>Server "${server.name}" is ${server.status}</p>`
|
|
).join('');
|
|
|
|
document.getElementById('recentActivity').innerHTML = activity || '<p>No recent activity</p>';
|
|
} catch (error) {
|
|
showAlert('Error loading dashboard: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Servers
|
|
async function loadServers() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/servers`);
|
|
const servers = await response.json();
|
|
|
|
if (servers.length === 0) {
|
|
document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>No servers found</h3><p>Add your first server to get started</p></div>';
|
|
return;
|
|
}
|
|
|
|
const table = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Hostname</th>
|
|
<th>Port</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${servers.map(server => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(server.name)}</strong></td>
|
|
<td>${escapeHtml(server.hostname)}</td>
|
|
<td>${server.grpc_port}</td>
|
|
<td><span class="status-badge status-${server.status}">${server.status}</span></td>
|
|
<td>
|
|
<div class="actions">
|
|
<button class="btn btn-small btn-success" onclick="testConnection('${server.id}')">Test</button>
|
|
<button class="btn btn-small btn-danger" onclick="deleteServer('${server.id}', '${escapeHtml(server.name)}')">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('serversTable').innerHTML = table;
|
|
} catch (error) {
|
|
document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>Error loading servers</h3><p>' + error.message + '</p></div>';
|
|
}
|
|
}
|
|
|
|
// Templates
|
|
async function loadTemplates() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/templates`);
|
|
const templates = await response.json();
|
|
|
|
if (templates.length === 0) {
|
|
document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>No templates found</h3><p>Templates help you quickly configure server inbounds</p></div>';
|
|
return;
|
|
}
|
|
|
|
const table = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Protocol</th>
|
|
<th>Default Port</th>
|
|
<th>TLS Required</th>
|
|
<th>Active</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${templates.map(template => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(template.name)}</strong></td>
|
|
<td>${template.protocol}</td>
|
|
<td>${template.default_port}</td>
|
|
<td>${template.requires_tls ? 'Yes' : 'No'}</td>
|
|
<td><span class="status-badge ${template.is_active ? 'status-online' : 'status-offline'}">${template.is_active ? 'Active' : 'Inactive'}</span></td>
|
|
<td>
|
|
<div class="actions">
|
|
<button class="btn btn-small btn-secondary">Edit</button>
|
|
<button class="btn btn-small btn-danger">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('templatesTable').innerHTML = table;
|
|
} catch (error) {
|
|
document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>Error loading templates</h3><p>' + error.message + '</p></div>';
|
|
}
|
|
}
|
|
|
|
// Certificates
|
|
async function loadCertificates() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/certificates`);
|
|
const certificates = await response.json();
|
|
|
|
if (certificates.length === 0) {
|
|
document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>No certificates found</h3><p>Upload or generate SSL certificates for your servers</p></div>';
|
|
return;
|
|
}
|
|
|
|
const table = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Domain</th>
|
|
<th>Type</th>
|
|
<th>Expires</th>
|
|
<th>Auto Renew</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${certificates.map(cert => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(cert.name)}</strong></td>
|
|
<td>${escapeHtml(cert.domain)}</td>
|
|
<td>${cert.cert_type}</td>
|
|
<td>${new Date(cert.expires_at).toLocaleDateString()}</td>
|
|
<td>${cert.auto_renew ? 'Yes' : 'No'}</td>
|
|
<td>
|
|
<div class="actions">
|
|
<button class="btn btn-small btn-secondary">View</button>
|
|
<button class="btn btn-small btn-danger">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('certificatesTable').innerHTML = table;
|
|
} catch (error) {
|
|
document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>Error loading certificates</h3><p>' + error.message + '</p></div>';
|
|
}
|
|
}
|
|
|
|
// Users
|
|
async function loadUsers() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/users`);
|
|
const data = await response.json();
|
|
const users = data.users || data;
|
|
|
|
if (users.length === 0) {
|
|
document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>No users found</h3><p>Create user accounts for access management</p></div>';
|
|
return;
|
|
}
|
|
|
|
const table = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Comment</th>
|
|
<th>Telegram ID</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${users.map(user => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(user.name)}</strong></td>
|
|
<td>${escapeHtml(user.comment || '-')}</td>
|
|
<td>${user.telegram_id || '-'}</td>
|
|
<td>${new Date(user.created_at).toLocaleDateString()}</td>
|
|
<td>
|
|
<div class="actions">
|
|
<button class="btn btn-small btn-secondary" onclick="editUserAccess('${user.id}', '${escapeHtml(user.name)}')">Manage Access</button>
|
|
<button class="btn btn-small btn-danger">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('usersTable').innerHTML = table;
|
|
} catch (error) {
|
|
document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>Error loading users</h3><p>' + error.message + '</p></div>';
|
|
}
|
|
}
|
|
|
|
// Server form submission
|
|
document.getElementById('serverForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const name = document.getElementById('serverName').value.trim();
|
|
const hostname = document.getElementById('serverHostname').value.trim();
|
|
const grpc_port = document.getElementById('serverPort').value;
|
|
const api_credentials = document.getElementById('serverCredentials').value.trim();
|
|
|
|
if (!name || !hostname) {
|
|
showAlert('Name and hostname are required', 'error');
|
|
return;
|
|
}
|
|
|
|
const serverData = { name, hostname };
|
|
if (grpc_port) serverData.grpc_port = parseInt(grpc_port);
|
|
if (api_credentials) serverData.api_credentials = api_credentials;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/servers`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(serverData)
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to create server');
|
|
|
|
showAlert('Server created successfully', 'success');
|
|
document.getElementById('serverForm').reset();
|
|
loadServers();
|
|
} catch (error) {
|
|
showAlert('Error creating server: ' + error.message, 'error');
|
|
}
|
|
});
|
|
|
|
// Server actions
|
|
async function testConnection(serverId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/servers/${serverId}/test`, {
|
|
method: 'POST'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.connected) {
|
|
showAlert('Connection successful!', 'success');
|
|
} else {
|
|
showAlert('Connection failed', 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert('Error testing connection: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteServer(serverId, serverName) {
|
|
if (!confirm(`Are you sure you want to delete server "${serverName}"?`)) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/servers/${serverId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete server');
|
|
|
|
showAlert('Server deleted successfully', 'success');
|
|
loadServers();
|
|
} catch (error) {
|
|
showAlert('Error deleting server: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
function showAlert(message, type) {
|
|
const alert = document.getElementById('alert');
|
|
alert.textContent = message;
|
|
alert.className = `alert alert-${type} show`;
|
|
|
|
setTimeout(() => {
|
|
alert.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// User Access Management
|
|
async function editUserAccess(userId, userName) {
|
|
try {
|
|
// Load servers and their inbounds
|
|
const serversResponse = await fetch(`${API_BASE}/servers`);
|
|
const servers = await serversResponse.json();
|
|
|
|
let modalContent = `
|
|
<div class="modal-overlay" onclick="closeUserAccessModal()">
|
|
<div class="modal-content" onclick="event.stopPropagation()">
|
|
<div class="modal-header">
|
|
<h2>Manage Access: ${escapeHtml(userName)}</h2>
|
|
<button class="btn btn-small" onclick="closeUserAccessModal()">Close</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="userAccessForm">
|
|
<input type="hidden" id="editUserId" value="${userId}">
|
|
`;
|
|
|
|
for (const server of servers) {
|
|
modalContent += `
|
|
<div class="server-section">
|
|
<h3>${escapeHtml(server.name)} (${escapeHtml(server.hostname)}:${server.grpc_port})</h3>
|
|
`;
|
|
|
|
try {
|
|
const inboundsResponse = await fetch(`${API_BASE}/servers/${server.id}/inbounds`);
|
|
const inbounds = await inboundsResponse.json();
|
|
|
|
if (inbounds.length > 0) {
|
|
for (const inbound of inbounds) {
|
|
modalContent += `
|
|
<div class="inbound-item">
|
|
<label>
|
|
<input type="checkbox" name="access"
|
|
value="${server.id}|${inbound.id}"
|
|
data-server-id="${server.id}"
|
|
data-inbound-id="${inbound.id}"
|
|
data-server-name="${escapeHtml(server.name)}"
|
|
data-inbound-port="${inbound.port_override || 'default'}">
|
|
${escapeHtml(inbound.template_name || 'Unknown Template')}
|
|
</label>
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
modalContent += '<p>No inbounds configured</p>';
|
|
}
|
|
} catch (error) {
|
|
modalContent += '<p>Error loading inbounds</p>';
|
|
}
|
|
|
|
modalContent += '</div>';
|
|
}
|
|
|
|
modalContent += `
|
|
<div class="user-details">
|
|
<label>Email for Xray:
|
|
<input type="email" id="xrayEmail" placeholder="user@example.com" required>
|
|
</label>
|
|
<label>User ID (UUID for VLESS/VMess, password for Trojan):
|
|
<div class="input-group">
|
|
<input type="text" id="xrayUserId" placeholder="Generated UUID" required>
|
|
<button type="button" class="refresh-btn" onclick="generateUUID()" title="Generate new UUID">⟳</button>
|
|
</div>
|
|
</label>
|
|
<label>Level (0-255):
|
|
<input type="number" id="xrayLevel" min="0" max="255" value="0">
|
|
</label>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="submit" class="btn btn-primary">Save Access</button>
|
|
<button type="button" class="btn btn-secondary" onclick="closeUserAccessModal()">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalContent);
|
|
|
|
// Generate initial UUID
|
|
generateUUID();
|
|
|
|
// Handle form submission
|
|
document.getElementById('userAccessForm').addEventListener('submit', saveUserAccess);
|
|
|
|
} catch (error) {
|
|
showAlert('Error loading server data: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function closeUserAccessModal() {
|
|
const modal = document.querySelector('.modal-overlay');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
function generateUUID() {
|
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
document.getElementById('xrayUserId').value = uuid;
|
|
}
|
|
|
|
async function saveUserAccess(event) {
|
|
event.preventDefault();
|
|
|
|
const userId = document.getElementById('editUserId').value;
|
|
const email = document.getElementById('xrayEmail').value;
|
|
const xrayUserId = document.getElementById('xrayUserId').value;
|
|
const level = parseInt(document.getElementById('xrayLevel').value) || 0;
|
|
|
|
const checkedAccess = document.querySelectorAll('input[name="access"]:checked');
|
|
|
|
if (checkedAccess.length === 0) {
|
|
showAlert('Please select at least one server/inbound', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!email || !xrayUserId) {
|
|
showAlert('Please fill in all required fields', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
for (const checkbox of checkedAccess) {
|
|
const serverId = checkbox.dataset.serverId;
|
|
const inboundId = checkbox.dataset.inboundId;
|
|
|
|
const userData = {
|
|
email: email,
|
|
id: xrayUserId,
|
|
level: level
|
|
};
|
|
|
|
const response = await fetch(`${API_BASE}/servers/${serverId}/inbounds/${inboundId}/users`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(userData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to add user to ${checkbox.dataset.serverName} inbound ${checkbox.dataset.inboundPort}`);
|
|
}
|
|
}
|
|
|
|
showAlert('User access saved successfully', 'success');
|
|
closeUserAccessModal();
|
|
|
|
} catch (error) {
|
|
showAlert('Error saving user access: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Tasks
|
|
async function loadTasks() {
|
|
try {
|
|
// TODO: Add API endpoint for tasks when ready
|
|
// For now, show static task information
|
|
const tasksData = [
|
|
{
|
|
id: "xray_sync",
|
|
name: "Xray Synchronization",
|
|
description: "Synchronizes database state with xray servers",
|
|
schedule: "0 * * * * * (every minute)",
|
|
status: "running",
|
|
lastRun: new Date().toISOString(),
|
|
nextRun: new Date(Date.now() + 60000).toISOString(),
|
|
totalRuns: Math.floor(Math.random() * 50) + 10,
|
|
successCount: Math.floor(Math.random() * 45) + 8,
|
|
errorCount: Math.floor(Math.random() * 3),
|
|
lastDurationMs: Math.floor(Math.random() * 2000) + 500,
|
|
lastError: null
|
|
}
|
|
];
|
|
|
|
if (tasksData.length === 0) {
|
|
document.getElementById('tasksTable').innerHTML = '<div class="empty-state"><h3>No tasks configured</h3><p>Background tasks will appear here when configured</p></div>';
|
|
return;
|
|
}
|
|
|
|
const table = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Task Name</th>
|
|
<th>Schedule</th>
|
|
<th>Status</th>
|
|
<th>Last Run</th>
|
|
<th>Next Run</th>
|
|
<th>Performance</th>
|
|
<th>Duration</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${tasksData.map(task => {
|
|
const statusIcon = getTaskStatusIcon(task.status);
|
|
const successRate = task.totalRuns > 0 ? Math.round((task.successCount / task.totalRuns) * 100) : 0;
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<strong>${escapeHtml(task.name)}</strong>
|
|
<br><small class="text-muted">${escapeHtml(task.description)}</small>
|
|
</td>
|
|
<td><code>${escapeHtml(task.schedule)}</code></td>
|
|
<td>
|
|
<span class="status ${getTaskStatusClass(task.status)}">
|
|
${statusIcon} ${task.status}
|
|
</span>
|
|
${task.lastError ? `<br><small class="text-muted">Error: ${escapeHtml(task.lastError.substring(0, 50))}...</small>` : ''}
|
|
</td>
|
|
<td>${task.lastRun ? new Date(task.lastRun).toLocaleString() : '-'}</td>
|
|
<td>${task.nextRun ? new Date(task.nextRun).toLocaleString() : '-'}</td>
|
|
<td>
|
|
<div class="task-stats">
|
|
<div>✅ ${task.successCount} / ❌ ${task.errorCount}</div>
|
|
<div class="success-rate ${successRate >= 95 ? 'success-rate-good' : successRate >= 80 ? 'success-rate-ok' : 'success-rate-bad'}">
|
|
${successRate}% success
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${task.lastDurationMs ? task.lastDurationMs + 'ms' : '-'}</td>
|
|
</tr>
|
|
`}).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
document.getElementById('tasksTable').innerHTML = table;
|
|
|
|
} catch (error) {
|
|
document.getElementById('tasksTable').innerHTML = '<div class="error">Failed to load tasks: ' + error.message + '</div>';
|
|
}
|
|
}
|
|
|
|
function getTaskStatusIcon(status) {
|
|
switch(status) {
|
|
case 'running': return '🔄';
|
|
case 'success': return '✅';
|
|
case 'error': return '❌';
|
|
case 'idle': return '⏸️';
|
|
default: return '❓';
|
|
}
|
|
}
|
|
|
|
function getTaskStatusClass(status) {
|
|
switch(status) {
|
|
case 'running': return 'status-running';
|
|
case 'success': return 'status-success';
|
|
case 'error': return 'status-error';
|
|
case 'idle': return 'status-idle';
|
|
default: return 'status-unknown';
|
|
}
|
|
}
|
|
|
|
function refreshTasks() {
|
|
document.getElementById('tasksTable').innerHTML = '<div class="loading">Loading...</div>';
|
|
loadTasks();
|
|
}
|
|
|
|
// Initialize
|
|
loadPageData('dashboard');
|
|
</script>
|
|
</body>
|
|
</html> |