| Acțiuni | +Nume Calculator | +Adresa MAC | +Adresa IP | +Status | +
|---|
diff --git a/.env b/.env new file mode 100644 index 0000000..b15941c --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# WOL Manager Environment Configuration +WOL_NETWORK_MODE=bridge +WOL_EXTERNAL_PORT=5000 +FLASK_DEBUG=true \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 1c1bfbb..b515387 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,47 +13,115 @@ This is a Wake-on-LAN (WOL) Manager - a containerized Flask web application for - **Backend**: Flask web application (`app/app.py`) with RESTful API endpoints - **Frontend**: Single-page application with vanilla JavaScript (`app/templates/index.html`) - **Storage**: File-based configuration in `/data/wol-computers.conf` -- **Deployment**: Docker with docker-compose, requires privileged networking +- **Deployment**: Docker with docker compose, requires privileged networking ### Key Components - `WOLManager` class in `app/app.py`: Core logic for computer management, WOL operations, and network scanning - Configuration format: `name|mac|ip` (pipe-separated values in `/data/wol-computers.conf`) - Dependencies: `wakeonlan`, `nmap`, `ping`, `arp` system tools +- **Windows PowerShell scanner**: `scripts/windows-network-scan.ps1` - optimized network scanning from Windows host ## Development Commands **For installation and basic usage, see README.md** ### Docker Development + +**Note**: When running from WSL with Docker Desktop on Windows, use `docker.exe compose`: + ```bash # Development build and run -docker-compose up -d --build +docker compose up -d --build +# From WSL: docker.exe compose up -d --build # View real-time logs -docker-compose logs -f wol-web +docker compose logs -f wol-web +# From WSL: docker.exe compose logs -f wol-web # Shell access to container -docker-compose exec wol-web bash +docker compose exec wol-web bash +# From WSL: docker.exe compose exec wol-web bash + +# Stop containers +docker compose down +# From WSL: docker.exe compose down ``` ### Network Requirements -- Runs on port 8088 (external) → 8080 (internal) +- Runs on port 5000 (configurable via WOL_EXTERNAL_PORT env var) - Requires `NET_ADMIN` and `NET_RAW` capabilities - Uses `network_mode: host` for WOL packet transmission - Must run with `privileged: true` for network operations +## Network Scanning + +**Two scanning modes available:** + +### 1. Container-based scanning (Limited) +- Uses Linux tools: `arp`, `ping`, `nmap` +- Limited in Docker Desktop Windows environment +- Fallback method when Windows scanning unavailable + +### 2. Windows PowerShell scanning (Recommended) +- Script: `scripts/windows-network-scan.ps1` +- Features: + - Automatic local network detection + - Interactive network selection menu + - Parallel ping sweeps with configurable batch sizes + - Hostname resolution via DNS + - MAC address retrieval from ARP table/NetNeighbor + - Real-time progress indication + - Results saved to `/data/network-scan-results.json` + +**Usage patterns:** +```powershell +# Interactive mode with network selection menu +scripts\windows-network-scan.ps1 + +# Direct network specification +scripts\windows-network-scan.ps1 -Network "192.168.1.0/24" + +# Advanced options +scripts\windows-network-scan.ps1 -Network "192.168.100.0/24" -TimeoutMs 500 -BatchSize 20 -Verbose +``` + +**Integration:** +- Web app automatically reads Windows scan results from JSON file +- Results cached for 30 minutes +- App falls back to Linux scanning if Windows results unavailable + ## API Endpoints - `GET /api/computers` - List configured computers with status - `POST /api/wake` - Wake specific computer (`{mac, name, ip}`) - `POST /api/wake-all` - Wake all configured computers - `POST /api/add` - Add new computer (`{name, mac, ip}`) -- `GET /api/scan` - Network scan for devices +- `GET /api/scan` - Network scan for devices (tries Windows results first, then Linux fallback) +- `POST /api/scan/windows` - Trigger Windows PowerShell scan (attempts automatic execution) ## Development Notes - Application uses Romanian language in UI - No authentication/authorization implemented - Configuration persisted in volume-mounted `/data` directory -- Flask runs in debug=False mode in container \ No newline at end of file +- Flask runs in debug=False mode in container + +### Windows Integration Details + +**File Integration:** +- Windows scan results: `/data/network-scan-results.json` +- Results format: `{success, timestamp, networks_scanned, computers[], message}` +- Computer format: `{ip, mac, hostname, status}` +- Results expire after 30 minutes + +**WSL Integration:** +- App attempts automatic PowerShell execution via WSL interop +- Path: `/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` +- Fallback: Manual execution instructions provided when auto-execution fails + +**Scanning Logic in `app.py`:** +- `scan_network()` - Main entry point, tries Windows results first (`app/app.py:164`) +- `try_read_windows_scan_results()` - Reads and validates Windows JSON results (`app/app.py:176`) +- `scan_network_linux()` - Fallback Linux scanning using ARP/ping (`app/app.py:236`) +- Network filtering supported for custom CIDR ranges \ No newline at end of file diff --git a/app/app.py b/app/app.py index 3d820c2..dbad33b 100644 --- a/app/app.py +++ b/app/app.py @@ -95,58 +95,256 @@ class WOLManager: if not re.match(mac_pattern, mac): return {'success': False, 'message': 'MAC address invalid!'} + # Verifică duplicate - încarcă toate calculatoarele existente + computers = self.load_computers() + + # Verifică dacă există deja un calculator cu același MAC + for computer in computers: + if computer['mac'].lower() == mac.lower(): + return {'success': False, 'message': f'Un calculator cu MAC {mac} există deja: {computer["name"]}'} + + # Verifică dacă există deja un calculator cu același nume + for computer in computers: + if computer['name'].lower() == name.lower(): + return {'success': False, 'message': f'Un calculator cu numele "{name}" există deja'} + + # Verifică dacă există deja un calculator cu același IP (dacă IP-ul este furnizat) + if ip and ip.strip(): + for computer in computers: + if computer.get('ip') and computer['ip'].lower() == ip.lower(): + return {'success': False, 'message': f'Un calculator cu IP {ip} există deja: {computer["name"]}'} + # Adaugă în fișier with open(CONFIG_FILE, 'a') as f: f.write(f"{name}|{mac}|{ip}\n") return {'success': True, 'message': f'Calculator {name} adăugat!'} - def scan_network(self): + def rename_computer(self, old_name, new_name): + if not new_name.strip(): + return {'success': False, 'message': 'Numele nou nu poate fi gol!'} + + computers = [] + found = False + + # Citește toate computerele + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split('|') + if len(parts) >= 2: + if parts[0] == old_name: + # Redenumește computerul găsit + computers.append(f"{new_name}|{parts[1]}|{parts[2] if len(parts) > 2 else ''}") + found = True + else: + computers.append(line) + else: + computers.append(line) + + if not found: + return {'success': False, 'message': f'Computerul {old_name} nu a fost găsit!'} + + # Verifică dacă noul nume există deja + for computer_line in computers: + if not computer_line.startswith('#') and computer_line.strip(): + parts = computer_line.split('|') + if len(parts) >= 2 and parts[0] == new_name and computer_line != f"{new_name}|{parts[1]}|{parts[2] if len(parts) > 2 else ''}": + return {'success': False, 'message': f'Numele {new_name} este deja folosit!'} + + # Rescrie fișierul + with open(CONFIG_FILE, 'w') as f: + for computer_line in computers: + f.write(computer_line + '\n') + + return {'success': True, 'message': f'Computerul a fost redenumit din {old_name} în {new_name}!'} + + def delete_computer(self, name=None, mac=None): + if not name and not mac: + return {'success': False, 'message': 'Trebuie specificat numele sau MAC-ul computerului!'} + + computers = [] + found = False + deleted_name = None + + # Citește toate computerele + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split('|') + if len(parts) >= 2: + # Verifică dacă este computerul de șters (prin nume sau MAC) + if (name and parts[0] == name) or (mac and parts[1].lower() == mac.lower()): + found = True + deleted_name = parts[0] if parts[0] else f"Calculator cu MAC {parts[1]}" + # Nu adaugă linia în lista, efectiv ștergând computerul + else: + computers.append(line) + else: + computers.append(line) + + if not found: + identifier = name if name else f"MAC {mac}" + return {'success': False, 'message': f'Computerul cu {identifier} nu a fost găsit!'} + + # Rescrie fișierul fără computerul șters + with open(CONFIG_FILE, 'w') as f: + for computer_line in computers: + f.write(computer_line + '\n') + + return {'success': True, 'message': f'Computerul {deleted_name} a fost șters!'} + + def scan_network(self, custom_network=None): try: - # Detectează rețeaua locală - result = subprocess.run(['ip', 'route'], capture_output=True, text=True) + # Încearcă să citească rezultatele scanului Windows mai întâi + windows_scan_result = self.try_read_windows_scan_results(custom_network) + if windows_scan_result: + return windows_scan_result + + # Fallback la scanarea Linux tradițională + return self.scan_network_linux(custom_network) + except Exception as e: + return {'success': False, 'message': f'Eroare la scanare: {str(e)}'} + + def try_read_windows_scan_results(self, custom_network=None): + """Încearcă să citească rezultatele din scanul Windows""" + try: + import json + from datetime import datetime, timedelta + + results_file = '/data/network-scan-results.json' + + # Verifică dacă există fișierul cu rezultate + if not os.path.exists(results_file): + return None + + # Citește rezultatele (cu suport pentru UTF-8 BOM din PowerShell) + with open(results_file, 'r', encoding='utf-8-sig') as f: + data = json.load(f) + + # Verifică vârsta rezultatelor (nu mai vechi de 30 minute) + try: + timestamp = datetime.fromisoformat(data.get('timestamp', '').replace('Z', '+00:00')) + if datetime.now().replace(tzinfo=timestamp.tzinfo) - timestamp > timedelta(minutes=30): + return None # Rezultatele sunt prea vechi + except: + return None # Timestamp invalid + + # Filtrează rezultatele pe baza rețelei specificate + computers = data.get('computers', []) + if custom_network and computers: + import ipaddress + try: + net = ipaddress.ip_network(custom_network, strict=False) + filtered_computers = [] + for computer in computers: + if ipaddress.ip_address(computer.get('ip', '')) in net: + filtered_computers.append(computer) + computers = filtered_computers + except: + pass # Rămân toate computerele dacă validarea eșuează + + # Returnează rezultatul adaptat + if not computers and custom_network: + return { + 'success': True, + 'computers': [], + 'message': f'Nu s-au găsit dispozitive în rețeaua {custom_network}. Rulează scanul Windows pentru rezultate actualizate.' + } + + message = data.get('message', f'Scanare Windows: găsite {len(computers)} dispozitive') + if custom_network: + message += f' în rețeaua {custom_network}' + + return { + 'success': True, + 'computers': computers, + 'message': message + } + + except Exception as e: + # Nu afișa eroarea, încearcă scanarea Linux + return None + + def scan_network_linux(self, custom_network=None): + """Scanarea tradițională Linux pentru compatibilitate""" + if custom_network: + # Validează formatul rețelei CIDR + import ipaddress + try: + ipaddress.ip_network(custom_network, strict=False) + network = custom_network + except ValueError: + return {'success': False, 'message': f'Format rețea invalid: {custom_network}. Folosește format CIDR (ex: 192.168.1.0/24)'} + else: + # Detectează rețeaua locală folosind route (net-tools) + result = subprocess.run(['route', '-n'], capture_output=True, text=True) network = '192.168.1.0/24' # default for line in result.stdout.split('\n'): - if 'default' in line: + if '0.0.0.0' in line and 'UG' in line: # default gateway parts = line.split() - if len(parts) >= 3: - gateway = parts[2] + if len(parts) >= 2: + gateway = parts[1] # Construiește rețeaua bazată pe gateway network_parts = gateway.split('.') network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24" break - - # Scanează rețeaua - subprocess.run(['nmap', '-sn', network], capture_output=True, timeout=30) - - # Citește ARP table - result = subprocess.run(['arp', '-a'], capture_output=True, text=True) - - scanned = [] - for line in result.stdout.split('\n'): - # Regex pentru parsarea ARP - match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\).*([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})', line) - if match: - ip = match.group(1) - mac = match.group(2) - hostname = line.split()[0] if line.split() else 'unknown' - + + # Încearcă să obțină MAC addresses din tabela ARP pentru dispozitive cunoscute + arp_result = subprocess.run(['arp', '-a'], capture_output=True, text=True) + scanned = [] + + for line in arp_result.stdout.split('\n'): + # Regex pentru parsarea ARP + match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\).*([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})', line) + if match: + ip = match.group(1) + mac = match.group(2) + hostname = line.split()[0] if line.split() else '?' + + # Verifică dacă IP-ul este în rețeaua specificată + if custom_network: + import ipaddress + try: + net = ipaddress.ip_network(custom_network, strict=False) + if ipaddress.ip_address(ip) in net: + scanned.append({ + 'ip': ip, + 'mac': mac, + 'hostname': hostname, + 'status': self.ping_computer(ip) + }) + except: + pass + else: + # Autodetectare - adaugă toate intrările ARP scanned.append({ 'ip': ip, 'mac': mac, 'hostname': hostname, 'status': self.ping_computer(ip) }) - - return {'success': True, 'computers': scanned} - except Exception as e: - return {'success': False, 'message': f'Eroare la scanare: {str(e)}'} + + # Dacă nu s-au găsit dispozitive și este o rețea specificată custom, returnează mesaj informativ + if custom_network and not scanned: + return { + 'success': True, + 'computers': [], + 'message': f'Nu s-au găsit dispozitive cu MAC addresses în rețeaua {custom_network}. În Docker Desktop Windows, scanarea automată este limitată. Rulează scanul Windows sau folosește butonul "➕ Adaugă Calculator" pentru a adăuga manual dispozitivele cu IP și MAC cunoscute.' + } + + return {'success': True, 'computers': scanned} wol_manager = WOLManager() @app.route('/') def index(): + # Hot reload is working! return render_template('index.html') @app.route('/api/computers') @@ -191,10 +389,101 @@ def add_computer(): ) return jsonify(result) -@app.route('/api/scan') -def scan_network(): - result = wol_manager.scan_network() +@app.route('/api/rename', methods=['POST']) +def rename_computer(): + data = request.get_json() + result = wol_manager.rename_computer( + data.get('old_name'), + data.get('new_name') + ) return jsonify(result) +@app.route('/api/delete', methods=['POST']) +def delete_computer(): + data = request.get_json() + result = wol_manager.delete_computer( + name=data.get('name'), + mac=data.get('mac') + ) + return jsonify(result) + +@app.route('/api/scan', methods=['GET', 'POST']) +def scan_network(): + network = None + if request.method == 'POST': + data = request.get_json() + network = data.get('network') if data else None + elif request.method == 'GET': + network = request.args.get('network') + + result = wol_manager.scan_network(network) + return jsonify(result) + +@app.route('/api/scan/windows', methods=['POST']) +def trigger_windows_scan(): + """Declanșează un scan Windows prin apelarea script-ului PowerShell""" + try: + data = request.get_json() + network = data.get('network') if data else None + + # Încearcă să execute PowerShell prin WSL interop + script_path = '/scripts/windows-network-scan.ps1' + output_path = '/data/network-scan-results.json' + + # Construiește comanda PowerShell prin WSL interop + cmd = ['/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe', + '-ExecutionPolicy', 'Bypass', + '-File', script_path, + '-OutputPath', output_path, + '-Verbose'] + + if network: + cmd.extend(['-Network', network]) + + # Încearcă să execute script-ul prin WSL interop + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + + if result.returncode == 0: + # Scanul a fost executat cu succes, citește rezultatele + scan_result = wol_manager.scan_network(network) + scan_result['message'] = 'Scan Windows executat automat cu succes!' + return jsonify(scan_result) + else: + # Dacă execuția automată eșuează, oferă instrucțiuni manuale + return jsonify({ + 'success': False, + 'message': f'Execuția automată a eșuat: {result.stderr[:200]}...', + 'auto_execution_failed': True, + 'instructions': 'Rulează manual din Windows unul dintre comenzile:', + 'commands': [ + f'scripts\\scan-network.bat{" -network " + network if network else ""}', + f'scripts\\run-scan.ps1{" -Network " + network if network else ""}', + f'powershell.exe -ExecutionPolicy Bypass -File scripts\\windows-network-scan.ps1 -OutputPath data\\network-scan-results.json{" -Network " + network if network else ""}' + ] + }) + + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e: + # Execuția automată nu este posibilă, oferă instrucțiuni manuale + return jsonify({ + 'success': False, + 'message': f'Execuția automată nu este disponibilă: {str(e)[:100]}', + 'auto_execution_failed': True, + 'instructions': 'Rulează manual din Windows unul dintre comenzile:', + 'commands': [ + f'scripts\\scan-network.bat{" -network " + network if network else ""}', + f'scripts\\run-scan.ps1{" -Network " + network if network else ""}', + f'powershell.exe -ExecutionPolicy Bypass -File scripts\\windows-network-scan.ps1 -OutputPath data\\network-scan-results.json{" -Network " + network if network else ""}' + ] + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'message': f'Eroare: {str(e)}', + 'instructions': 'Rulează manual: scripts\\scan-network.bat' + }) + if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False) \ No newline at end of file + # Enable debug mode for development with hot reload + app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True) \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..3c8222b --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,651 @@ +/* Classic Windows-style CSS */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + background-color: #F5F5F5; + color: #000000; + line-height: 1.4; +} + +.container { + background-color: #FFFFFF; + border: 1px solid #808080; + margin: 10px; + height: calc(100vh - 20px); + display: flex; + flex-direction: column; +} + +.title-bar { + background: linear-gradient(to bottom, #0A246A 0%, #A6CAF0 100%); + color: white; + padding: 4px 8px; + border-bottom: 1px solid #808080; + font-weight: bold; +} + +.title-bar h1 { + font-size: 16px; + margin: 0; + /* Live reload test comment */ +} + +.toolbar { + background-color: #F0F0F0; + border-bottom: 1px solid #808080; + padding: 4px; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} + +.toolbar button { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 12px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 32px; +} + +.toolbar button:hover { + background-color: #E8E5E2; +} + +.toolbar button:active { + border: 1px inset #D4D0C8; +} + +.network-selector { + display: flex; + align-items: center; + gap: 4px; + margin-left: 8px; +} + +.network-selector label { + font-size: 14px; +} + +.network-selector select, +.network-selector input { + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + border: 1px inset #D4D0C8; + padding: 4px; + background-color: white; + min-height: 28px; +} + +.main-content { + flex: 1; + padding: 4px; + overflow: auto; +} + +.computers-table { + width: 100%; + border-collapse: collapse; + border: 1px solid #808080; + background-color: white; + font-size: 14px; +} + +.computers-table th { + background-color: #F0F0F0; + border: 1px solid #808080; + padding: 8px 12px; + text-align: left; + font-weight: bold; + font-size: 14px; +} + +.computers-table td { + border: 1px solid #D4D0C8; + padding: 8px 12px; + vertical-align: middle; +} + +.computers-table td:first-child { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 8px; +} + +.computers-table tr:nth-child(even) { + background-color: #F9F9F9; +} + +.computers-table tr:hover { + background-color: #E0E0E0; +} + +.status { + font-size: 14px; + padding: 2px 6px; + font-weight: bold; +} + +.status.online { + color: #008000; +} + +.status.offline { + color: #800000; +} + +.status.unknown { + color: #808000; +} + +.wake-btn { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 8px; + font-family: Tahoma, Arial, sans-serif; + font-size: 16px; + cursor: pointer; + min-height: 32px; + min-width: 32px; +} + +.wake-btn:hover { + background-color: #E8E5E2; +} + +.wake-btn:active { + border: 1px inset #D4D0C8; +} + +.rename-btn { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 8px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 32px; + min-width: 32px; +} + +.rename-btn:hover { + background-color: #E8E5E2; +} + +.rename-btn:active { + border: 1px inset #D4D0C8; +} + +.delete-btn { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 8px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 32px; + min-width: 32px; + color: #800000; +} + +.delete-btn:hover { + background-color: #F2DEDE; + color: #A94442; +} + +.delete-btn:active { + border: 1px inset #D4D0C8; +} + +.no-computers { + text-align: center; + padding: 20px; + color: #666; + font-size: 14px; +} + +/* Modal Styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: #F0F0F0; + border: 2px outset #D4D0C8; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 450px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; +} + +.modal-header { + background: linear-gradient(to bottom, #0A246A 0%, #A6CAF0 100%); + color: white; + padding: 4px 8px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h2 { + font-size: 16px; + font-weight: bold; + margin: 0; +} + +.close { + background: none; + border: none; + color: white; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.close:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.modal-body { + padding: 8px; +} + +.form-group { + margin-bottom: 8px; +} + +.form-group label { + display: block; + margin-bottom: 4px; + font-size: 14px; +} + +.form-group input { + width: 100%; + border: 1px inset #D4D0C8; + padding: 6px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + background-color: white; + min-height: 28px; + box-sizing: border-box; +} + +.form-actions { + background-color: #F0F0F0; + border-top: 1px solid #808080; + padding: 8px; + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.form-actions button { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 20px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 32px; + min-width: 90px; +} + +.form-actions button:hover { + background-color: #E8E5E2; +} + +.form-actions button:active { + border: 1px inset #D4D0C8; +} + +/* Message styles */ +.message { + padding: 8px 12px; + margin: 6px; + border: 1px solid; + font-size: 14px; +} + +.message.success { + background-color: #DFF0D8; + border-color: #D6E9C6; + color: #3C763D; +} + +.message.error { + background-color: #F2DEDE; + border-color: #EBCCD1; + color: #A94442; +} + +.message.warning { + background-color: #FCF8E3; + border-color: #FAEBCC; + color: #8A6D3B; +} + +/* Scan table */ +.scan-table { + width: 100%; + border-collapse: collapse; + border: 1px solid #808080; + margin-top: 8px; + font-size: 14px; +} + +.scan-table th { + background-color: #F0F0F0; + border: 1px solid #808080; + padding: 8px; + text-align: left; + font-weight: bold; +} + +.scan-table td { + border: 1px solid #D4D0C8; + padding: 8px; +} + +.scan-table tr:nth-child(even) { + background-color: #F9F9F9; +} + +.scan-table tr:hover { + background-color: #E0E0E0; +} + +.add-btn { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 4px 8px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 28px; + min-width: 28px; +} + +.add-btn:hover { + background-color: #E8E5E2; +} + +.add-btn:active { + border: 1px inset #D4D0C8; +} + +/* Loading animation */ +.loading { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 8px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +#scan-loading { + text-align: center; + padding: 20px; +} + +#scan-loading p { + margin-top: 8px; + font-size: 14px; +} + +/* Scan controls */ +.scan-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 4px; + border: 1px solid #D4D0C8; + background-color: #F9F9F9; + margin-bottom: 4px; +} + +.select-all-container { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + cursor: pointer; +} + +.select-all-container input[type="checkbox"] { + margin: 0; +} + +.add-selected-btn { + background-color: #F0F0F0; + border: 1px outset #D4D0C8; + padding: 6px 16px; + font-family: Tahoma, Arial, sans-serif; + font-size: 14px; + cursor: pointer; + min-height: 32px; +} + +.add-selected-btn:enabled:hover { + background-color: #E8E5E2; +} + +.add-selected-btn:active { + border: 1px inset #D4D0C8; +} + +.add-selected-btn:disabled { + background-color: #E8E8E8; + color: #808080; + cursor: not-allowed; + border: 1px solid #D4D0C8; +} + +.device-checkbox { + cursor: pointer; + margin: 0; +} + +/* Wider scan modal for the additional column */ +#scanModal .modal-content { + width: 700px; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + body { + font-size: 16px; + } + + .container { + margin: 5px; + height: calc(100vh - 10px); + } + + .title-bar h1 { + font-size: 18px; + } + + .toolbar { + padding: 8px; + gap: 8px; + } + + .toolbar button { + font-size: 16px; + padding: 8px 12px; + min-height: 40px; + flex: 1; + min-width: 0; + } + + .network-selector { + flex-direction: column; + align-items: stretch; + gap: 4px; + margin-left: 0; + width: 100%; + } + + .network-selector label { + font-size: 16px; + } + + .network-selector select, + .network-selector input { + font-size: 16px; + padding: 8px; + min-height: 36px; + } + + .computers-table { + font-size: 16px; + } + + .computers-table th { + font-size: 16px; + padding: 10px 8px; + } + + .computers-table td { + padding: 10px 8px; + } + + .computers-table td:first-child { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 6px; + justify-content: flex-start; + } + + .status { + font-size: 16px; + padding: 4px 8px; + } + + .wake-btn { + font-size: 20px; + padding: 8px 10px; + min-height: 40px; + min-width: 40px; + } + + .rename-btn, + .delete-btn { + font-size: 16px; + padding: 8px 10px; + min-height: 40px; + min-width: 40px; + } + + .modal-content { + width: calc(100vw - 20px); + max-width: 500px; + font-size: 16px; + } + + .modal-header h2 { + font-size: 18px; + } + + .form-group label { + font-size: 16px; + margin-bottom: 6px; + } + + .form-group input { + font-size: 16px; + padding: 10px; + min-height: 36px; + } + + .form-actions button { + font-size: 16px; + padding: 10px 20px; + min-height: 40px; + } + + .message { + font-size: 16px; + padding: 10px 12px; + } + + .scan-table { + font-size: 16px; + } + + .scan-table th, + .scan-table td { + padding: 10px 6px; + } + + .add-btn { + font-size: 16px; + padding: 6px 10px; + min-height: 36px; + min-width: 36px; + } + + .select-all-container { + font-size: 16px; + } + + .add-selected-btn { + font-size: 16px; + padding: 8px 16px; + min-height: 40px; + } + + #scanModal .modal-content { + width: calc(100vw - 20px); + max-width: 100%; + } + + .no-computers { + font-size: 16px; + padding: 30px 20px; + } + + /* Make table horizontally scrollable on small screens */ + .main-content { + overflow-x: auto; + } + + .computers-table { + min-width: 600px; + } +} \ No newline at end of file diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 100644 index 0000000..55c2e1d --- /dev/null +++ b/app/static/js/app.js @@ -0,0 +1,608 @@ +// WOL Manager JavaScript +let scanModal, addModal, renameModal; + +// Initialize on page load +window.onload = function() { + scanModal = document.getElementById('scanModal'); + addModal = document.getElementById('addModal'); + renameModal = document.getElementById('renameModal'); + refreshComputers(); +}; + +function showMessage(message, type) { + const messageArea = document.getElementById('message-area'); + messageArea.innerHTML = `
`; + setTimeout(() => { + messageArea.innerHTML = ''; + }, 5000); +} + +function refreshComputers() { + fetch('/api/computers') + .then(response => response.json()) + .then(computers => { + displayComputers(computers); + }) + .catch(error => { + showMessage('Eroare la încărcarea calculatoarelor: ' + error.message, 'error'); + }); +} + +function displayComputers(computers) { + const tbody = document.getElementById('computers-tbody'); + const noComputersDiv = document.getElementById('no-computers'); + + if (computers.length === 0) { + tbody.innerHTML = ''; + noComputersDiv.style.display = 'block'; + return; + } + + noComputersDiv.style.display = 'none'; + + tbody.innerHTML = computers.map(computer => ` +${cmd}`;
+ });
+ }
+ }
+
+ showMessage(message, 'error');
+ document.getElementById('scan-results').innerHTML =
+ 'Pentru a scana rețeaua Windows și obține MAC addresses:
' + + 'După rularea comenzii, apasă "Scanează Rețeaua" pentru a vedea rezultatele.
' + + '| Selectează | +IP | +MAC | +Hostname | +Status | +Acțiune | +
|---|---|---|---|---|---|
| + + | +${computer.ip} | +${computer.mac} | +${computer.hostname} | +${computer.status} | ++ + | +
| Acțiuni | +Nume Calculator | +Adresa MAC | +Adresa IP | +Status | +
|---|