Compare commits

...

2 Commits

Author SHA1 Message Date
acf234c600 Major feature enhancement: Windows PowerShell network scanning integration
- Added Windows PowerShell network scanner with auto-detection and interactive mode
- Implemented dual scanning system (Windows + Linux fallback)
- Added computer management features (rename, delete, duplicate checking)
- Enhanced UI with modern responsive design and Romanian localization
- Added comprehensive Windows-Linux integration with WSL interop
- Improved error handling and user feedback throughout
- Added hot reload for development and comprehensive documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 17:27:27 +03:00
616763c603 Configure flexible deployment with environment variables
- Change Flask port from 8080 to 5000 for consistency
- Add environment-based network mode configuration (bridge/host)
- Support both Windows Docker Desktop and Linux LXC deployments
- Remove obsolete docker-compose version and conflicting network settings
- Add .env.example with platform-specific configuration guidance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 18:00:08 +03:00
14 changed files with 2500 additions and 656 deletions

4
.env Normal file
View File

@@ -0,0 +1,4 @@
# WOL Manager Environment Configuration
WOL_NETWORK_MODE=bridge
WOL_EXTERNAL_PORT=5000
FLASK_DEBUG=true

20
.env.example Normal file
View File

@@ -0,0 +1,20 @@
# WOL Manager Environment Configuration
# Network mode configuration
# For Windows/macOS Docker Desktop: use "bridge"
# For Linux/LXC/Proxmox: use "host" for guaranteed WOL functionality
WOL_NETWORK_MODE=bridge
# External port mapping (only used with bridge mode)
# For Windows/macOS: set the external port you want to access
# For Linux with host mode: this setting is ignored
WOL_EXTERNAL_PORT=5000
# Examples:
# Windows Docker Desktop:
# WOL_NETWORK_MODE=bridge
# WOL_EXTERNAL_PORT=5000
# Linux LXC/Proxmox:
# WOL_NETWORK_MODE=host
# WOL_EXTERNAL_PORT=5000 # ignored in host mode

View File

