Status:
diff --git a/web/static/script_v14.js b/web/static/script_v14.js
new file mode 100644
index 00000000..5b3defd6
--- /dev/null
+++ b/web/static/script_v14.js
@@ -0,0 +1,924 @@
+// BetterDesk Console - Main JavaScript with Authentication v1.4.0
+// Global variables
+let allDevices = [];
+let currentDeviceId = null;
+let authToken = null;
+let userRole = null;
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', function() {
+ // Check authentication
+ checkAuth();
+
+ // Load data
+ loadDevices();
+ loadStats();
+
+ // Auto-refresh every 2 seconds
+ setInterval(() => {
+ loadDevices();
+ loadStats();
+ }, 2000);
+});
+
+// Authentication check
+function checkAuth() {
+ authToken = localStorage.getItem('authToken');
+ userRole = localStorage.getItem('role');
+
+ if (!authToken) {
+ window.location.href = '/login';
+ return false;
+ }
+
+ return true;
+}
+
+// Get auth headers for API calls
+function getAuthHeaders() {
+ return {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ };
+}
+
+// Handle authentication errors
+function handleAuthError(error, response) {
+ if (response && response.status === 401) {
+ // Token expired or invalid
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('username');
+ localStorage.removeItem('role');
+ window.location.href = '/login';
+ return true;
+ }
+ return false;
+}
+
+// Load devices from API
+async function loadDevices() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch('/api/devices', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ allDevices = data.devices;
+ renderDevices(allDevices);
+ updateNavStats(allDevices);
+ } else {
+ showToast('Error loading devices: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to load devices', 'error');
+ }
+}
+
+// Load statistics
+async function loadStats() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch('/api/stats', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('statTotal').textContent = data.stats.total;
+ document.getElementById('statActive').textContent = data.stats.active;
+ document.getElementById('statInactive').textContent = data.stats.inactive;
+ document.getElementById('statBanned').textContent = data.stats.banned || 0;
+ document.getElementById('statNotes').textContent = data.stats.with_notes;
+
+ // Update top bar stats
+ const topTotal = document.getElementById('topTotalDevices');
+ const topActive = document.getElementById('topActiveDevices');
+ if (topTotal) topTotal.querySelector('span').textContent = data.stats.total;
+ if (topActive) topActive.querySelector('span').textContent = data.stats.active;
+ }
+ } catch (error) {
+ console.error('Error loading stats:', error);
+ }
+}
+
+// Update navigation stats
+function updateNavStats(devices) {
+ const total = devices.length;
+ const active = devices.filter(d => d.online).length;
+
+ const totalDevicesEl = document.querySelector('#totalDevices span');
+ const activeDevicesEl = document.querySelector('#activeDevices span');
+
+ if (totalDevicesEl) totalDevicesEl.textContent = total;
+ if (activeDevicesEl) activeDevicesEl.textContent = active;
+}
+
+// Render devices table
+function renderDevices(devices) {
+ const tbody = document.getElementById('devicesTableBody');
+
+ if (devices.length === 0) {
+ tbody.innerHTML = `
+
+
+
+ No devices found
+
+
+ `;
+ return;
+ }
+
+ tbody.innerHTML = devices.map(device => {
+ const isBanned = device.is_banned === true || device.is_banned === 1;
+ const rowClass = isBanned ? 'style="opacity: 0.6; background: rgba(255, 0, 0, 0.05);"' : '';
+
+ // Check permissions for actions
+ const canEdit = userRole === 'admin' || userRole === 'operator';
+ const canBan = userRole === 'admin' || userRole === 'operator';
+
+ return `
+
+
+ ${escapeHtml(device.id)}
+ ${isBanned ? ' BANNED ' : ''}
+
+ ${escapeHtml(device.note) || 'No note '}
+
+
+
+ ${device.online ? 'Online' : 'Offline'}
+
+
+ ${formatDate(device.created_at)}
+
+
+
+
+
+
+
+ ${canEdit ? `
+
+
+
+ ` : ''}
+ ${canBan ? (isBanned ?
+ `
+
+ ` :
+ `
+
+ `
+ ) : ''}
+ ${canEdit ? `
+
+
+
+ ` : ''}
+
+
+ `}).join('');
+}
+
+// Filter devices by search
+function filterDevices() {
+ const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+ if (!searchTerm) {
+ renderDevices(allDevices);
+ return;
+ }
+
+ const filtered = allDevices.filter(device =>
+ device.id.toLowerCase().includes(searchTerm) ||
+ (device.note && device.note.toLowerCase().includes(searchTerm))
+ );
+
+ renderDevices(filtered);
+}
+
+// Connect to device via rustdesk:// protocol
+function connectDevice(deviceId) {
+ window.location.href = `rustdesk://${deviceId}`;
+ showToast(`Connecting to ${deviceId}...`);
+}
+
+// Show device details modal
+function showDetails(deviceId) {
+ const device = allDevices.find(d => d.id === deviceId);
+ if (!device) return;
+
+ const isBanned = device.is_banned === true || device.is_banned === 1;
+
+ const detailsContent = document.getElementById('detailsContent');
+ detailsContent.innerHTML = `
+
+
ID:
+
${escapeHtml(device.id)}
+
+
+
GUID:
+
${escapeHtml(device.guid) || 'N/A'}
+
+
+
UUID:
+
${escapeHtml(device.uuid) || 'N/A'}
+
+
+
Public Key:
+
${escapeHtml(device.pk) || 'N/A'}
+
+
+
User:
+
${escapeHtml(device.user) || 'N/A'}
+
+
+
Status:
+
+
+
+ ${device.online ? 'Online' : 'Offline'}
+
+
+
+ ${isBanned ? `
+
+
+
Banned At:
+
${device.banned_at ? formatDate(device.banned_at) : 'N/A'}
+
+
+
Banned By:
+
${escapeHtml(device.banned_by) || 'N/A'}
+
+
+
Ban Reason:
+
${escapeHtml(device.ban_reason) || 'No reason provided'}
+
+ ` : ''}
+
+
Note:
+
${escapeHtml(device.note) || 'No note'}
+
+
+
Created:
+
${formatDate(device.created_at)}
+
+
+
Info:
+
${escapeHtml(device.info) || 'N/A'}
+
+ `;
+
+ openModal('detailsModal');
+}
+
+// Edit device
+function editDevice(deviceId) {
+ const device = allDevices.find(d => d.id === deviceId);
+ if (!device) return;
+
+ currentDeviceId = deviceId;
+ document.getElementById('editDeviceId').value = deviceId;
+ document.getElementById('editNewId').value = '';
+ document.getElementById('editNote').value = device.note || '';
+
+ openModal('editModal');
+}
+
+// Save device changes
+async function saveDevice() {
+ if (!checkAuth()) return;
+
+ const newId = document.getElementById('editNewId').value.trim();
+ const note = document.getElementById('editNote').value.trim();
+
+ if (note.length > 500) {
+ showToast('Note is too long (max 500 characters)', 'error');
+ return;
+ }
+
+ if (newId && newId.length > 50) {
+ showToast('Device ID is too long (max 50 characters)', 'error');
+ return;
+ }
+
+ if (newId && !/^[a-zA-Z0-9_-]+$/.test(newId)) {
+ showToast('Device ID can only contain letters, numbers, underscores and hyphens', 'error');
+ return;
+ }
+
+ if (newId && newId !== currentDeviceId) {
+ if (!confirm(`⚠️ WARNING: Changing device ID!\n\nOld ID: ${currentDeviceId}\nNew ID: ${newId}\n\nAre you sure?`)) {
+ return;
+ }
+ }
+
+ const data = { note };
+ if (newId) data.new_id = newId;
+
+ try {
+ const response = await fetch(`/api/device/${currentDeviceId}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify(data)
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Device updated successfully');
+ closeEditModal();
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to update device', 'error');
+ }
+}
+
+// Delete device
+function deleteDevice(deviceId) {
+ currentDeviceId = deviceId;
+ document.getElementById('deleteDeviceId').textContent = deviceId;
+ openModal('deleteModal');
+}
+
+// Confirm delete
+async function confirmDelete() {
+ if (!checkAuth()) return;
+
+ const device = allDevices.find(d => d.id === currentDeviceId);
+
+ if (!confirm(`⚠️ DELETE DEVICE: ${currentDeviceId}\n\nAre you absolutely sure?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/device/${currentDeviceId}`, {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Device deleted successfully');
+ closeDeleteModal();
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to delete device', 'error');
+ }
+}
+
+// Copy public key to clipboard
+function copyPublicKey() {
+ const keyText = document.getElementById('publicKeyDisplay').textContent;
+ navigator.clipboard.writeText(keyText).then(() => {
+ showToast('Public key copied to clipboard');
+ }).catch(err => {
+ console.error('Error copying:', err);
+ showToast('Failed to copy public key', 'error');
+ });
+}
+
+// Refresh devices manually
+async function refreshDevices() {
+ showToast('Refreshing devices...');
+ await loadDevices();
+ await loadStats();
+}
+
+// Modal functions
+function openModal(modalId) {
+ document.getElementById(modalId).classList.add('active');
+}
+
+function closeModal(modalId) {
+ document.getElementById(modalId).classList.remove('active');
+}
+
+function closeEditModal() {
+ closeModal('editModal');
+ currentDeviceId = null;
+}
+
+function closeDeleteModal() {
+ closeModal('deleteModal');
+ currentDeviceId = null;
+}
+
+function closeDetailsModal() {
+ closeModal('detailsModal');
+}
+
+// Close modal when clicking outside
+window.onclick = function(event) {
+ if (event.target.classList.contains('modal')) {
+ event.target.classList.remove('active');
+ }
+}
+
+// Toast notification
+function showToast(message, type = 'success') {
+ const toast = document.getElementById('toast');
+ const icon = toast.querySelector('i');
+
+ if (type === 'error') {
+ icon.className = 'fas fa-exclamation-circle';
+ icon.style.color = 'var(--danger-color)';
+ } else {
+ icon.className = 'fas fa-check-circle';
+ icon.style.color = 'var(--success-color)';
+ }
+
+ document.getElementById('toastMessage').textContent = message;
+ toast.classList.add('show');
+
+ setTimeout(() => {
+ toast.classList.remove('show');
+ }, 3000);
+}
+
+// Utility functions
+function escapeHtml(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function formatDate(dateString) {
+ if (!dateString) return 'N/A';
+ const date = new Date(dateString);
+ const options = {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ };
+ return date.toLocaleDateString('en-US', options);
+}
+
+// Ban device
+async function banDevice(deviceId) {
+ if (!checkAuth()) return;
+
+ const reason = prompt(`⚠️ BAN DEVICE: ${deviceId}\n\nEnter ban reason (optional):`);
+
+ if (reason === null) return;
+
+ if (reason && reason.length > 500) {
+ showToast('Ban reason is too long (max 500 characters)', 'error');
+ return;
+ }
+
+ if (!confirm(`Are you sure you want to BAN device ${deviceId}?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/device/${deviceId}/ban`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ reason: reason || '',
+ banned_by: 'admin'
+ })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`Device ${deviceId} banned successfully`);
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to ban device', 'error');
+ }
+}
+
+// Unban device
+async function unbanDevice(deviceId) {
+ if (!checkAuth()) return;
+
+ if (!confirm(`✓ UNBAN DEVICE: ${deviceId}\n\nAre you sure?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/device/${deviceId}/unban`, {
+ method: 'POST',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`Device ${deviceId} unbanned successfully`);
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to unban device', 'error');
+ }
+}
+// ============================================================================
+// PUBLIC KEY VERIFICATION
+// ============================================================================
+
+async function verifyPasswordForKey() {
+ const password = document.getElementById('keyPassword').value;
+
+ if (!password) {
+ showToast('Please enter your password', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/key/verify', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ password: password })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ document.getElementById('publicKeyDisplay').textContent = result.key;
+ document.getElementById('keyPasswordForm').style.display = 'none';
+ document.getElementById('keyDisplay').style.display = 'block';
+ document.getElementById('keyPassword').value = '';
+ showToast('Public key revealed');
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ document.getElementById('keyPassword').value = '';
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to verify password', 'error');
+ }
+}
+
+function copyPublicKey() {
+ const keyText = document.getElementById('publicKeyDisplay').textContent;
+ navigator.clipboard.writeText(keyText).then(() => {
+ showToast('Public key copied to clipboard');
+ }).catch(err => {
+ showToast('Failed to copy', 'error');
+ });
+}
+
+// ============================================================================
+// PASSWORD CHANGE
+// ============================================================================
+
+function showChangePasswordModal() {
+ document.getElementById('changePasswordModal').classList.add('show');
+}
+
+function closeChangePasswordModal() {
+ document.getElementById('changePasswordModal').classList.remove('show');
+ document.getElementById('currentPassword').value = '';
+ document.getElementById('newPassword').value = '';
+ document.getElementById('confirmPassword').value = '';
+}
+
+async function confirmChangePassword() {
+ const currentPassword = document.getElementById('currentPassword').value;
+ const newPassword = document.getElementById('newPassword').value;
+ const confirmPassword = document.getElementById('confirmPassword').value;
+
+ if (!currentPassword || !newPassword || !confirmPassword) {
+ showToast('All fields are required', 'error');
+ return;
+ }
+
+ if (newPassword.length < 6) {
+ showToast('New password must be at least 6 characters', 'error');
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ showToast('New passwords do not match', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/auth/change-password', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ old_password: currentPassword,
+ new_password: newPassword
+ })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Password changed successfully');
+ closeChangePasswordModal();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to change password', 'error');
+ }
+}
+
+// ============================================================================
+// USER MANAGEMENT
+// ============================================================================
+
+async function loadUsers() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch('/api/users', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ renderUsers(data.users);
+ } else {
+ showToast('Error loading users: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to load users', 'error');
+ }
+}
+
+function renderUsers(users) {
+ const tbody = document.getElementById('usersTableBody');
+
+ if (users.length === 0) {
+ tbody.innerHTML = '
No users found ';
+ return;
+ }
+
+ tbody.innerHTML = users.map(user => {
+ const statusBadge = user.is_active ?
+ '
Active ' :
+ '
Inactive ';
+
+ const roleColor = user.role === 'admin' ? 'danger' :
+ user.role === 'operator' ? 'warning' : 'info';
+ const roleBadge = `
${user.role} `;
+
+ const createdDate = user.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A';
+ const lastLogin = user.last_login ? new Date(user.last_login).toLocaleString() : 'Never';
+
+ return `
+
+ ${user.username}
+ ${roleBadge}
+ ${statusBadge}
+ ${createdDate}
+ ${lastLogin}
+
+
+
+
+
+
+
+ ${user.is_active ?
+ `
+
+ ` :
+ `
+
+ `
+ }
+
+
+ `;
+ }).join('');
+}
+
+// Add User Modal
+function showAddUserModal() {
+ document.getElementById('addUserModal').classList.add('show');
+}
+
+function closeAddUserModal() {
+ document.getElementById('addUserModal').classList.remove('show');
+ document.getElementById('newUsername').value = '';
+ document.getElementById('newUserPassword').value = '';
+ document.getElementById('newUserRole').value = 'viewer';
+}
+
+async function confirmAddUser() {
+ const username = document.getElementById('newUsername').value.trim();
+ const password = document.getElementById('newUserPassword').value;
+ const role = document.getElementById('newUserRole').value;
+
+ if (!username || !password) {
+ showToast('Username and password are required', 'error');
+ return;
+ }
+
+ if (password.length < 6) {
+ showToast('Password must be at least 6 characters', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/users', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ username, password, role })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`User ${username} created successfully`);
+ closeAddUserModal();
+ loadUsers();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to create user', 'error');
+ }
+}
+
+// Edit User Modal
+function showEditUserModal(userId, username, role) {
+ document.getElementById('editUserId').value = userId;
+ document.getElementById('editUserUsername').value = username;
+ document.getElementById('editUserRole').value = role;
+ document.getElementById('resetUserPassword').value = '';
+ document.getElementById('editUserModal').classList.add('show');
+}
+
+function closeEditUserModal() {
+ document.getElementById('editUserModal').classList.remove('show');
+}
+
+async function confirmEditUser() {
+ const userId = document.getElementById('editUserId').value;
+ const role = document.getElementById('editUserRole').value;
+ const password = document.getElementById('resetUserPassword').value;
+
+ try {
+ // Change role
+ const roleResponse = await fetch(`/api/users/${userId}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ action: 'change_role', role })
+ });
+
+ if (handleAuthError(null, roleResponse)) return;
+
+ const roleResult = await roleResponse.json();
+
+ if (!roleResult.success) {
+ showToast('Error: ' + roleResult.error, 'error');
+ return;
+ }
+
+ // Reset password if provided
+ if (password && password.length >= 6) {
+ const passResponse = await fetch(`/api/users/${userId}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ action: 'reset_password', password })
+ });
+
+ const passResult = await passResponse.json();
+
+ if (!passResult.success) {
+ showToast('Role updated but password reset failed: ' + passResult.error, 'error');
+ closeEditUserModal();
+ loadUsers();
+ return;
+ }
+ }
+
+ showToast('User updated successfully');
+ closeEditUserModal();
+ loadUsers();
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to update user', 'error');
+ }
+}
+
+// Delete User Modal
+function showDeleteUserModal(userId, username) {
+ document.getElementById('deleteUserId').value = userId;
+ document.getElementById('deleteUserUsername').textContent = username;
+ document.getElementById('deleteUserModal').classList.add('show');
+}
+
+function closeDeleteUserModal() {
+ document.getElementById('deleteUserModal').classList.remove('show');
+}
+
+async function confirmDeleteUser() {
+ const userId = document.getElementById('deleteUserId').value;
+
+ try {
+ const response = await fetch(`/api/users/${userId}`, {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('User deleted successfully');
+ closeDeleteUserModal();
+ loadUsers();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to delete user', 'error');
+ }
+}
+
+// Toggle User Status
+async function toggleUserStatus(userId, activate) {
+ const action = activate ? 'activate' : 'deactivate';
+
+ try {
+ const response = await fetch(`/api/users/${userId}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ action })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`User ${activate ? 'activated' : 'deactivated'} successfully`);
+ loadUsers();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to change user status', 'error');
+ }
+}
\ No newline at end of file
diff --git a/web/static/script_v15.js b/web/static/script_v15.js
new file mode 100644
index 00000000..dc450a0a
--- /dev/null
+++ b/web/static/script_v15.js
@@ -0,0 +1,844 @@
+// BetterDesk Console v1.5.0 - Enhanced UI with Sidebar Navigation
+// Global variables
+let allDevices = [];
+let currentDeviceId = null;
+let authToken = null;
+let userRole = null;
+let username = null;
+let publicKeyCache = null;
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', function() {
+ // Check authentication
+ if (!checkAuth()) return;
+
+ // Setup user info in sidebar
+ setupUserInfo();
+
+ // Setup sidebar navigation
+ setupSidebar();
+
+ // Load initial data
+ loadDevices();
+ loadStats();
+
+ // Auto-refresh dashboard every 5 seconds
+ setInterval(() => {
+ const dashboardSection = document.getElementById('dashboard');
+ if (dashboardSection && dashboardSection.classList.contains('active')) {
+ loadDevices();
+ loadStats();
+ }
+ }, 5000);
+});
+
+// Authentication check
+function checkAuth() {
+ authToken = localStorage.getItem('authToken');
+ userRole = localStorage.getItem('role');
+ username = localStorage.getItem('username');
+
+ if (!authToken) {
+ window.location.href = '/login';
+ return false;
+ }
+
+ return true;
+}
+
+// Setup user info in sidebar
+function setupUserInfo() {
+ document.getElementById('sidebarUsername').textContent = username || 'User';
+ document.getElementById('sidebarRole').textContent = userRole || 'viewer';
+
+ // Show admin-only sections
+ if (userRole === 'admin') {
+ document.querySelectorAll('.admin-only').forEach(el => {
+ el.style.display = '';
+ });
+ }
+}
+
+// Setup sidebar navigation
+function setupSidebar() {
+ document.querySelectorAll('.sidebar-item').forEach(item => {
+ item.addEventListener('click', function(e) {
+ e.preventDefault();
+ const sectionId = this.dataset.section;
+
+ // Update active states
+ document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
+ this.classList.add('active');
+
+ // Show selected section
+ document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
+ document.getElementById(sectionId).classList.add('active');
+
+ // Load section-specific data
+ if (sectionId === 'users' && userRole === 'admin') {
+ loadUsers();
+ }
+ });
+ });
+}
+
+// Get auth headers for API calls
+function getAuthHeaders() {
+ return {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ };
+}
+
+// Handle authentication errors
+function handleAuthError(error, response) {
+ if (response && response.status === 401) {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('username');
+ localStorage.removeItem('role');
+ window.location.href = '/login';
+ return true;
+ }
+ return false;
+}
+
+// Logout function
+async function logout() {
+ try {
+ await fetch('/api/auth/logout', {
+ method: 'POST',
+ headers: getAuthHeaders()
+ });
+ } catch (error) {
+ console.error('Logout error:', error);
+ } finally {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('username');
+ localStorage.removeItem('role');
+ window.location.href = '/login';
+ }
+}
+
+// ============================================================================
+// DASHBOARD - DEVICE MANAGEMENT
+// ============================================================================
+
+// Load devices from API
+async function loadDevices() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch('/api/devices', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ allDevices = data.devices;
+ renderDevices(allDevices);
+ updateNavStats(allDevices);
+ } else {
+ showToast('Error loading devices: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to load devices', 'error');
+ }
+}
+
+// Load statistics
+async function loadStats() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch('/api/stats', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('statTotal').textContent = data.stats.total;
+ document.getElementById('statActive').textContent = data.stats.active;
+ document.getElementById('statInactive').textContent = data.stats.inactive;
+ document.getElementById('statBanned').textContent = data.stats.banned || 0;
+ document.getElementById('statNotes').textContent = data.stats.with_notes;
+ }
+ } catch (error) {
+ console.error('Error loading stats:', error);
+ }
+}
+
+// Update navigation stats
+function updateNavStats(devices) {
+ const total = devices.length;
+ const active = devices.filter(d => d.online).length;
+
+ const totalDevicesEl = document.querySelector('#totalDevices span');
+ const activeDevicesEl = document.querySelector('#activeDevices span');
+
+ if (totalDevicesEl) totalDevicesEl.textContent = total;
+ if (activeDevicesEl) activeDevicesEl.textContent = active;
+}
+
+// Render devices table
+function renderDevices(devices) {
+ const tbody = document.getElementById('devicesTableBody');
+
+ if (devices.length === 0) {
+ tbody.innerHTML = `
+
+
+
+ No devices found
+
+
+ `;
+ return;
+ }
+
+ tbody.innerHTML = devices.map(device => {
+ const isBanned = device.is_banned === true || device.is_banned === 1;
+ const rowClass = isBanned ? 'style="opacity: 0.6; background: rgba(255, 0, 0, 0.05);"' : '';
+
+ const canEdit = userRole === 'admin' || userRole === 'operator';
+ const canBan = userRole === 'admin' || userRole === 'operator';
+
+ return `
+
+
+ ${escapeHtml(device.id)}
+ ${isBanned ? ' BANNED ' : ''}
+
+ ${escapeHtml(device.note) || 'No note '}
+
+
+
+ ${device.online ? 'Online' : 'Offline'}
+
+
+ ${formatDate(device.created_at)}
+
+
+
+
+
+
+
+ ${canEdit ? `
+
+
+
+ ` : ''}
+ ${canBan ? (isBanned ?
+ `
+
+ ` :
+ `
+
+ `
+ ) : ''}
+ ${canEdit ? `
+
+
+
+ ` : ''}
+
+
+ `}).join('');
+}
+
+// Filter devices by search
+function filterDevices() {
+ const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+
+ if (!searchTerm) {
+ renderDevices(allDevices);
+ return;
+ }
+
+ const filtered = allDevices.filter(device =>
+ device.id.toLowerCase().includes(searchTerm) ||
+ (device.note && device.note.toLowerCase().includes(searchTerm))
+ );
+
+ renderDevices(filtered);
+}
+
+// Connect to device
+function connectDevice(deviceId) {
+ window.location.href = `rustdesk://${deviceId}`;
+ showToast(`Connecting to ${deviceId}...`);
+}
+
+// Show device details
+function showDetails(deviceId) {
+ const device = allDevices.find(d => d.id === deviceId);
+ if (!device) return;
+
+ const isBanned = device.is_banned === true || device.is_banned === 1;
+
+ const detailsContent = document.getElementById('detailsContent');
+ detailsContent.innerHTML = `
+
+
ID:
+
${escapeHtml(device.id)}
+
+
+
GUID:
+
${escapeHtml(device.guid) || 'N/A'}
+
+
+
UUID:
+
${escapeHtml(device.uuid) || 'N/A'}
+
+
+
Status:
+
+
+
+ ${device.online ? 'Online' : 'Offline'}
+
+
+
+ ${isBanned ? `
+
+
+
Banned At:
+
${device.banned_at ? formatDate(device.banned_at) : 'N/A'}
+
+
+
Banned By:
+
${escapeHtml(device.banned_by) || 'N/A'}
+
+
+
Ban Reason:
+
${escapeHtml(device.ban_reason) || 'No reason provided'}
+
+ ` : ''}
+
+
Note:
+
${escapeHtml(device.note) || 'No note'}
+
+
+
Created:
+
${formatDate(device.created_at)}
+
+ `;
+
+ openModal('detailsModal');
+}
+
+// Edit device
+function editDevice(deviceId) {
+ const device = allDevices.find(d => d.id === deviceId);
+ if (!device) return;
+
+ currentDeviceId = deviceId;
+ document.getElementById('editDeviceId').value = deviceId;
+ document.getElementById('editNewId').value = '';
+ document.getElementById('editNote').value = device.note || '';
+
+ openModal('editModal');
+}
+
+// Save device changes
+async function saveDevice() {
+ if (!checkAuth()) return;
+
+ const newId = document.getElementById('editNewId').value.trim();
+ const note = document.getElementById('editNote').value.trim();
+
+ if (note.length > 500) {
+ showToast('Note is too long (max 500 characters)', 'error');
+ return;
+ }
+
+ const data = { note };
+ if (newId) data.new_id = newId;
+
+ try {
+ const response = await fetch(`/api/device/${currentDeviceId}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify(data)
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Device updated successfully');
+ closeEditModal();
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to update device', 'error');
+ }
+}
+
+// Delete device
+function deleteDevice(deviceId) {
+ currentDeviceId = deviceId;
+ document.getElementById('deleteDeviceId').textContent = deviceId;
+ openModal('deleteModal');
+}
+
+// Confirm delete
+async function confirmDelete() {
+ if (!checkAuth()) return;
+
+ try {
+ const response = await fetch(`/api/device/${currentDeviceId}`, {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Device deleted successfully');
+ closeDeleteModal();
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to delete device', 'error');
+ }
+}
+
+// Ban device
+async function banDevice(deviceId) {
+ if (!checkAuth()) return;
+
+ const reason = prompt(`⚠️ BAN DEVICE: ${deviceId}\n\nEnter ban reason (optional):`);
+ if (reason === null) return;
+
+ try {
+ const response = await fetch(`/api/device/${deviceId}/ban`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ reason: reason || '',
+ banned_by: username
+ })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`Device ${deviceId} banned successfully`);
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to ban device', 'error');
+ }
+}
+
+// Unban device
+async function unbanDevice(deviceId) {
+ if (!checkAuth()) return;
+
+ if (!confirm(`✓ UNBAN DEVICE: ${deviceId}\n\nAre you sure?`)) return;
+
+ try {
+ const response = await fetch(`/api/device/${deviceId}/unban`, {
+ method: 'POST',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`Device ${deviceId} unbanned successfully`);
+ await loadDevices();
+ await loadStats();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to unban device', 'error');
+ }
+}
+
+// Refresh devices manually
+async function refreshDevices() {
+ showToast('Refreshing devices...');
+ await loadDevices();
+ await loadStats();
+}
+
+// ============================================================================
+// PUBLIC KEY SECTION
+// ============================================================================
+
+async function verifyPasswordForKey() {
+ const password = document.getElementById('keyPassword').value;
+
+ if (!password) {
+ showToast('Please enter your password', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/auth/verify-password', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ password: password })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ // Password correct, fetch public key
+ const keyResponse = await fetch('/api/public-key', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, keyResponse)) return;
+
+ const keyData = await keyResponse.json();
+
+ if (keyData.success) {
+ publicKeyCache = keyData.key;
+ document.getElementById('publicKeyDisplay').textContent = keyData.key;
+ document.getElementById('keyPasswordPrompt').style.display = 'none';
+ document.getElementById('keyContent').style.display = 'block';
+ document.getElementById('keyPassword').value = '';
+ showToast('Public key revealed');
+ } else {
+ showToast('Error loading key: ' + keyData.error, 'error');
+ }
+ } else {
+ showToast('Incorrect password', 'error');
+ document.getElementById('keyPassword').value = '';
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to verify password', 'error');
+ }
+}
+
+function copyPublicKey() {
+ const keyText = document.getElementById('publicKeyDisplay').textContent;
+ navigator.clipboard.writeText(keyText).then(() => {
+ showToast('Public key copied to clipboard');
+ }).catch(err => {
+ showToast('Failed to copy', 'error');
+ });
+}
+
+function lockKey() {
+ publicKeyCache = null;
+ document.getElementById('keyPasswordPrompt').style.display = 'block';
+ document.getElementById('keyContent').style.display = 'none';
+ document.getElementById('keyPassword').value = '';
+}
+
+// ============================================================================
+// SETTINGS - PASSWORD CHANGE
+// ============================================================================
+
+async function changePassword(event) {
+ event.preventDefault();
+
+ const currentPassword = document.getElementById('currentPassword').value;
+ const newPassword = document.getElementById('newPassword').value;
+ const confirmPassword = document.getElementById('confirmPassword').value;
+
+ if (newPassword !== confirmPassword) {
+ showToast('New passwords do not match', 'error');
+ return;
+ }
+
+ if (newPassword.length < 8) {
+ showToast('Password must be at least 8 characters', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/auth/change-password', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({
+ old_password: currentPassword,
+ new_password: newPassword
+ })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Password changed successfully');
+ document.getElementById('passwordForm').reset();
+ // New token issued, update local storage
+ if (result.token) {
+ authToken = result.token;
+ localStorage.setItem('authToken', result.token);
+ }
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to change password', 'error');
+ }
+}
+
+// ============================================================================
+// USER MANAGEMENT (ADMIN ONLY)
+// ============================================================================
+
+async function loadUsers() {
+ if (!checkAuth()) return;
+ if (userRole !== 'admin') return;
+
+ try {
+ const response = await fetch('/api/users', {
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ renderUsers(data.users);
+ } else {
+ showToast('Error loading users: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to load users', 'error');
+ }
+}
+
+function renderUsers(users) {
+ const tbody = document.getElementById('usersTableBody');
+
+ if (users.length === 0) {
+ tbody.innerHTML = '
No users found ';
+ return;
+ }
+
+ tbody.innerHTML = users.map(user => {
+ const statusClass = user.is_active ? 'status-active' : 'status-inactive';
+ const statusText = user.is_active ? 'Active' : 'Inactive';
+
+ let roleClass = 'role-viewer';
+ if (user.role === 'admin') roleClass = 'role-admin';
+ else if (user.role === 'operator') roleClass = 'role-operator';
+
+ const lastLogin = user.last_login ? formatDate(user.last_login) : 'Never';
+
+ return `
+
+ ${escapeHtml(user.username)}
+ ${user.role}
+ ${lastLogin}
+ ${statusText}
+
+
+
+
+
+
+
+
+
+ `;
+ }).join('');
+}
+
+// Add User Modal
+function showAddUserModal() {
+ openModal('addUserModal');
+}
+
+function closeAddUserModal() {
+ closeModal('addUserModal');
+ document.getElementById('newUsername').value = '';
+ document.getElementById('newUserPassword').value = '';
+ document.getElementById('newUserRole').value = 'viewer';
+}
+
+async function createUser() {
+ const username = document.getElementById('newUsername').value.trim();
+ const password = document.getElementById('newUserPassword').value;
+ const role = document.getElementById('newUserRole').value;
+
+ if (!username || !password) {
+ showToast('Username and password are required', 'error');
+ return;
+ }
+
+ if (password.length < 8) {
+ showToast('Password must be at least 8 characters', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/users', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ username, password, role })
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`User ${username} created successfully`);
+ closeAddUserModal();
+ loadUsers();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to create user', 'error');
+ }
+}
+
+// Edit/Delete User (placeholders - to be implemented with proper modals)
+function showEditUserModal(userId, username, role, isActive) {
+ // TODO: Implement edit user modal
+ showToast('Edit user functionality - coming soon');
+}
+
+function showDeleteUserModal(userId, username) {
+ if (!confirm(`⚠️ DELETE USER: ${username}\n\nAre you sure?`)) return;
+ deleteUser(userId);
+}
+
+async function deleteUser(userId) {
+ try {
+ const response = await fetch(`/api/users/${userId}`, {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ });
+
+ if (handleAuthError(null, response)) return;
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('User deleted successfully');
+ loadUsers();
+ } else {
+ showToast('Error: ' + result.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showToast('Failed to delete user', 'error');
+ }
+}
+
+// ============================================================================
+// MODAL FUNCTIONS
+// ============================================================================
+
+function openModal(modalId) {
+ document.getElementById(modalId).classList.add('active');
+}
+
+function closeModal(modalId) {
+ document.getElementById(modalId).classList.remove('active');
+}
+
+function closeEditModal() {
+ closeModal('editModal');
+ currentDeviceId = null;
+}
+
+function closeDeleteModal() {
+ closeModal('deleteModal');
+ currentDeviceId = null;
+}
+
+function closeDetailsModal() {
+ closeModal('detailsModal');
+}
+
+// Close modal when clicking outside
+window.onclick = function(event) {
+ if (event.target.classList.contains('modal')) {
+ event.target.classList.remove('active');
+ }
+}
+
+// ============================================================================
+// UTILITY FUNCTIONS
+// ============================================================================
+
+function showToast(message, type = 'success') {
+ const toast = document.getElementById('toast');
+ const icon = toast.querySelector('i');
+
+ if (type === 'error') {
+ icon.className = 'fas fa-exclamation-circle';
+ icon.style.color = 'var(--danger-color)';
+ } else {
+ icon.className = 'fas fa-check-circle';
+ icon.style.color = 'var(--success-color)';
+ }
+
+ document.getElementById('toastMessage').textContent = message;
+ toast.classList.add('show');
+
+ setTimeout(() => {
+ toast.classList.remove('show');
+ }, 3000);
+}
+
+function escapeHtml(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function formatDate(dateString) {
+ if (!dateString) return 'N/A';
+ const date = new Date(dateString);
+ const options = {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ };
+ return date.toLocaleDateString('en-US', options);
+}
diff --git a/web/static/sidebar.css b/web/static/sidebar.css
new file mode 100644
index 00000000..723a1b5e
--- /dev/null
+++ b/web/static/sidebar.css
@@ -0,0 +1,412 @@
+/* Sidebar Styles for BetterDesk Console v1.4.0 */
+
+:root {
+ --sidebar-width: 280px;
+}
+
+/* Layout adjustments for sidebar */
+body.has-sidebar {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar Container */
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: var(--sidebar-width);
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(20px);
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ flex-direction: column;
+ z-index: 1000;
+ overflow-y: auto;
+}
+
+/* Sidebar Header */
+.sidebar-header {
+ padding: 24px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: white;
+ font-size: 20px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.sidebar-brand i {
+ font-size: 28px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.brand-text {
+ /* Text always visible */
+}
+
+/* Toggle button removed - sidebar always expanded */
+
+/* Sidebar User Section */
+.sidebar-user {
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.user-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 24px;
+ flex-shrink: 0;
+}
+
+.user-info {
+ flex: 1;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.user-name {
+ color: white;
+ font-weight: 600;
+ font-size: 14px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.user-role {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Sidebar Menu */
+.sidebar-menu {
+ flex: 1;
+ padding: 12px;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.sidebar-menu::-webkit-scrollbar {
+ width: 4px;
+}
+
+.sidebar-menu::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ color: rgba(255, 255, 255, 0.8);
+ text-decoration: none;
+ border-radius: 12px;
+ transition: all 0.3s;
+ margin-bottom: 4px;
+ cursor: pointer;
+ position: relative;
+ white-space: nowrap;
+}
+
+.menu-item i {
+ font-size: 18px;
+ width: 20px;
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.menu-item span {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.menu-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ transform: translateX(4px);
+}
+
+.menu-item.active {
+ background: rgba(255, 255, 255, 0.15);
+ color: white;
+ font-weight: 600;
+}
+
+.menu-item.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 60%;
+ background: white;
+ border-radius: 0 4px 4px 0;
+}
+
+/* Sidebar Footer */
+.sidebar-footer {
+ padding: 12px;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.logout-btn {
+ color: #ff6b6b !important;
+ justify-content: flex-start;
+}
+
+.logout-btn:hover {
+ background: rgba(255, 107, 107, 0.1) !important;
+}
+
+/* Main Content Area */
+.main-content {
+ flex: 1;
+ margin-left: var(--sidebar-width);
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* Top Navbar */
+.top-navbar {
+ position: sticky;
+ top: 0;
+ z-index: 999;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(20px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 16px 24px;
+}
+
+.nav-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+
+.mobile-menu-toggle {
+ display: none;
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: white;
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 18px;
+}
+
+.page-title {
+ color: white;
+ font-size: 24px;
+ font-weight: 700;
+ margin: 0;
+ flex: 1;
+}
+
+.nav-stats {
+ display: flex;
+ gap: 12px;
+}
+
+.stat-badge {
+ background: rgba(255, 255, 255, 0.1);
+ padding: 8px 16px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: white;
+}
+
+.stat-badge i {
+ font-size: 16px;
+}
+
+.stat-badge.active {
+ background: rgba(76, 175, 80, 0.2);
+ color: #4caf50;
+}
+
+/* Content Container */
+.content-container {
+ flex: 1;
+ padding: 24px;
+ overflow-y: auto;
+}
+
+.page-content {
+ display: none;
+}
+
+.page-content.active {
+ display: block;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Action Bar */
+.action-bar {
+ margin-bottom: 24px;
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ :root {
+ --sidebar-width: 260px;
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ }
+
+ .sidebar.mobile-open {
+ transform: translateX(0);
+ }
+
+ .main-content {
+ margin-left: 0 !important;
+ }
+
+ .mobile-menu-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .page-title {
+ font-size: 20px;
+ }
+
+ .nav-stats {
+ display: none;
+ }
+
+ .content-container {
+ padding: 16px;
+ }
+
+ /* Overlay when sidebar is open on mobile */
+ .sidebar-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 999;
+ display: none;
+ }
+
+ .sidebar.mobile-open ~ .sidebar-overlay {
+ display: block;
+ }
+}
+
+/* Settings Page Styles */
+.settings-container {
+ padding: 24px;
+ border-radius: 16px;
+}
+
+.settings-section {
+ margin-bottom: 24px;
+}
+
+.settings-section h3 {
+ color: white;
+ margin-bottom: 16px;
+}
+
+/* Key Container */
+.key-container {
+ padding: 24px;
+ border-radius: 16px;
+}
+
+.key-display {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 16px;
+ border-radius: 12px;
+ margin: 16px 0;
+ word-break: break-all;
+}
+
+.key-display code {
+ color: #4caf50;
+ font-family: 'Courier New', monospace;
+}
+
+/* About Container */
+.about-container {
+ padding: 24px;
+ border-radius: 16px;
+ color: white;
+}
+
+.about-container h2 {
+ margin-bottom: 20px;
+}
+
+.about-container p {
+ margin: 12px 0;
+}
+
+.about-container ul {
+ list-style: none;
+ padding-left: 0;
+}
+
+.about-container li {
+ padding: 8px 0;
+ padding-left: 24px;
+ position: relative;
+}
+
+.about-container li::before {
+ content: '✓';
+ position: absolute;
+ left: 0;
+ color: #4caf50;
+}
diff --git a/web/static/sidebar.js b/web/static/sidebar.js
new file mode 100644
index 00000000..428f126e
--- /dev/null
+++ b/web/static/sidebar.js
@@ -0,0 +1,210 @@
+/* Sidebar JavaScript for BetterDesk Console v1.4.0 */
+
+// Initialize sidebar
+document.addEventListener('DOMContentLoaded', function() {
+ initializeSidebar();
+ loadUserInfo();
+ setupMenuNavigation();
+ setupMobileMenu();
+});
+
+function initializeSidebar() {
+ // Sidebar is always expanded - no toggle needed
+ console.log('Sidebar initialized (always expanded)');
+}
+
+function loadUserInfo() {
+ const username = localStorage.getItem('username') || 'User';
+ const role = localStorage.getItem('role') || 'viewer';
+
+ // Update sidebar user info
+ const usernameEl = document.getElementById('sidebarUsername');
+ const userRoleEl = document.getElementById('sidebarUserRole');
+
+ if (usernameEl) {
+ usernameEl.textContent = username;
+ }
+
+ if (userRoleEl) {
+ const roleNames = {
+ 'admin': 'Administrator',
+ 'operator': 'Operator',
+ 'viewer': 'Viewer'
+ };
+ userRoleEl.textContent = roleNames[role] || role;
+ }
+
+ // Show/hide menu items based on role
+ updateMenuVisibility(role);
+}
+
+function updateMenuVisibility(role) {
+ const menuUsers = document.getElementById('menuUsers');
+ const menuAudit = document.getElementById('menuAudit');
+ const menuSettings = document.getElementById('menuSettings');
+ const menuKey = document.getElementById('menuKey');
+
+ // Admin sees everything
+ if (role === 'admin') {
+ if (menuUsers) menuUsers.style.display = 'flex';
+ if (menuAudit) menuAudit.style.display = 'flex';
+ if (menuSettings) menuSettings.style.display = 'flex';
+ if (menuKey) menuKey.style.display = 'flex';
+ }
+ // Operator sees audit and settings
+ else if (role === 'operator') {
+ if (menuUsers) menuUsers.style.display = 'none';
+ if (menuAudit) menuAudit.style.display = 'flex';
+ if (menuSettings) menuSettings.style.display = 'flex';
+ if (menuKey) menuKey.style.display = 'none';
+ }
+ // Viewer sees only settings
+ else {
+ if (menuUsers) menuUsers.style.display = 'none';
+ if (menuAudit) menuAudit.style.display = 'none';
+ if (menuSettings) menuSettings.style.display = 'flex';
+ if (menuKey) menuKey.style.display = 'none';
+ }
+}
+
+function setupMenuNavigation() {
+ const menuItems = document.querySelectorAll('.menu-item[data-page]');
+
+ menuItems.forEach(item => {
+ item.addEventListener('click', function(e) {
+ e.preventDefault();
+
+ const page = this.dataset.page;
+
+ // Update active menu item
+ menuItems.forEach(mi => mi.classList.remove('active'));
+ this.classList.add('active');
+
+ // Show corresponding page
+ showPage(page);
+
+ // Close mobile menu if open
+ closeMobileMenu();
+ });
+ });
+}
+
+function showPage(pageName) {
+ // Hide all pages
+ const pages = document.querySelectorAll('.page-content');
+ pages.forEach(page => page.classList.remove('active'));
+
+ // Show selected page
+ const targetPage = document.getElementById(pageName + 'Page');
+ if (targetPage) {
+ targetPage.classList.add('active');
+ }
+
+ // Update page title
+ const pageTitles = {
+ 'dashboard': 'Device Management',
+ 'users': 'User Management',
+ 'audit': 'Audit Log',
+ 'settings': 'Settings',
+ 'key': 'Public Key',
+ 'about': 'About BetterDesk'
+ };
+
+ const pageTitle = document.getElementById('pageTitle');
+ if (pageTitle && pageTitles[pageName]) {
+ pageTitle.textContent = pageTitles[pageName];
+ }
+
+ // Load page-specific data
+ if (pageName === 'dashboard') {
+ if (typeof refreshDevices === 'function') {
+ refreshDevices();
+ }
+ } else if (pageName === 'users') {
+ if (typeof loadUsers === 'function') {
+ loadUsers();
+ }
+ }
+}
+
+function setupMobileMenu() {
+ const mobileToggle = document.getElementById('mobileMenuToggle');
+ const sidebar = document.getElementById('sidebar');
+
+ if (mobileToggle) {
+ mobileToggle.addEventListener('click', function() {
+ sidebar.classList.toggle('mobile-open');
+
+ // Create/remove overlay
+ if (sidebar.classList.contains('mobile-open')) {
+ createOverlay();
+ } else {
+ removeOverlay();
+ }
+ });
+ }
+}
+
+function createOverlay() {
+ const existing = document.querySelector('.sidebar-overlay');
+ if (existing) return;
+
+ const overlay = document.createElement('div');
+ overlay.className = 'sidebar-overlay';
+ overlay.addEventListener('click', closeMobileMenu);
+ document.body.appendChild(overlay);
+}
+
+function removeOverlay() {
+ const overlay = document.querySelector('.sidebar-overlay');
+ if (overlay) {
+ overlay.remove();
+ }
+}
+
+function closeMobileMenu() {
+ const sidebar = document.getElementById('sidebar');
+ sidebar.classList.remove('mobile-open');
+ removeOverlay();
+}
+
+async function logout() {
+ if (!confirm('Are you sure you want to logout?')) {
+ return;
+ }
+
+ const token = localStorage.getItem('authToken');
+
+ // Call logout API
+ try {
+ await fetch('/api/auth/logout', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+ } catch (error) {
+ console.error('Logout error:', error);
+ }
+
+ // Clear local storage
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('username');
+ localStorage.removeItem('role');
+
+ // Redirect to login
+ window.location.href = '/login';
+}
+
+function showChangePasswordModal() {
+ // TODO: Implement change password modal
+ alert('Change password functionality coming soon!');
+}
+
+// Export functions for use in other scripts
+window.sidebarFunctions = {
+ showPage,
+ logout,
+ loadUserInfo,
+ updateMenuVisibility
+};
diff --git a/web/static/style.css b/web/static/style.css
index 9fa8ecdc..b81c5b2b 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -699,3 +699,173 @@ textarea.form-control {
flex-wrap: wrap;
}
}
+/* About Page Styles */
+.about-container {
+ padding: 2rem;
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.about-container h2 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.about-section {
+ margin-bottom: 2.5rem;
+}
+
+.about-section h3 {
+ font-size: 1.4rem;
+ margin-bottom: 1rem;
+ color: var(--primary-color);
+}
+
+.about-section p {
+ margin-bottom: 0.75rem;
+ line-height: 1.6;
+ color: var(--text-secondary);
+}
+
+.about-section ul {
+ list-style: none;
+ padding-left: 0;
+}
+
+.about-section ul li {
+ margin-bottom: 0.75rem;
+ padding-left: 1.5rem;
+ position: relative;
+ line-height: 1.6;
+ color: var(--text-secondary);
+}
+
+.about-section ul li i {
+ position: absolute;
+ left: 0;
+ top: 0.25rem;
+ color: var(--success-color);
+}
+
+.about-section ul li strong {
+ color: var(--text-primary);
+}
+
+.github-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+ color: var(--text-primary);
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ border: 1px solid var(--glass-border);
+}
+
+.github-link:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(107, 114, 128, 0.4);
+ background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
+}
+
+.github-link i {
+ font-size: 1.2rem;
+}
+
+/* User Management Styles */
+.users-container {
+ padding: 2rem;
+}
+
+.users-container h2 {
+ margin-bottom: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+/* Badge Styles */
+.badge {
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+
+.badge-success {
+ background: var(--success-color);
+ color: white;
+}
+
+.badge-danger {
+ background: var(--danger-color);
+ color: white;
+}
+
+.badge-warning {
+ background: var(--warning-color);
+ color: white;
+}
+
+.badge-info {
+ background: var(--info-color);
+ color: white;
+}
+
+/* Settings Page Styles */
+.settings-container {
+ padding: 2rem;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.settings-container h2 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.settings-section {
+ margin-bottom: 2rem;
+ padding: 1.5rem;
+ background: rgba(30, 30, 30, 0.5);
+ border-radius: 12px;
+ border: 1px solid var(--glass-border);
+}
+
+.settings-section h3 {
+ font-size: 1.3rem;
+ margin-bottom: 1rem;
+ color: var(--primary-color);
+}
+
+.settings-section p {
+ margin-bottom: 1rem;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+/* Key Page Styles */
+.key-container {
+ padding: 2rem;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.key-container h2 {
+ font-size: 2rem;
+ margin-bottom: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
diff --git a/web/templates/index.html b/web/templates/index.html
deleted file mode 100644
index 6f8cc13e..00000000
--- a/web/templates/index.html
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
-
-
RustDesk Console - Dashboard
-
-
-
-
-
-
-
-
-
-
-
-
- RustDesk Console
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ID
- Note
- Status
- Created
- Actions
-
-
-
-
-
-
- Loading devices...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Device ID
-
-
-
- New Device ID (optional)
-
-
-
- Note
-
-
-
-
-
-
-
-
-
-
-
-
-
Are you sure you want to delete device ?
-
This action cannot be undone.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ public_key }}
-
-
- Copy to Clipboard
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/templates/index_v14.html b/web/templates/index_v14.html
new file mode 100644
index 00000000..627f9f0e
--- /dev/null
+++ b/web/templates/index_v14.html
@@ -0,0 +1,536 @@
+
+
+
+
+
+
BetterDesk Console - Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Device Management
+
+
+
+
+
+
+
+
+
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Note
+ Status
+ Created
+ Actions
+
+
+
+
+
+
+ Loading devices...
+
+
+
+
+
+
+
+
+
+
+
Devices management page (to be implemented)
+
+
+
+
+
User Management
+
+
+ Add New User
+
+
+
+
+
+
+ Username
+ Role
+ Status
+ Created
+ Last Login
+ Actions
+
+
+
+
+ Loading users...
+
+
+
+
+
+
+
+
+
+
Audit Log
+
Audit log page (to be implemented)
+
+
+
+
+
+
Settings
+
+
Account Security
+
Change your account password to keep your account secure.
+
+ Change Password
+
+
+
+
+
+
+
+
RustDesk Public Key
+
For security reasons, please verify your password to view the public key.
+
+ Enter Your Password
+
+
+ Show Public Key
+
+
+
+
+
+
+
+ Copy to Clipboard
+
+
+
+
+
+
+
+
About BetterDesk Console
+
+
+
Version Information
+
Version: 1.4.0
+
Build: v9
+
+
+
+
Features
+
+ Real-time device monitoring and management
+ Bidirectional ban enforcement (source + target)
+ User authentication & role-based access control
+ Comprehensive audit logging
+ HTTP API for device status
+ Modern glassmorphism UI design
+
+
+
+
+
Built With Open Source
+
This project is built using the following open source technologies:
+
+ RustDesk - Open source remote desktop software (AGPL-3.0)
+ Flask - Python web framework (BSD-3-Clause)
+ SQLite - Embedded database (Public Domain)
+ bcrypt - Password hashing library (Apache-2.0)
+ Font Awesome - Icon library (Font Awesome Free License)
+
+
+
+
+
+
+
License
+
MIT License - Free to use and modify
+
+
+
+
Author
+
Developed by UNITRONIX
+
With contributions from the open source community
+
+
+
+
+
+
+
+
+
+
+
+
+ Device ID
+
+
+
+ New Device ID (optional)
+
+
+
+ Note
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete device ?
+
This action cannot be undone.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username
+
+
+
+ Password
+
+
+
+ Role
+
+ Viewer (Read-only)
+ Operator (Can ban/unban devices)
+ Administrator (Full access)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username
+
+
+
+ Role
+
+ Viewer (Read-only)
+ Operator (Can ban/unban devices)
+ Administrator (Full access)
+
+
+
+ Reset Password (optional)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete user ?
+
This action cannot be undone. All user sessions will be terminated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/templates/index_v15.html b/web/templates/index_v15.html
new file mode 100644
index 00000000..88829e37
--- /dev/null
+++ b/web/templates/index_v15.html
@@ -0,0 +1,829 @@
+
+
+
+
+
+
BetterDesk Console v1.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Device Management
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Note
+ Status
+ Created
+ Actions
+
+
+
+
+
+
+ Loading devices...
+
+
+
+
+
+
+
+
+
+
+
+
+
Server Public Key
+
+
+
Protected Content
+
+ Enter your password to view the server public key
+
+
+
+ Unlock Key
+
+
+
+
+
+
RustDesk Public Key
+
+
+
+ Copy to Clipboard
+
+
+ Lock Key
+
+
+
+
+
+
+
+
+
+
+
+
User Management
+
+
+
+
+
+
+
+ Username
+ Role
+ Last Login
+ Status
+ Actions
+
+
+
+
+
+
+ Loading users...
+
+
+
+
+
+
+
+
+
+
+
+
About BetterDesk Console
+
+
+
Project Information
+
+ BetterDesk Console v1.5.0 - Advanced management interface for RustDesk Server
+
+
+ A powerful, secure web console for managing RustDesk remote desktop server. Features include
+ device management, user authentication with role-based access control, ban enforcement with
+ fail-closed security policy, and comprehensive audit logging.
+
+
+
+
+
+
+
Open Source Components
+
This project is built with the following open source software:
+
+
+
+
+
Security Features
+
+ Authentication System - Secure login with bcrypt password hashing
+ Role-Based Access Control - Admin, Operator, and Viewer roles
+ CSRF Protection - Cross-Site Request Forgery prevention
+ Rate Limiting - Protection against brute force attacks
+ Fail-Closed Policy - Banned devices cannot connect even if service restarts
+ Audit Logging - Complete history of user actions
+ Content Security Policy - Protection against XSS attacks
+
+
+
+
+
Credits
+
+ Developed by UNITRONIX
+ Special thanks to the RustDesk team and open source community.
+
+
+ © 2026 UNITRONIX. Released under AGPL-3.0 License.
+
+
+
+
+
+
+
+
+
+
+
+
+ Device ID
+
+
+
+ New Device ID (optional)
+
+
+
+ Note
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete device ?
+
This action cannot be undone.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username
+
+
+
+ Password
+
+
+ Minimum 8 characters, must contain letters and numbers
+
+
+
+ Role
+
+ Viewer (Read-only)
+ Operator (Can ban/unban, edit devices)
+ Admin (Full access)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/templates/login.html b/web/templates/login.html
new file mode 100644
index 00000000..4a677428
--- /dev/null
+++ b/web/templates/login.html
@@ -0,0 +1,378 @@
+
+
+
+
+
+
Login - BetterDesk Console
+
+
+
+
+
+
+
+
+
BetterDesk Console
+
RustDesk Management Dashboard
+
+
+
+
+
+
+
+
+
+
+ Version 1.5 • Secure Auth System
+
+
+
+
+
+
+