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-10-18 15:49:49 +03:00
/* Telegram Bot Styles */
. status-indicator {
display : flex ;
align-items : center ;
gap : 8 px ;
}
. status-dot {
width : 10 px ;
height : 10 px ;
border-radius : 50 % ;
}
. status-dot . status-active {
background : #34c759 ;
animation : pulse 2 s infinite ;
}
. status-dot . status-inactive {
background : #8e8e93 ;
}
@ keyframes pulse {
0 % {
box-shadow : 0 0 0 0 rgba ( 52 , 199 , 89 , 0.7 ) ;
}
70 % {
box-shadow : 0 0 0 10 px rgba ( 52 , 199 , 89 , 0 ) ;
}
100 % {
box-shadow : 0 0 0 0 rgba ( 52 , 199 , 89 , 0 ) ;
}
}
. status-text {
font-size : 14 px ;
font-weight : 500 ;
}
. form-input-group {
display : flex ;
gap : 8 px ;
align-items : center ;
}
. form-input-group . form-input {
flex : 1 ;
}
. form-input-group . button {
padding : 8 px 12 px ;
}
. form-checkbox {
display : flex ;
align-items : center ;
gap : 12 px ;
cursor : pointer ;
padding : 8 px 0 ;
}
. form-checkbox input [ type = "checkbox" ] {
width : 18 px ;
height : 18 px ;
margin : 0 ;
}
. admin-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
padding : 12 px 16 px ;
border-bottom : 1 px solid #f0f0f0 ;
}
. admin-item : last-child {
border-bottom : none ;
}
. admin-info {
flex : 1 ;
}
. admin-name {
font-weight : 500 ;
color : #1d1d1f ;
margin : 0 0 4 px 0 ;
}
. admin-telegram-id {
font-size : 12 px ;
color : #6e6e73 ;
font-family : 'SF Mono' , Monaco , 'Cascadia Code' , 'Roboto Mono' , Consolas , 'Courier New' , monospace ;
}
. user-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
padding : 12 px 16 px ;
border-bottom : 1 px solid #f0f0f0 ;
}
. user-item : last-child {
border-bottom : none ;
}
. user-info {
flex : 1 ;
}
. user-name {
font-weight : 500 ;
color : #1d1d1f ;
margin : 0 0 4 px 0 ;
}
. user-telegram-status {
display : flex ;
align-items : center ;
gap : 8 px ;
font-size : 12 px ;
color : #6e6e73 ;
}
. telegram-connected {
color : #34c759 ;
}
. telegram-not-connected {
color : #8e8e93 ;
}
. bot-info {
padding : 16 px ;
background : #f8f9fa ;
border-radius : 8 px ;
margin : 12 px 0 ;
}
. bot-info-item {
display : flex ;
justify-content : space-between ;
margin-bottom : 8 px ;
}
. bot-info-item : last-child {
margin-bottom : 0 ;
}
. bot-info-label {
font-weight : 500 ;
color : #6e6e73 ;
}
. bot-info-value {
color : #1d1d1f ;
font-family : 'SF Mono' , Monaco , 'Cascadia Code' , 'Roboto Mono' , Consolas , 'Courier New' , monospace ;
}
2025-10-19 04:13:36 +03:00
/* User Requests Styles */
. request-cards {
display : flex ;
flex-direction : column ;
gap : 16 px ;
}
. request-card {
border : 1 px solid #e0e0e0 ;
border-radius : 8 px ;
padding : 20 px ;
background : #fafafa ;
}
. request-header {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 16 px ;
}
. request-header h4 {
margin : 0 ;
font-size : 18 px ;
font-weight : 600 ;
}
. request-info {
margin-bottom : 16 px ;
}
. request-info p {
margin : 8 px 0 ;
color : #666 ;
}
. request-actions {
display : flex ;
gap : 12 px ;
}
. badge {
display : inline-flex ;
align-items : center ;
padding : 4 px 12 px ;
border-radius : 12 px ;
font-size : 12 px ;
font-weight : 600 ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. badge-warning {
background : #ffc107 ;
color : #000 ;
}
. badge-success {
background : #28a745 ;
color : white ;
}
. badge-danger {
background : #dc3545 ;
color : white ;
}
. badge-secondary {
background : #6c757d ;
color : white ;
}
. button-success {
background : #28a745 ;
color : white ;
border : none ;
}
. button-success : hover {
background : #218838 ;
}
. button-danger {
background : #dc3545 ;
color : white ;
border : none ;
}
. button-danger : hover {
background : #c82333 ;
}
. button-small {
padding : 6 px 12 px ;
font-size : 14 px ;
}
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" >
2025-10-18 15:49:49 +03:00
< a href = "#telegram" class = "nav-link" onclick = "showPage('telegram')" > Telegram Bot< / a >
2025-09-18 02:56:59 +03:00
< / li >
2025-10-19 04:13:36 +03:00
< li class = "nav-item" >
< a href = "#user-requests" class = "nav-link" onclick = "showPage('user-requests')" > User Requests< / a >
< / li >
2025-09-18 02:56:59 +03:00
< / 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 >
2025-10-18 15:49:49 +03:00
< section id = "telegram" class = "page-section" >
< div class = "page-header" >
< h1 class = "page-title" > Telegram Bot< / h1 >
< p class = "page-subtitle" > Configure and manage Telegram bot integration< / p >
< / div >
<!-- Bot Status Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Bot Status< / h2 >
< div id = "botStatusIndicator" class = "status-indicator" >
< span class = "status-dot status-inactive" > < / span >
< span class = "status-text" > Inactive< / span >
< / div >
< / div >
< div id = "botStatusInfo" class = "loading" > Loading...< / div >
< / div >
<!-- Bot Configuration Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Configuration< / h2 >
< div class = "card-actions" >
< button id = "saveConfigBtn" class = "button button-primary" onclick = "saveTelegramConfig()" disabled >
< span class = "icon" > 💾< / span >
Save Configuration
< / button >
< / div >
< / div >
< div class = "card-body" >
< form id = "telegramConfigForm" class = "form" >
< div class = "form-group" >
< label for = "botToken" class = "form-label" > Bot Token< / label >
< div class = "form-input-group" >
< input type = "password" id = "botToken" class = "form-input" placeholder = "Enter bot token from @BotFather" >
< button type = "button" class = "button button-outline" onclick = "toggleTokenVisibility()" >
< span class = "icon" > 👁< / span >
< / button >
< / div >
< div class = "form-help" >
Get your bot token from @BotFather on Telegram
< / div >
< / div >
< div class = "form-group" >
< label class = "form-checkbox" >
< input type = "checkbox" id = "botActive" onchange = "onBotActiveChange()" >
< span class = "checkmark" > < / span >
Enable Bot
< / label >
< div class = "form-help" >
When enabled, bot will start polling for messages
< / div >
< / div >
< / form >
< / div >
< / div >
<!-- Admins Management Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Bot Administrators< / h2 >
< button class = "button button-outline" onclick = "refreshAdmins()" >
< span class = "icon" > 🔄< / span >
Refresh
< / button >
< / div >
< div id = "adminsTable" class = "loading" > Loading...< / div >
< / div >
<!-- Admin Management Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Admin Management< / h2 >
< div class = "form-input-group" >
< input type = "text" id = "userSearchInput" class = "form-input" placeholder = "Search users by name, ID, or Telegram ID" style = "min-width: 300px;" >
< button class = "button button-outline" onclick = "searchUsers()" >
< span class = "icon" > 🔍< / span >
Search
< / button >
< / div >
< / div >
< div class = "card-body" >
< p class = "text-muted" > Search for users and manage admin privileges. Only users connected to Telegram can be promoted to admin.< / p >
< div id = "userSearchResults" > < / div >
< / div >
< / div >
<!-- Users List Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > All Users< / h2 >
< button class = "button button-outline" onclick = "refreshTelegramUsers()" >
< span class = "icon" > 🔄< / span >
Refresh
< / button >
< / div >
< div id = "telegramUsersTable" class = "loading" > Loading...< / div >
< / div >
<!-- Test Message Card -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Send Test Message< / h2 >
< / div >
< div class = "card-body" >
< form id = "testMessageForm" class = "form" >
< div class = "form-group" >
< label for = "testChatId" class = "form-label" > Chat ID< / label >
< input type = "number" id = "testChatId" class = "form-input" placeholder = "Enter chat ID" >
< / div >
< div class = "form-group" >
< label for = "testMessage" class = "form-label" > Message< / label >
< textarea id = "testMessage" class = "form-input" rows = "3" placeholder = "Enter test message" > < / textarea >
< / div >
< button type = "submit" class = "button button-primary" >
< span class = "icon" > 📤< / span >
Send Message
< / button >
< / form >
< / div >
< / div >
< / section >
2025-10-19 04:13:36 +03:00
<!-- User Requests -->
< section id = "user-requests" class = "page-section" >
< div class = "page-header" >
< h1 class = "page-title" > User Requests< / h1 >
< p class = "page-subtitle" > Manage access requests from Telegram users< / p >
< / div >
<!-- Request Status Overview -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Request Overview< / h2 >
< / div >
< div class = "card-body" >
< div class = "stats-grid" id = "requestStats" >
< div class = "stat-card" >
< div class = "stat-value" id = "pendingRequests" > 0< / div >
< div class = "stat-label" > Pending< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-value" id = "approvedRequests" > 0< / div >
< div class = "stat-label" > Approved< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-value" id = "declinedRequests" > 0< / div >
< div class = "stat-label" > Declined< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Pending Requests -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > Pending Requests< / h2 >
< button class = "button button-outline" onclick = "loadUserRequests()" >
< span class = "icon" > 🔄< / span >
Refresh
< / button >
< / div >
< div id = "pendingRequestsTable" class = "loading" > Loading...< / div >
< / div >
<!-- All Requests -->
< div class = "card" >
< div class = "card-header" >
< h2 class = "card-title" > All Requests< / h2 >
< / div >
< div id = "allRequestsTable" class = "loading" > Loading...< / div >
< / div >
< / section >
2025-09-18 02:56:59 +03:00
< / 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-10-18 15:49:49 +03:00
// Telegram Bot Functions
let currentTelegramConfig = null ;
async function loadTelegram ( ) {
await loadBotStatus ( ) ;
await loadTelegramConfig ( ) ;
await loadAdmins ( ) ;
await loadTelegramUsers ( ) ;
}
async function loadBotStatus ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /telegram/status ` ) ;
if ( response . ok ) {
const status = await response . json ( ) ;
updateBotStatusUI ( status ) ;
} else {
updateBotStatusUI ( { is _running : false , bot _info : null } ) ;
}
} catch ( error ) {
console . error ( 'Error loading bot status:' , error ) ;
updateBotStatusUI ( { is _running : false , bot _info : null } ) ;
}
}
function updateBotStatusUI ( status ) {
const indicator = document . getElementById ( 'botStatusIndicator' ) ;
const statusInfo = document . getElementById ( 'botStatusInfo' ) ;
const dot = indicator . querySelector ( '.status-dot' ) ;
const text = indicator . querySelector ( '.status-text' ) ;
if ( status . is _running ) {
dot . className = 'status-dot status-active' ;
text . textContent = 'Active' ;
if ( status . bot _info ) {
statusInfo . innerHTML = `
<div class="bot-info">
<div class="bot-info-item">
<span class="bot-info-label">Username:</span>
<span class="bot-info-value">@ ${ status . bot _info . username } </span>
</div>
<div class="bot-info-item">
<span class="bot-info-label">Name:</span>
<span class="bot-info-value"> ${ status . bot _info . first _name } </span>
</div>
</div>
` ;
}
} else {
dot . className = 'status-dot status-inactive' ;
text . textContent = 'Inactive' ;
statusInfo . innerHTML = '<p class="empty-state-text">Bot is not running</p>' ;
}
}
async function loadTelegramConfig ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /telegram/config ` ) ;
if ( response . ok ) {
currentTelegramConfig = await response . json ( ) ;
updateConfigForm ( currentTelegramConfig ) ;
} else if ( response . status === 404 ) {
currentTelegramConfig = null ;
updateConfigForm ( null ) ;
}
} catch ( error ) {
console . error ( 'Error loading config:' , error ) ;
currentTelegramConfig = null ;
updateConfigForm ( null ) ;
}
}
function updateConfigForm ( config ) {
const botTokenInput = document . getElementById ( 'botToken' ) ;
const botActiveCheckbox = document . getElementById ( 'botActive' ) ;
const saveBtn = document . getElementById ( 'saveConfigBtn' ) ;
if ( config ) {
botTokenInput . value = '••••••••••••••••' ; // Masked token
botActiveCheckbox . checked = config . is _active ;
} else {
botTokenInput . value = '' ;
botActiveCheckbox . checked = false ;
}
saveBtn . disabled = false ;
}
function toggleTokenVisibility ( ) {
const tokenInput = document . getElementById ( 'botToken' ) ;
const button = event . target . closest ( 'button' ) ;
if ( tokenInput . type === 'password' ) {
tokenInput . type = 'text' ;
button . innerHTML = '<span class="icon">🙈</span>' ;
} else {
tokenInput . type = 'password' ;
button . innerHTML = '<span class="icon">👁</span>' ;
}
}
function onBotActiveChange ( ) {
const checkbox = document . getElementById ( 'botActive' ) ;
const tokenInput = document . getElementById ( 'botToken' ) ;
if ( checkbox . checked && ! tokenInput . value ) {
showAlert ( 'Please enter a bot token first' , 'warning' ) ;
checkbox . checked = false ;
}
}
async function saveTelegramConfig ( ) {
const botToken = document . getElementById ( 'botToken' ) . value ;
const isActive = document . getElementById ( 'botActive' ) . checked ;
const saveBtn = document . getElementById ( 'saveConfigBtn' ) ;
if ( ! botToken || botToken === '••••••••••••••••' ) {
showAlert ( 'Please enter a valid bot token' , 'error' ) ;
return ;
}
saveBtn . disabled = true ;
saveBtn . textContent = 'Saving...' ;
try {
const method = currentTelegramConfig ? 'PUT' : 'POST' ;
const url = currentTelegramConfig ?
` ${ API _BASE } /telegram/config/ ${ currentTelegramConfig . id } ` :
` ${ API _BASE } /telegram/config ` ;
const response = await fetch ( url , {
method : method ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( {
bot _token : botToken ,
is _active : isActive
} )
} ) ;
if ( response . ok ) {
showAlert ( 'Configuration saved successfully' , 'success' ) ;
await loadTelegramConfig ( ) ;
await loadBotStatus ( ) ;
} else {
const error = await response . text ( ) ;
showAlert ( 'Error saving configuration: ' + error , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error saving configuration: ' + error . message , 'error' ) ;
} finally {
saveBtn . disabled = false ;
saveBtn . innerHTML = '<span class="icon">💾</span> Save Configuration' ;
}
}
async function loadAdmins ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /telegram/admins ` ) ;
if ( response . ok ) {
const admins = await response . json ( ) ;
renderAdmins ( admins ) ;
}
} catch ( error ) {
document . getElementById ( 'adminsTable' ) . innerHTML = '<div class="empty-state"><h3>Error loading admins</h3></div>' ;
}
}
function renderAdmins ( admins ) {
const container = document . getElementById ( 'adminsTable' ) ;
if ( admins . length === 0 ) {
container . innerHTML = '<div class="empty-state"><h3>No administrators</h3><p>Add administrators to manage the bot</p></div>' ;
return ;
}
const adminsHtml = admins . map ( admin => `
<div class="admin-item">
<div class="admin-info">
<h4 class="admin-name"> ${ admin . name } </h4>
<div class="admin-telegram-id"> ${ admin . telegram _id ? ` ID: ${ admin . telegram _id } ` : 'No Telegram ID' } </div>
</div>
<div class="admin-actions">
<button class="button button-outline button-small" onclick="removeAdmin(' ${ admin . user _id } ')">
<span class="icon">❌</span>
Remove
</button>
</div>
</div>
` ) . join ( '' ) ;
container . innerHTML = ` <div class="admins-list"> ${ adminsHtml } </div> ` ;
}
async function loadTelegramUsers ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /users ` ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
const users = data . users || data ; // Handle both paginated and direct array responses
renderTelegramUsers ( users ) ;
} else {
const errorText = await response . text ( ) ;
console . error ( 'Error response:' , response . status , errorText ) ;
document . getElementById ( 'telegramUsersTable' ) . innerHTML =
` <div class="empty-state"><h3>Error loading users</h3><p>Status: ${ response . status } </p><p> ${ errorText } </p></div> ` ;
}
} catch ( error ) {
console . error ( 'Network error:' , error ) ;
document . getElementById ( 'telegramUsersTable' ) . innerHTML =
` <div class="empty-state"><h3>Error loading users</h3><p>Network error: ${ error . message } </p></div> ` ;
}
}
function renderTelegramUsers ( users ) {
const container = document . getElementById ( 'telegramUsersTable' ) ;
if ( users . length === 0 ) {
container . innerHTML = '<div class="empty-state"><h3>No users</h3></div>' ;
return ;
}
const usersHtml = users . map ( user => `
<div class="user-item">
<div class="user-info">
<h4 class="user-name"> ${ user . name } </h4>
<div class="user-telegram-status">
${ user . telegram _id ?
` <span class="telegram-connected">📱 Connected (ID: ${ user . telegram _id } )</span> ` :
'<span class="telegram-not-connected">📱 Not connected</span>'
}
${ user . is _telegram _admin ? '<span class="admin-badge">👑 Admin</span>' : '' }
</div>
</div>
<div class="user-actions">
${ user . telegram _id && ! user . is _telegram _admin ?
` <button class="button button-primary button-small" onclick="makeAdmin(' ${ user . id } ')">
<span class="icon">👑</span>
Make Admin
</button> ` : ''
}
</div>
</div>
` ) . join ( '' ) ;
container . innerHTML = ` <div class="users-list"> ${ usersHtml } </div> ` ;
}
async function makeAdmin ( userId ) {
try {
const response = await fetch ( ` ${ API _BASE } /telegram/admins/ ${ userId } ` , {
method : 'POST'
} ) ;
if ( response . ok ) {
showAlert ( 'User promoted to admin' , 'success' ) ;
await loadAdmins ( ) ;
await loadTelegramUsers ( ) ;
} else {
showAlert ( 'Error promoting user to admin' , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error promoting user: ' + error . message , 'error' ) ;
}
}
async function removeAdmin ( userId ) {
if ( ! confirm ( 'Are you sure you want to remove admin privileges?' ) ) {
return ;
}
try {
const response = await fetch ( ` ${ API _BASE } /telegram/admins/ ${ userId } ` , {
method : 'DELETE'
} ) ;
if ( response . ok ) {
showAlert ( 'Admin privileges removed' , 'success' ) ;
await loadAdmins ( ) ;
await loadTelegramUsers ( ) ;
} else {
showAlert ( 'Error removing admin privileges' , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error removing admin: ' + error . message , 'error' ) ;
}
}
async function refreshAdmins ( ) {
await loadAdmins ( ) ;
}
async function refreshTelegramUsers ( ) {
await loadTelegramUsers ( ) ;
}
async function searchUsers ( ) {
const query = document . getElementById ( 'userSearchInput' ) . value . trim ( ) ;
if ( ! query ) {
document . getElementById ( 'userSearchResults' ) . innerHTML = '<p class="text-muted">Enter a search term to find users</p>' ;
return ;
}
try {
const response = await fetch ( ` ${ API _BASE } /users/search?q= ${ encodeURIComponent ( query ) } ` ) ;
if ( response . ok ) {
const users = await response . json ( ) ;
renderSearchResults ( users ) ;
} else {
document . getElementById ( 'userSearchResults' ) . innerHTML = '<div class="empty-state"><h3>Error searching users</h3></div>' ;
}
} catch ( error ) {
document . getElementById ( 'userSearchResults' ) . innerHTML = '<div class="empty-state"><h3>Search failed</h3></div>' ;
}
}
function renderSearchResults ( users ) {
const container = document . getElementById ( 'userSearchResults' ) ;
if ( users . length === 0 ) {
container . innerHTML = '<div class="empty-state"><h3>No users found</h3><p>Try a different search term</p></div>' ;
return ;
}
const usersHtml = users . map ( user => `
<div class="user-item" style="border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 12px;">
<div class="user-info">
<h4 class="user-name"> ${ user . name } </h4>
<div class="user-details" style="margin-top: 8px;">
<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">ID: ${ user . id } </p>
${ user . telegram _id ?
` <p style="margin: 4px 0; font-size: 14px; color: #6b7280;">📱 Telegram ID: ${ user . telegram _id } </p> ` :
'<p style="margin: 4px 0; font-size: 14px; color: #ef4444;">📱 Not connected to Telegram</p>'
}
${ user . is _telegram _admin ?
'<p style="margin: 4px 0; font-size: 14px; color: #059669;">👑 Current Admin</p>' :
'<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">Regular User</p>'
}
${ user . comment ? ` <p style="margin: 4px 0; font-size: 14px; color: #6b7280;">Comment: ${ user . comment } </p> ` : '' }
</div>
</div>
<div class="user-actions" style="margin-top: 12px;">
${ user . telegram _id && ! user . is _telegram _admin ?
` <button class="button button-primary" onclick="makeAdmin(' ${ user . id } ')" style="margin-right: 8px;">
<span class="icon">👑</span>
Make Admin
</button> ` : ''
}
${ user . telegram _id && user . is _telegram _admin ?
` <button class="button button-danger" onclick="removeAdmin(' ${ user . id } ')">
<span class="icon">👑</span>
Remove Admin
</button> ` : ''
}
${ ! user . telegram _id ?
'<span style="color: #6b7280; font-size: 14px;">User must connect to Telegram first</span>' : ''
}
</div>
</div>
` ) . join ( '' ) ;
container . innerHTML = usersHtml ;
}
// Add Enter key support for search
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
document . getElementById ( 'userSearchInput' ) . addEventListener ( 'keypress' , function ( e ) {
if ( e . key === 'Enter' ) {
searchUsers ( ) ;
}
} ) ;
} ) ;
// Test message form handler
document . getElementById ( 'testMessageForm' ) . addEventListener ( 'submit' , async function ( e ) {
e . preventDefault ( ) ;
const chatId = document . getElementById ( 'testChatId' ) . value ;
const message = document . getElementById ( 'testMessage' ) . value ;
if ( ! chatId || ! message ) {
showAlert ( 'Please fill all fields' , 'error' ) ;
return ;
}
try {
const response = await fetch ( ` ${ API _BASE } /telegram/send ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( {
chat _id : parseInt ( chatId ) ,
text : message
} )
} ) ;
if ( response . ok ) {
showAlert ( 'Message sent successfully' , 'success' ) ;
document . getElementById ( 'testMessage' ) . value = '' ;
} else {
const error = await response . text ( ) ;
showAlert ( 'Error sending message: ' + error , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error sending message: ' + error . message , 'error' ) ;
}
} ) ;
2025-10-19 04:13:36 +03:00
// User Requests Functions
async function loadUserRequests ( ) {
await loadRequestStats ( ) ;
await loadPendingRequests ( ) ;
await loadAllRequests ( ) ;
}
async function loadRequestStats ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /user-requests ` ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
const requests = data . items || [ ] ;
const stats = {
pending : requests . filter ( r => r . status === 'pending' ) . length ,
approved : requests . filter ( r => r . status === 'approved' ) . length ,
declined : requests . filter ( r => r . status === 'declined' ) . length
} ;
document . getElementById ( 'pendingRequests' ) . textContent = stats . pending ;
document . getElementById ( 'approvedRequests' ) . textContent = stats . approved ;
document . getElementById ( 'declinedRequests' ) . textContent = stats . declined ;
}
} catch ( error ) {
console . error ( 'Error loading request stats:' , error ) ;
}
}
async function loadPendingRequests ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /user-requests?status=pending ` ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
renderPendingRequests ( data . items || [ ] ) ;
} else {
document . getElementById ( 'pendingRequestsTable' ) . innerHTML = '<p class="error-message">Failed to load pending requests</p>' ;
}
} catch ( error ) {
console . error ( 'Error loading pending requests:' , error ) ;
document . getElementById ( 'pendingRequestsTable' ) . innerHTML = '<p class="error-message">Error loading pending requests</p>' ;
}
}
async function loadAllRequests ( ) {
try {
const response = await fetch ( ` ${ API _BASE } /user-requests ` ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
renderAllRequests ( data . items || [ ] ) ;
} else {
document . getElementById ( 'allRequestsTable' ) . innerHTML = '<p class="error-message">Failed to load requests</p>' ;
}
} catch ( error ) {
console . error ( 'Error loading requests:' , error ) ;
document . getElementById ( 'allRequestsTable' ) . innerHTML = '<p class="error-message">Error loading requests</p>' ;
}
}
function renderPendingRequests ( requests ) {
const container = document . getElementById ( 'pendingRequestsTable' ) ;
if ( requests . length === 0 ) {
container . innerHTML = '<p class="text-muted">No pending requests</p>' ;
return ;
}
const html = `
<div class="request-cards">
${ requests . map ( request => `
<div class="request-card">
<div class="request-header">
<h4> ${ escapeHtml ( request . full _name ) } </h4>
<span class="badge badge-warning">Pending</span>
</div>
<div class="request-info">
<p>📱 Telegram: ${ request . telegram _username ? '@' + escapeHtml ( request . telegram _username ) : 'ID: ' + request . telegram _id } </p>
<p>📅 Requested: ${ new Date ( request . created _at ) . toLocaleString ( ) } </p>
${ request . request _message ? ` <p>💬 Message: ${ escapeHtml ( request . request _message ) } </p> ` : '' }
</div>
<div class="request-actions">
<button class="button button-success" onclick="approveRequest(' ${ request . id } ')">
✅ Approve
</button>
<button class="button button-danger" onclick="declineRequest(' ${ request . id } ')">
❌ Decline
</button>
</div>
</div>
` ) . join ( '' ) }
</div>
` ;
container . innerHTML = html ;
container . classList . remove ( 'loading' ) ;
}
function renderAllRequests ( requests ) {
const container = document . getElementById ( 'allRequestsTable' ) ;
if ( requests . length === 0 ) {
container . innerHTML = '<p class="text-muted">No requests found</p>' ;
return ;
}
const html = `
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Telegram</th>
<th>Status</th>
<th>Requested</th>
<th>Processed By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${ requests . map ( request => `
<tr>
<td> ${ escapeHtml ( request . full _name ) } </td>
<td> ${ request . telegram _username ? '@' + escapeHtml ( request . telegram _username ) : 'ID: ' + request . telegram _id } </td>
<td>
<span class="badge badge- ${ getStatusBadgeClass ( request . status ) } ">
${ escapeHtml ( request . status ) }
</span>
</td>
<td> ${ new Date ( request . created _at ) . toLocaleString ( ) } </td>
<td> ${ request . processed _at ? new Date ( request . processed _at ) . toLocaleString ( ) : '-' } </td>
<td>
${ request . status === 'pending' ? `
<button class="button button-small button-success" onclick="approveRequest(' ${ request . id } ')">
Approve
</button>
<button class="button button-small button-danger" onclick="declineRequest(' ${ request . id } ')">
Decline
</button>
` : `
<button class="button button-small button-outline" onclick="deleteRequest(' ${ request . id } ')">
Delete
</button>
` }
</td>
</tr>
` ) . join ( '' ) }
</tbody>
</table>
</div>
` ;
container . innerHTML = html ;
container . classList . remove ( 'loading' ) ;
}
function getStatusBadgeClass ( status ) {
switch ( status ) {
case 'pending' : return 'warning' ;
case 'approved' : return 'success' ;
case 'declined' : return 'danger' ;
default : return 'secondary' ;
}
}
async function approveRequest ( requestId ) {
const message = prompt ( 'Optional message for the user:' ) ;
try {
const response = await fetch ( ` ${ API _BASE } /user-requests/ ${ requestId } /approve ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { response _message : message } )
} ) ;
if ( response . ok ) {
showAlert ( 'Request approved successfully' , 'success' ) ;
await loadUserRequests ( ) ;
} else {
const error = await response . text ( ) ;
showAlert ( 'Failed to approve request: ' + error , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error approving request: ' + error . message , 'error' ) ;
}
}
async function declineRequest ( requestId ) {
const message = prompt ( 'Reason for declining (optional):' ) ;
if ( message === null ) return ; // User cancelled
try {
const response = await fetch ( ` ${ API _BASE } /user-requests/ ${ requestId } /decline ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { response _message : message } )
} ) ;
if ( response . ok ) {
showAlert ( 'Request declined' , 'info' ) ;
await loadUserRequests ( ) ;
} else {
const error = await response . text ( ) ;
showAlert ( 'Failed to decline request: ' + error , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error declining request: ' + error . message , 'error' ) ;
}
}
async function deleteRequest ( requestId ) {
if ( ! confirm ( 'Are you sure you want to delete this request?' ) ) return ;
try {
const response = await fetch ( ` ${ API _BASE } /user-requests/ ${ requestId } ` , {
method : 'DELETE'
} ) ;
if ( response . ok ) {
showAlert ( 'Request deleted' , 'success' ) ;
await loadUserRequests ( ) ;
} else {
showAlert ( 'Failed to delete request' , 'error' ) ;
}
} catch ( error ) {
showAlert ( 'Error deleting request: ' + error . message , 'error' ) ;
}
}
2025-10-18 15:49:49 +03:00
// Update loadPageData function to include telegram
const originalLoadPageData = window . loadPageData ;
window . loadPageData = function ( page ) {
if ( page === 'telegram' ) {
loadTelegram ( ) ;
2025-10-19 04:13:36 +03:00
} else if ( page === 'user-requests' ) {
loadUserRequests ( ) ;
2025-10-18 15:49:49 +03:00
} else if ( originalLoadPageData ) {
originalLoadPageData ( page ) ;
}
} ;
2025-09-18 02:56:59 +03:00
// Initialize
loadPageData ( 'dashboard' ) ;
< / script >
< / body >
< / html >