@@ -13,43 +13,92 @@ 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 - **Backend**: Flask web application (`app/app.py`) with RESTful API endpoints
- **Frontend**: Single-page application with vanilla JavaScript (`app/templates/index.html`) - **Frontend**: Single-page application with vanilla JavaScript (`app/templates/index.html`)
- **Storage**: File-based configuration in `/data/wol-computers.conf` - **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 ### Key Components
- `WOLManager` class in `app/app.py`: Core logic for computer management, WOL operations, and network scanning - `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`) - Configuration format: `name|mac|ip` (pipe-separated values in `/data/wol-computers.conf`)
- Dependencies: `wakeonlan`, `nmap`, `ping`, `arp` system tools - Dependencies: `wakeonlan`, `nmap`, `ping`, `arp` system tools
- **Windows PowerShell scanner**: `scripts/windows-network-scan.ps1` - optimized network scanning from Windows host
## Development Commands ## Development Commands
**For installation and basic usage, see README.md** **For installation and basic usage, see README.md**
### Docker Development ### Docker Development
**Note**: When running from WSL with Docker Desktop on Windows, use `docker.exe compose`:
```bash ```bash
# Development build and run # 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 # 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 # 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 ### 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 - Requires `NET_ADMIN` and `NET_RAW` capabilities
- Uses `network_mode: host` for WOL packet transmission - Uses `network_mode: host` for WOL packet transmission
- Must run with `privileged: true` for network operations - 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 ## API Endpoints
- `GET /api/computers` - List configured computers with status - `GET /api/computers` - List configured computers with status
- `POST /api/wake` - Wake specific computer (`{mac, name, ip}`) - `POST /api/wake` - Wake specific computer (`{mac, name, ip}`)
- `POST /api/wake-all` - Wake all configured computers - `POST /api/wake-all` - Wake all configured computers
- `POST /api/add` - Add new computer (`{name, mac, ip}`) - `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 ## Development Notes
@@ -57,3 +106,22 @@ docker-compose exec wol-web bash
- No authentication/authorization implemented - No authentication/authorization implemented
- Configuration persisted in volume-mounted `/data` directory - Configuration persisted in volume-mounted `/data` directory
- Flask runs in debug=False mode in container - 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

View File

@@ -95,43 +95,234 @@ class WOLManager:
if not re.match(mac_pattern, mac): if not re.match(mac_pattern, mac):
return {'success': False, 'message': 'MAC address invalid!'} 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 # Adaugă în fișier
with open(CONFIG_FILE, 'a') as f: with open(CONFIG_FILE, 'a') as f:
f.write(f"{name}|{mac}|{ip}\n") f.write(f"{name}|{mac}|{ip}\n")
return {'success': True, 'message': f'Calculator {name} adăugat!'} 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: try:
# Detectează rețeaua locală # Încearcă să citească rezultatele scanului Windows mai întâi
result = subprocess.run(['ip', 'route'], capture_output=True, text=True) 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 network = '192.168.1.0/24' # default
for line in result.stdout.split('\n'): 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() parts = line.split()
if len(parts) >= 3: if len(parts) >= 2:
gateway = parts[2] gateway = parts[1]
# Construiește rețeaua bazată pe gateway # Construiește rețeaua bazată pe gateway
network_parts = gateway.split('.') network_parts = gateway.split('.')
network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24" network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24"
break break
# Scanează rețeaua # Încearcă să obțină MAC addresses din tabela ARP pentru dispozitive cunoscute
subprocess.run(['nmap', '-sn', network], capture_output=True, timeout=30) arp_result = subprocess.run(['arp', '-a'], capture_output=True, text=True)
scanned = []
# Citește ARP table for line in arp_result.stdout.split('\n'):
result = subprocess.run(['arp', '-a'], capture_output=True, text=True) # 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)
scanned = [] if match:
for line in result.stdout.split('\n'): ip = match.group(1)
# Regex pentru parsarea ARP mac = match.group(2)
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) hostname = line.split()[0] if line.split() else '?'
if match:
ip = match.group(1)
mac = match.group(2)
hostname = line.split()[0] if line.split() else 'unknown'
# 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({ scanned.append({
'ip': ip, 'ip': ip,
'mac': mac, 'mac': mac,
@@ -139,14 +330,21 @@ class WOLManager:
'status': self.ping_computer(ip) 'status': self.ping_computer(ip)
}) })
return {'success': True, 'computers': scanned} # Dacă nu s-au găsit dispozitive și este o rețea specificată custom, returnează mesaj informativ
except Exception as e: if custom_network and not scanned:
return {'success': False, 'message': f'Eroare la scanare: {str(e)}'} 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() wol_manager = WOLManager()
@app.route('/') @app.route('/')
def index(): def index():
# Hot reload is working!
return render_template('index.html') return render_template('index.html')
@app.route('/api/computers') @app.route('/api/computers')
@@ -191,10 +389,101 @@ def add_computer():
) )
return jsonify(result) return jsonify(result)
@app.route('/api/scan') @app.route('/api/rename', methods=['POST'])
def scan_network(): def rename_computer():
result = wol_manager.scan_network() data = request.get_json()
result = wol_manager.rename_computer(
data.get('old_name'),
data.get('new_name')
)
return jsonify(result) 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__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=False) # Enable debug mode for development with hot reload
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True)

651
app/static/css/style.css Normal file
View File

@@ -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;
}
}

608
app/static/js/app.js Normal file
View File

@@ -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 = `<div class="message ${type}">${message}</div>`;
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 => `
<tr>
<td>
<button class="wake-btn" onclick="wakeComputer('${computer.mac}', '${computer.name}', '${computer.ip || ''}')" title="Trezește calculatorul">
</button>
<button class="rename-btn" onclick="openRenameModal('${computer.name}')" title="Redenumește calculatorul">
📝
</button>
<button class="delete-btn" onclick="deleteComputer('${computer.name}', '${computer.mac}')" title="Șterge calculatorul">
🗑️
</button>
</td>
<td>${computer.name}</td>
<td style="font-family: monospace;">${computer.mac}</td>
<td>${computer.ip || '-'}</td>
<td><span class="status ${computer.status}">${computer.status}</span></td>
</tr>
`).join('');
}
function wakeComputer(mac, name, ip) {
showMessage(`Se trimite magic packet pentru ${name}...`, 'success');
fetch('/api/wake', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({mac: mac, name: name, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
setTimeout(refreshComputers, 2000);
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la trezirea calculatorului: ' + error.message, 'error');
});
}
function wakeAllComputers() {
showMessage('Se trezesc toate calculatoarele...', 'success');
fetch('/api/wake-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
let message = 'Comenzi trimise:<br>';
data.results.forEach(result => {
message += `${result.name}: ${result.result.message}<br>`;
});
showMessage(message, 'success');
setTimeout(refreshComputers, 3000);
})
.catch(error => {
showMessage('Eroare la trezirea calculatoarelor: ' + error.message, 'error');
});
}
function openAddModal() {
addModal.style.display = 'block';
}
function closeAddModal() {
addModal.style.display = 'none';
document.getElementById('computerName').value = '';
document.getElementById('computerMac').value = '';
document.getElementById('computerIp').value = '';
}
function addComputer() {
const name = document.getElementById('computerName').value;
const mac = document.getElementById('computerMac').value;
const ip = document.getElementById('computerIp').value;
if (!name || !mac) {
showMessage('Numele și MAC-ul sunt obligatorii!', 'error');
return;
}
fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: name, mac: mac, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
closeAddModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la adăugarea calculatorului: ' + error.message, 'error');
});
}
function toggleCustomNetwork() {
const select = document.getElementById('networkSelect');
const customInput = document.getElementById('customNetwork');
if (select.value === 'custom') {
customInput.style.display = 'inline';
customInput.focus();
} else {
customInput.style.display = 'none';
}
}
function getSelectedNetwork() {
const select = document.getElementById('networkSelect');
const customInput = document.getElementById('customNetwork');
if (select.value === 'custom') {
return customInput.value.trim();
} else {
return select.value;
}
}
function scanNetwork() {
scanModal.style.display = 'block';
document.getElementById('scan-loading').style.display = 'block';
document.getElementById('scan-results').innerHTML = '';
const network = getSelectedNetwork();
const requestData = network ? { network: network } : {};
fetch('/api/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(result => {
document.getElementById('scan-loading').style.display = 'none';
if (result.success) {
if (result.computers && result.computers.length > 0) {
displayScanResults(result.computers);
if (result.message) {
// Afișează mesajul deasupra tabelului
document.getElementById('scan-results').innerHTML =
`<div class="message" style="background: #fef3c7; color: #92400e; border-color: #fbbf24;">${result.message}</div>` +
document.getElementById('scan-results').innerHTML;
}
} else if (result.message) {
document.getElementById('scan-results').innerHTML =
`<div class="message" style="background: #fef3c7; color: #92400e; border-color: #fbbf24;">${result.message}</div>`;
}
} else {
document.getElementById('scan-results').innerHTML =
`<div class="message error">${result.message}</div>`;
}
})
.catch(error => {
document.getElementById('scan-loading').style.display = 'none';
document.getElementById('scan-results').innerHTML =
`<div class="message error">Eroare la scanare: ${error.message}</div>`;
});
}
function triggerWindowsScan() {
scanModal.style.display = 'block';
document.getElementById('scan-loading').style.display = 'block';
document.getElementById('scan-results').innerHTML = '';
const network = getSelectedNetwork();
const requestData = network ? { network: network } : {};
showMessage('Declanșând scanul Windows...', 'success');
fetch('/api/scan/windows', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
document.getElementById('scan-loading').style.display = 'none';
if (data.success && data.computers) {
showMessage(data.message || 'Scan Windows completat cu succes!', 'success');
displayScanResults(data.computers);
} else {
let message = data.message || 'Scanul Windows a eșuat';
if (data.instructions) {
message += '<br><br><strong>Instrucțiuni:</strong><br>' + data.instructions;
if (data.commands) {
message += '<br><strong>Comenzi disponibile:</strong>';
data.commands.forEach(cmd => {
message += `<br>• <code>${cmd}</code>`;
});
}
}
showMessage(message, 'error');
document.getElementById('scan-results').innerHTML =
'<div style="padding: 20px; text-align: center; color: #666;">' +
'<h3>Scanul Windows nu poate fi executat din container</h3>' +
'<p>Pentru a scana rețeaua Windows și obține MAC addresses:</p>' +
'<ol style="text-align: left; margin: 20px 0;">' +
'<li>Deschide Command Prompt sau PowerShell ca Administrator pe Windows</li>' +
'<li>Navighează la directorul proiectului WOL Manager</li>' +
'<li>Rulează una din comenzile de mai jos:</li>' +
'</ol>' +
'<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 10px 0; font-family: monospace;">' +
(data.commands ? data.commands.map(cmd => `<div style="margin: 5px 0;">${cmd}</div>`).join('') :
'scripts\\scan-network.bat') +
'</div>' +
'<p><small>După rularea comenzii, apasă "Scanează Rețeaua" pentru a vedea rezultatele.</small></p>' +
'</div>';
}
})
.catch(error => {
document.getElementById('scan-loading').style.display = 'none';
showMessage('Eroare la declanșarea scanului Windows: ' + error.message, 'error');
console.error('Error:', error);
});
}
function displayScanResults(computers) {
if (computers.length === 0) {
document.getElementById('scan-results').innerHTML =
'<div class="message error">Nu s-au găsit calculatoare în rețea</div>';
return;
}
let html = `
<div class="scan-controls">
<label class="select-all-container">
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
<span>Selectează toate</span>
</label>
<button class="add-selected-btn" onclick="addSelectedFromScan()" disabled title="Adaugă calculatoarele selectate">
Adaugă Selectate
</button>
</div>
<table class="scan-table">
<thead>
<tr>
<th>Selectează</th>
<th>IP</th>
<th>MAC</th>
<th>Hostname</th>
<th>Status</th>
<th>Acțiune</th>
</tr>
</thead>
<tbody>
`;
computers.forEach((computer, index) => {
html += `
<tr>
<td>
<input type="checkbox" class="device-checkbox"
data-hostname="${computer.hostname}"
data-mac="${computer.mac}"
data-ip="${computer.ip}"
onchange="updateAddButton()">
</td>
<td>${computer.ip}</td>
<td style="font-family: monospace;">${computer.mac}</td>
<td>${computer.hostname}</td>
<td><span class="status ${computer.status}">${computer.status}</span></td>
<td>
<button class="add-btn" onclick="addFromScan('${computer.hostname}', '${computer.mac}', '${computer.ip}')" title="Adaugă calculatorul">
</button>
</td>
</tr>
`;
});
html += '</tbody></table>';
document.getElementById('scan-results').innerHTML = html;
}
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAll');
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
deviceCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateAddButton();
}
function updateAddButton() {
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
const addButton = document.querySelector('.add-selected-btn');
const selectAllCheckbox = document.getElementById('selectAll');
// Enable/disable the "Add Selected" button
if (addButton) {
addButton.disabled = checkedBoxes.length === 0;
addButton.textContent = checkedBoxes.length > 0 ?
` Adaugă Selectate (${checkedBoxes.length})` : ' Adaugă Selectate';
}
// Update "Select All" checkbox state
if (selectAllCheckbox) {
if (checkedBoxes.length === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (checkedBoxes.length === deviceCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
function addSelectedFromScan() {
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
if (checkedBoxes.length === 0) {
showMessage('Nu ai selectat niciun dispozitiv!', 'error');
return;
}
const devices = Array.from(checkedBoxes).map(checkbox => ({
name: checkbox.dataset.hostname,
mac: checkbox.dataset.mac,
ip: checkbox.dataset.ip
}));
// Show progress message
showMessage(`Se adaugă ${devices.length} dispozitive...`, 'success');
// Add devices one by one
let addedCount = 0;
let failedCount = 0;
let duplicateCount = 0;
let failedDevices = [];
let duplicateDevices = [];
const addDevice = (device, index) => {
return fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(device)
})
.then(response => response.json())
.then(result => {
if (result.success) {
addedCount++;
} else {
// Verifică dacă este o eroare de duplicat
if (result.message.includes('există deja')) {
duplicateCount++;
duplicateDevices.push(`${device.name} (${result.message})`);
} else {
failedCount++;
failedDevices.push(`${device.name}: ${result.message}`);
}
console.warn(`Failed to add ${device.name}:`, result.message);
}
})
.catch(error => {
failedCount++;
failedDevices.push(`${device.name}: ${error.message}`);
console.error(`Error adding ${device.name}:`, error);
});
};
// Add all devices in parallel
Promise.all(devices.map(addDevice))
.then(() => {
let message = '';
let messageType = 'success';
if (addedCount > 0 && failedCount === 0 && duplicateCount === 0) {
message = `${addedCount} dispozitive adăugate cu succes!`;
} else if (addedCount > 0) {
message = `${addedCount} dispozitive adăugate cu succes`;
if (duplicateCount > 0) {
message += `, ${duplicateCount} duplicate ignorate`;
}
if (failedCount > 0) {
message += `, ${failedCount} eșuate`;
}
message += '.';
if (duplicateCount > 0 || failedCount > 0) {
messageType = 'warning';
}
} else {
if (duplicateCount > 0 && failedCount === 0) {
message = `Toate ${duplicateCount} dispozitivele selectate există deja în sistem.`;
messageType = 'warning';
} else if (duplicateCount > 0) {
message = `${duplicateCount} dispozitive duplicate, ${failedCount} eșuate.`;
messageType = 'error';
} else {
message = `Toate ${failedCount} dispozitivele au eșuat să fie adăugate.`;
messageType = 'error';
}
}
// Dacă există duplicate sau erori, afișează detalii suplimentare în consolă
if (duplicateDevices.length > 0) {
console.info('Dispozitive duplicate:', duplicateDevices);
}
if (failedDevices.length > 0) {
console.warn('Dispozitive eșuate:', failedDevices);
}
showMessage(message, messageType);
if (addedCount > 0) {
closeScanModal();
refreshComputers();
}
});
}
function addFromScan(hostname, mac, ip) {
fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: hostname, mac: mac, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
closeScanModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la adăugarea calculatorului: ' + error.message, 'error');
});
}
function closeScanModal() {
scanModal.style.display = 'none';
}
function openRenameModal(currentName) {
document.getElementById('currentName').value = currentName;
document.getElementById('newName').value = '';
renameModal.style.display = 'block';
document.getElementById('newName').focus();
}
function closeRenameModal() {
renameModal.style.display = 'none';
document.getElementById('currentName').value = '';
document.getElementById('newName').value = '';
}
function performRename() {
const oldName = document.getElementById('currentName').value;
const newName = document.getElementById('newName').value.trim();
if (!newName) {
showMessage('Numele nou nu poate fi gol!', 'error');
return;
}
if (oldName === newName) {
showMessage('Numele nou trebuie să fie diferit de cel actual!', 'error');
return;
}
fetch('/api/rename', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({old_name: oldName, new_name: newName})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
closeRenameModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la redenumirea calculatorului: ' + error.message, 'error');
});
}
function deleteComputer(name, mac) {
const displayName = name && name.trim() ? name : `Calculator cu MAC ${mac}`;
if (!confirm(`Sigur vrei să ștergi calculatorul "${displayName}"?`)) {
return;
}
fetch('/api/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: name, mac: mac})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la ștergerea calculatorului: ' + error.message, 'error');
});
}
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target == addModal) {
closeAddModal();
}
if (event.target == scanModal) {
closeScanModal();
}
if (event.target == renameModal) {
closeRenameModal();
}
}
// Allow Enter key to perform rename
document.addEventListener('keydown', function(event) {
if (event.key === 'Enter' && renameModal.style.display === 'block') {
performRename();
}
});

View File

@@ -4,369 +4,80 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wake-on-LAN Manager</title> <title>Wake-on-LAN Manager</title>
<style> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
h1 {
text-align: center;
color: #2d3748;
margin-bottom: 30px;
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
justify-content: center;
}
button {
background: linear-gradient(45deg, #4f46e5, #7c3aed);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(79, 70, 229, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(79, 70, 229, 0.4);
}
button:active {
transform: translateY(0);
}
button.wake-all {
background: linear-gradient(45deg, #10b981, #059669);
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
button.wake-all:hover {
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
}
button.scan {
background: linear-gradient(45deg, #f59e0b, #d97706);
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.3);
}
button.scan:hover {
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.4);
}
.computer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.computer-card {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.computer-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
border-color: #667eea;
}
.computer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.computer-name {
font-size: 1.2rem;
font-weight: 700;
color: #2d3748;
}
.status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status.online {
background: #d1fae5;
color: #065f46;
}
.status.offline {
background: #fee2e2;
color: #991b1b;
}
.status.unknown {
background: #fef3c7;
color: #92400e;
}
.computer-info {
margin-bottom: 15px;
color: #4a5568;
}
.computer-info div {
margin-bottom: 5px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.wake-btn {
width: 100%;
background: linear-gradient(45deg, #10b981, #059669);
margin-top: 10px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 30px;
border-radius: 20px;
width: 90%;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.modal h2 {
color: #2d3748;
margin-bottom: 20px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.message {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
font-weight: 600;
text-align: center;
}
.message.success {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.message.error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fca5a5;
}
.scan-results {
margin-top: 20px;
}
.scan-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.scan-table th,
.scan-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.scan-table th {
background: #f9fafb;
font-weight: 600;
color: #374151;
}
.scan-table tr:hover {
background: #f9fafb;
}
.add-btn {
background: linear-gradient(45deg, #10b981, #059669);
font-size: 12px;
padding: 6px 12px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
}
.close:hover {
color: #000;
}
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 20px;
}
.computer-grid {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
max-width: 300px;
}
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🚀 Wake-on-LAN Manager</h1> <div class="title-bar">
<h1>Wake-on-LAN Manager</h1>
</div>
<div id="message-area"></div> <div id="message-area"></div>
<div class="controls"> <div class="toolbar">
<button onclick="refreshComputers()">🔄 Refresh</button> <button onclick="refreshComputers()">Refresh</button>
<button onclick="openAddModal()" class="scan"> Adaugă Calculator</button> <button onclick="openAddModal()" title="Adaugă Calculator"> Adaugă Calculator</button>
<button onclick="scanNetwork()" class="scan">🔍 Scanează Rețeaua</button> <div class="network-selector">
<button onclick="wakeAllComputers()" class="wake-all">⚡ Trezește Toate</button> <label>Rețea:</label>
<select id="networkSelect" onchange="toggleCustomNetwork()">
<option value="">Autodetectare</option>
<option value="192.168.1.0/24">192.168.1.0/24</option>
<option value="192.168.0.0/24">192.168.0.0/24</option>
<option value="10.0.0.0/24">10.0.0.0/24</option>
<option value="10.0.20.0/24">10.0.20.0/24</option>
<option value="custom">Personalizat...</option>
</select>
<input type="text" id="customNetwork" placeholder="ex: 192.168.100.0/24" style="display: none;">
</div>
<button onclick="scanNetwork()">Scanează Rețeaua</button>
<button onclick="triggerWindowsScan()" title="Scan Windows pentru MAC addresses">Scan Windows</button>
<button onclick="wakeAllComputers()">Trezește Toate</button>
</div> </div>
<div id="computers-grid" class="computer-grid"> <div class="main-content">
<!-- Calculatoarele vor fi încărcate aici --> <table id="computers-table" class="computers-table">
<thead>
<tr>
<th>Acțiuni</th>
<th>Nume Calculator</th>
<th>Adresa MAC</th>
<th>Adresa IP</th>
<th>Status</th>
</tr>
</thead>
<tbody id="computers-tbody">
<!-- Calculatoarele vor fi încărcate aici -->
</tbody>
</table>
<div id="no-computers" class="no-computers" style="display: none;">
<p>Nu există calculatoare configurate. Adaugă calculatoare sau scanează rețeaua pentru a începe.</p>
</div>
</div> </div>
</div> </div>
<!-- Modal pentru adăugarea calculatoarelor --> <!-- Modal pentru adăugarea calculatoarelor -->
<div id="addModal" class="modal"> <div id="addModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeAddModal()">&times;</span> <div class="modal-header">
<h2>Adaugă Calculator Nou</h2> <h2>Adaugă Calculator Nou</h2>
<div class="form-group"> <span class="close" onclick="closeAddModal()">&times;</span>
<label for="computerName">Nume Calculator:</label>
<input type="text" id="computerName" placeholder="ex: PC Birou">
</div> </div>
<div class="form-group"> <div class="modal-body">
<label for="computerMac">Adresa MAC:</label> <div class="form-group">
<input type="text" id="computerMac" placeholder="ex: 00:11:22:33:44:55"> <label for="computerName">Nume Calculator:</label>
</div> <input type="text" id="computerName" placeholder="ex: PC Birou">
<div class="form-group"> </div>
<label for="computerIp">IP (opțional):</label> <div class="form-group">
<input type="text" id="computerIp" placeholder="ex: 192.168.1.100"> <label for="computerMac">Adresa MAC:</label>
<input type="text" id="computerMac" placeholder="ex: 00:11:22:33:44:55">
</div>
<div class="form-group">
<label for="computerIp">IP (opțional):</label>
<input type="text" id="computerIp" placeholder="ex: 192.168.1.100">
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" onclick="closeAddModal()" class="btn-secondary">Anulează</button> <button type="button" onclick="closeAddModal()">Anulează</button>
<button type="button" onclick="addComputer()">Adaugă</button> <button type="button" onclick="addComputer()">Adaugă</button>
</div> </div>
</div> </div>
@@ -375,265 +86,44 @@
<!-- Modal pentru scanarea rețelei --> <!-- Modal pentru scanarea rețelei -->
<div id="scanModal" class="modal"> <div id="scanModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeScanModal()">&times;</span> <div class="modal-header">
<h2>Calculatoare din Rețea</h2> <h2>Calculatoare din Rețea</h2>
<div id="scan-loading" style="display: none; text-align: center;"> <span class="close" onclick="closeScanModal()">&times;</span>
<div class="loading"></div> </div>
<p>Scanez rețeaua...</p> <div class="modal-body">
<div id="scan-loading" style="display: none;">
<div class="loading"></div>
<p>Scanez rețeaua...</p>
</div>
<div id="scan-results"></div>
</div> </div>
<div id="scan-results"></div>
</div> </div>
</div> </div>
<script> <!-- Modal pentru redenumirea calculatoarelor -->
let scanModal = document.getElementById('scanModal'); <div id="renameModal" class="modal">
let addModal = document.getElementById('addModal'); <div class="modal-content">
<div class="modal-header">
// Încarcă calculatoarele la start <h2>Redenumește Calculator</h2>
window.onload = function() { <span class="close" onclick="closeRenameModal()">&times;</span>
refreshComputers(); </div>
}; <div class="modal-body">
<div class="form-group">
function showMessage(message, type) { <label for="currentName">Nume Actual:</label>
const messageArea = document.getElementById('message-area'); <input type="text" id="currentName" readonly>
messageArea.innerHTML = `<div class="message ${type}">${message}</div>`;
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 grid = document.getElementById('computers-grid');
if (computers.length === 0) {
grid.innerHTML = `
<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #6b7280;">
<p style="font-size: 1.2rem; margin-bottom: 10px;">📱 Nu există calculatoare configurate</p>
<p>Adaugă calculatoare sau scanează rețeaua pentru a începe</p>
</div>
`;
return;
}
grid.innerHTML = computers.map(computer => `
<div class="computer-card">
<div class="computer-header">
<div class="computer-name">${computer.name}</div>
<div class="status ${computer.status}">${computer.status}</div>
</div>
<div class="computer-info">
<div>🔧 MAC: ${computer.mac}</div>
${computer.ip ? `<div>🌐 IP: ${computer.ip}</div>` : ''}
</div>
<button class="wake-btn" onclick="wakeComputer('${computer.mac}', '${computer.name}', '${computer.ip || ''}')">
⚡ Trezește ${computer.name}
</button>
</div> </div>
`).join(''); <div class="form-group">
} <label for="newName">Nume Nou:</label>
<input type="text" id="newName" placeholder="Introdu noul nume">
</div>
</div>
<div class="form-actions">
<button type="button" onclick="closeRenameModal()">Anulează</button>
<button type="button" onclick="performRename()">Redenumește</button>
</div>
</div>
</div>
function wakeComputer(mac, name, ip) { <script src="{{ url_for('static', filename='js/app.js') }}"></script>
showMessage(`Se trimite magic packet pentru ${name}...`, 'success');
fetch('/api/wake', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({mac: mac, name: name, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
setTimeout(refreshComputers, 2000);
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la trezirea calculatorului: ' + error.message, 'error');
});
}
function wakeAllComputers() {
showMessage('Se trezesc toate calculatoarele...', 'success');
fetch('/api/wake-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
let message = 'Comenzi trimise:\n';
data.results.forEach(result => {
message += `${result.name}: ${result.result.message}\n`;
});
showMessage(message.replace(/\n/g, '<br>'), 'success');
setTimeout(refreshComputers, 3000);
})
.catch(error => {
showMessage('Eroare la trezirea calculatoarelor: ' + error.message, 'error');
});
}
function openAddModal() {
addModal.style.display = 'block';
}
function closeAddModal() {
addModal.style.display = 'none';
document.getElementById('computerName').value = '';
document.getElementById('computerMac').value = '';
document.getElementById('computerIp').value = '';
}
function addComputer() {
const name = document.getElementById('computerName').value;
const mac = document.getElementById('computerMac').value;
const ip = document.getElementById('computerIp').value;
if (!name || !mac) {
showMessage('Numele și MAC-ul sunt obligatorii!', 'error');
return;
}
fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: name, mac: mac, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
closeAddModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la adăugarea calculatorului: ' + error.message, 'error');
});
}
function scanNetwork() {
scanModal.style.display = 'block';
document.getElementById('scan-loading').style.display = 'block';
document.getElementById('scan-results').innerHTML = '';
fetch('/api/scan')
.then(response => response.json())
.then(result => {
document.getElementById('scan-loading').style.display = 'none';
if (result.success) {
displayScanResults(result.computers);
} else {
document.getElementById('scan-results').innerHTML =
`<div class="message error">${result.message}</div>`;
}
})
.catch(error => {
document.getElementById('scan-loading').style.display = 'none';
document.getElementById('scan-results').innerHTML =
`<div class="message error">Eroare la scanare: ${error.message}</div>`;
});
}
function displayScanResults(computers) {
if (computers.length === 0) {
document.getElementById('scan-results').innerHTML =
'<div class="message error">Nu s-au găsit calculatoare în rețea</div>';
return;
}
let html = `
<table class="scan-table">
<thead>
<tr>
<th>IP</th>
<th>MAC</th>
<th>Hostname</th>
<th>Status</th>
<th>Acțiune</th>
</tr>
</thead>
<tbody>
`;
computers.forEach(computer => {
html += `
<tr>
<td>${computer.ip}</td>
<td style="font-family: monospace;">${computer.mac}</td>
<td>${computer.hostname}</td>
<td><span class="status ${computer.status}">${computer.status}</span></td>
<td>
<button class="add-btn" onclick="addFromScan('${computer.hostname}', '${computer.mac}', '${computer.ip}')">
Adaugă
</button>
</td>
</tr>
`;
});
html += '</tbody></table>';
document.getElementById('scan-results').innerHTML = html;
}
function addFromScan(hostname, mac, ip) {
fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({name: hostname, mac: mac, ip: ip})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showMessage(result.message, 'success');
closeScanModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la adăugarea calculatorului: ' + error.message, 'error');
});
}
function closeScanModal() {
scanModal.style.display = 'none';
}
// Închide modalurile când se dă click în afara lor
window.onclick = function(event) {
if (event.target == addModal) {
closeAddModal();
}
if (event.target == scanModal) {
closeScanModal();
}
}
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,86 @@
{
"message": "Scanare completata cu succes. Gasite 13 dispozitive.",
"networks_scanned": "10.0.20.0/24",
"timestamp": "2025-09-05T17:03:30.367Z",
"computers": [
{
"status": "online",
"ip": "10.0.20.1",
"mac": "6c:5a:b0:20:ff:7c",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.36",
"mac": "9c:6b:00:18:5f:23",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.122",
"mac": "d4:3d:7e:de:05:f7",
"hostname": "svnroa"
},
{
"status": "online",
"ip": "10.0.20.144",
"mac": "",
"hostname": "host.docker.internal"
},
{
"status": "online",
"ip": "10.0.20.161",
"mac": "de:ad:be:ef:10:04",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.162",
"mac": "bc:24:11:05:58:5c",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.163",
"mac": "bc:24:11:3e:9d:70",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.164",
"mac": "bc:24:11:90:b4:04",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.165",
"mac": "bc:24:11:da:c2:63",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.170",
"mac": "de:ad:be:ef:10:00",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.200",
"mac": "fc:3f:db:0a:0d:d8",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.201",
"mac": "58:47:ca:7d:51:3b",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.210",
"mac": "b0:a7:b9:4c:28:2c",
"hostname": ""
}
],
"success": true
}

13
data/wol-computers.conf Normal file
View File

@@ -0,0 +1,13 @@
# Format: name|mac|ip
ROUTER|6c:5a:b0:20:ff:7c|10.0.20.1
ROA|9c:6b:00:18:5f:23|10.0.20.36
SVNROA|d4:3d:7e:de:05:f7|10.0.20.122
PVEMINI-1|de:ad:be:ef:10:04|10.0.20.161
PVEMINI-2|bc:24:11:3e:9d:70|10.0.20.163
PVEMINI-3|bc:24:11:05:58:5c|10.0.20.162
PVEMINI-4|bc:24:11:da:c2:63|10.0.20.165
PVEMINI-5|bc:24:11:90:b4:04|10.0.20.164
PVEMINI-PORTAINER|de:ad:be:ef:10:00|10.0.20.170
PVE1|fc:3f:db:0a:0d:d8|10.0.20.200
PVEMINI|74:38:b7:fd:0b:b6|10.0.20.221
|c2:f5:a3:17:a2:dc|10.0.20.229

View File

@@ -1,25 +1,22 @@
version: '3.8'
services: services:
wol-web: wol-web:
build: . build: .
container_name: wol-manager container_name: wol-manager
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8088:8080" - "${WOL_EXTERNAL_PORT:-5000}:5000"
volumes: volumes:
- ./data:/data - ./data:/data
networks: - ./scripts:/scripts
- wol-network - ./app:/app
- /mnt/c:/mnt/c
- /run/WSL:/run/WSL
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
# Necesare pentru Wake-on-LAN - WSL_INTEROP=${WSL_INTEROP}
network_mode: host - FLASK_DEBUG=${FLASK_DEBUG:-false}
network_mode: "${WOL_NETWORK_MODE:-bridge}"
privileged: true privileged: true
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- NET_RAW - NET_RAW
networks:
wol-network:
driver: bridge

View File

@@ -7,6 +7,7 @@ Aplicație web pentru managementul și trezirea calculatoarelor din rețeaua loc
- 🚀 Interfață web modernă pentru gestionarea calculatoarelor - 🚀 Interfață web modernă pentru gestionarea calculatoarelor
- ⚡ Trimitere magic packets Wake-on-LAN - ⚡ Trimitere magic packets Wake-on-LAN
- 🔍 Scanare automată a rețelei pentru detectarea dispozitivelor - 🔍 Scanare automată a rețelei pentru detectarea dispozitivelor
- 🖥️ **Scanare separată Windows** - script PowerShell optimizat pentru scanarea precisă din Windows
- 📱 Design responsive (mobile-friendly) - 📱 Design responsive (mobile-friendly)
- 🐳 Containerizată cu Docker pentru deployment ușor - 🐳 Containerizată cu Docker pentru deployment ușor
@@ -23,27 +24,30 @@ Aplicație web pentru managementul și trezirea calculatoarelor din rețeaua loc
mkdir -p data mkdir -p data
# Pornește serviciile # Pornește serviciile
docker-compose up -d docker compose up -d
# Vezi logs # Vezi logs
docker-compose logs -f wol-web docker compose logs -f wol-web
# Oprește serviciile # Oprește serviciile
docker-compose down docker compose down
``` ```
## Accesare ## Accesare
Aplicația va fi disponibilă pe: **http://IP_HOST:8088** Aplicația va fi disponibilă pe: **http://IP_HOST:5000**
## Structura Proiectului ## Structura Proiectului
``` ```
├── app/ # Directorul aplicației ├── app/ # Directorul aplicației
│ ├── app.py # Aplicația Flask principală │ ├── app.py # Aplicația Flask principală
│ ├── requirements.txt # Dependențele Python │ ├── requirements.txt # Dependințele Python
│ └── templates/ │ └── templates/
│ └── index.html # Interfața web │ └── index.html # Interfața web
├── scripts/ # Script-uri pentru scanarea Windows
│ ├── windows-network-scan.ps1 # Script PowerShell pentru scanare
│ └── run-scan.ps1 # Wrapper pentru scanare
├── Dockerfile # Configurație Docker ├── Dockerfile # Configurație Docker
├── docker-compose.yml # Orchestrare servicii ├── docker-compose.yml # Orchestrare servicii
├── start.sh # Script de pornire rapidă ├── start.sh # Script de pornire rapidă
@@ -51,6 +55,35 @@ Aplicația va fi disponibilă pe: **http://IP_HOST:8088**
└── data/ # Directorul pentru configurații (creat automat) └── data/ # Directorul pentru configurații (creat automat)
``` ```
## Scanare Rețea
### Scanare automată din container (limitată)
Aplicația poate scana automat din container, dar cu limitări în Docker Desktop pe Windows.
### Scanare separată Windows (recomandată)
Pentru rezultate complete, rulează script-ul PowerShell din Windows:
```powershell
# Scanare cu meniu interactiv
scripts\windows-network-scan.ps1
# Scanare rețea specificată
scripts\windows-network-scan.ps1 -Network "192.168.1.0/24"
# Cu opțiuni avansate
scripts\windows-network-scan.ps1 -Network "192.168.100.0/24" -TimeoutMs 500 -BatchSize 20 -Verbose
```
**Funcționalități script Windows:**
- 🔍 Detectare automată rețele locale
- 📋 Meniu interactiv pentru selecția rețelei
- ⚡ Scanare paralelizată cu batch-uri configurabile
- 🏷️ Rezolvare hostname-uri
- 📊 Afișare progres în timp real
- 💾 Salvare rezultate în JSON pentru aplicația web
Rezultatele scanului sunt salvate în `data/network-scan-results.json` și citite automat de aplicația web.
## Configurare ## Configurare
Calculatoarele sunt stocate în fișierul `data/wol-computers.conf` cu formatul: Calculatoarele sunt stocate în fișierul `data/wol-computers.conf` cu formatul:

182
scripts/run-scan.ps1 Normal file
View File

@@ -0,0 +1,182 @@
# Quick launcher for Windows network scan
# Usage: .\run-scan.ps1 [-Network "192.168.1.0/24"] [-BatchSize 5] [-Fast] [-Verbose]
# Or run without parameters for interactive menu
param(
[string]$Network = "",
[int]$BatchSize = 0,
[switch]$Verbose,
[switch]$Fast
)
function Show-ScanOptionsMenu {
Write-Host "`nWOL Manager - Optiuni de Scanare" -ForegroundColor Yellow
Write-Host "=================================" -ForegroundColor Yellow
Write-Host ""
# Scan mode selection
Write-Host "Selecteaza modul de scanare:" -ForegroundColor White
Write-Host " 1. Scan rapid (20 paralele, 500ms timeout)" -ForegroundColor Yellow
Write-Host " 2. Scan standard (20 paralele, 1000ms timeout)" -ForegroundColor Cyan
Write-Host " 3. Scan conservativ (10 paralele, 1500ms timeout)" -ForegroundColor Green
Write-Host " 4. Configurare personalizata" -ForegroundColor Magenta
Write-Host ""
Write-Host " 0. Anuleaza" -ForegroundColor Red
Write-Host ""
do {
$scanChoice = Read-Host "Selecteaza optiunea (0-4)"
try {
$scanChoiceNum = [int]$scanChoice
} catch {
Write-Host "Te rog introdu un numar valid!" -ForegroundColor Red
continue
}
if ($scanChoiceNum -eq 0) {
Write-Host "Scanare anulata." -ForegroundColor Yellow
exit 0
}
elseif ($scanChoiceNum -ge 1 -and $scanChoiceNum -le 4) {
break
} else {
Write-Host "Optiune invalida! Selecteaza intre 0 si 4." -ForegroundColor Red
}
} while ($true)
# Set scan parameters based on choice
$scanParams = @{}
switch ($scanChoiceNum) {
1 {
$scanParams.BatchSize = 20
$scanParams.TimeoutMs = 500
$scanParams.Description = "Rapid"
}
2 {
$scanParams.BatchSize = 20
$scanParams.TimeoutMs = 1000
$scanParams.Description = "Standard"
}
3 {
$scanParams.BatchSize = 10
$scanParams.TimeoutMs = 1500
$scanParams.Description = "Conservativ"
}
4 {
Write-Host "`nConfigurare personalizata:" -ForegroundColor Magenta
do {
$customBatch = Read-Host "Numar de procese paralele (1-50, recomandat: 3-10)"
try {
$batchNum = [int]$customBatch
if ($batchNum -ge 1 -and $batchNum -le 50) {
$scanParams.BatchSize = $batchNum
break
} else {
Write-Host "Valoare intre 1 si 50!" -ForegroundColor Red
}
} catch {
Write-Host "Te rog introdu un numar valid!" -ForegroundColor Red
}
} while ($true)
do {
$customTimeout = Read-Host "Timeout per ping in ms (100-5000, recomandat: 500-2000)"
try {
$timeoutNum = [int]$customTimeout
if ($timeoutNum -ge 100 -and $timeoutNum -le 5000) {
$scanParams.TimeoutMs = $timeoutNum
break
} else {
Write-Host "Valoare intre 100 si 5000!" -ForegroundColor Red
}
} catch {
Write-Host "Te rog introdu un numar valid!" -ForegroundColor Red
}
} while ($true)
$scanParams.Description = "Personalizat"
}
}
# Ask for verbose mode
Write-Host ""
$verboseChoice = Read-Host "Afiseaza progres detaliat? (y/n, default: n)"
$scanParams.Verbose = ($verboseChoice -eq "y" -or $verboseChoice -eq "Y")
return $scanParams
}
$scriptPath = Join-Path $PSScriptRoot "windows-network-scan.ps1"
$outputPath = Join-Path (Split-Path $PSScriptRoot -Parent) "data\network-scan-results.json"
Write-Host "WOL Manager - Windows Network Scanner" -ForegroundColor Yellow
Write-Host "=====================================" -ForegroundColor Yellow
if (-not (Test-Path $scriptPath)) {
Write-Error "Scanner script not found: $scriptPath"
exit 1
}
# Determine if we should show interactive menu
$hasParams = $PSBoundParameters.Count -gt 0 -or $Network -or $BatchSize -gt 0 -or $Verbose -or $Fast
if (-not $hasParams) {
# Show interactive menu
$menuParams = Show-ScanOptionsMenu
$BatchSize = $menuParams.BatchSize
$TimeoutMs = $menuParams.TimeoutMs
$Verbose = $menuParams.Verbose
$scanMode = $menuParams.Description
Write-Host "`nMod selectat: $scanMode ($BatchSize paralele, $($TimeoutMs)ms timeout)" -ForegroundColor Green
}
# Set up scan parameters
$params = @{
OutputPath = $outputPath
}
# Handle command line parameters vs interactive menu
if ($hasParams) {
# Command line mode
if ($Fast) {
$params.TimeoutMs = 200
$params.BatchSize = 20
Write-Host "Fast scan mode enabled (200ms timeout, 20 parallel)" -ForegroundColor Yellow
} else {
$params.TimeoutMs = 1000
$params.BatchSize = if ($BatchSize -gt 0) { $BatchSize } else { 5 }
Write-Host "Standard scan mode ($($params.BatchSize) parallel, $($params.TimeoutMs)ms timeout)" -ForegroundColor Green
}
if ($Verbose) {
$params.Verbose = $true
}
} else {
# Interactive menu mode - parameters already set above
$params.BatchSize = $BatchSize
$params.TimeoutMs = $TimeoutMs
if ($Verbose) {
$params.Verbose = $true
}
}
if ($Network) {
$params.Network = $Network
Write-Host "Scanning network: $Network" -ForegroundColor Cyan
} else {
Write-Host "Network will be selected interactively" -ForegroundColor Cyan
}
try {
& $scriptPath @params
if ($LASTEXITCODE -eq 0) {
Write-Host "`nScan completed successfully!" -ForegroundColor Green
Write-Host "Results saved to: $outputPath" -ForegroundColor Green
}
} catch {
Write-Error "Failed to run network scan: $_"
exit 1
}

