2025-09-18 02:56:59 +03:00
<!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 : 100 vh ;
}
. layout {
display : flex ;
min-height : 100 vh ;
}
. sidebar {
width : 260 px ;
background : white ;
border-right : 1 px solid #d2d2d7 ;
padding : 20 px 0 ;
}
. sidebar-header {
padding : 0 20 px 20 px ;
border-bottom : 1 px solid #f0f0f0 ;
margin-bottom : 20 px ;
}
. sidebar-header h1 {
font-size : 20 px ;
font-weight : 600 ;
color : #1d1d1f ;
}
. nav-menu {
list-style : none ;
}
. nav-item {
margin-bottom : 2 px ;
}
. nav-link {
display : block ;
padding : 12 px 20 px ;
color : #424245 ;
text-decoration : none ;
font-weight : 400 ;
transition : all 0.2 s ;
}
. nav-link : hover ,
. nav-link . active {
background : #f0f0f0 ;
color : #0071e3 ;
}
. main-content {
flex : 1 ;
padding : 30 px ;
overflow-y : auto ;
}
. page-header {
margin-bottom : 30 px ;
}
. page-title {
font-size : 32 px ;
font-weight : 600 ;
margin-bottom : 8 px ;
}
. page-subtitle {
color : #6e6e73 ;
font-size : 16 px ;
}
. card {
background : white ;
border-radius : 12 px ;
padding : 24 px ;
margin-bottom : 24 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. card-header {
margin-bottom : 20 px ;
padding-bottom : 16 px ;
border-bottom : 1 px solid #f0f0f0 ;
}
. card-title {
font-size : 18 px ;
font-weight : 600 ;
margin-bottom : 4 px ;
}
. card-subtitle {
color : #6e6e73 ;
font-size : 14 px ;
}
. btn {
padding : 10 px 20 px ;
border : none ;
border-radius : 8 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
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 : 1 px 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 : 6 px 12 px ;
font-size : 12 px ;
}
. stats-grid {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 250 px , 1 fr ) ) ;
gap : 20 px ;
margin-bottom : 30 px ;
}
. stat-card {
background : white ;
border-radius : 12 px ;
padding : 20 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. stat-value {
font-size : 32 px ;
font-weight : 700 ;
color : #0071e3 ;
margin-bottom : 4 px ;
}
. stat-label {
color : #6e6e73 ;
font-size : 14 px ;
font-weight : 500 ;
}
. table {
width : 100 % ;
border-collapse : collapse ;
}
. table th ,
. table td {
text-align : left ;
padding : 12 px ;
border-bottom : 1 px solid #f0f0f0 ;
}
. table th {
font-weight : 600 ;
color : #6e6e73 ;
font-size : 12 px ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. table tr : hover {
background : #f8f9fa ;
}
. status-badge {
padding : 4 px 8 px ;
border-radius : 12 px ;
font-size : 11 px ;
font-weight : 600 ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. 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.875 rem ;
}
. success-rate {
margin-top : 2 px ;
font-size : 0.75 rem ;
font-weight : 500 ;
}
. success-rate-good {
color : #28a745 ;
}
. success-rate-ok {
color : #ffc107 ;
}
. success-rate-bad {
color : #dc3545 ;
}
. actions {
display : flex ;
gap : 8 px ;
}
. page-section {
display : none ;
}
. page-section . active {
display : block ;
}
2025-09-23 14:17:32 +01:00
. 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 : 12 px ;
padding : 24 px ;
max-width : 600 px ;
width : 90 % ;
max-height : 80 vh ;
overflow-y : auto ;
box-shadow : 0 10 px 30 px rgba ( 0 , 0 , 0 , 0.2 ) ;
}
. modal-header {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 20 px ;
}
. modal-header h2 {
margin : 0 ;
}
. modal-body {
margin-bottom : 20 px ;
}
. modal-actions {
display : flex ;
justify-content : flex-end ;
gap : 10 px ;
padding-top : 20 px ;
border-top : 1 px solid #eee ;
}
. server-section {
margin-bottom : 20 px ;
padding : 15 px ;
background : #f8f9fa ;
border-radius : 8 px ;
}
. server-section h3 {
margin : 0 0 10 px 0 ;
font-size : 14 px ;
color : #333 ;
}
. inbound-item {
margin : 5 px 0 ;
}
. inbound-item label {
display : flex ;
align-items : center ;
gap : 8 px ;
cursor : pointer ;
}
. inbound-item input [ type = "checkbox" ] {
cursor : pointer ;
}
. required {
color : #dc3545 ;
}
2025-09-18 02:56:59 +03:00
. form-grid {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 300 px , 1 fr ) ) ;
gap : 20 px ;
}
. form-group {
margin-bottom : 16 px ;
}
. form-label {
display : block ;
margin-bottom : 6 px ;
font-weight : 500 ;
font-size : 14 px ;
color : #1d1d1f ;
}
. form-input {
width : 100 % ;
padding : 10 px 12 px ;
border : 1 px solid #d2d2d7 ;
border-radius : 8 px ;
font-size : 14 px ;
transition : border-color 0.2 s ;
}
. form-input : focus {
outline : none ;
border-color : #0071e3 ;
}
. form-select {
width : 100 % ;
padding : 10 px 12 px ;
border : 1 px solid #d2d2d7 ;
border-radius : 8 px ;
font-size : 14 px ;
background : white ;
}
. alert {
padding : 12 px 16 px ;
border-radius : 8 px ;
margin-bottom : 20 px ;
display : none ;
}
. alert . show {
display : block ;
}
. alert-success {
background : #d4edda ;
color : #155724 ;
border : 1 px solid #c3e6cb ;
}
. alert-error {
background : #f8d7da ;
color : #721c24 ;
border : 1 px solid #f5c6cb ;
}
. empty-state {
text-align : center ;
padding : 60 px 20 px ;
color : #6e6e73 ;
}
. empty-state h3 {
margin-bottom : 8 px ;
color : #1d1d1f ;
}
. loading {
text-align : center ;
padding : 40 px ;
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 : 12 px ;
max-width : 800 px ;
max-height : 80 vh ;
overflow-y : auto ;
width : 90 % ;
box-shadow : 0 10 px 30 px rgba ( 0 , 0 , 0 , 0.3 ) ;
}
. modal-header {
padding : 20 px ;
border-bottom : 1 px solid #f0f0f0 ;
display : flex ;
justify-content : space-between ;
align-items : center ;
}
. modal-header h2 {
margin : 0 ;
font-size : 18 px ;
font-weight : 600 ;
}
. modal-body {
padding : 20 px ;
}
. server-section {
margin-bottom : 30 px ;
padding : 15 px ;
border : 1 px solid #f0f0f0 ;
border-radius : 8 px ;
}
. server-section h3 {
margin : 0 0 15 px 0 ;
font-size : 16 px ;
font-weight : 600 ;
color : #1d1d1f ;
}
. inbound-item {
margin : 10 px 0 ;
}
. inbound-item label {
display : flex ;
align-items : center ;
cursor : pointer ;
padding : 8 px ;
border-radius : 4 px ;
transition : background-color 0.2 s ;
}
. inbound-item label : hover {
background : #f8f9fa ;
}
. inbound-item input [ type = "checkbox" ] {
margin-right : 10 px ;
}
. user-details {
margin-top : 30 px ;
padding : 20 px ;
background : #f8f9fa ;
border-radius : 8 px ;
}
. user-details label {
display : block ;
margin-bottom : 15 px ;
font-weight : 500 ;
}
. user-details input {
width : 100 % ;
padding : 8 px 12 px ;
border : 1 px solid #d2d2d7 ;
border-radius : 4 px ;
margin-top : 5 px ;
}
. input-group {
display : flex ;
margin-top : 5 px ;
}
. input-group input {
margin-top : 0 ;
border-radius : 4 px 0 0 4 px ;
border-right : none ;
}
. refresh-btn {
background : #f8f9fa ;
border : 1 px solid #d2d2d7 ;
border-radius : 0 4 px 4 px 0 ;
padding : 8 px 12 px ;
cursor : pointer ;
font-size : 16 px ;
transition : background-color 0.2 s ;
}
. refresh-btn : hover {
background : #e9ecef ;
}
. modal-actions {
margin-top : 20 px ;
display : flex ;
gap : 10 px ;
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" >
2025-09-23 14:17:32 +01:00
< label class = "form-label" for = "serverHostname" > Public Hostname *< / label >
2025-09-18 02:56:59 +03:00
< input type = "text" id = "serverHostname" class = "form-input" placeholder = "server.example.com" >
2025-09-23 14:17:32 +01:00
< small class = "form-help" > Hostname that clients will connect to< / small >
< / div >
< div class = "form-group" >
< label class = "form-label" for = "serverGrpcHostname" > gRPC Hostname< / label >
< input type = "text" id = "serverGrpcHostname" class = "form-input" placeholder = "192.168.1.100 or leave empty to use public hostname" >
< small class = "form-help" > Internal address for gRPC API (optional, defaults to public hostname)< / small >
2025-09-18 02:56:59 +03:00
< / 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 >
2025-09-23 14:17:32 +01:00
< button class = "btn btn-primary" onclick = "showCreateUserModal()" > + Create User< / button >
2025-09-18 02:56:59 +03:00
< / 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>
2025-09-23 14:17:32 +01:00
<th>Public Hostname</th>
<th>gRPC Address</th>
2025-09-18 02:56:59 +03:00
<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>
2025-09-23 14:17:32 +01:00
<td> ${ escapeHtml ( server . grpc _hostname ) } : ${ server . grpc _port } </td>
2025-09-18 02:56:59 +03:00
<td><span class="status-badge status- ${ server . status } "> ${ server . status } </span></td>
<td>
<div class="actions">
2025-09-23 14:17:32 +01:00
<button class="btn btn-small btn-primary" onclick="editServer(' ${ server . id } ')">Edit</button>
2025-09-18 02:56:59 +03:00
<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 ( ) ;
2025-09-23 14:17:32 +01:00
const grpc _hostname = document . getElementById ( 'serverGrpcHostname' ) . value . trim ( ) ;
2025-09-18 02:56:59 +03:00
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 } ;
2025-09-23 14:17:32 +01:00
if ( grpc _hostname ) serverData . grpc _hostname = grpc _hostname ;
2025-09-18 02:56:59 +03:00
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' ) ;
}
}
2025-09-23 14:17:32 +01:00
async function editServer ( serverId ) {
try {
// Fetch server data
const response = await fetch ( ` ${ API _BASE } /servers/ ${ serverId } ` ) ;
if ( ! response . ok ) throw new Error ( 'Failed to fetch server' ) ;
const server = await response . json ( ) ;
// Create edit modal
const modalContent = `
<div class="modal-overlay" onclick="closeEditServerModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Edit Server: ${ escapeHtml ( server . name ) } </h2>
<button class="btn btn-small" onclick="closeEditServerModal()">Close</button>
</div>
<div class="modal-body">
<form id="editServerForm">
<input type="hidden" id="editServerId" value=" ${ server . id } ">
<div class="form-group">
<label class="form-label" for="editServerName">Server Name *</label>
<input type="text" id="editServerName" class="form-input" value=" ${ escapeHtml ( server . name ) } " required>
</div>
<div class="form-group">
<label class="form-label" for="editServerHostname">Public Hostname *</label>
<input type="text" id="editServerHostname" class="form-input" value=" ${ escapeHtml ( server . hostname ) } " required>
<small class="form-help">Hostname that clients will connect to</small>
</div>
<div class="form-group">
<label class="form-label" for="editServerGrpcHostname">gRPC Hostname</label>
<input type="text" id="editServerGrpcHostname" class="form-input" value=" ${ escapeHtml ( server . grpc _hostname || '' ) } " placeholder="Leave empty to use public hostname">
<small class="form-help">Internal address for gRPC API (optional)</small>
</div>
<div class="form-group">
<label class="form-label" for="editServerPort">gRPC Port</label>
<input type="number" id="editServerPort" class="form-input" value=" ${ server . grpc _port } ">
</div>
<div class="form-group">
<label class="form-label" for="editServerCredentials">API Credentials</label>
<input type="text" id="editServerCredentials" class="form-input" placeholder=" ${ server . has _credentials ? 'Leave empty to keep existing credentials' : 'Optional credentials' } ">
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Update Server</button>
<button type="button" class="btn btn-secondary" onclick="closeEditServerModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
` ;
document . body . insertAdjacentHTML ( 'beforeend' , modalContent ) ;
// Handle form submission
document . getElementById ( 'editServerForm' ) . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
await updateServer ( ) ;
} ) ;
} catch ( error ) {
showAlert ( 'Error loading server: ' + error . message , 'error' ) ;
}
}
function closeEditServerModal ( ) {
const modal = document . querySelector ( '.modal-overlay' ) ;
if ( modal ) {
modal . remove ( ) ;
}
}
async function updateServer ( ) {
const serverId = document . getElementById ( 'editServerId' ) . value ;
const name = document . getElementById ( 'editServerName' ) . value . trim ( ) ;
const hostname = document . getElementById ( 'editServerHostname' ) . value . trim ( ) ;
const grpc _hostname = document . getElementById ( 'editServerGrpcHostname' ) . value . trim ( ) ;
const grpc _port = document . getElementById ( 'editServerPort' ) . value ;
const api _credentials = document . getElementById ( 'editServerCredentials' ) . value . trim ( ) ;
if ( ! name || ! hostname ) {
showAlert ( 'Name and hostname are required' , 'error' ) ;
return ;
}
const serverData = { name , hostname } ;
if ( grpc _hostname ) serverData . grpc _hostname = grpc _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/ ${ serverId } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( serverData )
} ) ;
if ( ! response . ok ) throw new Error ( 'Failed to update server' ) ;
showAlert ( 'Server updated successfully' , 'success' ) ;
closeEditServerModal ( ) ;
loadServers ( ) ;
} catch ( error ) {
showAlert ( 'Error updating server: ' + error . message , 'error' ) ;
}
}
2025-09-18 02:56:59 +03:00
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 {
2025-09-23 14:17:32 +01:00
// First, load existing user access
const userAccessResponse = await fetch ( ` ${ API _BASE } /users/ ${ userId } /access ` ) ;
const existingAccess = await userAccessResponse . ok ? await userAccessResponse . json ( ) : [ ] ;
2025-09-18 02:56:59 +03:00
// 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 } ">
2025-09-23 14:17:32 +01:00
<input type="hidden" id="editUserName" value=" ${ escapeHtml ( userName ) } ">
2025-09-18 02:56:59 +03:00
` ;
for ( const server of servers ) {
modalContent += `
<div class="server-section">
2025-09-23 14:17:32 +01:00
<h3> ${ escapeHtml ( server . name ) } ( ${ escapeHtml ( server . hostname ) } )</h3>
<p><small>gRPC: ${ escapeHtml ( server . grpc _hostname ) } : ${ server . grpc _port } </small></p>
2025-09-18 02:56:59 +03:00
` ;
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 ) {
2025-09-23 14:17:32 +01:00
// Check if user already has access to this inbound
const hasAccess = existingAccess . some ( a =>
a . server _inbound _id === inbound . id
) ;
2025-09-18 02:56:59 +03:00
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 ) } "
2025-09-23 14:17:32 +01:00
data-inbound-port=" ${ inbound . port _override || 'default' } "
${ hasAccess ? 'checked' : '' } >
${ escapeHtml ( inbound . template _name || 'Unknown Template' ) }
${ hasAccess ? '<span class="status-badge status-online">Active</span>' : '' }
2025-09-18 02:56:59 +03:00
</label>
</div>
` ;
}
} else {
modalContent += '<p>No inbounds configured</p>' ;
}
} catch ( error ) {
modalContent += '<p>Error loading inbounds</p>' ;
}
modalContent += '</div>' ;
}
modalContent += `
<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 ) ;
// 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 ( ) ;
}
}
2025-09-23 14:17:32 +01:00
// Show create user modal
function showCreateUserModal ( ) {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateUserModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Create New User</h2>
<button class="btn btn-small" onclick="closeCreateUserModal()">Close</button>
</div>
<div class="modal-body">
<form id="createUserForm">
<div class="form-grid">
<label>
Name: <span class="required">*</span>
<input type="text" name="name" required placeholder="User name">
</label>
<label>
Comment:
<input type="text" name="comment" placeholder="Optional comment">
</label>
<label>
Telegram ID:
<input type="number" name="telegram_id" placeholder="Optional Telegram ID">
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateUserModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
` ;
document . body . insertAdjacentHTML ( 'beforeend' , modalContent ) ;
document . getElementById ( 'createUserForm' ) . addEventListener ( 'submit' , createUser ) ;
}
function closeCreateUserModal ( ) {
const modal = document . querySelector ( '.modal-overlay' ) ;
if ( modal ) {
modal . remove ( ) ;
}
}
async function createUser ( event ) {
event . preventDefault ( ) ;
const formData = new FormData ( event . target ) ;
const userData = {
name : formData . get ( 'name' ) ,
comment : formData . get ( 'comment' ) || null ,
telegram _id : formData . get ( 'telegram_id' ) ? parseInt ( formData . get ( 'telegram_id' ) ) : null
} ;
try {
const response = await fetch ( ` ${ API _BASE } /users ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( userData )
} ) ;
if ( ! response . ok ) {
const error = await response . json ( ) ;
throw new Error ( error . message || 'Failed to create user' ) ;
}
showAlert ( 'User created successfully' , 'success' ) ;
closeCreateUserModal ( ) ;
loadUsers ( ) ; // Reload users table
} catch ( error ) {
showAlert ( 'Error creating user: ' + error . message , 'error' ) ;
}
}
2025-09-18 02:56:59 +03:00
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 ;
2025-09-23 14:17:32 +01:00
const userName = document . getElementById ( 'editUserName' ) . value ;
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
const allCheckboxes = document . querySelectorAll ( 'input[name="access"]' ) ;
2025-09-18 02:56:59 +03:00
try {
2025-09-23 14:17:32 +01:00
// Get current user access
const currentAccessResponse = await fetch ( ` ${ API _BASE } /users/ ${ userId } /access ` ) ;
const currentAccess = await currentAccessResponse . ok ? await currentAccessResponse . json ( ) : [ ] ;
// Process each checkbox
for ( const checkbox of allCheckboxes ) {
2025-09-18 02:56:59 +03:00
const serverId = checkbox . dataset . serverId ;
const inboundId = checkbox . dataset . inboundId ;
2025-09-23 14:17:32 +01:00
const currentlyHasAccess = currentAccess . some ( a =>
a . server _inbound _id === inboundId
) ;
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
if ( checkbox . checked && ! currentlyHasAccess ) {
// Grant access - pass user_id to use existing user
const grantData = {
user _id : userId ,
name : userName ,
level : 0
} ;
const response = await fetch ( ` ${ API _BASE } /servers/ ${ serverId } /inbounds/ ${ inboundId } /users ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( grantData )
} ) ;
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
if ( ! response . ok && response . status !== 409 ) { // 409 = already exists
throw new Error ( ` Failed to grant access ` ) ;
}
} else if ( ! checkbox . checked && currentlyHasAccess ) {
// Revoke access
const accessRecord = currentAccess . find ( a =>
a . server _inbound _id === inboundId
) ;
if ( accessRecord ) {
const response = await fetch ( ` ${ API _BASE } /servers/ ${ serverId } /inbounds/ ${ inboundId } /users/ ${ accessRecord . id } ` , {
method : 'DELETE'
} ) ;
if ( ! response . ok ) {
throw new Error ( ` Failed to revoke access ` ) ;
}
}
2025-09-18 02:56:59 +03:00
}
}
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 >