Files
OutFleet/static/admin.html
2025-09-18 02:56:59 +03:00

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>