Files
OutFleet/static/admin.html

2275 lines
91 KiB
HTML
Raw Normal View History

2025-09-18 02:56:59 +03:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xray Admin Panel</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f7;
color: #1d1d1f;
min-height: 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;
2025-09-24 00:30:03 +01:00
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header-content {
flex: 1;
2025-09-18 02:56:59 +03:00
}
.page-title {
font-size: 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;
}
2025-09-23 14:17:32 +01:00
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 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;
}
2025-09-18 02:56:59 +03:00
.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;
}
2025-09-24 00:30:03 +01:00
/* 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;
}
2025-09-18 02:56:59 +03:00
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h1>Xray Admin</h1>
</div>
<nav>
<ul class="nav-menu">
<li class="nav-item">
<a href="#dashboard" class="nav-link active" onclick="showPage('dashboard')">Dashboard</a>
</li>
<li class="nav-item">
<a href="#servers" class="nav-link" onclick="showPage('servers')">Servers</a>
</li>
<li class="nav-item">
<a href="#templates" class="nav-link" onclick="showPage('templates')">Templates</a>
</li>
<li class="nav-item">
<a href="#certificates" class="nav-link" onclick="showPage('certificates')">Certificates</a>
</li>
2025-09-24 00:30:03 +01:00
<li class="nav-item">
<a href="#dns-providers" class="nav-link" onclick="showPage('dns-providers')">DNS Providers</a>
</li>
<li class="nav-item">
<a href="#tasks" class="nav-link" onclick="showPage('tasks')">Tasks</a>
</li>
2025-09-18 02:56:59 +03:00
<li class="nav-item">
<a href="#users" class="nav-link" onclick="showPage('users')">Users</a>
</li>
<li class="nav-item">
<a href="#tasks" class="nav-link" onclick="showPage('tasks')">Tasks</a>
</li>
</ul>
</nav>
</aside>
<main class="main-content">
<div id="alert" class="alert"></div>
<!-- Dashboard -->
<section id="dashboard" class="page-section active">
<div class="page-header">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Overview of your Xray infrastructure</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalServers">-</div>
<div class="stat-label">Total Servers</div>
</div>
<div class="stat-card">
<div class="stat-value" id="onlineServers">-</div>
<div class="stat-label">Online Servers</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalCertificates">-</div>
<div class="stat-label">Certificates</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalUsers">-</div>
<div class="stat-label">Users</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Recent Activity</h2>
<p class="card-subtitle">Latest server status and activity</p>
</div>
<div id="recentActivity" class="loading">Loading...</div>
</div>
</section>
<!-- Servers -->
<section id="servers" class="page-section">
<div class="page-header">
<h1 class="page-title">Servers</h1>
<p class="page-subtitle">Manage your Xray server instances</p>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Add New Server</h2>
<p class="card-subtitle">Register a new Xray server instance</p>
</div>
<form id="serverForm" class="form-grid">
<div class="form-group">
<label class="form-label" for="serverName">Server Name *</label>
<input type="text" id="serverName" class="form-input" placeholder="Enter server name">
</div>
<div class="form-group">
2025-09-23 14:17:32 +01:00
<label class="form-label" for="serverHostname">Public Hostname *</label>
2025-09-18 02:56:59 +03:00
<input type="text" id="serverHostname" class="form-input" placeholder="server.example.com">
2025-09-23 14:17:32 +01:00
<small class="form-help">Hostname that clients will connect to</small>
</div>
<div class="form-group">
<label class="form-label" for="serverGrpcHostname">gRPC Hostname</label>
<input type="text" id="serverGrpcHostname" class="form-input" placeholder="192.168.1.100 or leave empty to use public hostname">
<small class="form-help">Internal address for gRPC API (optional, defaults to public hostname)</small>
2025-09-18 02:56:59 +03:00
</div>
<div class="form-group">
<label class="form-label" for="serverPort">gRPC Port</label>
<input type="number" id="serverPort" class="form-input" placeholder="2053">
</div>
<div class="form-group">
<label class="form-label" for="serverCredentials">API Credentials</label>
<input type="text" id="serverCredentials" class="form-input" placeholder="Optional credentials">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Add Server</button>
</div>
</form>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Servers List</h2>
</div>
<div id="serversTable" class="loading">Loading...</div>
</div>
</section>
<!-- Templates -->
<section id="templates" class="page-section">
<div class="page-header">
<h1 class="page-title">Inbound Templates</h1>
<p class="page-subtitle">Manage reusable inbound configurations</p>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Templates List</h2>
</div>
<div id="templatesTable" class="loading">Loading...</div>
</div>
</section>
<!-- Certificates -->
<section id="certificates" class="page-section">
<div class="page-header">
2025-09-24 00:30:03 +01:00
<div class="page-header-content">
<h1 class="page-title">SSL Certificates</h1>
<p class="page-subtitle">Manage SSL/TLS certificates for your servers</p>
</div>
<button class="btn btn-primary" onclick="showCreateCertificateModal()">+ Create Certificate</button>
2025-09-18 02:56:59 +03:00
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Certificates List</h2>
</div>
<div id="certificatesTable" class="loading">Loading...</div>
</div>
</section>
2025-09-24 00:30:03 +01:00
<!-- DNS Providers -->
<section id="dns-providers" class="page-section">
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">DNS Providers</h1>
<p class="page-subtitle">Manage DNS provider credentials for Let's Encrypt certificates</p>
</div>
<button class="btn btn-primary" onclick="showCreateDnsProviderModal()">+ Add DNS Provider</button>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">DNS Providers List</h2>
</div>
<div id="dnsProvidersTable" class="loading">Loading...</div>
</div>
</section>
<!-- Tasks -->
<section id="tasks" class="page-section">
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">Scheduled Tasks</h1>
<p class="page-subtitle">Monitor and manage background tasks</p>
</div>
<button class="btn btn-secondary" onclick="refreshTasks()">🔄 Refresh</button>
</div>
<!-- Task Summary Cards -->
<div class="task-summary-grid">
<div class="summary-card">
<div class="summary-icon">📋</div>
<div class="summary-content">
<h3 id="totalTasks">-</h3>
<p>Total Tasks</p>
</div>
</div>
<div class="summary-card">
<div class="summary-icon">🏃</div>
<div class="summary-content">
<h3 id="runningTasks">-</h3>
<p>Running</p>
</div>
</div>
<div class="summary-card">
<div class="summary-icon"></div>
<div class="summary-content">
<h3 id="successTasks">-</h3>
<p>Successful</p>
</div>
</div>
<div class="summary-card">
<div class="summary-icon"></div>
<div class="summary-content">
<h3 id="errorTasks">-</h3>
<p>Failed</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Tasks List</h2>
</div>
<div id="tasksTable" class="loading">Loading...</div>
</div>
</section>
2025-09-18 02:56:59 +03:00
<!-- Users -->
<section id="users" class="page-section">
<div class="page-header">
2025-09-24 00:30:03 +01:00
<div class="page-header-content">
<h1 class="page-title">Users</h1>
<p class="page-subtitle">Manage user accounts and access</p>
</div>
2025-09-23 14:17:32 +01:00
<button class="btn btn-primary" onclick="showCreateUserModal()">+ Create User</button>
2025-09-18 02:56:59 +03:00
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Users List</h2>
</div>
<div id="usersTable" class="loading">Loading...</div>
</div>
</section>
<section id="tasks" class="page-section">
<div class="page-header">
<h1 class="page-title">Background Tasks</h1>
<p class="page-subtitle">Monitor scheduled tasks and background jobs</p>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Active Tasks</h2>
<button class="button button-outline" onclick="refreshTasks()">
<span class="icon">🔄</span>
Refresh
</button>
</div>
<div id="tasksTable" class="loading">Loading...</div>
</div>
</section>
</main>
</div>
<script>
const API_BASE = '/api';
let currentPage = 'dashboard';
// Navigation
function showPage(page) {
// Hide all pages
document.querySelectorAll('.page-section').forEach(section => {
section.classList.remove('active');
});
// Remove active class from all nav links
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// Show selected page
document.getElementById(page).classList.add('active');
document.querySelector(`[onclick="showPage('${page}')"]`).classList.add('active');
currentPage = page;
// Load page data
loadPageData(page);
}
function loadPageData(page) {
switch(page) {
case 'dashboard':
loadDashboard();
break;
case 'servers':
loadServers();
break;
case 'templates':
loadTemplates();
break;
case 'certificates':
loadCertificates();
break;
2025-09-24 00:30:03 +01:00
case 'dns-providers':
loadDnsProviders();
break;
case 'tasks':
loadTasks();
break;
2025-09-18 02:56:59 +03:00
case 'users':
loadUsers();
break;
case 'tasks':
loadTasks();
break;
}
}
// Dashboard
async function loadDashboard() {
try {
// Load statistics
const [servers, certificates, users] = await Promise.all([
fetch(`${API_BASE}/servers`).then(r => r.json()),
fetch(`${API_BASE}/certificates`).then(r => r.json()),
fetch(`${API_BASE}/users`).then(r => r.json())
]);
document.getElementById('totalServers').textContent = servers.length;
document.getElementById('onlineServers').textContent = servers.filter(s => s.status === 'online').length;
document.getElementById('totalCertificates').textContent = certificates.length;
document.getElementById('totalUsers').textContent = users.users ? users.users.length : users.length;
// Recent activity
const activity = servers.slice(0, 5).map(server =>
`<p>Server "${server.name}" is ${server.status}</p>`
).join('');
document.getElementById('recentActivity').innerHTML = activity || '<p>No recent activity</p>';
} catch (error) {
showAlert('Error loading dashboard: ' + error.message, 'error');
}
}
// Servers
async function loadServers() {
try {
const response = await fetch(`${API_BASE}/servers`);
const servers = await response.json();
if (servers.length === 0) {
document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>No servers found</h3><p>Add your first server to get started</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
2025-09-23 14:17:32 +01:00
<th>Public Hostname</th>
<th>gRPC Address</th>
2025-09-18 02:56:59 +03:00
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${servers.map(server => `
<tr>
<td><strong>${escapeHtml(server.name)}</strong></td>
<td>${escapeHtml(server.hostname)}</td>
2025-09-23 14:17:32 +01:00
<td>${escapeHtml(server.grpc_hostname)}:${server.grpc_port}</td>
2025-09-18 02:56:59 +03:00
<td><span class="status-badge status-${server.status}">${server.status}</span></td>
<td>
<div class="actions">
2025-09-23 14:17:32 +01:00
<button class="btn btn-small btn-primary" onclick="editServer('${server.id}')">Edit</button>
2025-09-18 02:56:59 +03:00
<button class="btn btn-small btn-success" onclick="testConnection('${server.id}')">Test</button>
<button class="btn btn-small btn-danger" onclick="deleteServer('${server.id}', '${escapeHtml(server.name)}')">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('serversTable').innerHTML = table;
} catch (error) {
document.getElementById('serversTable').innerHTML = '<div class="empty-state"><h3>Error loading servers</h3><p>' + error.message + '</p></div>';
}
}
// Templates
async function loadTemplates() {
try {
const response = await fetch(`${API_BASE}/templates`);
const templates = await response.json();
if (templates.length === 0) {
document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>No templates found</h3><p>Templates help you quickly configure server inbounds</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Protocol</th>
<th>Default Port</th>
<th>TLS Required</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${templates.map(template => `
<tr>
<td><strong>${escapeHtml(template.name)}</strong></td>
<td>${template.protocol}</td>
<td>${template.default_port}</td>
<td>${template.requires_tls ? 'Yes' : 'No'}</td>
<td><span class="status-badge ${template.is_active ? 'status-online' : 'status-offline'}">${template.is_active ? 'Active' : 'Inactive'}</span></td>
<td>
<div class="actions">
<button class="btn btn-small btn-secondary">Edit</button>
<button class="btn btn-small btn-danger">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('templatesTable').innerHTML = table;
} catch (error) {
document.getElementById('templatesTable').innerHTML = '<div class="empty-state"><h3>Error loading templates</h3><p>' + error.message + '</p></div>';
}
}
// Certificates
async function loadCertificates() {
try {
const response = await fetch(`${API_BASE}/certificates`);
const certificates = await response.json();
if (certificates.length === 0) {
document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>No certificates found</h3><p>Upload or generate SSL certificates for your servers</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Domain</th>
<th>Type</th>
<th>Expires</th>
<th>Auto Renew</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${certificates.map(cert => `
<tr>
<td><strong>${escapeHtml(cert.name)}</strong></td>
<td>${escapeHtml(cert.domain)}</td>
<td>${cert.cert_type}</td>
<td>${new Date(cert.expires_at).toLocaleDateString()}</td>
<td>${cert.auto_renew ? 'Yes' : 'No'}</td>
<td>
<div class="actions">
<button class="btn btn-small btn-secondary">View</button>
<button class="btn btn-small btn-danger">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('certificatesTable').innerHTML = table;
} catch (error) {
document.getElementById('certificatesTable').innerHTML = '<div class="empty-state"><h3>Error loading certificates</h3><p>' + error.message + '</p></div>';
}
}
// Users
async function loadUsers() {
try {
const response = await fetch(`${API_BASE}/users`);
const data = await response.json();
const users = data.users || data;
if (users.length === 0) {
document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>No users found</h3><p>Create user accounts for access management</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Comment</th>
<th>Telegram ID</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${users.map(user => `
<tr>
<td><strong>${escapeHtml(user.name)}</strong></td>
<td>${escapeHtml(user.comment || '-')}</td>
<td>${user.telegram_id || '-'}</td>
<td>${new Date(user.created_at).toLocaleDateString()}</td>
<td>
<div class="actions">
<button class="btn btn-small btn-secondary" onclick="editUserAccess('${user.id}', '${escapeHtml(user.name)}')">Manage Access</button>
<button class="btn btn-small btn-danger">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('usersTable').innerHTML = table;
} catch (error) {
document.getElementById('usersTable').innerHTML = '<div class="empty-state"><h3>Error loading users</h3><p>' + error.message + '</p></div>';
}
}
// Server form submission
document.getElementById('serverForm').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('serverName').value.trim();
const hostname = document.getElementById('serverHostname').value.trim();
2025-09-23 14:17:32 +01:00
const grpc_hostname = document.getElementById('serverGrpcHostname').value.trim();
2025-09-18 02:56:59 +03:00
const grpc_port = document.getElementById('serverPort').value;
const api_credentials = document.getElementById('serverCredentials').value.trim();
if (!name || !hostname) {
showAlert('Name and hostname are required', 'error');
return;
}
const serverData = { name, hostname };
2025-09-23 14:17:32 +01:00
if (grpc_hostname) serverData.grpc_hostname = grpc_hostname;
2025-09-18 02:56:59 +03:00
if (grpc_port) serverData.grpc_port = parseInt(grpc_port);
if (api_credentials) serverData.api_credentials = api_credentials;
try {
const response = await fetch(`${API_BASE}/servers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverData)
});
if (!response.ok) throw new Error('Failed to create server');
showAlert('Server created successfully', 'success');
document.getElementById('serverForm').reset();
loadServers();
} catch (error) {
showAlert('Error creating server: ' + error.message, 'error');
}
});
// Server actions
async function testConnection(serverId) {
try {
const response = await fetch(`${API_BASE}/servers/${serverId}/test`, {
method: 'POST'
});
const result = await response.json();
if (result.connected) {
showAlert('Connection successful!', 'success');
} else {
showAlert('Connection failed', 'error');
}
} catch (error) {
showAlert('Error testing connection: ' + error.message, 'error');
}
}
2025-09-23 14:17:32 +01:00
async function editServer(serverId) {
try {
// Fetch server data
const response = await fetch(`${API_BASE}/servers/${serverId}`);
if (!response.ok) throw new Error('Failed to fetch server');
const server = await response.json();
// Create edit modal
const modalContent = `
<div class="modal-overlay" onclick="closeEditServerModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Edit Server: ${escapeHtml(server.name)}</h2>
<button class="btn btn-small" onclick="closeEditServerModal()">Close</button>
</div>
<div class="modal-body">
<form id="editServerForm">
<input type="hidden" id="editServerId" value="${server.id}">
<div class="form-group">
<label class="form-label" for="editServerName">Server Name *</label>
<input type="text" id="editServerName" class="form-input" value="${escapeHtml(server.name)}" required>
</div>
<div class="form-group">
<label class="form-label" for="editServerHostname">Public Hostname *</label>
<input type="text" id="editServerHostname" class="form-input" value="${escapeHtml(server.hostname)}" required>
<small class="form-help">Hostname that clients will connect to</small>
</div>
<div class="form-group">
<label class="form-label" for="editServerGrpcHostname">gRPC Hostname</label>
<input type="text" id="editServerGrpcHostname" class="form-input" value="${escapeHtml(server.grpc_hostname || '')}" placeholder="Leave empty to use public hostname">
<small class="form-help">Internal address for gRPC API (optional)</small>
</div>
<div class="form-group">
<label class="form-label" for="editServerPort">gRPC Port</label>
<input type="number" id="editServerPort" class="form-input" value="${server.grpc_port}">
</div>
<div class="form-group">
<label class="form-label" for="editServerCredentials">API Credentials</label>
<input type="text" id="editServerCredentials" class="form-input" placeholder="${server.has_credentials ? 'Leave empty to keep existing credentials' : 'Optional credentials'}">
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Update Server</button>
<button type="button" class="btn btn-secondary" onclick="closeEditServerModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalContent);
// Handle form submission
document.getElementById('editServerForm').addEventListener('submit', async (e) => {
e.preventDefault();
await updateServer();
});
} catch (error) {
showAlert('Error loading server: ' + error.message, 'error');
}
}
function closeEditServerModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
async function updateServer() {
const serverId = document.getElementById('editServerId').value;
const name = document.getElementById('editServerName').value.trim();
const hostname = document.getElementById('editServerHostname').value.trim();
const grpc_hostname = document.getElementById('editServerGrpcHostname').value.trim();
const grpc_port = document.getElementById('editServerPort').value;
const api_credentials = document.getElementById('editServerCredentials').value.trim();
if (!name || !hostname) {
showAlert('Name and hostname are required', 'error');
return;
}
const serverData = { name, hostname };
if (grpc_hostname) serverData.grpc_hostname = grpc_hostname;
if (grpc_port) serverData.grpc_port = parseInt(grpc_port);
if (api_credentials) serverData.api_credentials = api_credentials;
try {
const response = await fetch(`${API_BASE}/servers/${serverId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverData)
});
if (!response.ok) throw new Error('Failed to update server');
showAlert('Server updated successfully', 'success');
closeEditServerModal();
loadServers();
} catch (error) {
showAlert('Error updating server: ' + error.message, 'error');
}
}
2025-09-18 02:56:59 +03:00
async function deleteServer(serverId, serverName) {
if (!confirm(`Are you sure you want to delete server "${serverName}"?`)) return;
try {
const response = await fetch(`${API_BASE}/servers/${serverId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete server');
showAlert('Server deleted successfully', 'success');
loadServers();
} catch (error) {
showAlert('Error deleting server: ' + error.message, 'error');
}
}
// Utility functions
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = `alert alert-${type} show`;
setTimeout(() => {
alert.classList.remove('show');
}, 3000);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// User Access Management
async function editUserAccess(userId, userName) {
try {
2025-09-23 14:17:32 +01:00
// First, load existing user access
const userAccessResponse = await fetch(`${API_BASE}/users/${userId}/access`);
const existingAccess = await userAccessResponse.ok ? await userAccessResponse.json() : [];
2025-09-18 02:56:59 +03:00
// Load servers and their inbounds
const serversResponse = await fetch(`${API_BASE}/servers`);
const servers = await serversResponse.json();
let modalContent = `
<div class="modal-overlay" onclick="closeUserAccessModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Manage Access: ${escapeHtml(userName)}</h2>
<button class="btn btn-small" onclick="closeUserAccessModal()">Close</button>
</div>
<div class="modal-body">
<form id="userAccessForm">
<input type="hidden" id="editUserId" value="${userId}">
2025-09-23 14:17:32 +01:00
<input type="hidden" id="editUserName" value="${escapeHtml(userName)}">
2025-09-18 02:56:59 +03:00
`;
for (const server of servers) {
modalContent += `
<div class="server-section">
2025-09-23 14:17:32 +01:00
<h3>${escapeHtml(server.name)} (${escapeHtml(server.hostname)})</h3>
<p><small>gRPC: ${escapeHtml(server.grpc_hostname)}:${server.grpc_port}</small></p>
2025-09-18 02:56:59 +03:00
`;
try {
const inboundsResponse = await fetch(`${API_BASE}/servers/${server.id}/inbounds`);
const inbounds = await inboundsResponse.json();
if (inbounds.length > 0) {
for (const inbound of inbounds) {
2025-09-23 14:17:32 +01:00
// Check if user already has access to this inbound
const hasAccess = existingAccess.some(a =>
a.server_inbound_id === inbound.id
);
2025-09-18 02:56:59 +03:00
modalContent += `
<div class="inbound-item">
<label>
<input type="checkbox" name="access"
value="${server.id}|${inbound.id}"
data-server-id="${server.id}"
data-inbound-id="${inbound.id}"
data-server-name="${escapeHtml(server.name)}"
2025-09-23 14:17:32 +01:00
data-inbound-port="${inbound.port_override || 'default'}"
${hasAccess ? 'checked' : ''}>
${escapeHtml(inbound.template_name || 'Unknown Template')}
${hasAccess ? '<span class="status-badge status-online">Active</span>' : ''}
2025-09-18 02:56:59 +03:00
</label>
</div>
`;
}
} else {
modalContent += '<p>No inbounds configured</p>';
}
} catch (error) {
modalContent += '<p>Error loading inbounds</p>';
}
modalContent += '</div>';
}
modalContent += `
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Save Access</button>
<button type="button" class="btn btn-secondary" onclick="closeUserAccessModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalContent);
// Handle form submission
document.getElementById('userAccessForm').addEventListener('submit', saveUserAccess);
} catch (error) {
showAlert('Error loading server data: ' + error.message, 'error');
}
}
function closeUserAccessModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
2025-09-23 14:17:32 +01:00
// Show create user modal
function showCreateUserModal() {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateUserModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Create New User</h2>
<button class="btn btn-small" onclick="closeCreateUserModal()">Close</button>
</div>
<div class="modal-body">
<form id="createUserForm">
<div class="form-grid">
<label>
Name: <span class="required">*</span>
<input type="text" name="name" required placeholder="User name">
</label>
<label>
Comment:
<input type="text" name="comment" placeholder="Optional comment">
</label>
<label>
Telegram ID:
<input type="number" name="telegram_id" placeholder="Optional Telegram ID">
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateUserModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalContent);
document.getElementById('createUserForm').addEventListener('submit', createUser);
}
function closeCreateUserModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
async function createUser(event) {
event.preventDefault();
const formData = new FormData(event.target);
const userData = {
name: formData.get('name'),
comment: formData.get('comment') || null,
telegram_id: formData.get('telegram_id') ? parseInt(formData.get('telegram_id')) : null
};
try {
const response = await fetch(`${API_BASE}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create user');
}
showAlert('User created successfully', 'success');
closeCreateUserModal();
loadUsers(); // Reload users table
} catch (error) {
showAlert('Error creating user: ' + error.message, 'error');
}
}
2025-09-18 02:56:59 +03:00
function generateUUID() {
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
document.getElementById('xrayUserId').value = uuid;
}
async function saveUserAccess(event) {
event.preventDefault();
const userId = document.getElementById('editUserId').value;
2025-09-23 14:17:32 +01:00
const userName = document.getElementById('editUserName').value;
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
const allCheckboxes = document.querySelectorAll('input[name="access"]');
2025-09-18 02:56:59 +03:00
try {
2025-09-23 14:17:32 +01:00
// Get current user access
const currentAccessResponse = await fetch(`${API_BASE}/users/${userId}/access`);
const currentAccess = await currentAccessResponse.ok ? await currentAccessResponse.json() : [];
// Process each checkbox
for (const checkbox of allCheckboxes) {
2025-09-18 02:56:59 +03:00
const serverId = checkbox.dataset.serverId;
const inboundId = checkbox.dataset.inboundId;
2025-09-23 14:17:32 +01:00
const currentlyHasAccess = currentAccess.some(a =>
a.server_inbound_id === inboundId
);
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
if (checkbox.checked && !currentlyHasAccess) {
// Grant access - pass user_id to use existing user
const grantData = {
user_id: userId,
name: userName,
level: 0
};
const response = await fetch(`${API_BASE}/servers/${serverId}/inbounds/${inboundId}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(grantData)
});
2025-09-18 02:56:59 +03:00
2025-09-23 14:17:32 +01:00
if (!response.ok && response.status !== 409) { // 409 = already exists
throw new Error(`Failed to grant access`);
}
} else if (!checkbox.checked && currentlyHasAccess) {
// Revoke access
const accessRecord = currentAccess.find(a =>
a.server_inbound_id === inboundId
);
if (accessRecord) {
const response = await fetch(`${API_BASE}/servers/${serverId}/inbounds/${inboundId}/users/${accessRecord.id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Failed to revoke access`);
}
}
2025-09-18 02:56:59 +03:00
}
}
showAlert('User access saved successfully', 'success');
closeUserAccessModal();
} catch (error) {
showAlert('Error saving user access: ' + error.message, 'error');
}
}
// Tasks
async function loadTasks() {
try {
// TODO: Add API endpoint for tasks when ready
// For now, show static task information
const tasksData = [
{
id: "xray_sync",
name: "Xray Synchronization",
description: "Synchronizes database state with xray servers",
schedule: "0 * * * * * (every minute)",
status: "running",
lastRun: new Date().toISOString(),
nextRun: new Date(Date.now() + 60000).toISOString(),
totalRuns: Math.floor(Math.random() * 50) + 10,
successCount: Math.floor(Math.random() * 45) + 8,
errorCount: Math.floor(Math.random() * 3),
lastDurationMs: Math.floor(Math.random() * 2000) + 500,
lastError: null
}
];
if (tasksData.length === 0) {
document.getElementById('tasksTable').innerHTML = '<div class="empty-state"><h3>No tasks configured</h3><p>Background tasks will appear here when configured</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Task Name</th>
<th>Schedule</th>
<th>Status</th>
<th>Last Run</th>
<th>Next Run</th>
<th>Performance</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
${tasksData.map(task => {
const statusIcon = getTaskStatusIcon(task.status);
const successRate = task.totalRuns > 0 ? Math.round((task.successCount / task.totalRuns) * 100) : 0;
return `
<tr>
<td>
<strong>${escapeHtml(task.name)}</strong>
<br><small class="text-muted">${escapeHtml(task.description)}</small>
</td>
<td><code>${escapeHtml(task.schedule)}</code></td>
<td>
<span class="status ${getTaskStatusClass(task.status)}">
${statusIcon} ${task.status}
</span>
${task.lastError ? `<br><small class="text-muted">Error: ${escapeHtml(task.lastError.substring(0, 50))}...</small>` : ''}
</td>
<td>${task.lastRun ? new Date(task.lastRun).toLocaleString() : '-'}</td>
<td>${task.nextRun ? new Date(task.nextRun).toLocaleString() : '-'}</td>
<td>
<div class="task-stats">
<div>✅ ${task.successCount} / ❌ ${task.errorCount}</div>
<div class="success-rate ${successRate >= 95 ? 'success-rate-good' : successRate >= 80 ? 'success-rate-ok' : 'success-rate-bad'}">
${successRate}% success
</div>
</div>
</td>
<td>${task.lastDurationMs ? task.lastDurationMs + 'ms' : '-'}</td>
</tr>
`}).join('')}
</tbody>
</table>
`;
document.getElementById('tasksTable').innerHTML = table;
} catch (error) {
document.getElementById('tasksTable').innerHTML = '<div class="error">Failed to load tasks: ' + error.message + '</div>';
}
}
function getTaskStatusIcon(status) {
switch(status) {
case 'running': return '🔄';
case 'success': return '✅';
case 'error': return '❌';
case 'idle': return '⏸️';
default: return '❓';
}
}
function getTaskStatusClass(status) {
switch(status) {
case 'running': return 'status-running';
case 'success': return 'status-success';
case 'error': return 'status-error';
case 'idle': return 'status-idle';
default: return 'status-unknown';
}
}
function refreshTasks() {
document.getElementById('tasksTable').innerHTML = '<div class="loading">Loading...</div>';
loadTasks();
}
2025-09-24 00:30:03 +01:00
// DNS Providers
async function loadDnsProviders() {
try {
const response = await fetch(`${API_BASE}/dns-providers`);
const providers = await response.json();
if (providers.length === 0) {
document.getElementById('dnsProvidersTable').innerHTML = '<div class="empty-state"><h3>No DNS providers found</h3><p>Add DNS provider credentials to enable Let\'s Encrypt certificates</p></div>';
return;
}
const table = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Provider Type</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${providers.map(provider => `
<tr>
<td><strong>${escapeHtml(provider.name)}</strong></td>
<td>${provider.provider_type}</td>
<td><span class="status-badge ${provider.is_active ? 'status-online' : 'status-offline'}">${provider.is_active ? 'Active' : 'Inactive'}</span></td>
<td>${new Date(provider.created_at).toLocaleDateString()}</td>
<td>
<div class="actions">
<button class="btn btn-small btn-secondary" onclick="editDnsProvider('${provider.id}')">Edit</button>
<button class="btn btn-small btn-danger" onclick="deleteDnsProvider('${provider.id}', '${escapeHtml(provider.name)}')">Delete</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('dnsProvidersTable').innerHTML = table;
} catch (error) {
document.getElementById('dnsProvidersTable').innerHTML = '<div class="empty-state"><h3>Error loading DNS providers</h3><p>' + error.message + '</p></div>';
}
}
// Show create DNS provider modal
function showCreateDnsProviderModal() {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateDnsProviderModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Add DNS Provider</h2>
<button class="btn btn-small" onclick="closeCreateDnsProviderModal()">Close</button>
</div>
<div class="modal-body">
<form id="createDnsProviderForm">
<div class="form-group">
<label class="form-label" for="dnsProviderName">Name *</label>
<input type="text" id="dnsProviderName" class="form-input" placeholder="Enter provider name" required>
</div>
<div class="form-group">
<label class="form-label" for="dnsProviderType">Provider Type *</label>
<select id="dnsProviderType" class="form-select" required>
<option value="">Select provider type</option>
<option value="cloudflare">Cloudflare</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="dnsProviderToken">API Token *</label>
<input type="password" id="dnsProviderToken" class="form-input" placeholder="Enter API token" required>
<small class="form-help">For Cloudflare: Create an API token with Zone:Read and Zone:DNS:Edit permissions</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="dnsProviderActive" checked>
Active
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Add Provider</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateDnsProviderModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalContent);
document.getElementById('createDnsProviderForm').addEventListener('submit', createDnsProvider);
}
function closeCreateDnsProviderModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
async function createDnsProvider(event) {
event.preventDefault();
const name = document.getElementById('dnsProviderName').value.trim();
const provider_type = document.getElementById('dnsProviderType').value;
const api_token = document.getElementById('dnsProviderToken').value.trim();
const is_active = document.getElementById('dnsProviderActive').checked;
if (!name || !provider_type || !api_token) {
showAlert('All fields are required', 'error');
return;
}
const providerData = { name, provider_type, api_token, is_active };
try {
const response = await fetch(`${API_BASE}/dns-providers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(providerData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create DNS provider');
}
showAlert('DNS provider created successfully', 'success');
closeCreateDnsProviderModal();
loadDnsProviders();
} catch (error) {
showAlert('Error creating DNS provider: ' + error.message, 'error');
}
}
async function deleteDnsProvider(providerId, providerName) {
if (!confirm(`Are you sure you want to delete DNS provider "${providerName}"?`)) return;
try {
const response = await fetch(`${API_BASE}/dns-providers/${providerId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete DNS provider');
showAlert('DNS provider deleted successfully', 'success');
loadDnsProviders();
} catch (error) {
showAlert('Error deleting DNS provider: ' + error.message, 'error');
}
}
// Show create certificate modal
function showCreateCertificateModal() {
const modalContent = `
<div class="modal-overlay" onclick="closeCreateCertificateModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Create Certificate</h2>
<button class="btn btn-small" onclick="closeCreateCertificateModal()">Close</button>
</div>
<div class="modal-body">
<form id="createCertificateForm">
<div class="form-group">
<label class="form-label" for="certName">Certificate Name *</label>
<input type="text" id="certName" class="form-input" placeholder="Enter certificate name" required>
</div>
<div class="form-group">
<label class="form-label" for="certDomain">Domain *</label>
<input type="text" id="certDomain" class="form-input" placeholder="example.com" required>
</div>
<div class="form-group">
<label class="form-label" for="certType">Certificate Type *</label>
<select id="certType" class="form-select" required onchange="toggleCertificateFields()">
<option value="">Select certificate type</option>
<option value="self_signed">Self-Signed</option>
<option value="letsencrypt">Let's Encrypt</option>
<option value="imported">Imported</option>
</select>
</div>
<div id="letsencryptFields" style="display: none;">
<div class="form-group">
<label class="form-label" for="dnsProvider">DNS Provider *</label>
<select id="dnsProvider" class="form-select">
<option value="">Loading...</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="acmeEmail">ACME Email *</label>
<input type="email" id="acmeEmail" class="form-input" placeholder="admin@example.com">
</div>
</div>
<div id="importedFields" style="display: none;">
<div class="form-group">
<label class="form-label" for="certPem">Certificate PEM *</label>
<textarea id="certPem" class="form-input" rows="8" placeholder="-----BEGIN CERTIFICATE-----"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="keyPem">Private Key PEM *</label>
<textarea id="keyPem" class="form-input" rows="8" placeholder="-----BEGIN PRIVATE KEY-----"></textarea>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="autoRenew" checked>
Auto-renew certificate
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Create Certificate</button>
<button type="button" class="btn btn-secondary" onclick="closeCreateCertificateModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalContent);
document.getElementById('createCertificateForm').addEventListener('submit', createCertificate);
loadDnsProvidersForSelect();
}
function closeCreateCertificateModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
function toggleCertificateFields() {
const certType = document.getElementById('certType').value;
const letsencryptFields = document.getElementById('letsencryptFields');
const importedFields = document.getElementById('importedFields');
letsencryptFields.style.display = certType === 'letsencrypt' ? 'block' : 'none';
importedFields.style.display = certType === 'imported' ? 'block' : 'none';
}
async function loadDnsProvidersForSelect() {
try {
const response = await fetch(`${API_BASE}/dns-providers/cloudflare/active`);
const providers = await response.json();
const select = document.getElementById('dnsProvider');
select.innerHTML = providers.length > 0
? providers.map(p => `<option value="${p.id}">${escapeHtml(p.name)}</option>`).join('')
: '<option value="">No active Cloudflare providers</option>';
} catch (error) {
const select = document.getElementById('dnsProvider');
select.innerHTML = '<option value="">Error loading providers</option>';
}
}
async function createCertificate(event) {
event.preventDefault();
const name = document.getElementById('certName').value.trim();
const domain = document.getElementById('certDomain').value.trim();
const cert_type = document.getElementById('certType').value;
const auto_renew = document.getElementById('autoRenew').checked;
if (!name || !domain || !cert_type) {
showAlert('Name, domain, and certificate type are required', 'error');
return;
}
const certData = { name, domain, cert_type, auto_renew, certificate_pem: '', private_key: '' };
if (cert_type === 'letsencrypt') {
const dns_provider_id = document.getElementById('dnsProvider').value;
const acme_email = document.getElementById('acmeEmail').value.trim();
if (!dns_provider_id || !acme_email) {
showAlert('DNS provider and ACME email are required for Let\'s Encrypt certificates', 'error');
return;
}
certData.dns_provider_id = dns_provider_id;
certData.acme_email = acme_email;
} else if (cert_type === 'imported') {
const certificate_pem = document.getElementById('certPem').value.trim();
const private_key = document.getElementById('keyPem').value.trim();
if (!certificate_pem || !private_key) {
showAlert('Certificate and private key PEM are required for imported certificates', 'error');
return;
}
certData.certificate_pem = certificate_pem;
certData.private_key = private_key;
}
try {
showAlert('Creating certificate...', 'success');
const response = await fetch(`${API_BASE}/certificates`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(certData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create certificate');
}
showAlert('Certificate created successfully', 'success');
closeCreateCertificateModal();
loadCertificates();
} catch (error) {
showAlert('Error creating certificate: ' + error.message, 'error');
}
}
// Tasks
async function loadTasks() {
try {
const response = await fetch(`${API_BASE}/tasks`);
const data = await response.json();
// Update summary cards
document.getElementById('totalTasks').textContent = data.summary.total_tasks;
document.getElementById('runningTasks').textContent = data.summary.running_tasks;
document.getElementById('successTasks').textContent = data.summary.successful_tasks;
document.getElementById('errorTasks').textContent = data.summary.failed_tasks;
if (Object.keys(data.tasks).length === 0) {
document.getElementById('tasksTable').innerHTML = '<div class="empty-state"><h3>No tasks found</h3><p>No scheduled tasks are configured</p></div>';
return;
}
const tasksHtml = Object.entries(data.tasks).map(([taskId, task]) => {
const statusClass = task.status.toLowerCase();
const lastRun = task.last_run ? new Date(task.last_run).toLocaleString() : 'Never';
const nextRun = task.next_run ? new Date(task.next_run).toLocaleString() : 'Not scheduled';
const duration = task.last_duration_ms ? `${task.last_duration_ms}ms` : '-';
const successRate = task.total_runs > 0 ? Math.round((task.success_count / task.total_runs) * 100) : 0;
return `
<div class="task-item">
<div class="task-header">
<div class="task-info">
<h3>${escapeHtml(task.name)}</h3>
<p class="task-description">${escapeHtml(task.description)}</p>
<div class="task-schedule">📅 ${escapeHtml(task.schedule)}</div>
</div>
<div class="task-status-container">
<span class="task-status ${statusClass}">${task.status}</span>
<div class="task-actions">
<button class="btn btn-primary btn-small" onclick="triggerTask('${taskId}')">▶️ Run Now</button>
<button class="btn btn-secondary btn-small" onclick="refreshTasks()">🔄 Refresh</button>
</div>
</div>
</div>
<div class="task-stats">
<div class="stat">
<label>Last Run:</label>
<span>${lastRun}</span>
</div>
<div class="stat">
<label>Next Run:</label>
<span>${nextRun}</span>
</div>
<div class="stat">
<label>Total Runs:</label>
<span>${task.total_runs}</span>
</div>
<div class="stat">
<label>Success Rate:</label>
<span>${successRate}% (${task.success_count}/${task.total_runs})</span>
</div>
<div class="stat">
<label>Last Duration:</label>
<span>${duration}</span>
</div>
${task.last_error ? `
<div class="stat error">
<label>Last Error:</label>
<span>${escapeHtml(task.last_error)}</span>
</div>
` : ''}
</div>
</div>
`;
}).join('');
document.getElementById('tasksTable').innerHTML = `
<div class="tasks-list">
${tasksHtml}
</div>
`;
} catch (error) {
document.getElementById('tasksTable').innerHTML = '<div class="empty-state"><h3>Error loading tasks</h3><p>' + error.message + '</p></div>';
}
}
async function refreshTasks() {
loadTasks();
}
async function triggerTask(taskId) {
try {
const response = await fetch(`${API_BASE}/tasks/${taskId}/trigger`, {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to trigger task');
}
const result = await response.json();
showAlert(result.message, 'success');
// Refresh tasks after a short delay to show updated status
setTimeout(() => {
loadTasks();
}, 1000);
} catch (error) {
showAlert('Error triggering task: ' + error.message, 'error');
}
}
2025-09-18 02:56:59 +03:00
// Initialize
loadPageData('dashboard');
</script>
</body>
</html>