Clean up v14 assets, add v15 deploy script and cache busting

Removed obsolete v14 static and template files, and deleted legacy scripts and performance config. Added scripts/deploy_v15.sh for clean v15 deployment. Updated app_v14.py to support cache busting via version injection and cache headers. Updated documentation and troubleshooting to reflect new manual key permission steps and streamlined upgrade process. Bumped version to v1.5.1.
This commit is contained in:
UNITRONIX
2026-02-01 01:14:16 +01:00
parent bc624a4a32
commit 890f3a0411
17 changed files with 405 additions and 3143 deletions
-244
View File
@@ -1,244 +0,0 @@
# 📝 Changelog - Naprawa Problemów Użytkowników
## [31 Stycznia 2026] - Naprawa Krytycznych Błędów
### 🐛 Naprawione Problemy
#### Problem 1: Docker - "sh: executable file not found in $PATH"
**Zgłaszający:** Użytkownik GitHub
**Symptomy:**
```
Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed:
runc create failed: unable to start container process:
error during container init: exec: "sh": executable file not found in $PATH
```
**Przyczyna:**
- Obraz bazowy `python:3.11-slim` w niektórych przypadkach nie zawiera bash
- Skrypt `docker-entrypoint.sh` wymaga bash ze względu na zaawansowane funkcje
**Rozwiązanie:**
- ✅ Dodano instalację `bash` do `Dockerfile.console`
- ✅ Dodano dokumentację wyjaśniającą wymagania shell'a
- ✅ Zmieniono komentarze w `docker-entrypoint.sh`
**Zmodyfikowane pliki:**
- `Dockerfile.console` - dodano bash do apt-get install
- `docker-entrypoint.sh` - dodano komentarz wyjaśniający
---
#### Problem 2: PowerShell - "Write-Info is not recognized"
**Zgłaszający:** Użytkownik Windows 11
**Symptomy:**
```powershell
The term 'Write-Info' is not recognized as the name of a cmdlet,
function, script file, or operable program.
```
**Przyczyna:**
- Konflikt nazw funkcji z wbudowanymi cmdletami PowerShell (`Write-Error`, `Write-Warning`)
- Niektóre wersje PowerShell mogą mieć problemy z niestandardowymi funkcjami
- Brak wymogu minimalnej wersji PowerShell
**Rozwiązanie:**
- ✅ Zmieniono nazwy funkcji pomocniczych:
- `Write-Error``Write-ErrorMsg`
- `Write-Warning``Write-WarningMsg`
- `Write-Info``Write-InfoMsg`
- ✅ Dodano `#Requires -Version 5.1`
- ✅ Dodano `Set-StrictMode -Version Latest`
- ✅ Dodano regiony dla lepszej organizacji kodu
- ✅ Zamieniono wszystkie 58 wywołań funkcji na nowe nazwy
**Zmodyfikowane pliki:**
- `install-improved.ps1` - pełna refaktoryzacja funkcji helper
---
### 📄 Nowe Pliki Dokumentacji
#### 1. `TROUBLESHOOTING.md`
Kompletny przewodnik rozwiązywania problemów zawierający:
- Szczegółowy opis obu problemów
- Dokładne przyczyny i rozwiązania
- Instrukcje testowania
- Diagnostykę dla zaawansowanych użytkowników
- Alternatywne rozwiązania
#### 2. `QUICK_FIX.md`
Szybki przewodnik dla zgłaszających problemy:
- Krok po kroku instrukcje naprawy
- Komendy do skopiowania i wklejenia
- Checklist weryfikacji
- Informacje diagnostyczne do zgłoszenia jeśli problemy persist
#### 3. `OPTIMIZATION_SUMMARY.md`
Podsumowanie optymalizacji GPU (wcześniejsza praca):
- Lista zoptymalizowanych plików
- Metryki wydajności
- Instrukcje dla użytkowników
#### 4. `docs/GPU_OPTIMIZATION.md`
Szczegółowa dokumentacja optymalizacji wydajności panelu web
#### 5. `docs/GPU_FIX_QUICKSTART.md`
Szybki start dla problemów z wydajnością GPU
#### 6. `web/static/performance-config.css`
Plik konfiguracyjny z 4 profilami wydajności
---
### 🔄 Zaktualizowane Pliki
#### `README.md`
- ✅ Dodano sekcję "Recent Fixes" na górze Troubleshooting
- ✅ Dodano linki do nowych przewodników
- ✅ Podkreślono naprawione problemy
#### `Dockerfile.console`
```diff
+ RUN apt-get update && apt-get install -y \
+ sqlite3 \
+ curl \
+ bash \
+ && rm -rf /var/lib/apt/lists/*
```
#### `docker-entrypoint.sh`
```diff
#!/bin/bash
+ # Docker Entrypoint for BetterDesk Console
+ # This script requires bash due to array syntax and advanced features
set -e
```
#### `install-improved.ps1`
```diff
+ #Requires -Version 5.1
+ Set-StrictMode -Version Latest
+ #region Helper Functions
- function Write-Error { ... }
- function Write-Warning { ... }
- function Write-Info { ... }
+ function Write-ErrorMsg { ... }
+ function Write-WarningMsg { ... }
+ function Write-InfoMsg { ... }
+ #endregion
# + 58 zmian wywołań funkcji w całym pliku
```
---
### 📊 Statystyki Zmian
| Kategoria | Wartość |
|-----------|---------|
| Zmodyfikowane pliki | 4 |
| Nowe pliki dokumentacji | 6 |
| Linie kodu zmienionych | ~150 |
| Wywołania funkcji zaktualizowanych | 58 |
| Problemy naprawione | 2 |
| Zgłaszający pomóc | 2+ |
---
### ✅ Weryfikacja
#### Docker:
```bash
# Test kompilacji
docker-compose build --no-cache betterdesk-console
✅ Buduje się bez błędów
# Test uruchomienia
docker-compose up -d betterdesk-console
✅ Kontener startuje poprawnie
# Test funkcjonalności
docker-compose logs betterdesk-console | grep "Starting BetterDesk Console"
✅ Aplikacja się uruchamia
```
#### PowerShell:
```powershell
# Test składni
Get-Command .\install-improved.ps1 -Syntax
Składnia poprawna
# Test wykonania
.\install-improved.ps1 -WhatIf
Uruchamia się bez błędów
# Test funkcji
(Get-Content .\install-improved.ps1) -match "Write-ErrorMsg|Write-WarningMsg|Write-InfoMsg"
Wszystkie funkcje zaktualizowane
```
---
### 🎯 Dla Zgłaszających
#### Użytkownik Problem 1 (Docker):
```bash
git pull origin main
docker-compose down -v
docker-compose build --no-cache
docker-compose up -d
docker-compose logs -f betterdesk-console
```
**Status:** ✅ Powinno działać
#### Użytkownik Problem 2 (PowerShell):
```powershell
git pull origin main
.\install-improved.ps1
```
**Status:** ✅ Powinno działać
---
### 📞 Wsparcie
Jeśli problemy nadal występują po aktualizacji:
1. Sprawdź dokumentację:
- [QUICK_FIX.md](QUICK_FIX.md) dla szybkich rozwiązań
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) dla szczegółów
2. Uruchom diagnostykę:
```bash
# Docker
docker version
docker-compose config
# PowerShell
$PSVersionTable
Get-ExecutionPolicy -List
```
3. Zgłoś issue na GitHub z:
- Opisem problemu
- Wyjściem z diagnostyki
- Logami błędów
---
### 🙏 Podziękowania
Dziękujemy użytkownikom za zgłoszenie problemów:
- Użytkownik zgłaszający problem Docker
- Użytkownik zgłaszający problem PowerShell
Wasze zgłoszenia pomogły ulepszyć projekt dla całej społeczności!
---
**Data:** 31 Stycznia 2026
**Wersja:** 1.5.0
**Autor napraw:** UNITRONIX Team + GitHub Copilot
**Status:** ✅ Ukończone i przetestowane
+27 -31
View File
@@ -5,7 +5,7 @@
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![RustDesk](https://img.shields.io/badge/RustDesk-1.1.14-green.svg)
![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)
![Version](https://img.shields.io/badge/version-1.5.0-brightgreen.svg)
![Version](https://img.shields.io/badge/version-1.5.1-brightgreen.svg)
![Security](https://img.shields.io/badge/API-X--API--Key--Auth-green.svg)
![Access](https://img.shields.io/badge/LAN-Accessible-blue.svg)
@@ -306,27 +306,29 @@ sudo ./install-improved.sh
### 🔄 Updating Existing Installation
If you already have BetterDesk Console installed and want to upgrade to v1.5.0:
If you already have BetterDesk Console installed, the same `install-improved.sh` script handles updates:
```bash
cd Rustdesk-FreeConsole
# Make the update script executable
chmod +x update-to-v1.5.0.sh
# Pull latest changes
git pull origin main
# Run as root
sudo ./update-to-v1.5.0.sh
# Run installer (auto-detects existing installation)
chmod +x install-improved.sh
sudo ./install-improved.sh
```
**Update features:**
- ✅ Auto-detects existing BetterDesk installation
- ✅ Automatic backup before changes
- ✅ Database migration (adds `last_online`, `is_deleted` columns)
- ✅ Authentication tables creation
- ✅ API key generation and configuration
- ✅ Preserves existing configuration
- ✅ Preserves existing configuration and encryption keys
- ✅ Creates default admin user (if needed)
- ✅ Updates HBBS/HBBR binaries
-Rollback capability if update fails
-Version detection and smart upgrade path
### 🐳 Docker Installation & Update
@@ -499,14 +501,7 @@ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}"
This is the most common issue after installation. **Don't panic!**
**Quick Fix:**
```bash
cd /path/to/Rustdesk-FreeConsole
sudo bash repair-keys.sh
# Select option 5: Restore from backup
```
**If BetterDesk broke your setup:**
**Quick Fix - Restore from backup:**
```bash
# Find most recent backup
BACKUP=$(ls -d /opt/rustdesk-backup-* | sort | tail -1)
@@ -529,20 +524,21 @@ cat /opt/rustdesk/id_ed25519.pub
- **[KEY_TROUBLESHOOTING.md](docs/KEY_TROUBLESHOOTING.md)** - Complete key management guide
- **[UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)** - Updating existing installations
### 🔧 Using the Repair Tool
### 🔧 Key Permission Issues
The `repair-keys.sh` tool can fix most key-related issues:
If you experience key permission problems, fix them manually:
```bash
sudo bash repair-keys.sh
```
# Set correct permissions for encryption keys
sudo chmod 600 /opt/rustdesk/id_ed25519
sudo chmod 644 /opt/rustdesk/id_ed25519.pub
**Available options:**
1. 📋 Show current key information
2. 🔐 Verify and fix key permissions
3. 📤 Export public key
4. 🔄 Regenerate keys (⚠️ BREAKS connections)
5. 💾 Restore keys from backup
# Restart services
sudo systemctl restart rustdesksignal rustdeskrelay betterdesk
# Verify public key is readable
cat /opt/rustdesk/id_ed25519.pub
```
### 🐳 Docker Issues
@@ -683,9 +679,9 @@ conn.close()
| Symptom | Cause | Solution |
|---------|-------|----------|
| "Key mismatch" | Keys changed during install | Restore from backup or use repair tool |
| "Key mismatch" | Keys changed during install | Restore from backup (see Troubleshooting above) |
| Wrong key in WebConsole | Multiple `.pub` files | Remove incorrect files or upgrade to v9+ |
| Services won't start | Permission issues | Run `sudo bash repair-keys.sh` → option 2 |
| Services won't start | Permission issues | Fix with: `sudo chmod 600 /opt/rustdesk/id_ed25519` |
| Can't find backups | Skipped backup during install | Check `/opt/rustdesk-backup-*` directories |
| Docker detected | Running RustDesk in container | Choose "Web Console only" option |
| **No admin login (Docker)** | Missing database migration | Run `./fix-admin.sh` or see Docker Issues section |
@@ -748,9 +744,9 @@ sudo systemctl daemon-reload
sudo systemctl restart hbbs
```
#### 4. Update Script Cannot Find Installation
#### 4. Installer Cannot Find Installation
**Symptoms:** `update-to-v1.5.0.sh` reports "Installation directory not found"
**Symptoms:** `install-improved.sh` reports "Installation directory not found"
**Cause:** Non-standard installation path.
@@ -768,7 +764,7 @@ python3 migrations/v1.5.0_fix_online_status.py /path/to/db_v2.sqlite3
**Before asking for help:**
1. Check the troubleshooting guides above
2. Try the repair tool: `sudo bash repair-keys.sh`
2. Run the installation script again (it auto-detects and can fix issues): `sudo ./install-improved.sh`
3. Collect diagnostics:
```bash
sudo journalctl -u rustdesksignal -n 50 > ~/rustdesk_logs.txt
+14 -12
View File
@@ -1,9 +1,15 @@
v1.5.0
v1.5.1
BetterDesk Console Version 1.5.0
Authentication System & Modern UI
BetterDesk Console Version 1.5.1
GPU Optimization & Clean Deployment
New in v1.5.0:
New in v1.5.1:
- 🚀 GPU optimization (removed backdrop-filter, heavy animations)
- 🧹 Clean project structure (removed old files)
- 🔒 Client Generator disabled (under development)
- 🔄 Cache busting system for automatic updates
Previous v1.5.0:
- 🔐 Authentication system with bcrypt password hashing
- 👥 Role-based access control (Admin, Operator, Viewer)
- 🎨 Sidebar navigation with 5 main sections
@@ -12,8 +18,7 @@ New in v1.5.0:
- 👤 User Management panel (admin only)
- 🛡️ CSRF protection and rate limiting
- 📊 Extended About page with open source credits
- 🔧 New repair-keys.sh diagnostic tool
- 📚 Complete troubleshooting documentation
- Complete troubleshooting documentation
- ⚠️ Visual warnings for dangerous operations
- ✅ Pre-flight validation checks
@@ -24,14 +29,11 @@ Critical Fixes:
- Fixed Docker installation conflicts
- Added automatic key backup verification
Installation:
Installation & Update:
sudo bash install-improved.sh
Repair Tool:
sudo bash repair-keys.sh
Documentation:
- docs/KEY_TROUBLESHOOTING.md - Complete guide
- docs/QUICK_FIX.md - Fast solutions
- docs/RELEASE_NOTES_v9.md - Full changelog
- docs/UPDATE_GUIDE.md - Update instructions
- README.md - Full documentation
+15 -18
View File
@@ -145,21 +145,26 @@ sudo systemctl start rustdesksignal rustdeskrelay
cat /opt/rustdesk/id_ed25519.pub
```
### Solution 2: Using the Repair Tool
### Solution 2: Fix Key Permissions Manually
**Easiest method** - use the included repair tool:
**Easiest method** - fix permissions directly:
```bash
cd /path/to/Rustdesk-FreeConsole
sudo bash repair-keys.sh
# Set correct permissions for encryption keys
sudo chmod 600 /opt/rustdesk/id_ed25519
sudo chmod 644 /opt/rustdesk/id_ed25519.pub
# Verify ownership
sudo chown root:root /opt/rustdesk/id_ed25519*
# Restart services
sudo systemctl restart rustdesksignal rustdeskrelay betterdesk
```
**Options available:**
1. Show current key information
2. Verify and fix permissions
3. Export public key
4. Regenerate keys (last resort)
5. Restore from backup
**Verify it works:**
```bash
cat /opt/rustdesk/id_ed25519.pub
```
### Solution 3: Multiple .pub Files Exist
@@ -200,14 +205,6 @@ stat /opt/rustdesk/*.pub
**Fix - Last Resort** (regenerate keys):
```bash
# Use repair tool for safety
sudo bash repair-keys.sh
# Select option 4: Regenerate keys
```
**OR manually:**
```bash
# STOP! Make backup first!
sudo cp -r /opt/rustdesk /opt/rustdesk-backup-emergency
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
# ==============================================
# BetterDesk Console v1.5 - Complete Clean Deployment
# Date: 2026-02-01
# ==============================================
set -e
echo "========================================"
echo " BetterDesk Console v1.5 Deployment"
echo "========================================"
echo ""
WEB_DIR="/opt/BetterDeskConsole/web"
BACKUP_DIR="/opt/BetterDeskConsole/backups/pre_v15_$(date +%Y%m%d_%H%M%S)"
# Create backup
echo "[1/5] Creating backup..."
sudo mkdir -p "$BACKUP_DIR"
sudo cp -r "$WEB_DIR" "$BACKUP_DIR/"
echo " Backup created: $BACKUP_DIR"
# Clean old files from templates
echo ""
echo "[2/5] Cleaning old template files..."
sudo rm -f "$WEB_DIR/templates/base.html"
sudo rm -f "$WEB_DIR/templates/clients.html"
sudo rm -f "$WEB_DIR/templates/dashboard.html"
sudo rm -f "$WEB_DIR/templates/minimal_client.html"
sudo rm -f "$WEB_DIR/templates/minimal_client.html.backup"
sudo rm -f "$WEB_DIR/templates/settings.html"
sudo rm -f "$WEB_DIR/templates/updates.html"
sudo rm -f "$WEB_DIR/templates/index_v14.html"
echo " Old templates removed"
# Clean old files from static
echo ""
echo "[3/5] Cleaning old static files..."
sudo rm -f "$WEB_DIR/static/clients.css"
sudo rm -f "$WEB_DIR/static/clients.js"
sudo rm -f "$WEB_DIR/static/dashboard.css"
sudo rm -f "$WEB_DIR/static/dashboard.js"
sudo rm -f "$WEB_DIR/static/minimal_client.css"
sudo rm -f "$WEB_DIR/static/minimal_client.css.backup"
sudo rm -f "$WEB_DIR/static/minimal_client.js"
sudo rm -f "$WEB_DIR/static/minimal_client.js.backup2"
sudo rm -f "$WEB_DIR/static/settings.css"
sudo rm -f "$WEB_DIR/static/sidebar.js"
sudo rm -f "$WEB_DIR/static/sidebar.css"
sudo rm -f "$WEB_DIR/static/updates.css"
sudo rm -f "$WEB_DIR/static/updates.js"
sudo rm -f "$WEB_DIR/static/performance-config.css"
sudo rm -f "$WEB_DIR/static/script_v14.js"
sudo rm -f "$WEB_DIR/static/script.js"
echo " Old static files removed"
# Clean old Python files
echo ""
echo "[4/5] Cleaning old Python files..."
sudo rm -f "$WEB_DIR/app.py"
sudo rm -f "$WEB_DIR/app.py.backup-"
echo " Old Python files removed"
# Deploy new files from /tmp/
echo ""
echo "[5/5] Deploying new files..."
if [ -f /tmp/deploy_v15/app_v14.py ]; then
# Templates
sudo cp /tmp/deploy_v15/index_v15.html "$WEB_DIR/templates/"
sudo cp /tmp/deploy_v15/login.html "$WEB_DIR/templates/"
sudo cp /tmp/deploy_v15/client_generator.html "$WEB_DIR/templates/"
# Static files
sudo cp /tmp/deploy_v15/style.css "$WEB_DIR/static/"
sudo cp /tmp/deploy_v15/script_v15.js "$WEB_DIR/static/"
sudo cp /tmp/deploy_v15/client_generator.css "$WEB_DIR/static/"
sudo cp /tmp/deploy_v15/client_generator.js "$WEB_DIR/static/"
# Python app
sudo cp /tmp/deploy_v15/app_v14.py "$WEB_DIR/"
sudo cp /tmp/deploy_v15/auth.py "$WEB_DIR/"
sudo cp /tmp/deploy_v15/client_generator_module.py "$WEB_DIR/"
echo " New files deployed"
else
echo " ERROR: Deploy files not found in /tmp/deploy_v15/"
echo " Please upload files first"
exit 1
fi
# Set permissions
echo ""
echo "Setting permissions..."
# Use current user as owner (parameterized for different environments)
DEPLOY_USER="${DEPLOY_USER:-$(whoami)}"
sudo chown -R "$DEPLOY_USER:$DEPLOY_USER" "$WEB_DIR"
sudo chmod -R 755 "$WEB_DIR"
# Restart service
echo ""
echo "Restarting BetterDesk service..."
sudo systemctl restart betterdesk
sleep 2
# Show status
echo ""
echo "========================================"
echo " Deployment Complete!"
echo "========================================"
echo ""
echo "Files deployed:"
ls -la "$WEB_DIR/templates/" 2>/dev/null | grep -E "\.html$" | awk '{print " • " $NF}'
echo ""
ls -la "$WEB_DIR/static/" 2>/dev/null | grep -E "\.(css|js)$" | awk '{print " • " $NF}'
echo ""
echo "Service status:"
sudo systemctl status betterdesk --no-pager | head -5
echo ""
echo "Backup location: $BACKUP_DIR"
echo ""
+37
View File
@@ -26,6 +26,43 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', os.urandom(32))
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Manual CSRF for API
# Load version for cache busting
VERSION_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'VERSION')
APP_VERSION = 'v1.5.0' # Default
try:
if os.path.exists(VERSION_FILE):
with open(VERSION_FILE, 'r') as f:
version_line = f.readline().strip()
if version_line:
APP_VERSION = version_line
except:
pass
# Context processor to inject version into all templates
@app.context_processor
def inject_version():
return {'app_version': APP_VERSION}
# Cache control for static files
@app.after_request
def add_cache_headers(response):
"""Add cache control headers to responses."""
if request.path.startswith('/static/'):
# Static files: cache for 1 year if versioned, otherwise 5 minutes
if 'v=' in request.query_string.decode():
response.cache_control.max_age = 31536000 # 1 year
response.cache_control.public = True
else:
response.cache_control.max_age = 300 # 5 minutes
elif request.path == '/' or request.path.endswith('.html'):
# HTML pages: no cache (always get fresh)
response.cache_control.no_cache = True
response.cache_control.no_store = True
response.cache_control.must_revalidate = True
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# Initialize CSRF protection
csrf = CSRFProtect()
csrf.init_app(app)
View File
+16 -12
View File
@@ -13,11 +13,25 @@ document.addEventListener('DOMContentLoaded', function() {
function checkAuthentication() {
const token = localStorage.getItem('authToken');
const username = localStorage.getItem('username');
const role = localStorage.getItem('role');
if (!token) {
window.location.href = '/login';
return;
}
// Setup user info in sidebar
document.getElementById('sidebarUsername').textContent = username || 'User';
document.getElementById('sidebarRole').textContent = role || 'viewer';
// Show admin-only sections
if (role === 'admin') {
document.querySelectorAll('.admin-only').forEach(el => {
el.style.display = '';
});
}
// Verify token
fetch('/api/auth/verify', {
headers: {
@@ -27,20 +41,10 @@ function checkAuthentication() {
.then(response => {
if (!response.ok) {
localStorage.removeItem('authToken');
localStorage.removeItem('username');
localStorage.removeItem('role');
window.location.href = '/login';
}
return response.json();
})
.then(data => {
if (data.user) {
document.getElementById('sidebarUsername').textContent = data.user.username || 'User';
const roleNames = {
'admin': 'Administrator',
'operator': 'Operator',
'viewer': 'Viewer'
};
document.getElementById('sidebarUserRole').textContent = roleNames[data.user.role] || data.user.role;
}
})
.catch(() => {
window.location.href = '/login';
-168
View File
@@ -1,168 +0,0 @@
/**
* Performance Configuration for BetterDesk Console
*
* Include this file AFTER style.css in your HTML to apply additional optimizations
* Include this file AFTER style.css in your HTML to apply additional optimizations
*
* <link rel="stylesheet" href="/static/performance-config.css">
*/
/* ============================================
PROFILE 1: MAXIMUM PERFORMANCE (WEAK GPU)
PROFILE 1: MAXIMUM PERFORMANCE (WEAK GPU)
============================================ */
/* Uncomment the section below for maximum performance */
/* Uncomment section below for maximum performance */
/*
* {
animation: none !important;
transition: none !important;
}
.bg-gradient {
display: none !important;
}
.glass-effect,
.stat-card,
.btn,
.modal {
box-shadow: none !important;
}
.btn:hover,
.stat-card:hover,
.action-btn:hover {
transform: none !important;
}
*/
/* ============================================
PROFILE 2: BALANCED (MEDIUM GPU)
PROFILE 2: BALANCED (MEDIUM GPU)
============================================ */
/* Active by default - minimal animations */
/* Active by default - minimal animations */
/* Shorter transition times */
/* Shorter transition times */
.btn,
.stat-card,
.modal,
.action-btn {
transition-duration: 0.15s !important;
}
/* Zmniejszone cienie */
/* Reduced shadows */
.glass-effect,
.stat-card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
}
/* Disable gradient backgrounds in buttons */
/* Disable gradient backgrounds in buttons */
.btn-primary {
background: var(--primary-color) !important;
}
.btn-danger {
background: var(--danger-color) !important;
}
/* ============================================
PROFIL 3: PREMIUM (MOCNE GPU)
PROFILE 3: PREMIUM (STRONG GPU)
============================================ */
/* Uncomment below for full visual effects */
/* Uncomment below for full visual effects */
/*
.glass-effect {
backdrop-filter: blur(8px) !important;
-webkit-backdrop-filter: blur(8px) !important;
}
.sidebar {
background: rgba(255, 255, 255, 0.05) !important;
backdrop-filter: blur(15px) !important;
-webkit-backdrop-filter: blur(15px) !important;
}
.modal {
background: rgba(0, 0, 0, 0.7) !important;
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
}
.bg-gradient {
width: 150% !important;
height: 150% !important;
animation: gradientShiftSmooth 15s ease-in-out infinite !important;
}
@keyframes gradientShiftSmooth {
0%, 100% {
transform: translate(-16%, -16%) scale(1.1);
opacity: 1;
}
50% {
transform: translate(-20%, -20%) scale(1.15);
opacity: 0.98;
}
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
}
.btn-danger {
background: linear-gradient(135deg, var(--danger-color), #ff6b6b) !important;
}
*/
/* ============================================
PROFILE 4: ULTRA ECONOMICAL (VERY WEAK GPU)
PROFILE 4: ULTRA SAVING (VERY WEAK GPU)
============================================ */
/* Uncomment below only in case of extreme problems */
/* Uncomment below only for extreme performance issues */
/*
* {
animation: none !important;
transition: none !important;
transform: none !important;
box-shadow: none !important;
}
.bg-gradient,
.stat-icon,
.btn::before,
.btn::after {
display: none !important;
}
.glass-effect,
.sidebar,
.modal {
background: rgba(30, 30, 30, 0.98) !important;
}
.btn:hover,
.stat-card:hover {
opacity: 0.9 !important;
}
*/
/* ============================================
CUSTOM TWEAKS
============================================ */
/* Add your own customizations below */
/* Add your custom tweaks below */
-519
View File
@@ -1,519 +0,0 @@
// Global variables
let allDevices = [];
let currentDeviceId = null;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
loadDevices();
loadStats();
// Auto-refresh every 2 seconds
setInterval(() => {
loadDevices();
loadStats();
}, 2000);
});
// Load devices from API
async function loadDevices() {
try {
const response = await fetch('/api/devices');
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() {
try {
const response = await fetch('/api/stats');
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;
document.querySelector('#totalDevices span').textContent = total;
document.querySelector('#activeDevices span').textContent = active;
}
// Render devices table
function renderDevices(devices) {
const tbody = document.getElementById('devicesTableBody');
if (devices.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="loading">
<i class="fas fa-inbox"></i>
<span>No devices found</span>
</td>
</tr>
`;
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);"' : '';
return `
<tr ${rowClass}>
<td>
<strong>${escapeHtml(device.id)}</strong>
${isBanned ? '<br><span class="status-badge" style="background: #e74c3c; font-size: 0.75rem; margin-top: 4px;"><i class="fas fa-ban"></i> BANNED</span>' : ''}
</td>
<td>${escapeHtml(device.note) || '<span style="color: var(--text-secondary);">No note</span>'}</td>
<td>
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
<i class="fas fa-circle"></i>
${device.online ? 'Online' : 'Offline'}
</span>
</td>
<td>${formatDate(device.created_at)}</td>
<td>
<button class="action-btn connect" onclick="connectDevice('${escapeHtml(device.id)}')" title="Connect" ${isBanned ? 'disabled style="opacity: 0.3; cursor: not-allowed;"' : ''}>
<i class="fas fa-plug"></i>
</button>
<button class="action-btn details" onclick="showDetails('${escapeHtml(device.id)}')" title="Details">
<i class="fas fa-info-circle"></i>
</button>
<button class="action-btn edit" onclick="editDevice('${escapeHtml(device.id)}')" title="Edit">
<i class="fas fa-edit"></i>
</button>
${isBanned ?
`<button class="action-btn" onclick="unbanDevice('${escapeHtml(device.id)}')" title="Unban" style="background: #27ae60;">
<i class="fas fa-check-circle"></i>
</button>` :
`<button class="action-btn" onclick="banDevice('${escapeHtml(device.id)}')" title="Ban" style="background: #e74c3c;">
<i class="fas fa-ban"></i>
</button>`
}
<button class="action-btn delete" onclick="deleteDevice('${escapeHtml(device.id)}')" title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
`}).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 = `
<div class="detail-item">
<div class="detail-label">ID:</div>
<div class="detail-value">${escapeHtml(device.id)}</div>
</div>
<div class="detail-item">
<div class="detail-label">GUID:</div>
<div class="detail-value">${escapeHtml(device.guid) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">UUID:</div>
<div class="detail-value">${escapeHtml(device.uuid) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Public Key:</div>
<div class="detail-value">${escapeHtml(device.pk) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">User:</div>
<div class="detail-value">${escapeHtml(device.user) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Status:</div>
<div class="detail-value">
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
<i class="fas fa-circle"></i>
${device.online ? 'Online' : 'Offline'}
</span>
</div>
</div>
${isBanned ? `
<div class="detail-item" style="background: rgba(231, 76, 60, 0.1); padding: 12px; border-radius: 8px; margin: 12px 0;">
<div class="detail-label" style="color: #e74c3c; font-weight: bold;"><i class="fas fa-ban"></i> BAN STATUS:</div>
<div class="detail-value" style="color: #e74c3c; font-weight: bold;">BANNED</div>
</div>
<div class="detail-item">
<div class="detail-label">Banned At:</div>
<div class="detail-value">${device.banned_at ? formatDate(device.banned_at) : 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Banned By:</div>
<div class="detail-value">${escapeHtml(device.banned_by) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Ban Reason:</div>
<div class="detail-value">${escapeHtml(device.ban_reason) || 'No reason provided'}</div>
</div>
` : ''}
<div class="detail-item">
<div class="detail-label">Note:</div>
<div class="detail-value">${escapeHtml(device.note) || 'No note'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Created:</div>
<div class="detail-value">${formatDate(device.created_at)}</div>
</div>
<div class="detail-item">
<div class="detail-label">Info:</div>
<div class="detail-value">${escapeHtml(device.info) || 'N/A'}</div>
</div>
`;
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() {
const newId = document.getElementById('editNewId').value.trim();
const note = document.getElementById('editNote').value.trim();
// Validation
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;
}
// Warning if changing ID
if (newId && newId !== currentDeviceId) {
if (!confirm(`⚠️ WARNING: Changing device ID!\n\nChanging the device ID may cause access issues.\nThe device will need to re-register with the new ID.\n\nOld ID: ${currentDeviceId}\nNew ID: ${newId}\n\nAre you sure you want to continue?`)) {
return;
}
}
const data = { note };
if (newId) {
data.new_id = newId;
}
try {
const response = await fetch(`/api/device/${currentDeviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
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() {
// Additional confirmation with explicit warning
const device = allDevices.find(d => d.id === currentDeviceId);
const hasNote = device && device.note;
const confirmMsg = `⚠️ DELETE DEVICE CONFIRMATION\n\n` +
`Device ID: ${currentDeviceId}\n` +
(hasNote ? `Note: ${device.note}\n` : '') +
`Status: ${device && device.online ? 'Online' : 'Offline'}\n\n` +
`This will remove the device from the console.\n` +
`The device can re-register by connecting again.\n\n` +
`Are you absolutely sure?`;
if (!confirm(confirmMsg)) {
return;
}
try {
const response = await fetch(`/api/device/${currentDeviceId}`, {
method: 'DELETE'
});
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');
}
}
// Show public key modal
function showPublicKey() {
openModal('keyModal');
}
// 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');
}
function closeKeyModal() {
closeModal('keyModal');
}
// 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');
// Update icon based on type
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) {
const reason = prompt(`⚠️ BAN DEVICE: ${deviceId}\n\nEnter ban reason (optional):`);
// User cancelled
if (reason === null) {
return;
}
const confirmMsg = `Are you sure you want to BAN device ${deviceId}?\n\n` +
`This will:\n` +
`- Prevent the device from connecting\n` +
`- Block all remote access attempts\n` +
`- Mark device as banned in the console\n\n` +
`Ban reason: ${reason || '(no reason provided)'}\n\n` +
`Continue?`;
if (!confirm(confirmMsg)) {
return;
}
try {
const response = await fetch(`/api/device/${deviceId}/ban`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
reason: reason || '',
banned_by: 'admin'
})
});
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) {
const confirmMsg = `✓ UNBAN DEVICE: ${deviceId}\n\n` +
`This will:\n` +
`- Allow the device to connect again\n` +
`- Remove all ban restrictions\n` +
`- Clear ban information\n\n` +
`Are you sure?`;
if (!confirm(confirmMsg)) {
return;
}
try {
const response = await fetch(`/api/device/${deviceId}/unban`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
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');
}
}
-924
View File
@@ -1,924 +0,0 @@
// 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 = `
<tr>
<td colspan="5" class="loading">
<i class="fas fa-inbox"></i>
<span>No devices found</span>
</td>
</tr>
`;
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 `
<tr ${rowClass}>
<td>
<strong>${escapeHtml(device.id)}</strong>
${isBanned ? '<br><span class="status-badge" style="background: #e74c3c; font-size: 0.75rem; margin-top: 4px;"><i class="fas fa-ban"></i> BANNED</span>' : ''}
</td>
<td>${escapeHtml(device.note) || '<span style="color: var(--text-secondary);">No note</span>'}</td>
<td>
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
<i class="fas fa-circle"></i>
${device.online ? 'Online' : 'Offline'}
</span>
</td>
<td>${formatDate(device.created_at)}</td>
<td>
<button class="action-btn connect" onclick="connectDevice('${escapeHtml(device.id)}')" title="Connect" ${isBanned ? 'disabled style="opacity: 0.3; cursor: not-allowed;"' : ''}>
<i class="fas fa-plug"></i>
</button>
<button class="action-btn details" onclick="showDetails('${escapeHtml(device.id)}')" title="Details">
<i class="fas fa-info-circle"></i>
</button>
${canEdit ? `
<button class="action-btn edit" onclick="editDevice('${escapeHtml(device.id)}')" title="Edit">
<i class="fas fa-edit"></i>
</button>
` : ''}
${canBan ? (isBanned ?
`<button class="action-btn" onclick="unbanDevice('${escapeHtml(device.id)}')" title="Unban" style="background: #27ae60;">
<i class="fas fa-check-circle"></i>
</button>` :
`<button class="action-btn" onclick="banDevice('${escapeHtml(device.id)}')" title="Ban" style="background: #e74c3c;">
<i class="fas fa-ban"></i>
</button>`
) : ''}
${canEdit ? `
<button class="action-btn delete" onclick="deleteDevice('${escapeHtml(device.id)}')" title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
` : ''}
</td>
</tr>
`}).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 = `
<div class="detail-item">
<div class="detail-label">ID:</div>
<div class="detail-value">${escapeHtml(device.id)}</div>
</div>
<div class="detail-item">
<div class="detail-label">GUID:</div>
<div class="detail-value">${escapeHtml(device.guid) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">UUID:</div>
<div class="detail-value">${escapeHtml(device.uuid) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Public Key:</div>
<div class="detail-value">${escapeHtml(device.pk) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">User:</div>
<div class="detail-value">${escapeHtml(device.user) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Status:</div>
<div class="detail-value">
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
<i class="fas fa-circle"></i>
${device.online ? 'Online' : 'Offline'}
</span>
</div>
</div>
${isBanned ? `
<div class="detail-item" style="background: rgba(231, 76, 60, 0.1); padding: 12px; border-radius: 8px; margin: 12px 0;">
<div class="detail-label" style="color: #e74c3c; font-weight: bold;"><i class="fas fa-ban"></i> BAN STATUS:</div>
<div class="detail-value" style="color: #e74c3c; font-weight: bold;">BANNED</div>
</div>
<div class="detail-item">
<div class="detail-label">Banned At:</div>
<div class="detail-value">${device.banned_at ? formatDate(device.banned_at) : 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Banned By:</div>
<div class="detail-value">${escapeHtml(device.banned_by) || 'N/A'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Ban Reason:</div>
<div class="detail-value">${escapeHtml(device.ban_reason) || 'No reason provided'}</div>
</div>
` : ''}
<div class="detail-item">
<div class="detail-label">Note:</div>
<div class="detail-value">${escapeHtml(device.note) || 'No note'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Created:</div>
<div class="detail-value">${formatDate(device.created_at)}</div>
</div>
<div class="detail-item">
<div class="detail-label">Info:</div>
<div class="detail-value">${escapeHtml(device.info) || 'N/A'}</div>
</div>
`;
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 = '<tr><td colspan="6" class="no-data">No users found</td></tr>';
return;
}
tbody.innerHTML = users.map(user => {
const statusBadge = user.is_active ?
'<span class="badge badge-success">Active</span>' :
'<span class="badge badge-danger">Inactive</span>';
const roleColor = user.role === 'admin' ? 'danger' :
user.role === 'operator' ? 'warning' : 'info';
const roleBadge = `<span class="badge badge-${roleColor}">${user.role}</span>`;
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 `
<tr>
<td>${user.username}</td>
<td>${roleBadge}</td>
<td>${statusBadge}</td>
<td>${createdDate}</td>
<td>${lastLogin}</td>
<td class="actions-column">
<button class="btn-icon" onclick="showEditUserModal(${user.id}, '${user.username}', '${user.role}')" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn-icon danger" onclick="showDeleteUserModal(${user.id}, '${user.username}')" title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
${user.is_active ?
`<button class="btn-icon" onclick="toggleUserStatus(${user.id}, false)" title="Deactivate">
<i class="fas fa-user-slash"></i>
</button>` :
`<button class="btn-icon success" onclick="toggleUserStatus(${user.id}, true)" title="Activate">
<i class="fas fa-user-check"></i>
</button>`
}
</td>
</tr>
`;
}).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');
}
}
-412
View File
@@ -1,412 +0,0 @@
/* 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 - Zoptymalizowany dla GPU */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--sidebar-width);
background: rgba(20, 20, 20, 0.95);
/* backdrop-filter disabled for better performance */
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(20, 20, 20, 0.95);
/* backdrop-filter disabled for better performance */
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: translate3d(0, 10px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 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;
}
-209
View File
@@ -1,209 +0,0 @@
/* 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() {
alert('Change password functionality coming soon!');
}
// Export functions for use in other scripts
window.sidebarFunctions = {
showPage,
logout,
loadUserInfo,
updateMenuVisibility
};
+165 -54
View File
@@ -3,87 +3,199 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>BetterDesk Console - Client Generator</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='sidebar.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='client_generator.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=app_version) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='client_generator.css', v=app_version) }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Sidebar Styles - Same as Dashboard */
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
background: rgba(15, 23, 42, 0.98);
border-right: 1px solid rgba(255, 255, 255, 0.1);
padding: 20px 0;
z-index: 1000;
overflow-y: auto;
}
.sidebar-header {
padding: 0 20px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 20px;
}
.sidebar-logo {
display: flex;
align-items: center;
gap: 12px;
color: #fff;
font-size: 1.2rem;
font-weight: 600;
}
.sidebar-logo i {
font-size: 1.5rem;
color: var(--primary-color, #667eea);
}
.sidebar-user {
margin-top: 15px;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
font-size: 0.85rem;
}
.sidebar-user-name {
color: #fff;
font-weight: 500;
margin-bottom: 4px;
}
.sidebar-user-role {
color: rgba(255, 255, 255, 0.6);
font-size: 0.75rem;
text-transform: uppercase;
}
.sidebar-menu {
padding: 0 10px;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
margin-bottom: 4px;
color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
}
.sidebar-item:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.sidebar-item.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.sidebar-item i {
width: 20px;
text-align: center;
}
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 15px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(15, 23, 42, 0.95);
}
.logout-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
border: none;
border-radius: 8px;
cursor: pointer;
width: 100%;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.logout-btn:hover {
background: rgba(231, 76, 60, 0.2);
}
/* Main Content */
.main-content {
margin-left: 260px;
padding: 20px;
min-height: 100vh;
}
</style>
</head>
<body class="has-sidebar">
<body>
<!-- Background gradient -->
<div class="bg-gradient"></div>
<!-- Sidebar -->
<aside class="sidebar glass-effect" id="sidebar">
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-brand">
<i class="fas fa-desktop"></i>
<span class="brand-text">BetterDesk</span>
<div class="sidebar-logo">
<i class="fas fa-shield-halved"></i>
<span>BetterDesk v1.5</span>
</div>
<div class="sidebar-user">
<div class="sidebar-user-name" id="sidebarUsername">Loading...</div>
<div class="sidebar-user-role" id="sidebarRole">...</div>
</div>
</div>
<div class="sidebar-user">
<div class="user-avatar">
<i class="fas fa-user-circle"></i>
</div>
<div class="user-info">
<div class="user-name" id="sidebarUsername">Admin</div>
<div class="user-role" id="sidebarUserRole">Administrator</div>
</div>
</div>
<nav class="sidebar-menu">
<a href="/" class="menu-item">
<i class="fas fa-gauge"></i>
<div class="sidebar-menu">
<a class="sidebar-item" href="/">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
<a href="#" class="menu-item" data-page="users" id="menuUsers" style="display: none;">
<i class="fas fa-users"></i>
<span>User Management</span>
</a>
<a href="#" class="menu-item" data-page="audit" id="menuAudit" style="display: none;">
<i class="fas fa-clipboard-list"></i>
<span>Audit Log</span>
</a>
<a href="/client-generator" class="menu-item active">
<a class="sidebar-item active" href="/client-generator">
<i class="fas fa-wrench"></i>
<span>Client Generator</span>
</a>
<a href="#" class="menu-item" data-page="settings" id="menuSettings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</a>
<a href="#" class="menu-item" data-page="key" id="menuKey">
<a class="sidebar-item" href="/?section=publickey">
<i class="fas fa-key"></i>
<span>Public Key</span>
</a>
<a href="#" class="menu-item" data-page="about">
<a class="sidebar-item" href="/?section=settings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</a>
<a class="sidebar-item admin-only" href="/?section=users" style="display: none;">
<i class="fas fa-users-cog"></i>
<span>User Management</span>
</a>
<a class="sidebar-item" href="/?section=about">
<i class="fas fa-info-circle"></i>
<span>About</span>
</a>
</nav>
</div>
<div class="sidebar-footer">
<button class="menu-item logout-btn" onclick="logout()">
<button class="logout-btn" onclick="logout()">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
</aside>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Top Navigation -->
<nav class="top-navbar glass-effect">
<div class="nav-content">
<button class="mobile-menu-toggle" id="mobileMenuToggle">
<i class="fas fa-bars"></i>
</button>
<h1 class="page-title">
<i class="fas fa-wrench"></i> RustDesk Custom Client Builder
</h1>
</div>
</nav>
<!-- Page Header -->
<div style="margin-bottom: 30px;">
<h1 style="color: #fff; font-size: 2rem; margin-bottom: 10px; display: flex; align-items: center; gap: 15px;">
<i class="fas fa-wrench" style="color: var(--primary-color, #667eea);"></i>
RustDesk Custom Client Builder
</h1>
<p style="color: rgba(255, 255, 255, 0.6); margin: 0;">Generate custom RustDesk clients with your server configuration</p>
</div>
<!-- Content Container -->
<div class="content-container">
@@ -541,7 +653,6 @@
</div>
</div>
<script src="{{ url_for('static', filename='sidebar.js') }}"></script>
<script src="{{ url_for('static', filename='client_generator.js') }}"></script>
<script src="{{ url_for('static', filename='client_generator.js', v=app_version) }}"></script>
</body>
</html>
-536
View File
@@ -1,536 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BetterDesk Console - Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='sidebar.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="has-sidebar">
<!-- Background gradient -->
<div class="bg-gradient"></div>
<!-- Sidebar -->
<aside class="sidebar glass-effect" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-brand">
<i class="fas fa-desktop"></i>
<span class="brand-text">BetterDesk</span>
</div>
</div>
<div class="sidebar-user">
<div class="user-avatar">
<i class="fas fa-user-circle"></i>
</div>
<div class="user-info">
<div class="user-name" id="sidebarUsername">Admin</div>
<div class="user-role" id="sidebarUserRole">Administrator</div>
</div>
</div>
<nav class="sidebar-menu">
<a href="#" class="menu-item active" data-page="dashboard">
<i class="fas fa-gauge"></i>
<span>Dashboard</span>
</a>
<a href="#" class="menu-item" data-page="users" id="menuUsers" style="display: none;">
<i class="fas fa-users"></i>
<span>User Management</span>
</a>
<a href="#" class="menu-item" data-page="audit" id="menuAudit" style="display: none;">
<i class="fas fa-clipboard-list"></i>
<span>Audit Log</span>
</a>
<a href="#" class="menu-item" data-page="settings" id="menuSettings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</a>
<a href="#" class="menu-item" data-page="key" id="menuKey">
<i class="fas fa-key"></i>
<span>Public Key</span>
</a>
<a href="#" class="menu-item" data-page="about">
<i class="fas fa-info-circle"></i>
<span>About</span>
</a>
</nav>
<div class="sidebar-footer">
<button class="menu-item logout-btn" onclick="logout()">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
</aside>
<!-- Main Content -->
<div class="main-content">
<!-- Top Navigation -->
<nav class="top-navbar glass-effect">
<div class="nav-content">
<button class="mobile-menu-toggle" id="mobileMenuToggle">
<i class="fas fa-bars"></i>
</button>
<h1 class="page-title" id="pageTitle">Device Management</h1>
<div class="nav-stats">
<div class="stat-badge" id="topTotalDevices">
<i class="fas fa-server"></i>
<span>0</span>
</div>
<div class="stat-badge active" id="topActiveDevices">
<i class="fas fa-circle-check"></i>
<span>0</span>
</div>
</div>
</div>
</nav>
<!-- Content Container -->
<div class="content-container">
<!-- Dashboard Page -->
<div class="page-content active" id="dashboardPage">
<!-- Action Buttons -->
<div class="action-bar">
<button class="btn btn-primary" onclick="refreshDevices()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
<!-- Stats Cards -->
<div class="stats-grid fade-in">
<div class="stat-card glass-effect">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="fas fa-server"></i>
</div>
<div class="stat-info">
<div class="stat-label">Total Devices</div>
<div class="stat-value" id="statTotal">0</div>
</div>
</div>
<div class="stat-card glass-effect">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<i class="fas fa-circle-check"></i>
</div>
<div class="stat-info">
<div class="stat-label">Active</div>
<div class="stat-value" id="statActive">0</div>
</div>
</div>
<div class="stat-card glass-effect">
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<i class="fas fa-circle-xmark"></i>
</div>
<div class="stat-info">
<div class="stat-label">Inactive</div>
<div class="stat-value" id="statInactive">0</div>
</div>
</div>
<div class="stat-card glass-effect">
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<i class="fas fa-ban"></i>
</div>
<div class="stat-info">
<div class="stat-label">Banned</div>
<div class="stat-value" id="statBanned">0</div>
</div>
</div>
<div class="stat-card glass-effect">
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
<i class="fas fa-sticky-note"></i>
</div>
<div class="stat-info">
<div class="stat-label">With Notes</div>
<div class="stat-value" id="statNotes">0</div>
</div>
</div>
</div>
<!-- Devices Table -->
<div class="table-container glass-effect">
<div class="table-header">
<h2><i class="fas fa-list"></i> Devices</h2>
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Search devices..." onkeyup="filterDevices()">
</div>
</div>
<div class="table-wrapper">
<table class="devices-table" id="devicesTable">
<thead>
<tr>
<th>ID</th>
<th>Note</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="devicesTableBody">
<tr>
<td colspan="5" class="loading">
<div class="spinner"></div>
<span>Loading devices...</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Other pages will be added here -->
<div class="page-content" id="devicesPage">
<p>Devices management page (to be implemented)</p>
</div>
<div class="page-content" id="usersPage">
<div class="users-container glass-effect">
<h2><i class="fas fa-users"></i> User Management</h2>
<div class="action-bar" style="margin-bottom: 20px;">
<button class="btn btn-primary" onclick="showAddUserModal()">
<i class="fas fa-user-plus"></i> Add New User
</button>
</div>
<div class="table-wrapper">
<table class="devices-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
<th>Last Login</th>
<th class="actions-column">Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr>
<td colspan="6" class="no-data">Loading users...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="page-content" id="auditPage">
<div class="audit-container glass-effect">
<h2><i class="fas fa-clipboard-list"></i> Audit Log</h2>
<p>Audit log page (to be implemented)</p>
</div>
</div>
<div class="page-content" id="settingsPage">
<div class="settings-container glass-effect">
<h2><i class="fas fa-cog"></i> Settings</h2>
<div class="settings-section">
<h3>Account Security</h3>
<p>Change your account password to keep your account secure.</p>
<button class="btn btn-primary" onclick="showChangePasswordModal()">
<i class="fas fa-lock"></i> Change Password
</button>
</div>
</div>
</div>
<div class="page-content" id="keyPage">
<div class="key-container glass-effect">
<h2><i class="fas fa-key"></i> RustDesk Public Key</h2>
<p style="margin-bottom: 20px;">For security reasons, please verify your password to view the public key.</p>
<div class="form-group" id="keyPasswordForm">
<label for="keyPassword">Enter Your Password</label>
<input type="password" id="keyPassword" class="form-control" placeholder="Your password">
<button class="btn btn-primary" onclick="verifyPasswordForKey()" style="margin-top: 15px;">
<i class="fas fa-unlock"></i> Show Public Key
</button>
</div>
<div id="keyDisplay" style="display: none;">
<div class="key-display">
<code id="publicKeyDisplay"></code>
</div>
<button class="btn btn-primary" onclick="copyPublicKey()">
<i class="fas fa-copy"></i> Copy to Clipboard
</button>
</div>
</div>
</div>
<div class="page-content" id="aboutPage">
<div class="about-container glass-effect">
<h2><i class="fas fa-info-circle"></i> About BetterDesk Console</h2>
<div class="about-section">
<h3>Version Information</h3>
<p><strong>Version:</strong> 1.4.0</p>
<p><strong>Build:</strong> v9</p>
</div>
<div class="about-section">
<h3>Features</h3>
<ul>
<li><i class="fas fa-check-circle"></i> Real-time device monitoring and management</li>
<li><i class="fas fa-check-circle"></i> Bidirectional ban enforcement (source + target)</li>
<li><i class="fas fa-check-circle"></i> User authentication & role-based access control</li>
<li><i class="fas fa-check-circle"></i> Comprehensive audit logging</li>
<li><i class="fas fa-check-circle"></i> HTTP API for device status</li>
<li><i class="fas fa-check-circle"></i> Modern glassmorphism UI design</li>
</ul>
</div>
<div class="about-section">
<h3>Built With Open Source</h3>
<p>This project is built using the following open source technologies:</p>
<ul>
<li><strong>RustDesk</strong> - Open source remote desktop software (AGPL-3.0)</li>
<li><strong>Flask</strong> - Python web framework (BSD-3-Clause)</li>
<li><strong>SQLite</strong> - Embedded database (Public Domain)</li>
<li><strong>bcrypt</strong> - Password hashing library (Apache-2.0)</li>
<li><strong>Font Awesome</strong> - Icon library (Font Awesome Free License)</li>
</ul>
</div>
<div class="about-section">
<h3>Repository & Documentation</h3>
<p><strong>GitHub Repository:</strong></p>
<p>
<a href="https://github.com/UNITRONIX/Rustdesk-FreeConsole" target="_blank" class="github-link">
<i class="fab fa-github"></i> github.com/UNITRONIX/Rustdesk-FreeConsole
</a>
</p>
<p style="margin-top: 15px;">
Find documentation, installation guides, and source code in the repository.
</p>
</div>
<div class="about-section">
<h3>License</h3>
<p>MIT License - Free to use and modify</p>
</div>
<div class="about-section">
<h3>Author</h3>
<p>Developed by <strong>UNITRONIX</strong></p>
<p>With contributions from the open source community</p>
</div>
</div>
</div>
</div>
</div>
<!-- Modals (same as before) -->
<div id="editModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-edit"></i> Edit Device</h2>
<button class="modal-close" onclick="closeEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="editDeviceId">Device ID</label>
<input type="text" id="editDeviceId" readonly class="form-control">
</div>
<div class="form-group">
<label for="editNewId">New Device ID (optional)</label>
<input type="text" id="editNewId" class="form-control" placeholder="Leave empty to keep current ID">
</div>
<div class="form-group">
<label for="editNote">Note</label>
<textarea id="editNote" class="form-control" rows="3" placeholder="Enter device note..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-primary" onclick="saveDevice()">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
<div id="deleteModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-trash-alt"></i> Confirm Delete</h2>
<button class="modal-close" onclick="closeDeleteModal()">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete device <strong id="deleteDeviceId"></strong>?</p>
<p class="warning-text">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeDeleteModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-danger" onclick="confirmDelete()">
<i class="fas fa-trash-alt"></i> Delete
</button>
</div>
</div>
</div>
<div id="detailsModal" class="modal">
<div class="modal-content glass-effect modal-large">
<div class="modal-header">
<h2><i class="fas fa-info-circle"></i> Device Details</h2>
<button class="modal-close" onclick="closeDetailsModal()">&times;</button>
</div>
<div class="modal-body">
<div id="detailsContent" class="details-grid"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeDetailsModal()">
<i class="fas fa-times"></i> Close
</button>
</div>
</div>
</div>
<!-- Change Password Modal -->
<div id="changePasswordModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-lock"></i> Change Password</h2>
<button class="modal-close" onclick="closeChangePasswordModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="currentPassword">Current Password</label>
<input type="password" id="currentPassword" class="form-control" placeholder="Enter current password">
</div>
<div class="form-group">
<label for="newPassword">New Password</label>
<input type="password" id="newPassword" class="form-control" placeholder="Enter new password (min 6 characters)">
</div>
<div class="form-group">
<label for="confirmPassword">Confirm New Password</label>
<input type="password" id="confirmPassword" class="form-control" placeholder="Confirm new password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeChangePasswordModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-primary" onclick="confirmChangePassword()">
<i class="fas fa-save"></i> Change Password
</button>
</div>
</div>
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-user-plus"></i> Add New User</h2>
<button class="modal-close" onclick="closeAddUserModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="newUsername">Username</label>
<input type="text" id="newUsername" class="form-control" placeholder="Enter username">
</div>
<div class="form-group">
<label for="newUserPassword">Password</label>
<input type="password" id="newUserPassword" class="form-control" placeholder="Enter password (min 6 characters)">
</div>
<div class="form-group">
<label for="newUserRole">Role</label>
<select id="newUserRole" class="form-control">
<option value="viewer">Viewer (Read-only)</option>
<option value="operator">Operator (Can ban/unban devices)</option>
<option value="admin">Administrator (Full access)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddUserModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-primary" onclick="confirmAddUser()">
<i class="fas fa-save"></i> Create User
</button>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div id="editUserModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-user-edit"></i> Edit User</h2>
<button class="modal-close" onclick="closeEditUserModal()">&times;</button>
</div>
<div class="modal-body">
<input type="hidden" id="editUserId">
<div class="form-group">
<label for="editUserUsername">Username</label>
<input type="text" id="editUserUsername" class="form-control" readonly>
</div>
<div class="form-group">
<label for="editUserRole">Role</label>
<select id="editUserRole" class="form-control">
<option value="viewer">Viewer (Read-only)</option>
<option value="operator">Operator (Can ban/unban devices)</option>
<option value="admin">Administrator (Full access)</option>
</select>
</div>
<div class="form-group">
<label for="resetUserPassword">Reset Password (optional)</label>
<input type="password" id="resetUserPassword" class="form-control" placeholder="Leave empty to keep current password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeEditUserModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-primary" onclick="confirmEditUser()">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
<!-- Delete User Modal -->
<div id="deleteUserModal" class="modal">
<div class="modal-content glass-effect">
<div class="modal-header">
<h2><i class="fas fa-user-times"></i> Delete User</h2>
<button class="modal-close" onclick="closeDeleteUserModal()">&times;</button>
</div>
<div class="modal-body">
<input type="hidden" id="deleteUserId">
<p>Are you sure you want to delete user <strong id="deleteUserUsername"></strong>?</p>
<p class="warning-text">This action cannot be undone. All user sessions will be terminated.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeDeleteUserModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button class="btn btn-danger" onclick="confirmDeleteUser()">
<i class="fas fa-trash-alt"></i> Delete User
</button>
</div>
</div>
</div>
<!-- Toast Notification -->
<div id="toast" class="toast glass-effect">
<i class="fas fa-check-circle"></i>
<span id="toastMessage"></span>
</div>
<script src="{{ url_for('static', filename='script_v14.js') }}"></script>
<script src="{{ url_for('static', filename='sidebar.js') }}"></script>
</body>
</html>
+7 -3
View File
@@ -3,8 +3,11 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>BetterDesk Console v1.5</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=app_version) }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Sidebar Styles */
@@ -357,9 +360,10 @@
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
<a class="sidebar-item" href="/client-generator">
<a class="sidebar-item disabled" style="opacity: 0.5; cursor: not-allowed; pointer-events: none;" title="Under Development">
<i class="fas fa-wrench"></i>
<span>Client Generator</span>
<span style="font-size: 0.6rem; background: #ffc107; color: #000; padding: 2px 6px; border-radius: 4px; margin-left: 8px;">DEV</span>
</a>
<a class="sidebar-item" data-section="publickey">
<i class="fas fa-key"></i>
@@ -828,6 +832,6 @@
<span id="toastMessage"></span>
</div>
<script src="{{ url_for('static', filename='script_v15.js') }}"></script>
<script src="{{ url_for('static', filename='script_v15.js', v=app_version) }}"></script>
</body>
</html>
+4 -1
View File
@@ -3,8 +3,11 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Login - BetterDesk Console</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', v=app_version) }}">
<style>
/* Login page specific styles - Dark Theme */
.login-container {