Files
OutFleet/static/admin.html
AB from home.homenet d972f10f83 TG almost works
2025-10-19 04:13:36 +03:00

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>