Files
atlas/web/templates/management.html
Othman Hendy Suseo 3a25138d5b
Some checks failed
CI / test-build (push) Has been cancelled
fix login form
2025-12-20 03:47:13 +00:00

721 lines
27 KiB
HTML

{{define "management-content"}}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">System Management</h1>
<p class="text-slate-400">Manage services and users</p>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-slate-800">
<nav class="flex gap-4">
<button onclick="switchTab('services')" id="tab-services" class="tab-button px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium">
Services
</button>
<button onclick="switchTab('users')" id="tab-users" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
Users
</button>
</nav>
</div>
<!-- Services Tab -->
<div id="content-services" class="tab-content">
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<h2 class="text-lg font-semibold text-white">Service Management</h2>
<div class="flex gap-2">
<button onclick="loadServiceStatus()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
</div>
<div class="p-4 space-y-4">
<!-- Services List -->
<div id="services-list" class="space-y-4">
<p class="text-slate-400 text-sm">Loading services...</p>
</div>
</div>
</div>
</div>
<!-- Users Tab -->
<div id="content-users" class="tab-content hidden">
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<h2 class="text-lg font-semibold text-white">User Management</h2>
<div class="flex gap-2">
<button onclick="showCreateUserModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Create User
</button>
<button onclick="loadUsers()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
</div>
<div id="users-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
</div>
</div>
<!-- Service Logs Modal -->
<div id="service-logs-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-white" id="service-logs-title">Service Logs</h3>
<div class="flex items-center gap-2">
<input type="number" id="logs-lines" value="50" min="10" max="1000" class="w-20 px-2 py-1 bg-slate-900 border border-slate-700 rounded text-white text-sm">
<button onclick="loadServiceLogs()" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Refresh</button>
<button onclick="closeModal('service-logs-modal')" class="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">Close</button>
</div>
</div>
<div id="service-logs-content" class="flex-1 overflow-y-auto bg-slate-900 rounded p-4 text-sm text-slate-300 font-mono whitespace-pre-wrap">
Loading logs...
</div>
</div>
</div>
<!-- Create User Modal -->
<div id="create-user-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
<h3 class="text-xl font-semibold text-white mb-4">Create User</h3>
<form id="create-user-form" onsubmit="createUser(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Username *</label>
<input type="text" name="username" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Email</label>
<input type="email" name="email" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Password *</label>
<input type="password" name="password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Role *</label>
<select name="role" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
<option value="viewer">Viewer</option>
<option value="operator">Operator</option>
<option value="administrator">Administrator</option>
</select>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('create-user-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Create
</button>
</div>
</form>
</div>
</div>
<!-- Edit User Modal -->
<div id="edit-user-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
<h3 class="text-xl font-semibold text-white mb-4">Edit User</h3>
<form id="edit-user-form" onsubmit="updateUser(event)" class="space-y-4">
<input type="hidden" id="edit-user-id" name="id">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Username</label>
<input type="text" id="edit-username" disabled class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-slate-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Email</label>
<input type="email" id="edit-email" name="email" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Role</label>
<select id="edit-role" name="role" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
<option value="viewer">Viewer</option>
<option value="operator">Operator</option>
<option value="administrator">Administrator</option>
</select>
</div>
<div>
<label class="flex items-center gap-2 text-sm text-slate-300">
<input type="checkbox" id="edit-active" name="active" class="rounded bg-slate-900 border-slate-700">
<span>Active</span>
</label>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('edit-user-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Update
</button>
</div>
</form>
</div>
</div>
<!-- Change Password Modal -->
<div id="change-password-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
<h3 class="text-xl font-semibold text-white mb-4">Change Password</h3>
<form id="change-password-form" onsubmit="changePassword(event)" class="space-y-4">
<input type="hidden" id="change-password-user-id" name="id">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Current Password *</label>
<input type="password" name="old_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">New Password *</label>
<input type="password" name="new_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Confirm New Password *</label>
<input type="password" name="confirm_password" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('change-password-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Change Password
</button>
</div>
</form>
</div>
</div>
<script>
// Check authentication on page load
(function() {
const token = localStorage.getItem('atlas_token');
if (!token) {
// No token, redirect to login
window.location.href = '/login?return=' + encodeURIComponent(window.location.pathname);
return;
}
})();
function getAuthHeaders() {
const token = localStorage.getItem('atlas_token');
return {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
}
function switchTab(tab) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-button').forEach(el => {
el.classList.remove('border-blue-600', 'text-blue-400', 'font-medium');
el.classList.add('border-transparent', 'text-slate-400');
});
// Show selected tab
document.getElementById(`content-${tab}`).classList.remove('hidden');
document.getElementById(`tab-${tab}`).classList.remove('border-transparent', 'text-slate-400');
document.getElementById(`tab-${tab}`).classList.add('border-blue-600', 'text-blue-400', 'font-medium');
// Load data for the tab
if (tab === 'services') loadServiceStatus();
else if (tab === 'users') loadUsers();
}
function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
// ===== Service Management =====
let currentServiceName = null;
async function loadServiceStatus() {
const token = localStorage.getItem('atlas_token');
const listEl = document.getElementById('services-list');
if (!token) {
listEl.innerHTML = `
<div class="text-center py-4">
<p class="text-slate-400 mb-2">Authentication required</p>
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
</div>
`;
return;
}
listEl.innerHTML = '<p class="text-slate-400 text-sm">Loading services...</p>';
try {
const res = await fetch('/api/v1/services', { headers: getAuthHeaders() });
const data = await res.json().catch(() => null);
if (!res.ok) {
if (res.status === 401) {
localStorage.removeItem('atlas_token');
localStorage.removeItem('atlas_user');
listEl.innerHTML = `
<div class="text-center py-4">
<p class="text-slate-400 mb-2">Session expired. Please login again.</p>
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
</div>
`;
return;
}
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
return;
}
const services = (data && data.services) ? data.services : [];
if (services.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No services found</p>';
return;
}
// Render all services
listEl.innerHTML = services.map(svc => {
const statusBadge = getStatusBadge(svc.status);
const statusColor = getStatusColor(svc.status);
return `
<div class="bg-slate-900 rounded-lg border border-slate-700 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-md font-semibold text-white">${escapeHtml(svc.display_name)}</h3>
<div class="px-3 py-1 rounded text-xs font-medium ${statusColor}">
${statusBadge}
</div>
</div>
<div class="text-xs text-slate-500 font-mono mb-4 max-h-24 overflow-y-auto whitespace-pre-wrap">
${escapeHtml((svc.output || '').split('\n').slice(-5).join('\n')) || 'No status information'}
</div>
<div class="flex gap-2 flex-wrap">
<button onclick="serviceAction('${svc.name}', 'start')" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs">
Start
</button>
<button onclick="serviceAction('${svc.name}', 'stop')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-xs">
Stop
</button>
<button onclick="serviceAction('${svc.name}', 'restart')" class="px-3 py-1.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-xs">
Restart
</button>
<button onclick="serviceAction('${svc.name}', 'reload')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs">
Reload
</button>
<button onclick="showServiceLogs('${svc.name}', '${escapeHtml(svc.display_name)}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs">
View Logs
</button>
</div>
</div>
`;
}).join('');
} catch (err) {
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
function getStatusBadge(status) {
switch(status) {
case 'running': return 'Running';
case 'stopped': return 'Stopped';
case 'failed': return 'Failed';
case 'not-found': return 'Not Found';
default: return 'Unknown';
}
}
function getStatusColor(status) {
switch(status) {
case 'running': return 'bg-green-900 text-green-300';
case 'stopped': return 'bg-red-900 text-red-300';
case 'failed': return 'bg-red-900 text-red-300';
case 'not-found': return 'bg-slate-700 text-slate-300';
default: return 'bg-slate-700 text-slate-300';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function serviceAction(serviceName, action) {
const token = localStorage.getItem('atlas_token');
if (!token) {
alert('Please login first');
window.location.href = '/login?return=/management';
return;
}
if (!confirm(`Are you sure you want to ${action} ${serviceName}?`)) return;
try {
const res = await fetch(`/api/v1/services/${action}?service=${encodeURIComponent(serviceName)}`, {
method: 'POST',
headers: getAuthHeaders()
});
const data = await res.json().catch(() => null);
if (res.ok) {
alert(`${serviceName} ${action}ed successfully`);
loadServiceStatus();
} else {
const errorMsg = (data && data.error) ? data.error : `Failed to ${action} service`;
alert(`Error: ${errorMsg}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
function showServiceLogs(serviceName, displayName) {
const token = localStorage.getItem('atlas_token');
if (!token) {
alert('Please login first');
window.location.href = '/login?return=/management';
return;
}
currentServiceName = serviceName;
document.getElementById('service-logs-title').textContent = `${displayName} Logs`;
document.getElementById('service-logs-modal').classList.remove('hidden');
loadServiceLogs();
}
async function loadServiceLogs() {
const token = localStorage.getItem('atlas_token');
if (!token) {
document.getElementById('service-logs-content').textContent = 'Authentication required';
return;
}
if (!currentServiceName) {
document.getElementById('service-logs-content').textContent = 'No service selected';
return;
}
const lines = document.getElementById('logs-lines').value || '50';
try {
const res = await fetch(`/api/v1/services/logs?service=${encodeURIComponent(currentServiceName)}&lines=${lines}`, {
headers: getAuthHeaders()
});
const data = await res.json().catch(() => null);
if (!res.ok) {
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
document.getElementById('service-logs-content').textContent = `Error: ${errorMsg}`;
return;
}
document.getElementById('service-logs-content').textContent = data.logs || 'No logs available';
} catch (err) {
document.getElementById('service-logs-content').textContent = `Error: ${err.message}`;
}
}
// ===== User Management =====
async function loadUsers(forceRefresh = false) {
const token = localStorage.getItem('atlas_token');
const listEl = document.getElementById('users-list');
if (!token) {
listEl.innerHTML = `
<div class="text-center py-8">
<p class="text-slate-400 mb-2">Authentication required to view users</p>
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
</div>
`;
return;
}
// Show loading state
listEl.innerHTML = '<p class="text-slate-400 text-sm">Loading users...</p>';
try {
// Add cache busting parameter if force refresh
const url = forceRefresh ? `/api/v1/users?_t=${Date.now()}` : '/api/v1/users';
const res = await fetch(url, {
headers: getAuthHeaders(),
cache: forceRefresh ? 'no-cache' : 'default'
});
const rawText = await res.text();
let data = null;
try {
data = JSON.parse(rawText);
} catch (parseErr) {
console.error('Failed to parse JSON response:', parseErr, 'Raw response:', rawText);
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid JSON response from server</p>';
return;
}
if (!res.ok) {
if (res.status === 401) {
localStorage.removeItem('atlas_token');
localStorage.removeItem('atlas_user');
listEl.innerHTML = `
<div class="text-center py-8">
<p class="text-slate-400 mb-2">Session expired. Please login again.</p>
<a href="/login?return=/management" class="text-blue-400 hover:text-blue-300">Click here to login</a>
</div>
`;
return;
}
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
return;
}
if (!data || !Array.isArray(data)) {
console.error('Invalid response format:', data);
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format (expected array)</p>';
return;
}
console.log(`Loaded ${data.length} users:`, data);
if (data.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No users found</p>';
return;
}
// Sort users by ID to ensure consistent ordering
const sortedUsers = data.sort((a, b) => {
const idA = (a.id || '').toLowerCase();
const idB = (b.id || '').toLowerCase();
return idA.localeCompare(idB);
});
listEl.innerHTML = sortedUsers.map(user => {
// Escape HTML to prevent XSS
const escapeHtml = (text) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
const username = escapeHtml(user.username || 'N/A');
const email = user.email ? escapeHtml(user.email) : '';
const userId = escapeHtml(user.id || 'N/A');
const role = escapeHtml(user.role || 'N/A');
const active = user.active !== false;
return `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-white">${username}</h3>
${active ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Active</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Inactive</span>'}
<span class="px-2 py-1 rounded text-xs font-medium bg-blue-900 text-blue-300">${role}</span>
</div>
<div class="text-sm text-slate-400 space-y-1">
${email ? `<p>Email: <span class="text-slate-300">${email}</span></p>` : ''}
<p>ID: <span class="text-slate-300 font-mono text-xs">${userId}</span></p>
</div>
</div>
<div class="flex gap-2 ml-4">
<button onclick="showEditUserModal('${userId}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Edit
</button>
<button onclick="showChangePasswordModal('${userId}', '${username}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Change Password
</button>
<button onclick="deleteUser('${userId}', '${username}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Delete
</button>
</div>
</div>
</div>
`;
}).join('');
} catch (err) {
document.getElementById('users-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
function showCreateUserModal() {
document.getElementById('create-user-modal').classList.remove('hidden');
}
async function createUser(e) {
e.preventDefault();
const formData = new FormData(e.target);
try {
const res = await fetch('/api/v1/users', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
username: formData.get('username'),
email: formData.get('email') || '',
password: formData.get('password'),
role: formData.get('role')
})
});
const rawText = await res.text();
let data = null;
try {
data = JSON.parse(rawText);
} catch (parseErr) {
console.error('Failed to parse create user response:', parseErr, 'Raw:', rawText);
alert('Error: Invalid response from server');
return;
}
console.log('Create user response:', res.status, data);
if (res.ok || res.status === 201) {
console.log('User created successfully, refreshing list...');
closeModal('create-user-modal');
e.target.reset();
// Force reload users list - add cache busting
await loadUsers(true);
alert('User created successfully');
} else {
const errorMsg = (data && data.error) ? data.error : 'Failed to create user';
console.error('Create user failed:', errorMsg);
alert(`Error: ${errorMsg}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function showEditUserModal(userId) {
try {
const res = await fetch(`/api/v1/users/${userId}`, { headers: getAuthHeaders() });
if (!res.ok) {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to load user'}`);
return;
}
const user = await res.json();
document.getElementById('edit-user-id').value = user.id;
document.getElementById('edit-username').value = user.username || '';
document.getElementById('edit-email').value = user.email || '';
document.getElementById('edit-role').value = user.role || 'Viewer';
document.getElementById('edit-active').checked = user.active !== false;
document.getElementById('edit-user-modal').classList.remove('hidden');
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function updateUser(e) {
e.preventDefault();
const formData = new FormData(e.target);
const userId = formData.get('id');
try {
const res = await fetch(`/api/v1/users/${userId}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({
email: formData.get('email') || '',
role: formData.get('role'),
active: formData.get('active') === 'on'
})
});
if (res.ok) {
closeModal('edit-user-modal');
await loadUsers(true);
alert('User updated successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to update user'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function deleteUser(userId, username) {
if (!confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) return;
try {
const res = await fetch(`/api/v1/users/${userId}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
await loadUsers(true);
alert('User deleted successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to delete user'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
function showChangePasswordModal(userId, username) {
document.getElementById('change-password-user-id').value = userId;
document.getElementById('change-password-modal').classList.remove('hidden');
}
async function changePassword(e) {
e.preventDefault();
const formData = new FormData(e.target);
const userId = document.getElementById('change-password-user-id').value;
const newPassword = formData.get('new_password');
const confirmPassword = formData.get('confirm_password');
if (newPassword !== confirmPassword) {
alert('New passwords do not match');
return;
}
try {
const res = await fetch(`/api/v1/users/${userId}/password`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({
old_password: formData.get('old_password'),
new_password: newPassword
})
});
if (res.ok) {
closeModal('change-password-modal');
e.target.reset();
alert('Password changed successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to change password'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Load initial data when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
loadServiceStatus();
// Load users if on users tab
const usersTab = document.getElementById('tab-users');
if (usersTab && usersTab.classList.contains('border-blue-600')) {
loadUsers();
}
});
} else {
loadServiceStatus();
// Load users if on users tab
const usersTab = document.getElementById('tab-users');
if (usersTab && usersTab.classList.contains('border-blue-600')) {
loadUsers();
}
}
</script>
{{end}}
{{define "management.html"}}
{{template "base" .}}
{{end}}