mirror of
				https://github.com/house-of-vanity/OutFleet.git
				synced 2025-10-25 17:59:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			3369 lines
		
	
	
		
			134 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			3369 lines
		
	
	
		
			134 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!DOCTYPE html>
 | |
| <html lang="en">
 | |
| <head>
 | |
|     <meta charset="UTF-8">
 | |
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | |
|     <title>Xray Admin Panel</title>
 | |
|     <style>
 | |
|         * {
 | |
|             margin: 0;
 | |
|             padding: 0;
 | |
|             box-sizing: border-box;
 | |
|         }
 | |
|         
 | |
|         body {
 | |
|             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
 | |
|             background: #f5f5f7;
 | |
|             color: #1d1d1f;
 | |
|             min-height: 100vh;
 | |
|         }
 | |
|         
 | |
|         .layout {
 | |
|             display: flex;
 | |
|             min-height: 100vh;
 | |
|         }
 | |
|         
 | |
|         .sidebar {
 | |
|             width: 260px;
 | |
|             background: white;
 | |
|             border-right: 1px solid #d2d2d7;
 | |
|             padding: 20px 0;
 | |
|         }
 | |
|         
 | |
|         .sidebar-header {
 | |
|             padding: 0 20px 20px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|             margin-bottom: 20px;
 | |
|         }
 | |
|         
 | |
