mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
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:
@@ -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
|
||||
@@ -5,7 +5,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 */
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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()">×</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()">×</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()">×</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()">×</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()">×</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()">×</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()">×</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user