View File

@@ -0,0 +1,403 @@
# Windows Network Scanner for WOL Manager
# This script scans the local network for devices and extracts their MAC addresses
# Results are saved to a JSON file that can be read by the Docker container
param(
[string]$Network = "",
[string]$OutputPath = ".\data\network-scan-results.json",
[int]$TimeoutMs = 1000,
[int]$BatchSize = 10,
[switch]$Verbose
)
function Write-Log {
param([string]$Message)
if ($Verbose) {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $Message" -ForegroundColor Green
}
}
function Get-LocalNetworks {
Write-Log "Detecting local networks..."
$networks = @()
# Get all network adapters that are up and have an IP address
$adapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual")
}
foreach ($adapter in $adapters) {
$networkAddr = $adapter.IPAddress
$prefixLength = $adapter.PrefixLength
# Simple network calculation for /24 networks (most common)
if ($prefixLength -eq 24) {
$parts = $networkAddr.Split('.')
if ($parts.Length -eq 4) {
$networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0"
$networks += "$networkAddress/24"
Write-Log "Found network: $networkAddress/24 from adapter $($adapter.IPAddress)"
}
}
elseif ($prefixLength -eq 16) {
$parts = $networkAddr.Split('.')
if ($parts.Length -eq 4) {
$networkAddress = "$($parts[0]).$($parts[1]).0.0"
$networks += "$networkAddress/16"
Write-Log "Found network: $networkAddress/16 from adapter $($adapter.IPAddress)"
}
}
else {
Write-Log "Skipping unsupported prefix length /$prefixLength for $networkAddr"
}
}
# Add common default networks if none found
if ($networks.Count -eq 0) {
$networks += "192.168.1.0/24"
Write-Log "No networks auto-detected, using default: 192.168.1.0/24"
}
return $networks
}
function Show-NetworkSelectionMenu {
Write-Host "`nWOL Manager - Selectare Retea" -ForegroundColor Yellow
Write-Host "===============================" -ForegroundColor Yellow
# Try to detect local networks
$detectedNetworks = @()
$adapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual")
}
foreach ($adapter in $adapters) {
$parts = $adapter.IPAddress.Split('.')
if ($parts.Length -eq 4) {
$networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24"
if ($detectedNetworks -notcontains $networkAddr) {
$detectedNetworks += $networkAddr
}
}
}
# Show menu
Write-Host "`nSelecteaza reteaua de scanat:" -ForegroundColor White
Write-Host ""
$menuOptions = @()
$optionIndex = 1
# Add detected networks
if ($detectedNetworks.Count -gt 0) {
Write-Host "Retele detectate automat:" -ForegroundColor Green
foreach ($detectedNetwork in $detectedNetworks) {
Write-Host " $optionIndex. $detectedNetwork" -ForegroundColor Cyan
$menuOptions += $detectedNetwork
$optionIndex++
}
Write-Host ""
}
# Add common networks
Write-Host "Retele comune:" -ForegroundColor Green
$commonNetworks = @("192.168.1.0/24", "192.168.0.0/24", "10.0.0.0/24", "10.0.20.0/24", "172.16.0.0/24")
foreach ($commonNetwork in $commonNetworks) {
if ($detectedNetworks -notcontains $commonNetwork) {
Write-Host " $optionIndex. $commonNetwork" -ForegroundColor Cyan
$menuOptions += $commonNetwork
$optionIndex++
}
}
Write-Host ""
Write-Host " $optionIndex. Introdu manual reteaua" -ForegroundColor Yellow
$menuOptions += "custom"
Write-Host ""
Write-Host " 0. Iesire" -ForegroundColor Red
Write-Host ""
# Get user choice
do {
$choice = Read-Host "Selecteaza optiunea (0-$($menuOptions.Count))"
# Convert to integer for comparison
try {
$choiceNum = [int]$choice
} catch {
Write-Host "Te rog introdu un numar valid!" -ForegroundColor Red
continue
}
if ($choiceNum -eq 0) {
Write-Host "Scanare anulata." -ForegroundColor Yellow
exit 0
}
elseif ($choiceNum -ge 1 -and $choiceNum -le $menuOptions.Count) {
if ($menuOptions[$choiceNum - 1] -eq "custom") {
do {
$customNetwork = Read-Host "Introdu reteaua in format CIDR (ex: 192.168.100.0/24)"
if ($customNetwork -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$') {
return @($customNetwork)
} else {
Write-Host "Format invalid! Foloseste formatul: 192.168.1.0/24" -ForegroundColor Red
}
} while ($true)
} else {
return @($menuOptions[$choiceNum - 1])
}
} else {
Write-Host "Optiune invalida! Selecteaza intre 0 si $($menuOptions.Count)." -ForegroundColor Red
}
} while ($true)
}
function Test-NetworkAddress {
param(
[string]$IPAddress,
[int]$TimeoutMs = 1000
)
try {
$ping = New-Object System.Net.NetworkInformation.Ping
$result = $ping.Send($IPAddress, $TimeoutMs)
return $result.Status -eq "Success"
}
catch {
return $false
}
}
function Get-NetworkDevices {
param([string]$NetworkCIDR)
Write-Log "Scanning network: $NetworkCIDR"
# Parse network CIDR
$parts = $NetworkCIDR -split '/'
$networkAddress = $parts[0]
$prefixLength = [int]$parts[1]
# Calculate IP range
$networkParts = $networkAddress -split '\.'
$baseIP = "$($networkParts[0]).$($networkParts[1]).$($networkParts[2])"
# Determine scan range based on prefix length
if ($prefixLength -eq 24) {
$startIP = 1
$endIP = 254
} else {
Write-Log "Only /24 networks are supported in this version"
return @()
}
$devices = @()
$aliveIPs = @()
# Simplified ping sweep with progress indication
Write-Log "Starting ping sweep for $($endIP - $startIP + 1) addresses..."
# Use the configurable batch size parameter
$totalIPs = $endIP - $startIP + 1
$processed = 0
Write-Log "Scanning $totalIPs addresses in batches of $BatchSize..."
# Process IPs in smaller batches to avoid overwhelming the system
for ($batch = $startIP; $batch -le $endIP; $batch += $BatchSize) {
$batchEnd = [Math]::Min($batch + $BatchSize - 1, $endIP)
$jobs = @()
Write-Log "Batch progress: $([Math]::Floor(($batch - $startIP) * 100 / $totalIPs))% - Scanning IPs $batch to $batchEnd"
# Create jobs for current batch
for ($i = $batch; $i -le $batchEnd; $i++) {
$ip = "$baseIP.$i"
$job = Start-Job -ScriptBlock {
param($IPAddress, $TimeoutMs)
try {
$ping = New-Object System.Net.NetworkInformation.Ping
$result = $ping.Send($IPAddress, $TimeoutMs)
return @{
IP = $IPAddress
Success = ($result.Status -eq "Success")
ResponseTime = if ($result.Status -eq "Success") { $result.RoundtripTime } else { $null }
}
}
catch {
return @{
IP = $IPAddress
Success = $false
ResponseTime = $null
}
}
} -ArgumentList $ip, $TimeoutMs
$jobs += @{ Job = $job; IP = $ip }
}
# Wait for batch to complete with timeout
$maxWaitTime = ($TimeoutMs * 2) / 1000 # Convert to seconds, double the ping timeout
foreach ($jobInfo in $jobs) {
$result = Wait-Job -Job $jobInfo.Job -Timeout $maxWaitTime | Receive-Job
Remove-Job -Job $jobInfo.Job -Force
if ($result -and $result.Success) {
$aliveIPs += $result.IP
Write-Log "Found alive host: $($result.IP) ($($result.ResponseTime)ms)"
}
$processed++
}
# Show progress more frequently
$progressPercent = [Math]::Floor($processed * 100 / $totalIPs)
Write-Log "Progress: $processed/$totalIPs addresses scanned ($progressPercent%)"
# Small delay between batches to prevent system overload
Start-Sleep -Milliseconds 100
}
Write-Log "Found $($aliveIPs.Count) alive hosts"
# Get ARP table to find MAC addresses
Write-Log "Retrieving MAC addresses from ARP table..."
$arpEntries = @{}
try {
# Use Get-NetNeighbor for Windows 8/Server 2012 and later
$neighbors = Get-NetNeighbor -AddressFamily IPv4 | Where-Object { $_.State -ne "Unreachable" }
foreach ($neighbor in $neighbors) {
if ($neighbor.LinkLayerAddress -and $neighbor.LinkLayerAddress -ne "00-00-00-00-00-00") {
$arpEntries[$neighbor.IPAddress] = $neighbor.LinkLayerAddress.Replace('-', ':').ToLower()
}
}
}
catch {
Write-Log "Get-NetNeighbor failed, trying arp command..."
# Fallback to arp command
try {
$arpOutput = arp -a
foreach ($line in $arpOutput) {
if ($line -match '(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]{17})\s+\w+') {
$ip = $matches[1]
$mac = $matches[2].Replace('-', ':').ToLower()
if ($mac -ne "00:00:00:00:00:00") {
$arpEntries[$ip] = $mac
}
}
}
}
catch {
Write-Log "Warning: Could not retrieve ARP table"
}
}
# Build device list
foreach ($ip in $aliveIPs) {
$device = @{
ip = $ip
mac = if ($arpEntries.ContainsKey($ip)) { $arpEntries[$ip] } else { "" }
hostname = ""
status = "online"
}
# Try to resolve hostname
try {
$hostname = [System.Net.Dns]::GetHostEntry($ip).HostName
if ($hostname -and $hostname -ne $ip) {
$device.hostname = $hostname
}
}
catch {
# Hostname resolution failed, leave empty
}
$devices += $device
if ($device.mac) {
Write-Log "Device found: $ip -> $($device.mac) ($($device.hostname))"
} else {
Write-Log "Device found: $ip (no MAC address available)"
}
}
return $devices
}
# Main execution
try {
Write-Log "Starting Windows network scan..."
# Determine networks to scan
$networksToScan = @()
if ($Network) {
# Use specified network
$networksToScan += $Network
} else {
# Show interactive menu for network selection
$networksToScan = Show-NetworkSelectionMenu
}
if ($networksToScan.Count -eq 0) {
Write-Error "No networks to scan found"
exit 1
}
$allDevices = @()
# Scan each network
foreach ($net in $networksToScan) {
Write-Log "Processing network: $net"
$devices = Get-NetworkDevices -NetworkCIDR $net
$allDevices += $devices
}
# Prepare result object
$result = @{
success = $true
timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ")
networks_scanned = $networksToScan
computers = $allDevices
message = "Scanare completata cu succes. Gasite $($allDevices.Count) dispozitive."
}
# Ensure output directory exists
$outputDir = Split-Path -Parent $OutputPath
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
# Save results to JSON
$result | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Log "Results saved to: $OutputPath"
Write-Log "Scan completed successfully. Found $($allDevices.Count) devices."
# Also output to console for immediate feedback
if ($allDevices.Count -gt 0) {
Write-Host "`nDevices found:" -ForegroundColor Yellow
foreach ($device in $allDevices) {
$macInfo = if ($device.mac) { $device.mac } else { "No MAC" }
$hostInfo = if ($device.hostname) { " ($($device.hostname))" } else { "" }
Write-Host " $($device.ip) -> $macInfo$hostInfo" -ForegroundColor Cyan
}
}
}
catch {
$errorResult = @{
success = $false
timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ")
message = "Eroare la scanare: $($_.Exception.Message)"
computers = @()
}
$errorResult | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Error "Scan failed: $($_.Exception.Message)"
exit 1
}

View File

@@ -8,6 +8,6 @@ mkdir -p data
# Pornește containerele # Pornește containerele
docker-compose up -d docker-compose up -d
echo "✅ Aplicația rulează pe http://IP_LXC:8088" echo "✅ Aplicația rulează pe http://IP_LXC:5000"
echo "📊 Vezi logs cu: docker-compose logs -f" echo "📊 Vezi logs cu: docker-compose logs -f"
echo "⏹️ Oprește cu: docker-compose down" echo "⏹️ Oprește cu: docker-compose down"