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 ;
2025-09-24 00:30:03 +01:00
display : flex ;
justify-content : space-between ;
align-items : flex-start ;
}
. page-header-content {
flex : 1 ;
2025-09-18 02:56:59 +03:00
}
. 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 ;
}
2025-09-24 00:30:03 +01:00
/* Task Summary Cards */
. task-summary-grid {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ));
gap : 20 px ;
margin-bottom : 30 px ;
}
. summary-card {
background : white ;
border-radius : 12 px ;
padding : 20 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 );
display : flex ;
align-items : center ;
gap : 15 px ;
}
. summary-icon {
font-size : 32 px ;
width : 60 px ;
height : 60 px ;
display : flex ;
align-items : center ;
justify-content : center ;
background : #f5f5f7 ;
border-radius : 12 px ;
}
. summary-content h3 {
font-size : 24 px ;
font-weight : 600 ;
margin : 0 0 4 px 0 ;
color : #1d1d1f ;
}
. summary-content p {
margin : 0 ;
color : #6e6e73 ;
font-size : 14 px ;
}
/* Task Status Badges */
. task-status {
padding : 4 px 12 px ;
border-radius : 20 px ;
font-size : 12 px ;
font-weight : 600 ;
text-transform : uppercase ;
}
. task-status . running {
background : #e3f2fd ;
color : #1976d2 ;
}
. task-status . success {
background : #e8f5e8 ;
color : #2e7d32 ;
}
. task-status . error {
background : #ffebee ;
color : #c62828 ;
}
. task-status . idle {
background : #f5f5f5 ;
color : #616161 ;
}
/* Task Actions */
. task-actions {
display : flex ;
gap : 8 px ;
}
. task-actions . btn {
padding : 6 px 12 px ;
font-size : 12 px ;
}
/* Task List Items */
. task-item {
background : white ;
border-radius : 12 px ;
padding : 20 px ;
margin-bottom : 16 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 );
border : 1 px solid #f0f0f0 ;
}
. task-header {
display : flex ;
justify-content : space-between ;
align-items : flex-start ;
margin-bottom : 12 px ;
}
. task-info h3 {
font-size : 18 px ;
font-weight : 600 ;
margin : 0 0 4 px 0 ;
color : #1d1d1f ;
}
. task-description {
color : #6e6e73 ;
font-size : 14 px ;
margin : 0 0 8 px 0 ;
line-height : 1.4 ;
}
. task-schedule {
color : #8e8e93 ;
font-size : 13 px ;
font-family : 'SF Mono' , Monaco , 'Cascadia Code' , 'Roboto Mono' , Consolas , 'Courier New' , monospace ;
margin : 0 ;
}
. task-status-container {
display : flex ;
align-items : center ;
gap : 10 px ;
}
. task-stats {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 120 px , 1 fr ));
gap : 16 px ;
margin-top : 16 px ;
padding-top : 16 px ;
border-top : 1 px solid #f0f0f0 ;
}
. stat {
text-align : center ;
}
. stat . stat-value {
font-size : 20 px ;
font-weight : 600 ;
color : #1d1d1f ;
margin : 0 0 2 px 0 ;
}
. stat . stat-label {
font-size : 12 px ;
color : #8e8e93 ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
margin : 0 ;
}
2025-09-18 02:56:59 +03:00
</ 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 >
2025-09-24 00:30:03 +01:00
< li class = "nav-item" >
< a href = "#dns-providers" class = "nav-link" onclick = "showPage('dns-providers')" > DNS Providers</ a >
</ li >
< li class = "nav-item" >
< a href = "#tasks" class = "nav-link" onclick = "showPage('tasks')" > Tasks</ a >
</ li >
2025-09-18 02:56:59 +03:00
< 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" >
2025-09-24 00:30:03 +01:00
< div class = "page-header-content" >
< h1 class = "page-title" > SSL Certificates</ h1 >
< p class = "page-subtitle" > Manage SSL/TLS certificates for your servers</ p >
</ div >
< button class = "btn btn-primary" onclick = "showCreateCertificateModal()" > + Create Certificate</ button >
2025-09-18 02:56:59 +03:00
</ 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 >
2025-09-24 00:30:03 +01:00
<!-- DNS Providers -->
< section id = "dns-providers" class = "page-section" >
< div class = "page-header" >
< div class = "page-header-content" >
< h1 class = "page-title" > DNS Providers</ h1 >
< p class = "page-subtitle" > Manage DNS provider credentials for Let's Encrypt certificates</ p >
</ div >
< button class = "btn btn-primary" onclick = "showCreateDnsProviderModal()" > + Add DNS Provider</ button >
</ div >
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > DNS Providers List</ h2 >
</ div >
< div id = "dnsProvidersTable" class = "loading" > Loading...</ div >
</ div >
</ section >
<!-- Tasks -->
< section id = "tasks" class = "page-section" >
< div class = "page-header" >
< div class = "page-header-content" >
< h1 class = "page-title" > Scheduled Tasks</ h1 >
< p class = "page-subtitle" > Monitor and manage background tasks</ p >
</ div >
< button class = "btn btn-secondary" onclick = "refreshTasks()" > 🔄 Refresh</ button >
</ div >
<!-- Task Summary Cards -->
< div class = "task-summary-grid" >
< div class = "summary-card" >
< div class = "summary-icon" > 📋</ div >
< div class = "summary-content" >
< h3 id = "totalTasks" > -</ h3 >
< p > Total Tasks</ p >
</ div >
</ div >
< div class = "summary-card" >
< div class = "summary-icon" > 🏃</ div >
< div class = "summary-content" >
< h3 id = "runningTasks" > -</ h3 >
< p > Running</ p >
</ div >
</ div >
< div class = "summary-card" >
< div class = "summary-icon" > ✅</ div >
< div class = "summary-content" >
< h3 id = "successTasks" > -</ h3 >
< p > Successful</ p >
</ div >
</ div >
< div class = "summary-card" >
< div class = "summary-icon" > ❌</ div >
< div class = "summary-content" >
< h3 id = "errorTasks" > -</ h3 >
< p > Failed</ p >
</ div >
</ div >
</ div >
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Tasks List</ h2 >
</ div >
< div id = "tasksTable" class = "loading" > Loading...</ div >
</ div >
</ section >
2025-09-18 02:56:59 +03:00
<!-- Users -->
< section id = "users" class = "page-section" >
< div class = "page-header" >
2025-09-24 00:30:03 +01:00
< div class = "page-header-content" >
< h1 class = "page-title" > Users</ h1 >
< p class = "page-subtitle" > Manage user accounts and access</ p >
</ div >
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 ;
2025-09-24 00:30:03 +01:00
case 'dns-providers' :
loadDnsProviders ();
break ;
case 'tasks' :
loadTasks ();
break ;
2025-09-18 02:56:59 +03:00
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 ();
}
2025-09-24 00:30:03 +01:00
// DNS Providers
async function loadDnsProviders () {
try {
const response = await fetch ( ` ${ API_BASE } /dns-providers` );
const providers = await response . json ();
if ( providers . length === 0 ) {
document . getElementById ( 'dnsProvidersTable' ). innerHTML = '<div class="empty-state"><h3>No DNS providers found</h3><p>Add DNS provider credentials to enable Let\'s Encrypt certificates</p></div>' ;
return ;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Provider Type</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${ providers . map ( provider => `
<tr>
<td><strong> ${ escapeHtml ( provider . name ) } </strong></td>
<td> ${ provider . provider_type } </td>
<td><span class="status-badge ${ provider . is_active ? 'status-online' : 'status-offline' } "> ${ provider . is_active ? 'Active' : 'Inactive' } </span></td>
<td> ${ new Date ( provider . created_at ). toLocaleDateString () } </td>
<td>
<div class="actions">
<button class="btn btn-small btn-secondary" onclick="editDnsProvider(' ${ provider . id } ')">Edit</button>
<button class="btn btn-small btn-danger" onclick="deleteDnsProvider(' ${ provider . id } ', ' ${ escapeHtml ( provider . name ) } ')">Delete</button>
</div>
</td>
</tr>
` ). join ( '' ) }
</tbody>
</table>
` ;
document . getElementById ( 'dnsProvidersTable' ). innerHTML = table ;
} catch ( error ) {
document . getElementById ( 'dnsProvidersTable' ). innerHTML = '<div class="empty-state"><h3>Error loading DNS providers</h3><p>' + error . message + '</p></div>' ;
}
}
// Show create DNS provider modal
function showCreateDnsProviderModal () {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateDnsProviderModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Add DNS Provider</h2>
<button class="btn btn-small" onclick="closeCreateDnsProviderModal()">Close</button>
</div>
<div class="modal-body">
<form id="createDnsProviderForm">
<div class="form-group">
<label class="form-label" for="dnsProviderName">Name *</label>
<input type="text" id="dnsProviderName" class="form-input" placeholder="Enter provider name" required>
</div>
<div class="form-group">
<label class="form-label" for="dnsProviderType">Provider Type *</label>
<select id="dnsProviderType" class="form-select" required>
<option value="">Select provider type</option>
<option value="cloudflare">Cloudflare</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="dnsProviderToken">API Token *</label>
<input type="password" id="dnsProviderToken" class="form-input" placeholder="Enter API token" required>
<small class="form-help">For Cloudflare: Create an API token with Zone:Read and Zone:DNS:Edit permissions</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="dnsProviderActive" checked>
Active
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Add Provider</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateDnsProviderModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
` ;
document . body . insertAdjacentHTML ( 'beforeend' , modalContent );
document . getElementById ( 'createDnsProviderForm' ). addEventListener ( 'submit' , createDnsProvider );
}
function closeCreateDnsProviderModal () {
const modal = document . querySelector ( '.modal-overlay' );
if ( modal ) {
modal . remove ();
}
}
async function createDnsProvider ( event ) {
event . preventDefault ();
const name = document . getElementById ( 'dnsProviderName' ). value . trim ();
const provider_type = document . getElementById ( 'dnsProviderType' ). value ;
const api_token = document . getElementById ( 'dnsProviderToken' ). value . trim ();
const is_active = document . getElementById ( 'dnsProviderActive' ). checked ;
if ( ! name || ! provider_type || ! api_token ) {
showAlert ( 'All fields are required' , 'error' );
return ;
}
const providerData = { name , provider_type , api_token , is_active };
try {
const response = await fetch ( ` ${ API_BASE } /dns-providers` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( providerData )
});
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( error . message || 'Failed to create DNS provider' );
}
showAlert ( 'DNS provider created successfully' , 'success' );
closeCreateDnsProviderModal ();
loadDnsProviders ();
} catch ( error ) {
showAlert ( 'Error creating DNS provider: ' + error . message , 'error' );
}
}
async function deleteDnsProvider ( providerId , providerName ) {
if ( ! confirm ( `Are you sure you want to delete DNS provider " ${ providerName } "?` )) return ;
try {
const response = await fetch ( ` ${ API_BASE } /dns-providers/ ${ providerId } ` , {
method : 'DELETE'
});
if ( ! response . ok ) throw new Error ( 'Failed to delete DNS provider' );
showAlert ( 'DNS provider deleted successfully' , 'success' );
loadDnsProviders ();
} catch ( error ) {
showAlert ( 'Error deleting DNS provider: ' + error . message , 'error' );
}
}
// Show create certificate modal
function showCreateCertificateModal () {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateCertificateModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Create Certificate</h2>
<button class="btn btn-small" onclick="closeCreateCertificateModal()">Close</button>
</div>
<div class="modal-body">
<form id="createCertificateForm">
<div class="form-group">
<label class="form-label" for="certName">Certificate Name *</label>
<input type="text" id="certName" class="form-input" placeholder="Enter certificate name" required>
</div>
<div class="form-group">
<label class="form-label" for="certDomain">Domain *</label>
<input type="text" id="certDomain" class="form-input" placeholder="example.com" required>
</div>
<div class="form-group">
<label class="form-label" for="certType">Certificate Type *</label>
<select id="certType" class="form-select" required onchange="toggleCertificateFields()">
<option value="">Select certificate type</option>
<option value="self_signed">Self-Signed</option>
<option value="letsencrypt">Let's Encrypt</option>
<option value="imported">Imported</option>
</select>
</div>
<div id="letsencryptFields" style="display: none;">
<div class="form-group">
<label class="form-label" for="dnsProvider">DNS Provider *</label>
<select id="dnsProvider" class="form-select">
<option value="">Loading...</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="acmeEmail">ACME Email *</label>
<input type="email" id="acmeEmail" class="form-input" placeholder="admin@example.com">
</div>
</div>
<div id="importedFields" style="display: none;">
<div class="form-group">
<label class="form-label" for="certPem">Certificate PEM *</label>
<textarea id="certPem" class="form-input" rows="8" placeholder="-----BEGIN CERTIFICATE-----"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="keyPem">Private Key PEM *</label>
<textarea id="keyPem" class="form-input" rows="8" placeholder="-----BEGIN PRIVATE KEY-----"></textarea>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="autoRenew" checked>
Auto-renew certificate
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Create Certificate</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateCertificateModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
` ;
document . body . insertAdjacentHTML ( 'beforeend' , modalContent );
document . getElementById ( 'createCertificateForm' ). addEventListener ( 'submit' , createCertificate );
loadDnsProvidersForSelect ();
}
function closeCreateCertificateModal () {
const modal = document . querySelector ( '.modal-overlay' );
if ( modal ) {
modal . remove ();
}
}
function toggleCertificateFields () {
const certType = document . getElementById ( 'certType' ). value ;
const letsencryptFields = document . getElementById ( 'letsencryptFields' );
const importedFields = document . getElementById ( 'importedFields' );
letsencryptFields . style . display = certType === 'letsencrypt' ? 'block' : 'none' ;
importedFields . style . display = certType === 'imported' ? 'block' : 'none' ;
}
async function loadDnsProvidersForSelect () {
try {
const response = await fetch ( ` ${ API_BASE } /dns-providers/cloudflare/active` );
const providers = await response . json ();
const select = document . getElementById ( 'dnsProvider' );
select . innerHTML = providers . length > 0
? providers . map ( p => `<option value=" ${ p . id } "> ${ escapeHtml ( p . name ) } </option>` ). join ( '' )
: '<option value="">No active Cloudflare providers</option>' ;
} catch ( error ) {
const select = document . getElementById ( 'dnsProvider' );
select . innerHTML = '<option value="">Error loading providers</option>' ;
}
}
async function createCertificate ( event ) {
event . preventDefault ();
const name = document . getElementById ( 'certName' ). value . trim ();
const domain = document . getElementById ( 'certDomain' ). value . trim ();
const cert_type = document . getElementById ( 'certType' ). value ;
const auto_renew = document . getElementById ( 'autoRenew' ). checked ;
if ( ! name || ! domain || ! cert_type ) {
showAlert ( 'Name, domain, and certificate type are required' , 'error' );
return ;
}
const certData = { name , domain , cert_type , auto_renew , certificate_pem : '' , private_key : '' };
if ( cert_type === 'letsencrypt' ) {
const dns_provider_id = document . getElementById ( 'dnsProvider' ). value ;
const acme_email = document . getElementById ( 'acmeEmail' ). value . trim ();
if ( ! dns_provider_id || ! acme_email ) {
showAlert ( 'DNS provider and ACME email are required for Let\'s Encrypt certificates' , 'error' );
return ;
}
certData . dns_provider_id = dns_provider_id ;
certData . acme_email = acme_email ;
} else if ( cert_type === 'imported' ) {
const certificate_pem = document . getElementById ( 'certPem' ). value . trim ();
const private_key = document . getElementById ( 'keyPem' ). value . trim ();
if ( ! certificate_pem || ! private_key ) {
showAlert ( 'Certificate and private key PEM are required for imported certificates' , 'error' );
return ;
}
certData . certificate_pem = certificate_pem ;
certData . private_key = private_key ;
}
try {
showAlert ( 'Creating certificate...' , 'success' );
const response = await fetch ( ` ${ API_BASE } /certificates` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( certData )
});
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( error . message || 'Failed to create certificate' );
}
showAlert ( 'Certificate created successfully' , 'success' );
closeCreateCertificateModal ();
loadCertificates ();
} catch ( error ) {
showAlert ( 'Error creating certificate: ' + error . message , 'error' );
}
}
// Tasks
async function loadTasks () {
try {
const response = await fetch ( ` ${ API_BASE } /tasks` );
const data = await response . json ();
// Update summary cards
document . getElementById ( 'totalTasks' ). textContent = data . summary . total_tasks ;
document . getElementById ( 'runningTasks' ). textContent = data . summary . running_tasks ;
document . getElementById ( 'successTasks' ). textContent = data . summary . successful_tasks ;
document . getElementById ( 'errorTasks' ). textContent = data . summary . failed_tasks ;
if ( Object . keys ( data . tasks ). length === 0 ) {
document . getElementById ( 'tasksTable' ). innerHTML = '<div class="empty-state"><h3>No tasks found</h3><p>No scheduled tasks are configured</p></div>' ;
return ;
}
const tasksHtml = Object . entries ( data . tasks ). map (([ taskId , task ]) => {
const statusClass = task . status . toLowerCase ();
const lastRun = task . last_run ? new Date ( task . last_run ). toLocaleString () : 'Never' ;
const nextRun = task . next_run ? new Date ( task . next_run ). toLocaleString () : 'Not scheduled' ;
const duration = task . last_duration_ms ? ` ${ task . last_duration_ms } ms` : '-' ;
const successRate = task . total_runs > 0 ? Math . round (( task . success_count / task . total_runs ) * 100 ) : 0 ;
return `
<div class="task-item">
<div class="task-header">
<div class="task-info">
<h3> ${ escapeHtml ( task . name ) } </h3>
<p class="task-description"> ${ escapeHtml ( task . description ) } </p>
<div class="task-schedule">📅 ${ escapeHtml ( task . schedule ) } </div>
</div>
<div class="task-status-container">
<span class="task-status ${ statusClass } "> ${ task . status } </span>
<div class="task-actions">
<button class="btn btn-primary btn-small" onclick="triggerTask(' ${ taskId } ')">▶️ Run Now</button>
<button class="btn btn-secondary btn-small" onclick="refreshTasks()">🔄 Refresh</button>
</div>
</div>
</div>
<div class="task-stats">
<div class="stat">
<label>Last Run:</label>
<span> ${ lastRun } </span>
</div>
<div class="stat">
<label>Next Run:</label>
<span> ${ nextRun } </span>
</div>
<div class="stat">
<label>Total Runs:</label>
<span> ${ task . total_runs } </span>
</div>
<div class="stat">
<label>Success Rate:</label>
<span> ${ successRate } % ( ${ task . success_count } / ${ task . total_runs } )</span>
</div>
<div class="stat">
<label>Last Duration:</label>
<span> ${ duration } </span>
</div>
${ task . last_error ? `
<div class="stat error">
<label>Last Error:</label>
<span> ${ escapeHtml ( task . last_error ) } </span>
</div>
` : '' }
</div>
</div>
` ;
}). join ( '' );
document . getElementById ( 'tasksTable' ). innerHTML = `
<div class="tasks-list">
${ tasksHtml }
</div>
` ;
} catch ( error ) {
document . getElementById ( 'tasksTable' ). innerHTML = '<div class="empty-state"><h3>Error loading tasks</h3><p>' + error . message + '</p></div>' ;
}
}
async function refreshTasks () {
loadTasks ();
}
async function triggerTask ( taskId ) {
try {
const response = await fetch ( ` ${ API_BASE } /tasks/ ${ taskId } /trigger` , {
method : 'POST'
});
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( error . message || 'Failed to trigger task' );
}
const result = await response . json ();
showAlert ( result . message , 'success' );
// Refresh tasks after a short delay to show updated status
setTimeout (() => {
loadTasks ();
}, 1000 );
} catch ( error ) {
showAlert ( 'Error triggering task: ' + error . message , 'error' );
}
}
2025-09-18 02:56:59 +03:00
// Initialize
loadPageData ( 'dashboard' );
</ script >
</ body >
</ html >