|         .sidebar-header h1 {
 | |
|             font-size: 20px;
 | |
|             font-weight: 600;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
|         
 | |
|         .nav-menu {
 | |
|             list-style: none;
 | |
|         }
 | |
|         
 | |
|         .nav-item {
 | |
|             margin-bottom: 2px;
 | |
|         }
 | |
|         
 | |
|         .nav-link {
 | |
|             display: block;
 | |
|             padding: 12px 20px;
 | |
|             color: #424245;
 | |
|             text-decoration: none;
 | |
|             font-weight: 400;
 | |
|             transition: all 0.2s;
 | |
|         }
 | |
|         
 | |
|         .nav-link:hover,
 | |
|         .nav-link.active {
 | |
|             background: #f0f0f0;
 | |
|             color: #0071e3;
 | |
|         }
 | |
|         
 | |
|         .main-content {
 | |
|             flex: 1;
 | |
|             padding: 30px;
 | |
|             overflow-y: auto;
 | |
|         }
 | |
|         
 | |
|         .page-header {
 | |
|             margin-bottom: 30px;
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: flex-start;
 | |
|         }
 | |
| 
 | |
|         .page-header-content {
 | |
|             flex: 1;
 | |
|         }
 | |
|         
 | |
|         .page-title {
 | |
|             font-size: 32px;
 | |
|             font-weight: 600;
 | |
|             margin-bottom: 8px;
 | |
|         }
 | |
|         
 | |
|         .page-subtitle {
 | |
|             color: #6e6e73;
 | |
|             font-size: 16px;
 | |
|         }
 | |
|         
 | |
|         .card {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             padding: 24px;
 | |
|             margin-bottom: 24px;
 | |
|             box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | |
|         }
 | |
|         
 | |
|         .card-header {
 | |
|             margin-bottom: 20px;
 | |
|             padding-bottom: 16px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|         }
 | |
|         
 | |
|         .card-title {
 | |
|             font-size: 18px;
 | |
|             font-weight: 600;
 | |
|             margin-bottom: 4px;
 | |
|         }
 | |
|         
 | |
|         .card-subtitle {
 | |
|             color: #6e6e73;
 | |
|             font-size: 14px;
 | |
|         }
 | |
|         
 | |
|         .btn {
 | |
|             padding: 10px 20px;
 | |
|             border: none;
 | |
|             border-radius: 8px;
 | |
|             font-size: 14px;
 | |
|             font-weight: 500;
 | |
|             cursor: pointer;
 | |
|             transition: all 0.2s;
 | |
|             text-decoration: none;
 | |
|             display: inline-block;
 | |
|             text-align: center;
 | |
|         }
 | |
|         
 | |
|         .btn-primary {
 | |
|             background: #0071e3;
 | |
|             color: white;
 | |
|         }
 | |
|         
 | |
|         .btn-primary:hover {
 | |
|             background: #0077ed;
 | |
|         }
 | |
|         
 | |
|         .btn-secondary {
 | |
|             background: #f5f5f7;
 | |
|             color: #1d1d1f;
 | |
|             border: 1px solid #d2d2d7;
 | |
|         }
 | |
|         
 | |
|         .btn-secondary:hover {
 | |
|             background: #e8e8ed;
 | |
|         }
 | |
|         
 | |
|         .btn-success {
 | |
|             background: #28a745;
 | |
|             color: white;
 | |
|         }
 | |
|         
 | |
|         .btn-success:hover {
 | |
|             background: #218838;
 | |
|         }
 | |
|         
 | |
|         .btn-danger {
 | |
|             background: #dc3545;
 | |
|             color: white;
 | |
|         }
 | |
|         
 | |
|         .btn-danger:hover {
 | |
|             background: #c82333;
 | |
|         }
 | |
|         
 | |
|         .btn-small {
 | |
|             padding: 6px 12px;
 | |
|             font-size: 12px;
 | |
|         }
 | |
|         
 | |
|         .stats-grid {
 | |
|             display: grid;
 | |
|             grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 | |
|             gap: 20px;
 | |
|             margin-bottom: 30px;
 | |
|         }
 | |
|         
 | |
|         .stat-card {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             padding: 20px;
 | |
|             box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | |
|         }
 | |
|         
 | |
|         .stat-value {
 | |
|             font-size: 32px;
 | |
|             font-weight: 700;
 | |
|             color: #0071e3;
 | |
|             margin-bottom: 4px;
 | |
|         }
 | |
|         
 | |
|         .stat-label {
 | |
|             color: #6e6e73;
 | |
|             font-size: 14px;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .table {
 | |
|             width: 100%;
 | |
|             border-collapse: collapse;
 | |
|         }
 | |
|         
 | |
|         .table th,
 | |
|         .table td {
 | |
|             text-align: left;
 | |
|             padding: 12px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|         }
 | |
|         
 | |
|         .table th {
 | |
|             font-weight: 600;
 | |
|             color: #6e6e73;
 | |
|             font-size: 12px;
 | |
|             text-transform: uppercase;
 | |
|             letter-spacing: 0.5px;
 | |
|         }
 | |
|         
 | |
|         .table tr:hover {
 | |
|             background: #f8f9fa;
 | |
|         }
 | |
|         
 | |
|         .status-badge {
 | |
|             padding: 4px 8px;
 | |
|             border-radius: 12px;
 | |
|             font-size: 11px;
 | |
|             font-weight: 600;
 | |
|             text-transform: uppercase;
 | |
|             letter-spacing: 0.5px;
 | |
|         }
 | |
|         
 | |
|         .status-online {
 | |
|             background: #d4edda;
 | |
|             color: #155724;
 | |
|         }
 | |
|         
 | |
|         .status-offline {
 | |
|             background: #f8d7da;
 | |
|             color: #721c24;
 | |
|         }
 | |
|         
 | |
|         .status-unknown {
 | |
|             background: #e2e3e5;
 | |
|             color: #6c757d;
 | |
|         }
 | |
|         
 | |
|         .status-success {
 | |
|             color: #28a745;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .status-error {
 | |
|             color: #dc3545;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .status-running {
 | |
|             color: #007bff;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .status-idle {
 | |
|             color: #6c757d;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .task-stats {
 | |
|             font-size: 0.875rem;
 | |
|         }
 | |
|         
 | |
|         .success-rate {
 | |
|             margin-top: 2px;
 | |
|             font-size: 0.75rem;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .success-rate-good {
 | |
|             color: #28a745;
 | |
|         }
 | |
|         
 | |
|         .success-rate-ok {
 | |
|             color: #ffc107;
 | |
|         }
 | |
|         
 | |
|         .success-rate-bad {
 | |
|             color: #dc3545;
 | |
|         }
 | |
|         
 | |
|         .actions {
 | |
|             display: flex;
 | |
|             gap: 8px;
 | |
|         }
 | |
|         
 | |
|         .page-section {
 | |
|             display: none;
 | |
|         }
 | |
|         
 | |
|         .page-section.active {
 | |
|             display: block;
 | |
|         }
 | |
|         
 | |
|         .modal-overlay {
 | |
|             position: fixed;
 | |
|             top: 0;
 | |
|             left: 0;
 | |
|             right: 0;
 | |
|             bottom: 0;
 | |
|             background: rgba(0, 0, 0, 0.5);
 | |
|             display: flex;
 | |
|             justify-content: center;
 | |
|             align-items: center;
 | |
|             z-index: 1000;
 | |
|         }
 | |
|         
 | |
|         .modal-content {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             padding: 24px;
 | |
|             max-width: 600px;
 | |
|             width: 90%;
 | |
|             max-height: 80vh;
 | |
|             overflow-y: auto;
 | |
|             box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 | |
|         }
 | |
|         
 | |
|         .modal-header {
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: center;
 | |
|             margin-bottom: 20px;
 | |
|         }
 | |
|         
 | |
|         .modal-header h2 {
 | |
|             margin: 0;
 | |
|         }
 | |
|         
 | |
|         .modal-body {
 | |
|             margin-bottom: 20px;
 | |
|         }
 | |
|         
 | |
|         .modal-actions {
 | |
|             display: flex;
 | |
|             justify-content: flex-end;
 | |
|             gap: 10px;
 | |
|             padding-top: 20px;
 | |
|             border-top: 1px solid #eee;
 | |
|         }
 | |
|         
 | |
|         .server-section {
 | |
|             margin-bottom: 20px;
 | |
|             padding: 15px;
 | |
|             background: #f8f9fa;
 | |
|             border-radius: 8px;
 | |
|         }
 | |
|         
 | |
|         .server-section h3 {
 | |
|             margin: 0 0 10px 0;
 | |
|             font-size: 14px;
 | |
|             color: #333;
 | |
|         }
 | |
|         
 | |
|         .inbound-item {
 | |
|             margin: 5px 0;
 | |
|         }
 | |
|         
 | |
|         .inbound-item label {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 8px;
 | |
|             cursor: pointer;
 | |
|         }
 | |
|         
 | |
|         .inbound-item input[type="checkbox"] {
 | |
|             cursor: pointer;
 | |
|         }
 | |
|         
 | |
|         .required {
 | |
|             color: #dc3545;
 | |
|         }
 | |
|         
 | |
|         .form-grid {
 | |
|             display: grid;
 | |
|             grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
 | |
|             gap: 20px;
 | |
|         }
 | |
|         
 | |
|         .form-group {
 | |
|             margin-bottom: 16px;
 | |
|         }
 | |
|         
 | |
|         .form-label {
 | |
|             display: block;
 | |
|             margin-bottom: 6px;
 | |
|             font-weight: 500;
 | |
|             font-size: 14px;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
|         
 | |
|         .form-input {
 | |
|             width: 100%;
 | |
|             padding: 10px 12px;
 | |
|             border: 1px solid #d2d2d7;
 | |
|             border-radius: 8px;
 | |
|             font-size: 14px;
 | |
|             transition: border-color 0.2s;
 | |
|         }
 | |
|         
 | |
|         .form-input:focus {
 | |
|             outline: none;
 | |
|             border-color: #0071e3;
 | |
|         }
 | |
|         
 | |
|         .form-select {
 | |
|             width: 100%;
 | |
|             padding: 10px 12px;
 | |
|             border: 1px solid #d2d2d7;
 | |
|             border-radius: 8px;
 | |
|             font-size: 14px;
 | |
|             background: white;
 | |
|         }
 | |
|         
 | |
|         .alert {
 | |
|             padding: 12px 16px;
 | |
|             border-radius: 8px;
 | |
|             margin-bottom: 20px;
 | |
|             display: none;
 | |
|         }
 | |
|         
 | |
|         .alert.show {
 | |
|             display: block;
 | |
|         }
 | |
|         
 | |
|         .alert-success {
 | |
|             background: #d4edda;
 | |
|             color: #155724;
 | |
|             border: 1px solid #c3e6cb;
 | |
|         }
 | |
|         
 | |
|         .alert-error {
 | |
|             background: #f8d7da;
 | |
|             color: #721c24;
 | |
|             border: 1px solid #f5c6cb;
 | |
|         }
 | |
|         
 | |
|         .empty-state {
 | |
|             text-align: center;
 | |
|             padding: 60px 20px;
 | |
|             color: #6e6e73;
 | |
|         }
 | |
|         
 | |
|         .empty-state h3 {
 | |
|             margin-bottom: 8px;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
|         
 | |
|         .loading {
 | |
|             text-align: center;
 | |
|             padding: 40px;
 | |
|             color: #6e6e73;
 | |
|         }
 | |
|         
 | |
|         /* Modal styles */
 | |
|         .modal-overlay {
 | |
|             position: fixed;
 | |
|             top: 0;
 | |
|             left: 0;
 | |
|             right: 0;
 | |
|             bottom: 0;
 | |
|             background: rgba(0, 0, 0, 0.5);
 | |
|             display: flex;
 | |
|             justify-content: center;
 | |
|             align-items: center;
 | |
|             z-index: 1000;
 | |
|         }
 | |
|         
 | |
|         .modal-content {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             max-width: 800px;
 | |
|             max-height: 80vh;
 | |
|             overflow-y: auto;
 | |
|             width: 90%;
 | |
|             box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
 | |
|         }
 | |
|         
 | |
|         .modal-header {
 | |
|             padding: 20px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: center;
 | |
|         }
 | |
|         
 | |
|         .modal-header h2 {
 | |
|             margin: 0;
 | |
|             font-size: 18px;
 | |
|             font-weight: 600;
 | |
|         }
 | |
|         
 | |
|         .modal-body {
 | |
|             padding: 20px;
 | |
|         }
 | |
|         
 | |
|         .server-section {
 | |
|             margin-bottom: 30px;
 | |
|             padding: 15px;
 | |
|             border: 1px solid #f0f0f0;
 | |
|             border-radius: 8px;
 | |
|         }
 | |
|         
 | |
|         .server-section h3 {
 | |
|             margin: 0 0 15px 0;
 | |
|             font-size: 16px;
 | |
|             font-weight: 600;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
|         
 | |
|         .inbound-item {
 | |
|             margin: 10px 0;
 | |
|         }
 | |
|         
 | |
|         .inbound-item label {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             cursor: pointer;
 | |
|             padding: 8px;
 | |
|             border-radius: 4px;
 | |
|             transition: background-color 0.2s;
 | |
|         }
 | |
|         
 | |
|         .inbound-item label:hover {
 | |
|             background: #f8f9fa;
 | |
|         }
 | |
|         
 | |
|         .inbound-item input[type="checkbox"] {
 | |
|             margin-right: 10px;
 | |
|         }
 | |
|         
 | |
|         .user-details {
 | |
|             margin-top: 30px;
 | |
|             padding: 20px;
 | |
|             background: #f8f9fa;
 | |
|             border-radius: 8px;
 | |
|         }
 | |
|         
 | |
|         .user-details label {
 | |
|             display: block;
 | |
|             margin-bottom: 15px;
 | |
|             font-weight: 500;
 | |
|         }
 | |
|         
 | |
|         .user-details input {
 | |
|             width: 100%;
 | |
|             padding: 8px 12px;
 | |
|             border: 1px solid #d2d2d7;
 | |
|             border-radius: 4px;
 | |
|             margin-top: 5px;
 | |
|         }
 | |
|         
 | |
|         .input-group {
 | |
|             display: flex;
 | |
|             margin-top: 5px;
 | |
|         }
 | |
|         
 | |
|         .input-group input {
 | |
|             margin-top: 0;
 | |
|             border-radius: 4px 0 0 4px;
 | |
|             border-right: none;
 | |
|         }
 | |
|         
 | |
|         .refresh-btn {
 | |
|             background: #f8f9fa;
 | |
|             border: 1px solid #d2d2d7;
 | |
|             border-radius: 0 4px 4px 0;
 | |
|             padding: 8px 12px;
 | |
|             cursor: pointer;
 | |
|             font-size: 16px;
 | |
|             transition: background-color 0.2s;
 | |
|         }
 | |
|         
 | |
|         .refresh-btn:hover {
 | |
|             background: #e9ecef;
 | |
|         }
 | |
|         
 | |
|         .modal-actions {
 | |
|             margin-top: 20px;
 | |
|             display: flex;
 | |
|             gap: 10px;
 | |
|             justify-content: flex-end;
 | |
|         }
 | |
| 
 | |
|         /* Task Summary Cards */
 | |
|         .task-summary-grid {
 | |
|             display: grid;
 | |
|             grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
 | |
|             gap: 20px;
 | |
|             margin-bottom: 30px;
 | |
|         }
 | |
| 
 | |
|         .summary-card {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             padding: 20px;
 | |
|             box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 15px;
 | |
|         }
 | |
| 
 | |
|         .summary-icon {
 | |
|             font-size: 32px;
 | |
|             width: 60px;
 | |
|             height: 60px;
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             justify-content: center;
 | |
|             background: #f5f5f7;
 | |
|             border-radius: 12px;
 | |
|         }
 | |
| 
 | |
|         .summary-content h3 {
 | |
|             font-size: 24px;
 | |
|             font-weight: 600;
 | |
|             margin: 0 0 4px 0;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
| 
 | |
|         .summary-content p {
 | |
|             margin: 0;
 | |
|             color: #6e6e73;
 | |
|             font-size: 14px;
 | |
|         }
 | |
| 
 | |
|         /* Task Status Badges */
 | |
|         .task-status {
 | |
|             padding: 4px 12px;
 | |
|             border-radius: 20px;
 | |
|             font-size: 12px;
 | |
|             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: 8px;
 | |
|         }
 | |
| 
 | |
|         .task-actions .btn {
 | |
|             padding: 6px 12px;
 | |
|             font-size: 12px;
 | |
|         }
 | |
| 
 | |
|         /* Task List Items */
 | |
|         .task-item {
 | |
|             background: white;
 | |
|             border-radius: 12px;
 | |
|             padding: 20px;
 | |
|             margin-bottom: 16px;
 | |
|             box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | |
|             border: 1px solid #f0f0f0;
 | |
|         }
 | |
| 
 | |
|         .task-header {
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: flex-start;
 | |
|             margin-bottom: 12px;
 | |
|         }
 | |
| 
 | |
|         .task-info h3 {
 | |
|             font-size: 18px;
 | |
|             font-weight: 600;
 | |
|             margin: 0 0 4px 0;
 | |
|             color: #1d1d1f;
 | |
|         }
 | |
| 
 | |
|         .task-description {
 | |
|             color: #6e6e73;
 | |
|             font-size: 14px;
 | |
|             margin: 0 0 8px 0;
 | |
|             line-height: 1.4;
 | |
|         }
 | |
| 
 | |
|         .task-schedule {
 | |
|             color: #8e8e93;
 | |
|             font-size: 13px;
 | |
|             font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
 | |
|             margin: 0;
 | |
|         }
 | |
| 
 | |
|         .task-status-container {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 10px;
 | |
|         }
 | |
| 
 | |
|         .task-stats {
 | |
|             display: grid;
 | |
|             grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
 | |
|             gap: 16px;
 | |
|             margin-top: 16px;
 | |
|             padding-top: 16px;
 | |
|             border-top: 1px solid #f0f0f0;
 | |
|         }
 | |
| 
 | |
|         .stat {
 | |
|             text-align: center;
 | |
|         }
 | |
| 
 | |
|         .stat .stat-value {
 | |
|             font-size: 20px;
 | |
|             font-weight: 600;
 | |
|             color: #1d1d1f;
 | |
|             margin: 0 0 2px 0;
 | |
|         }
 | |
| 
 | |
|         .stat .stat-label {
 | |
|             font-size: 12px;
 | |
|             color: #8e8e93;
 | |
|             text-transform: uppercase;
 | |
|             letter-spacing: 0.5px;
 | |
|             margin: 0;
 | |
|         }
 | |
| 
 | |
|         /* Telegram Bot Styles */
 | |
|         .status-indicator {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 8px;
 | |
|         }
 | |
| 
 | |
|         .status-dot {
 | |
|             width: 10px;
 | |
|             height: 10px;
 | |
|             border-radius: 50%;
 | |
|         }
 | |
| 
 | |
|         .status-dot.status-active {
 | |
|             background: #34c759;
 | |
|             animation: pulse 2s 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 10px rgba(52, 199, 89, 0);
 | |
|             }
 | |
|             100% {
 | |
|                 box-shadow: 0 0 0 0 rgba(52, 199, 89, 0);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         .status-text {
 | |
|             font-size: 14px;
 | |
|             font-weight: 500;
 | |
|         }
 | |
| 
 | |
|         .form-input-group {
 | |
|             display: flex;
 | |
|             gap: 8px;
 | |
|             align-items: center;
 | |
|         }
 | |
| 
 | |
|         .form-input-group .form-input {
 | |
|             flex: 1;
 | |
|         }
 | |
| 
 | |
|         .form-input-group .button {
 | |
|             padding: 8px 12px;
 | |
|         }
 | |
| 
 | |
|         .form-checkbox {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 12px;
 | |
|             cursor: pointer;
 | |
|             padding: 8px 0;
 | |
|         }
 | |
| 
 | |
|         .form-checkbox input[type="checkbox"] {
 | |
|             width: 18px;
 | |
|             height: 18px;
 | |
|             margin: 0;
 | |
|         }
 | |
| 
 | |
|         .admin-item {
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: center;
 | |
|             padding: 12px 16px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|         }
 | |
| 
 | |
|         .admin-item:last-child {
 | |
|             border-bottom: none;
 | |
|         }
 | |
| 
 | |
|         .admin-info {
 | |
|             flex: 1;
 | |
|         }
 | |
| 
 | |
|         .admin-name {
 | |
|             font-weight: 500;
 | |
|             color: #1d1d1f;
 | |
|             margin: 0 0 4px 0;
 | |
|         }
 | |
| 
 | |
|         .admin-telegram-id {
 | |
|             font-size: 12px;
 | |
|             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: 12px 16px;
 | |
|             border-bottom: 1px solid #f0f0f0;
 | |
|         }
 | |
| 
 | |
|         .user-item:last-child {
 | |
|             border-bottom: none;
 | |
|         }
 | |
| 
 | |
|         .user-info {
 | |
|             flex: 1;
 | |
|         }
 | |
| 
 | |
|         .user-name {
 | |
|             font-weight: 500;
 | |
|             color: #1d1d1f;
 | |
|             margin: 0 0 4px 0;
 | |
|         }
 | |
| 
 | |
|         .user-telegram-status {
 | |
|             display: flex;
 | |
|             align-items: center;
 | |
|             gap: 8px;
 | |
|             font-size: 12px;
 | |
|             color: #6e6e73;
 | |
|         }
 | |
| 
 | |
|         .telegram-connected {
 | |
|             color: #34c759;
 | |
|         }
 | |
| 
 | |
|         .telegram-not-connected {
 | |
|             color: #8e8e93;
 | |
|         }
 | |
| 
 | |
|         .bot-info {
 | |
|             padding: 16px;
 | |
|             background: #f8f9fa;
 | |
|             border-radius: 8px;
 | |
|             margin: 12px 0;
 | |
|         }
 | |
| 
 | |
|         .bot-info-item {
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             margin-bottom: 8px;
 | |
|         }
 | |
| 
 | |
|         .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;
 | |
|         }
 | |
|         
 | |
|         /* User Requests Styles */
 | |
|         .request-cards {
 | |
|             display: flex;
 | |
|             flex-direction: column;
 | |
|             gap: 16px;
 | |
|         }
 | |
|         
 | |
|         .request-card {
 | |
|             border: 1px solid #e0e0e0;
 | |
|             border-radius: 8px;
 | |
|             padding: 20px;
 | |
|             background: #fafafa;
 | |
|         }
 | |
|         
 | |
|         .request-header {
 | |
|             display: flex;
 | |
|             justify-content: space-between;
 | |
|             align-items: center;
 | |
|             margin-bottom: 16px;
 | |
|         }
 | |
|         
 | |
|         .request-header h4 {
 | |
|             margin: 0;
 | |
|             font-size: 18px;
 | |
|             font-weight: 600;
 | |
|         }
 | |
|         
 | |
|         .request-info {
 | |
|             margin-bottom: 16px;
 | |
|         }
 | |
|         
 | |
|         .request-info p {
 | |
|             margin: 8px 0;
 | |
|             color: #666;
 | |
|         }
 | |
|         
 | |
|         .request-actions {
 | |
|             display: flex;
 | |
|             gap: 12px;
 | |
|         }
 | |
|         
 | |
|         .badge {
 | |
|             display: inline-flex;
 | |
|             align-items: center;
 | |
|             padding: 4px 12px;
 | |
|             border-radius: 12px;
 | |
|             font-size: 12px;
 | |
|             font-weight: 600;
 | |
|             text-transform: uppercase;
 | |
|             letter-spacing: 0.5px;
 | |
|         }
 | |
|         
 | |
|         .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: 6px 12px;
 | |
|             font-size: 14px;
 | |
|         }
 | |
|     </style>
 | |
| </head>
 | |
| <body>
 | |
|     <div class="layout">
 | |
|         <aside class="sidebar">
 | |
|             <div class="sidebar-header">
 | |
|                 <h1>Xray Admin</h1>
 | |
|             </div>
 | |
|             <nav>
 | |
|                 <ul class="nav-menu">
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#dashboard" class="nav-link active" onclick="showPage('dashboard')">Dashboard</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#servers" class="nav-link" onclick="showPage('servers')">Servers</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#templates" class="nav-link" onclick="showPage('templates')">Templates</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#certificates" class="nav-link" onclick="showPage('certificates')">Certificates</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#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>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#users" class="nav-link" onclick="showPage('users')">Users</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#telegram" class="nav-link" onclick="showPage('telegram')">Telegram Bot</a>
 | |
|                     </li>
 | |
|                     <li class="nav-item">
 | |
|                         <a href="#user-requests" class="nav-link" onclick="showPage('user-requests')">User Requests</a>
 | |
|                     </li>
 | |
|                 </ul>
 | |
|             </nav>
 | |
|         </aside>
 | |
|         
 | |
|         <main class="main-content">
 | |
|             <div id="alert" class="alert"></div>
 | |
|             
 | |
|             <!-- Dashboard -->
 | |
|             <section id="dashboard" class="page-section active">
 | |
|                 <div class="page-header">
 | |
|                     <h1 class="page-title">Dashboard</h1>
 | |
|                     <p class="page-subtitle">Overview of your Xray infrastructure</p>
 | |
|                 </div>
 | |
|                 
 | |
|                 <div class="stats-grid">
 | |
|                     <div class="stat-card">
 | |
|                         <div class="stat-value" id="totalServers">-</div>
 | |
|                         <div class="stat-label">Total Servers</div>
 | |
|                     </div>
 | |
|                     <div class="stat-card">
 | |
|                         <div class="stat-value" id="onlineServers">-</div>
 | |
|                         <div class="stat-label">Online Servers</div>
 | |
|                     </div>
 | |
|                     <div class="stat-card">
 | |
|                         <div class="stat-value" id="totalCertificates">-</div>
 | |
|                         <div class="stat-label">Certificates</div>
 | |
|                     </div>
 | |
|                     <div class="stat-card">
 | |
|                         <div class="stat-value" id="totalUsers">-</div>
 | |
|                         <div class="stat-label">Users</div>
 | |
|                     </div>
 | |
|                 </div>
 | |
|                 
 | |
|                 <div class="card">
 | |
|                     <div class="card-header">
 | |
|                         <h2 class="card-title">Recent Activity</h2>
 | |
|                         <p class="card-subtitle">Latest server status and activity</p>
 | |
|                     </div>
 | |
|                     <div id="recentActivity" class="loading">Loading...</div>
 | |
|                 </div>
 | |
|             </section>
 | |
|             
 | |
|             <!-- Servers -->
 | |
|             <section id="servers" class="page-section">
 | |
|                 <div class="page-header">
 | |
|                     <h1 class="page-title">Servers</h1>
 | |
|                     <p class="page-subtitle">Manage your Xray server instances</p>
 | |
|                 </div>
 | |
|                 
 | |
|                 <div class="card">
 | |
|                     <div class="card-header">
 | |
|                         <h2 class="card-title">Add New Server</h2>
 | |
|                         <p class="card-subtitle">Register a new Xray server instance</p>
 | |
|                     </div>
 | |
|                     <form id="serverForm" class="form-grid">
 | |
|                         <div class="form-group">
 | |
|                             <label class="form-label" for="serverName">Server Name *</label>
 | |
|                             <input type="text" id="serverName" class="form-input" placeholder="Enter server name">
 | |
|                         </div>
 | |
|                         <div class="form-group">
 | |
|                             <label class="form-label" for="serverHostname">Public Hostname *</label>
 | |
|                             <input type="text" id="serverHostname" class="form-input" placeholder="server.example.com">
 | |
|                             <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>
 | |
|                         </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">
 | |
|                     <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>
 | |
|                 </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>
 | |
| 
 | |
|             <!-- 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>
 | |
|             
 | |
|             <!-- Users -->
 | |
|             <section id="users" class="page-section">
 | |
|                 <div class="page-header">
 | |
|                     <div class="page-header-content">
 | |
|                         <h1 class="page-title">Users</h1>
 | |
|                         <p class="page-subtitle">Manage user accounts and access</p>
 | |
|                     </div>
 | |
|                     <button class="btn btn-primary" onclick="showCreateUserModal()">+ Create User</button>
 | |
|                 </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>
 | |
| 
 | |
|             <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>
 | |
|             
 | |
|             <!-- 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>
 | |
|         </main>
 | |
|     </div>
 | |
|     
 | |
|     <script>
 | |
|         const API_BASE = '/api';
 | |
|         let currentPage = 'dashboard';
 | |
|         
 | |
|         // Navigation
 | |
|         function showPage(page) {
 | |
|             // Hide all pages
 | |
|             document.querySelectorAll('.page-section').forEach(section => {
 | |
|                 section.classList.remove('active');
 | |
|             });
 | |
|             
 | |
|             // Remove active class from all nav links
 | |
|             document.querySelectorAll('.nav-link').forEach(link => {
 | |
|                 link.classList.remove('active');
 | |
|             });
 | |
|             
 | |
|             // Show selected page
 | |
|             document.getElementById(page).classList.add('active');
 | |
|             document.querySelector(`[onclick="showPage('${page}')"]`).classList.add('active');
 | |
|             
 | |
|             currentPage = page;
 | |
|             
 | |
|             // Load page data
 | |
|             loadPageData(page);
 | |
|         }
 | |
|         
 | |
|         function loadPageData(page) {
 | |
|             switch(page) {
 | |
|                 case 'dashboard':
 | |
|                     loadDashboard();
 | |
|                     break;
 | |
|                 case 'servers':
 | |
|                     loadServers();
 | |
|                     break;
 | |
|                 case 'templates':
 | |
|                     loadTemplates();
 | |
|                     break;
 | |
|                 case 'certificates':
 | |
|                     loadCertificates();
 | |
|                     break;
 | |
|                 case 'dns-providers':
 | |
|                     loadDnsProviders();
 | |
|                     break;
 | |
|                 case 'tasks':
 | |
|                     loadTasks();
 | |
|                     break;
 | |
|                 case 'users':
 | |
|                     loadUsers();
 | |
|                     break;
 | |
|                 case 'tasks':
 | |
|                     loadTasks();
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Dashboard
 | |
|         async function loadDashboard() {
 | |
|             try {
 | |
|                 // Load statistics
 | |
|                 const [servers, certificates, users] = await Promise.all([
 | |
|                     fetch(`${API_BASE}/servers`).then(r => r.json()),
 | |
|                     fetch(`${API_BASE}/certificates`).then(r => r.json()),
 | |
|                     fetch(`${API_BASE}/users`).then(r => r.json())
 | |
|                 ]);
 | |
|                 
 | |
|                 document.getElementById('totalServers').textContent = servers.length;
 | |
|                 document.getElementById('onlineServers').textContent = servers.filter(s => s.status === 'online').length;
 | |
|                 document.getElementById('totalCertificates').textContent = certificates.length;
 | |
|                 document.getElementById('totalUsers').textContent = users.users ? users.users.length : users.length;
 | |
|                 
 | |
|                 // Recent activity
 | |
|                 const activity = servers.slice(0, 5).map(server => 
 | |
|                     `<p>Server "${server.name}" is ${server.status}</p>`
 | |
|                 ).join('');
 | |
|                 
 | |
|                 document.getElementById('recentActivity').innerHTML = activity || '<p>No recent activity</p>';
 | |
|             } catch (error) {
 | |
|                 showAlert('Error loading dashboard: ' + error.message, 'error');
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Servers
 | |
|         async function loadServers() {
 | |
|             try {
 | |
|                 const response = await fetch(`${API_BASE}/servers`);
 | |
|                 const servers = await response.json();
 | |
|                 
 | |
|                 if (servers.length === 0) {
 | |
|                     document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>No servers found</h3><p>Add your first server to get started</p></div>';
 | |
|                     return;
 | |
|                 }
 | |
|                 
 | |
|                 const table = `
 | |
|                     <table class="table">
 | |
|                         <thead>
 | |
|                             <tr>
 | |
|                                 <th>Name</th>
 | |
|                                 <th>Public Hostname</th>
 | |
|                                 <th>gRPC Address</th>
 | |
|                                 <th>Status</th>
 | |
|                                 <th>Actions</th>
 | |
|                             </tr>
 | |
|                         </thead>
 | |
|                         <tbody>
 | |
|                             ${servers.map(server => `
 | |
|                                 <tr>
 | |
|                                     <td><strong>${escapeHtml(server.name)}</strong></td>
 | |
|                                     <td>${escapeHtml(server.hostname)}</td>
 | |
|                                     <td>${escapeHtml(server.grpc_hostname)}:${server.grpc_port}</td>
 | |
|                                     <td><span class="status-badge status-${server.status}">${server.status}</span></td>
 | |
|                                     <td>
 | |
|                                         <div class="actions">
 | |
|                                             <button class="btn btn-small btn-primary" onclick="editServer('${server.id}')">Edit</button>
 | |
|                                             <button class="btn btn-small btn-success" onclick="testConnection('${server.id}')">Test</button>
 | |
|                                             <button class="btn btn-small btn-danger" onclick="deleteServer('${server.id}', '${escapeHtml(server.name)}')">Delete</button>
 | |
|                                         </div>
 | |
|                                     </td>
 | |
|                                 </tr>
 | |
|                             `).join('')}
 | |
|                         </tbody>
 | |
|                     </table>
 | |
|                 `;
 | |
|                 
 | |
|                 document.getElementById('serversTable').innerHTML = table;
 | |
|             } catch (error) {
 | |
|                 document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>Error loading servers</h3><p>' + error.message + '</p></div>';
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Templates
 | |
|         async function loadTemplates() {
 | |
|             try {
 | |
|                 const response = await fetch(`${API_BASE}/templates`);
 | |
|                 const templates = await response.json();
 | |
|                 
 | |
|                 if (templates.length === 0) {
 | |
|                     document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>No templates found</h3><p>Templates help you quickly configure server inbounds</p></div>';
 | |
|                     return;
 | |
|                 }
 | |
|                 
 | |
|                 const table = `
 | |
|                     <table class="table">
 | |
|                         <thead>
 | |
|                             <tr>
 | |
|                                 <th>Name</th>
 | |
|                                 <th>Protocol</th>
 | |
|                                 <th>Default Port</th>
 | |
|                                 <th>TLS Required</th>
 | |
|                                 <th>Active</th>
 | |
|                                 <th>Actions</th>
 | |
|                             </tr>
 | |
|                         </thead>
 | |
|                         <tbody>
 | |
|                             ${templates.map(template => `
 | |
|                                 <tr>
 | |
|                                     <td><strong>${escapeHtml(template.name)}</strong></td>
 | |
|                                     <td>${template.protocol}</td>
 | |
|                                     <td>${template.default_port}</td>
 | |
|                                     <td>${template.requires_tls ? 'Yes' : 'No'}</td>
 | |
|                                     <td><span class="status-badge ${template.is_active ? 'status-online' : 'status-offline'}">${template.is_active ? 'Active' : 'Inactive'}</span></td>
 | |
|                                     <td>
 | |
|                                         <div class="actions">
 | |
|                                             <button class="btn btn-small btn-secondary">Edit</button>
 | |
|                                             <button class="btn btn-small btn-danger">Delete</button>
 | |
|                                         </div>
 | |
|                                     </td>
 | |
|                                 </tr>
 | |
|                             `).join('')}
 | |
|                         </tbody>
 | |
|                     </table>
 | |
|                 `;
 | |
|                 
 | |
|                 document.getElementById('templatesTable').innerHTML = table;
 | |
|             } catch (error) {
 | |
|                 document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>Error loading templates</h3><p>' + error.message + '</p></div>';
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Certificates
 | |
|         async function loadCertificates() {
 | |
|             try {
 | |
|                 const response = await fetch(`${API_BASE}/certificates`);
 | |
|                 const certificates = await response.json();
 | |
|                 
 | |
|                 if (certificates.length === 0) {
 | |
|                     document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>No certificates found</h3><p>Upload or generate SSL certificates for your servers</p></div>';
 | |
|                     return;
 | |
|                 }
 | |
|                 
 | |
|                 const table = `
 | |
|                     <table class="table">
 | |
|                         <thead>
 | |
|                             <tr>
 | |
|                                 <th>Name</th>
 | |
|                                 <th>Domain</th>
 | |
|                                 <th>Type</th>
 | |
|                                 <th>Expires</th>
 | |
|                                 <th>Auto Renew</th>
 | |
|                                 <th>Actions</th>
 | |
|                             </tr>
 | |
|                         </thead>
 | |
|                         <tbody>
 | |
|                             ${certificates.map(cert => `
 | |
|                                 <tr>
 | |
|                                     <td><strong>${escapeHtml(cert.name)}</strong></td>
 | |
|                                     <td>${escapeHtml(cert.domain)}</td>
 | |
|                                     <td>${cert.cert_type}</td>
 | |
|                                     <td>${new Date(cert.expires_at).toLocaleDateString()}</td>
 | |
|                                     <td>${cert.auto_renew ? 'Yes' : 'No'}</td>
 | |
|                                     <td>
 | |
|                                         <div class="actions">
 | |
|                                             <button class="btn btn-small btn-secondary">View</button>
 | |
|                                             <button class="btn btn-small btn-danger">Delete</button>
 | |
|                                         </div>
 | |
|                                     </td>
 | |
|                                 </tr>
 | |
|                             `).join('')}
 | |
|                         </tbody>
 | |
|                     </table>
 | |
|                 `;
 | |
|                 
 | |
|                 document.getElementById('certificatesTable').innerHTML = table;
 | |
|             } catch (error) {
 | |
|                 document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>Error loading certificates</h3><p>' + error.message + '</p></div>';
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Users
 | |
|         async function loadUsers() {
 | |
|             try {
 | |
|                 const response = await fetch(`${API_BASE}/users`);
 | |
|                 const data = await response.json();
 | |
|                 const users = data.users || data;
 | |
|                 
 | |
|                 if (users.length === 0) {
 | |
|                     document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>No users found</h3><p>Create user accounts for access management</p></div>';
 | |
|                     return;
 | |
|                 }
 | |
|                 
 | |
|                 const table = `
 | |
|                     <table class="table">
 | |
|                         <thead>
 | |
|                             <tr>
 | |
|                                 <th>Name</th>
 | |
|                                 <th>Comment</th>
 | |
|                                 <th>Telegram ID</th>
 | |
|                                 <th>Created</th>
 | |
|                                 <th>Actions</th>
 | |
|                             </tr>
 | |
|                         </thead>
 | |
|                         <tbody>
 | |
|                             ${users.map(user => `
 | |
|                                 <tr>
 | |
|                                     <td><strong>${escapeHtml(user.name)}</strong></td>
 | |
|                                     <td>${escapeHtml(user.comment || '-')}</td>
 | |
|                                     <td>${user.telegram_id || '-'}</td>
 | |
|                                     <td>${new Date(user.created_at).toLocaleDateString()}</td>
 | |
|                                     <td>
 | |
|                                         <div class="actions">
 | |
|                                             <button class="btn btn-small btn-secondary" onclick="editUserAccess('${user.id}', '${escapeHtml(user.name)}')">Manage Access</button>
 | |
|                                             <button class="btn btn-small btn-danger">Delete</button>
 | |
|                                         </div>
 | |
|                                     </td>
 | |
|                                 </tr>
 | |
|                             `).join('')}
 | |
|                         </tbody>
 | |
|                     </table>
 | |
|                 `;
 | |
|                 
 | |
|                 document.getElementById('usersTable').innerHTML = table;
 | |
|             } catch (error) {
 | |
|                 document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>Error loading users</h3><p>' + error.message + '</p></div>';
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // Server form submission
 | |
|         document.getElementById('serverForm').addEventListener('submit', async (e) => {
 | |
|             e.preventDefault();
 | |
|             
 | |
|             const name = document.getElementById('serverName').value.trim();
 | |
|             const hostname = document.getElementById('serverHostname').value.trim();
 | |
|             const grpc_hostname = document.getElementById('serverGrpcHostname').value.trim();
 | |
|             const grpc_port = document.getElementById('serverPort').value;
 | |
|             const api_credentials = document.getElementById('serverCredentials').value.trim();
 | |
|             
 | |
|             if (!name || !hostname) {
 | |
|                 showAlert('Name and hostname are required', 'error');
 | |
|                 return;
 | |
|             }
 | |
|             
 | |
|             const serverData = { name, hostname };
 | |
|             if (grpc_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`, {
 | |
|                     method: 'POST',
 | |
|                     headers: { 'Content-Type': 'application/json' },
 | |
|                     body: JSON.stringify(serverData)
 | |
|                 });
 | |
|                 
 | |
|                 if (!response.ok) throw new Error('Failed to create server');
 | |
|                 
 | |
|                 showAlert('Server created successfully', 'success');
 | |
|                 document.getElementById('serverForm').reset();
 | |
|                 loadServers();
 | |
|             } catch (error) {
 | |
|                 showAlert('Error creating server: ' + error.message, 'error');
 | |
|             }
 | |
|         });
 | |
|         
 | |
|         // Server actions
 | |
|         async function testConnection(serverId) {
 | |
|             try {
 | |
|                 const response = await fetch(`${API_BASE}/servers/${serverId}/test`, {
 | |
|                     method: 'POST'
 | |
|                 });
 | |
|                 const result = await response.json();
 | |
|                 
 | |
|                 if (result.connected) {
 | |
|                     showAlert('Connection successful!', 'success');
 | |
|                 } else {
 | |
|                     showAlert('Connection failed', 'error');
 | |
|                 }
 | |
|             } catch (error) {
 | |
|                 showAlert('Error testing connection: ' + error.message, 'error');
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         async function 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');
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         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 {
 | |
|                 // First, load existing user access
 | |
|                 const userAccessResponse = await fetch(`${API_BASE}/users/${userId}/access`);
 | |
|                 const existingAccess = await userAccessResponse.ok ? await userAccessResponse.json() : [];
 | |
|                 
 | |
|                 // 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}">
 | |
|                                     <input type="hidden" id="editUserName" value="${escapeHtml(userName)}">
 | |
|                 `;
 | |
|                 
 | |
|                 for (const server of servers) {
 | |
|                     modalContent += `
 | |
|                         <div class="server-section">
 | |
|                             <h3>${escapeHtml(server.name)} (${escapeHtml(server.hostname)})</h3>
 | |
|                             <p><small>gRPC: ${escapeHtml(server.grpc_hostname)}:${server.grpc_port}</small></p>
 | |
|                     `;
 | |
|                     
 | |
|                     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) {
 | |
|                                 // Check if user already has access to this inbound
 | |
|                                 const hasAccess = existingAccess.some(a => 
 | |
|                                     a.server_inbound_id === inbound.id
 | |
|                                 );
 | |
|                                 
 | |
|                                 modalContent += `
 | |
|                                     <div class="inbound-item">
 | |
|                                         <label>
 | |
|                                             <input type="checkbox" name="access" 
 | |
|                                                    value="${server.id}|${inbound.id}" 
 | |
|                                                    data-server-id="${server.id}"
 | |
|                                                    data-inbound-id="${inbound.id}"
 | |
|                                                    data-server-name="${escapeHtml(server.name)}"
 | |
|                                                    data-inbound-port="${inbound.port_override || 'default'}"
 | |
|                                                    ${hasAccess ? 'checked' : ''}>
 | |
|                                             ${escapeHtml(inbound.template_name || 'Unknown Template')} 
 | |
|                                             ${hasAccess ? '<span class="status-badge status-online">Active</span>' : ''}
 | |
|                                         </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();
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         // 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');
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         function generateUUID() {
 | |
|             const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
 | |
|                 const r = Math.random() * 16 | 0;
 | |
|                 const v = c == 'x' ? r : (r & 0x3 | 0x8);
 | |
|                 return v.toString(16);
 | |
|             });
 | |
|             document.getElementById('xrayUserId').value = uuid;
 | |
|         }
 | |
|         
 | |
|         async function saveUserAccess(event) {
 | |
|             event.preventDefault();
 | |
|             
 | |
|             const userId = document.getElementById('editUserId').value;
 | |
|             const userName = document.getElementById('editUserName').value;
 | |
|             
 | |
|             const allCheckboxes = document.querySelectorAll('input[name="access"]');
 | |
|             
 | |
|             try {
 | |
|                 // 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) {
 | |
|                     const serverId = checkbox.dataset.serverId;
 | |
|                     const inboundId = checkbox.dataset.inboundId;
 | |
|                     
 | |
|                     const currentlyHasAccess = currentAccess.some(a => 
 | |
|                         a.server_inbound_id === inboundId
 | |
|                     );
 | |
|                     
 | |
|                     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)
 | |
|                         });
 | |
|                     
 | |
|                         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`);
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 
 | |
|                 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();
 | |
|         }
 | |
| 
 | |
|         // 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');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // 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');
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         // 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');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Update loadPageData function to include telegram
 | |
|         const originalLoadPageData = window.loadPageData;
 | |
|         window.loadPageData = function(page) {
 | |
|             if (page === 'telegram') {
 | |
|                 loadTelegram();
 | |
|             } else if (page === 'user-requests') {
 | |
|                 loadUserRequests();
 | |
|             } else if (originalLoadPageData) {
 | |
|                 originalLoadPageData(page);
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         // Initialize
 | |
|         loadPageData('dashboard');
 | |
|     </script>
 | |
| </body>
 | |
| </html> |