Compare commits
8 Commits
f7b0c28d1a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb712ae0b | |||
| 43e997ba47 | |||
| 3c14094650 | |||
| 072553953e | |||
| 86931a091e | |||
| 48db3a11d0 | |||
| acf234c600 | |||
| 616763c603 |
5
.env
Normal file
5
.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# WOL Manager Environment Configuration
|
||||
WOL_NETWORK_MODE=bridge
|
||||
WOL_EXTERNAL_PORT=5000
|
||||
FLASK_PORT=5000
|
||||
FLASK_DEBUG=true
|
||||
23
.env.example
Normal file
23
.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
|
||||
# Flask internal port (port on which Flask app runs inside container)
|
||||
FLASK_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
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
data/network-scan-results.json
|
||||
data/wol-computers.conf
|
||||
80
CLAUDE.md
80
CLAUDE.md
@@ -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
|
||||
- **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
|
||||
|
||||
@@ -57,3 +106,22 @@ docker-compose exec wol-web bash
|
||||
- No authentication/authorization implemented
|
||||
- Configuration persisted in volume-mounted `/data` directory
|
||||
- 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
|
||||
427
app/app.py
427
app/app.py
@@ -95,43 +95,311 @@ 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 edit_computer(self, old_name, new_name, new_mac, new_ip=None):
|
||||
"""Editează un calculator existent - permite modificarea numelui, MAC-ului și IP-ului"""
|
||||
if not new_name.strip():
|
||||
return {'success': False, 'message': 'Numele nou nu poate fi gol!'}
|
||||
|
||||
if not new_mac.strip():
|
||||
return {'success': False, 'message': 'Adresa MAC nu poate fi goală!'}
|
||||
|
||||
# Validează formatul MAC-ului (XX:XX:XX:XX:XX:XX)
|
||||
import re
|
||||
mac_pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'
|
||||
if not re.match(mac_pattern, new_mac):
|
||||
return {'success': False, 'message': 'Formatul MAC-ului este invalid! Folosește formatul XX:XX:XX:XX:XX:XX'}
|
||||
|
||||
# Normalizează MAC-ul (lowercase și cu :)
|
||||
new_mac = new_mac.lower().replace('-', ':')
|
||||
|
||||
computers = []
|
||||
found = False
|
||||
old_mac = 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:
|
||||
if parts[0] == old_name:
|
||||
old_mac = parts[1]
|
||||
found = True
|
||||
# Verifică dacă noul MAC este diferit și nu există deja
|
||||
if new_mac != old_mac.lower():
|
||||
# Verifică dacă noul MAC există deja la alt computer
|
||||
for check_line in computers:
|
||||
if not check_line.startswith('#') and check_line.strip():
|
||||
check_parts = check_line.split('|')
|
||||
if len(check_parts) >= 2 and check_parts[1].lower() == new_mac:
|
||||
return {'success': False, 'message': f'Adresa MAC {new_mac} este deja folosită de alt computer!'}
|
||||
|
||||
# Actualizează computerul cu noile valori
|
||||
new_ip_value = new_ip.strip() if new_ip else ''
|
||||
computers.append(f"{new_name}|{new_mac}|{new_ip_value}")
|
||||
else:
|
||||
# Verifică dacă noul MAC este folosit de alt computer
|
||||
if parts[1].lower() == new_mac and parts[0] != old_name:
|
||||
return {'success': False, 'message': f'Adresa MAC {new_mac} este deja folosită de computerul "{parts[0]}"!'}
|
||||
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 (doar dacă s-a schimbat numele)
|
||||
if new_name != old_name:
|
||||
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 not computer_line.startswith(f"{new_name}|{new_mac}|"):
|
||||
return {'success': False, 'message': f'Numele "{new_name}" este deja folosit de alt computer!'}
|
||||
|
||||
# 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 actualizat cu succes!'}
|
||||
|
||||
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]
|
||||
# Skip Docker bridge networks
|
||||
if gateway.startswith('172.17.') or gateway.startswith('172.18.') or gateway.startswith('172.19.') or gateway.startswith('172.20.'):
|
||||
continue
|
||||
# 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)
|
||||
|
||||
# Î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 result.stdout.split('\n'):
|
||||
|
||||
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 'unknown'
|
||||
hostname = line.split()[0] if line.split() else '?'
|
||||
|
||||
# Skip Docker bridge networks
|
||||
if ip.startswith('172.17.') or ip.startswith('172.18.') or ip.startswith('172.19.') or ip.startswith('172.20.'):
|
||||
continue
|
||||
|
||||
# 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 (fără Docker bridge)
|
||||
scanned.append({
|
||||
'ip': ip,
|
||||
'mac': mac,
|
||||
@@ -139,14 +407,29 @@ class WOLManager:
|
||||
'status': self.ping_computer(ip)
|
||||
})
|
||||
|
||||
# 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.'
|
||||
}
|
||||
|
||||
# Dacă nu s-au găsit dispozitive în autodetectare, probabil rulează în Docker
|
||||
if not custom_network and not scanned:
|
||||
return {
|
||||
'success': True,
|
||||
'computers': [],
|
||||
'message': 'Nu s-au găsit dispozitive în rețeaua locală. Aplicația rulează în Docker cu acces limitat la rețea. Pentru rezultate complete, rulează scanul Windows din sistemul host sau specifică manual o rețea CIDR.'
|
||||
}
|
||||
|
||||
return {'success': True, 'computers': scanned}
|
||||
except Exception as e:
|
||||
return {'success': False, 'message': f'Eroare la scanare: {str(e)}'}
|
||||
|
||||
wol_manager = WOLManager()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# Hot reload is working!
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/computers')
|
||||
@@ -191,10 +474,114 @@ 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/edit', methods=['POST'])
|
||||
def edit_computer():
|
||||
data = request.get_json()
|
||||
result = wol_manager.edit_computer(
|
||||
data.get('old_name'),
|
||||
data.get('new_name'),
|
||||
data.get('new_mac'),
|
||||
data.get('new_ip')
|
||||
)
|
||||
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=8080, debug=False)
|
||||
# Get port from environment variable or default to 5000
|
||||
port = int(os.environ.get('FLASK_PORT', 5000))
|
||||
# Enable debug mode for development with hot reload
|
||||
app.run(host='0.0.0.0', port=port, debug=True, use_reloader=True)
|
||||
727
app/static/css/style.css
Normal file
727
app/static/css/style.css
Normal file
@@ -0,0 +1,727 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.computer-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.computer-name.online {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.computer-name.offline {
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.computer-name.unknown {
|
||||
color: #808000;
|
||||
}
|
||||
|
||||
/* Keep status class for scan modal compatibility */
|
||||
.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;
|
||||
}
|
||||
|
||||
.edit-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;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
background-color: #E8E5E2;
|
||||
}
|
||||
|
||||
.edit-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;
|
||||
}
|
||||
|
||||
.device-checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Styles for existing/disabled devices in scan modal */
|
||||
.scan-table tr.device-exists {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.scan-table tr.device-exists td {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.scan-table tr.device-exists:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.scan-table tr.device-exists .add-btn {
|
||||
background-color: #e8e8e8;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
border: 1px solid #d4d4d4;
|
||||
}
|
||||
|
||||
/* Wider scan modal for the additional column with proper scrolling */
|
||||
#scanModal .modal-content {
|
||||
width: 700px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#scanModal .modal-body {
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.edit-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%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
#scanModal .modal-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.no-computers {
|
||||
font-size: 16px;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
/* Make table horizontally scrollable on small screens */
|
||||
.main-content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.computers-table {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.computer-name {
|
||||
font-size: 15px;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.computers-table td {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.computers-table td:first-child {
|
||||
padding: 4px 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
680
app/static/js/app.js
Normal file
680
app/static/js/app.js
Normal file
@@ -0,0 +1,680 @@
|
||||
// WOL Manager JavaScript
|
||||
let scanModal, addModal, editModal;
|
||||
|
||||
// Initialize on page load
|
||||
window.onload = function() {
|
||||
scanModal = document.getElementById('scanModal');
|
||||
addModal = document.getElementById('addModal');
|
||||
editModal = document.getElementById('editModal');
|
||||
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';
|
||||
|
||||
// Sort computers by IP address
|
||||
const sortedComputers = computers.slice().sort((a, b) => {
|
||||
const ipA = a.ip || '';
|
||||
const ipB = b.ip || '';
|
||||
|
||||
if (!ipA && !ipB) return 0;
|
||||
if (!ipA) return 1;
|
||||
if (!ipB) return -1;
|
||||
|
||||
const parseIP = (ip) => {
|
||||
return ip.split('.').map(num => parseInt(num, 10).toString().padStart(3, '0')).join('.');
|
||||
};
|
||||
|
||||
return parseIP(ipA).localeCompare(parseIP(ipB));
|
||||
});
|
||||
|
||||
tbody.innerHTML = sortedComputers.map(computer => `
|
||||
<tr>
|
||||
<td>
|
||||
<button class="wake-btn" onclick="wakeComputer('${computer.mac}', '${computer.name}', '${computer.ip || ''}')" title="Trezește calculatorul">
|
||||
⚡
|
||||
</button>
|
||||
<button class="edit-btn" onclick="openEditModal('${computer.name}', '${computer.mac}', '${computer.ip || ''}')" title="Editează calculatorul">
|
||||
📝
|
||||
</button>
|
||||
<button class="delete-btn" onclick="deleteComputer('${computer.name}', '${computer.mac}')" title="Șterge calculatorul">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
<td class="computer-name ${computer.status}">${computer.name}</td>
|
||||
<td>${computer.ip || '-'}</td>
|
||||
<td style="font-family: monospace;">${computer.mac}</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 } : {};
|
||||
|
||||
// Get current computers list to compare with scan results
|
||||
Promise.all([
|
||||
fetch('/api/computers').then(response => response.json()),
|
||||
fetch('/api/scan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
}).then(response => response.json())
|
||||
])
|
||||
.then(([existingComputers, result]) => {
|
||||
document.getElementById('scan-loading').style.display = 'none';
|
||||
|
||||
if (result.success) {
|
||||
if (result.computers && result.computers.length > 0) {
|
||||
displayScanResults(result.computers, existingComputers);
|
||||
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');
|
||||
|
||||
// Get current computers list to compare with scan results
|
||||
Promise.all([
|
||||
fetch('/api/computers').then(response => response.json()),
|
||||
fetch('/api/scan/windows', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
}).then(response => response.json())
|
||||
])
|
||||
.then(([existingComputers, 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, existingComputers);
|
||||
} 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, existingComputers = []) {
|
||||
if (computers.length === 0) {
|
||||
document.getElementById('scan-results').innerHTML =
|
||||
'<div class="message error">Nu s-au găsit calculatoare în rețea</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort computers by IP address
|
||||
const sortedComputers = computers.slice().sort((a, b) => {
|
||||
const ipA = a.ip || '';
|
||||
const ipB = b.ip || '';
|
||||
|
||||
if (!ipA && !ipB) return 0;
|
||||
if (!ipA) return 1;
|
||||
if (!ipB) return -1;
|
||||
|
||||
const parseIP = (ip) => {
|
||||
return ip.split('.').map(num => parseInt(num, 10).toString().padStart(3, '0')).join('.');
|
||||
};
|
||||
|
||||
return parseIP(ipA).localeCompare(parseIP(ipB));
|
||||
});
|
||||
|
||||
// Create a set of existing MAC addresses for quick lookup (normalize to lowercase)
|
||||
const existingMACs = new Set(existingComputers.map(comp => comp.mac.toLowerCase()));
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
sortedComputers.forEach((computer, index) => {
|
||||
const computerMAC = computer.mac.toLowerCase();
|
||||
const deviceExists = existingMACs.has(computerMAC);
|
||||
const rowClass = deviceExists ? 'device-exists' : '';
|
||||
const checkboxDisabled = deviceExists ? 'disabled' : '';
|
||||
const buttonDisabled = deviceExists ? 'disabled' : '';
|
||||
const buttonTitle = deviceExists ? 'Device-ul există deja în sistem' : 'Adaugă calculatorul';
|
||||
|
||||
html += `
|
||||
<tr class="${rowClass}">
|
||||
<td>
|
||||
<input type="checkbox" class="device-checkbox"
|
||||
data-hostname="${computer.hostname}"
|
||||
data-mac="${computer.mac}"
|
||||
data-ip="${computer.ip}"
|
||||
onchange="updateAddButton()"
|
||||
${checkboxDisabled}>
|
||||
</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="${buttonTitle}" ${buttonDisabled}>
|
||||
${deviceExists ? '✓' : '➕'}
|
||||
</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:not(:disabled)');
|
||||
|
||||
deviceCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
|
||||
updateAddButton();
|
||||
}
|
||||
|
||||
function updateAddButton() {
|
||||
const deviceCheckboxes = document.querySelectorAll('.device-checkbox:not(:disabled)');
|
||||
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked:not(:disabled)');
|
||||
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 (only consider enabled checkboxes)
|
||||
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:not(:disabled)');
|
||||
|
||||
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) {
|
||||
// Check if this is called from a disabled button (device already exists)
|
||||
if (event && event.target && event.target.hasAttribute('disabled')) {
|
||||
showMessage('Device-ul există deja în sistem!', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
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 openEditModal(currentName, currentMac, currentIp) {
|
||||
document.getElementById('editName').value = currentName;
|
||||
document.getElementById('editName').dataset.originalName = currentName;
|
||||
document.getElementById('editMac').value = currentMac;
|
||||
document.getElementById('editIp').value = currentIp || '';
|
||||
editModal.style.display = 'block';
|
||||
document.getElementById('editName').focus();
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
editModal.style.display = 'none';
|
||||
document.getElementById('editName').value = '';
|
||||
document.getElementById('editMac').value = '';
|
||||
document.getElementById('editIp').value = '';
|
||||
}
|
||||
|
||||
function performEdit() {
|
||||
const oldName = document.getElementById('editName').dataset.originalName || document.getElementById('editName').value;
|
||||
const newName = document.getElementById('editName').value.trim();
|
||||
const newMac = document.getElementById('editMac').value.trim();
|
||||
const newIp = document.getElementById('editIp').value.trim();
|
||||
|
||||
if (!newName) {
|
||||
showMessage('Numele nu poate fi gol!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newMac) {
|
||||
showMessage('Adresa MAC nu poate fi goală!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validare simplă pentru formatul MAC-ului
|
||||
const macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
if (!macPattern.test(newMac)) {
|
||||
showMessage('Formatul MAC-ului este invalid! Folosește formatul XX:XX:XX:XX:XX:XX', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/edit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
old_name: oldName,
|
||||
new_name: newName,
|
||||
new_mac: newMac,
|
||||
new_ip: newIp
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showMessage(result.message, 'success');
|
||||
closeEditModal();
|
||||
refreshComputers();
|
||||
} else {
|
||||
showMessage(result.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Eroare la editarea 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 == editModal) {
|
||||
closeEditModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Enter key to perform edit
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter' && editModal.style.display === 'block') {
|
||||
performEdit();
|
||||
}
|
||||
});
|
||||
@@ -4,355 +4,64 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wake-on-LAN Manager</title>
|
||||
<style>
|
||||
* {
|
||||
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>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<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 class="controls">
|
||||
<button onclick="refreshComputers()">🔄 Refresh</button>
|
||||
<button onclick="openAddModal()" class="scan">➕ Adaugă Calculator</button>
|
||||
<button onclick="scanNetwork()" class="scan">🔍 Scanează Rețeaua</button>
|
||||
<button onclick="wakeAllComputers()" class="wake-all">⚡ Trezește Toate</button>
|
||||
<div class="toolbar">
|
||||
<button onclick="refreshComputers()">Refresh</button>
|
||||
<button onclick="openAddModal()" title="Adaugă Calculator">➕ Adaugă Calculator</button>
|
||||
<div class="network-selector">
|
||||
<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 id="computers-grid" class="computer-grid">
|
||||
<div class="main-content">
|
||||
<table id="computers-table" class="computers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Acțiuni</th>
|
||||
<th>Nume Calculator</th>
|
||||
<th>Adresa IP</th>
|
||||
<th>Adresa MAC</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>
|
||||
|
||||
<!-- Modal pentru adăugarea calculatoarelor -->
|
||||
<div id="addModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeAddModal()">×</span>
|
||||
<div class="modal-header">
|
||||
<h2>Adaugă Calculator Nou</h2>
|
||||
<span class="close" onclick="closeAddModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="computerName">Nume Calculator:</label>
|
||||
<input type="text" id="computerName" placeholder="ex: PC Birou">
|
||||
@@ -365,8 +74,9 @@
|
||||
<label for="computerIp">IP (opțional):</label>
|
||||
<input type="text" id="computerIp" placeholder="ex: 192.168.1.100">
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,265 +85,48 @@
|
||||
<!-- Modal pentru scanarea rețelei -->
|
||||
<div id="scanModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeScanModal()">×</span>
|
||||
<div class="modal-header">
|
||||
<h2>Calculatoare din Rețea</h2>
|
||||
<div id="scan-loading" style="display: none; text-align: center;">
|
||||
<span class="close" onclick="closeScanModal()">×</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
let scanModal = document.getElementById('scanModal');
|
||||
let addModal = document.getElementById('addModal');
|
||||
|
||||
// Încarcă calculatoarele la start
|
||||
window.onload = function() {
|
||||
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 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>
|
||||
<!-- Modal pentru editarea calculatoarelor -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Editează Calculator</h2>
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="editName">Nume Calculator:</label>
|
||||
<input type="text" id="editName" placeholder="ex: PC Birou">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editMac">Adresa MAC:</label>
|
||||
<input type="text" id="editMac" placeholder="ex: 00:11:22:33:44:55">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editIp">IP (opțional):</label>
|
||||
<input type="text" id="editIp" placeholder="ex: 192.168.1.100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" onclick="closeEditModal()">Anulează</button>
|
||||
<button type="button" onclick="performEdit()">Salvează</button>
|
||||
</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>
|
||||
`).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:\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>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +1,23 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
wol-web:
|
||||
build: .
|
||||
container_name: wol-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8088:8080"
|
||||
- "${WOL_EXTERNAL_PORT:-5000}:5000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
networks:
|
||||
- wol-network
|
||||
- ./scripts:/scripts
|
||||
- ./app:/app
|
||||
- /mnt/c:/mnt/c
|
||||
- /run/WSL:/run/WSL
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
# Necesare pentru Wake-on-LAN
|
||||
network_mode: host
|
||||
- WSL_INTEROP=${WSL_INTEROP}
|
||||
- FLASK_DEBUG=${FLASK_DEBUG:-false}
|
||||
- FLASK_PORT=${FLASK_PORT:-5000}
|
||||
network_mode: "${WOL_NETWORK_MODE:-bridge}"
|
||||
privileged: true
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
|
||||
networks:
|
||||
wol-network:
|
||||
driver: bridge
|
||||
43
readme.md
43
readme.md
@@ -7,6 +7,7 @@ Aplicație web pentru managementul și trezirea calculatoarelor din rețeaua loc
|
||||
- 🚀 Interfață web modernă pentru gestionarea calculatoarelor
|
||||
- ⚡ Trimitere magic packets Wake-on-LAN
|
||||
- 🔍 Scanare automată a rețelei pentru detectarea dispozitivelor
|
||||
- 🖥️ **Scanare separată Windows** - script PowerShell optimizat pentru scanarea precisă din Windows
|
||||
- 📱 Design responsive (mobile-friendly)
|
||||
- 🐳 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
|
||||
|
||||
# Pornește serviciile
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
|
||||
# Vezi logs
|
||||
docker-compose logs -f wol-web
|
||||
docker compose logs -f wol-web
|
||||
|
||||
# Oprește serviciile
|
||||
docker-compose down
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Accesare
|
||||
|
||||
Aplicația va fi disponibilă pe: **http://IP_HOST:8088**
|
||||
Aplicația va fi disponibilă pe: **http://IP_HOST:5000**
|
||||
|
||||
## Structura Proiectului
|
||||
|
||||
```
|
||||
├── app/ # Directorul aplicației
|
||||
│ ├── app.py # Aplicația Flask principală
|
||||
│ ├── requirements.txt # Dependențele Python
|
||||
│ ├── requirements.txt # Dependințele Python
|
||||
│ └── templates/
|
||||
│ └── 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
|
||||
├── docker-compose.yml # Orchestrare servicii
|
||||
├── 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)
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
Calculatoarele sunt stocate în fișierul `data/wol-computers.conf` cu formatul:
|
||||
|
||||
182
scripts/run-scan.ps1
Normal file
182
scripts/run-scan.ps1
Normal 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
|
||||
}
|
||||
577
scripts/windows-network-scan.ps1
Normal file
577
scripts/windows-network-scan.ps1
Normal file
@@ -0,0 +1,577 @@
|
||||
# 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
|
||||
|
||||
# Network calculation for different prefix lengths
|
||||
$parts = $networkAddr.Split('.')
|
||||
if ($parts.Length -eq 4) {
|
||||
if ($prefixLength -eq 24) {
|
||||
$networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0"
|
||||
$networks += "$networkAddress/24"
|
||||
Write-Log "Found network: $networkAddress/24 from adapter $($adapter.IPAddress)"
|
||||
}
|
||||
elseif ($prefixLength -eq 20) {
|
||||
# For /20, calculate the network base (e.g., 10.0.20.144/20 -> 10.0.16.0/20)
|
||||
$thirdOctet = [int]$parts[2]
|
||||
$networkThird = $thirdOctet - ($thirdOctet % 16) # Round down to nearest /20 boundary
|
||||
$networkAddress = "$($parts[0]).$($parts[1]).$networkThird.0"
|
||||
$networks += "$networkAddress/20"
|
||||
Write-Log "Found network: $networkAddress/20 from adapter $($adapter.IPAddress)"
|
||||
}
|
||||
elseif ($prefixLength -eq 16) {
|
||||
$networkAddress = "$($parts[0]).$($parts[1]).0.0"
|
||||
$networks += "$networkAddress/16"
|
||||
Write-Log "Found network: $networkAddress/16 from adapter $($adapter.IPAddress)"
|
||||
}
|
||||
else {
|
||||
Write-Log "Unsupported prefix length /$prefixLength for $networkAddr, trying /24"
|
||||
# Fallback to /24 for unknown prefix lengths
|
||||
$networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0"
|
||||
$networks += "$networkAddress/24"
|
||||
Write-Log "Fallback network: $networkAddress/24 from adapter $($adapter.IPAddress)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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) {
|
||||
$prefixLength = $adapter.PrefixLength
|
||||
|
||||
if ($prefixLength -eq 24) {
|
||||
$networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24"
|
||||
} elseif ($prefixLength -eq 20) {
|
||||
$thirdOctet = [int]$parts[2]
|
||||
$networkThird = $thirdOctet - ($thirdOctet % 16)
|
||||
$networkAddr = "$($parts[0]).$($parts[1]).$networkThird.0/20"
|
||||
} elseif ($prefixLength -eq 16) {
|
||||
$networkAddr = "$($parts[0]).$($parts[1]).0.0/16"
|
||||
} else {
|
||||
# Fallback to /24
|
||||
$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-LocalHostMacAddress {
|
||||
param([string]$IPAddress)
|
||||
|
||||
Write-Log "Attempting to find MAC address for potential local host: $IPAddress"
|
||||
|
||||
try {
|
||||
# Method 1: Check if this IP matches any of the local network adapters
|
||||
$localAdapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
|
||||
$_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual")
|
||||
}
|
||||
|
||||
foreach ($adapter in $localAdapters) {
|
||||
if ($adapter.IPAddress -eq $IPAddress) {
|
||||
Write-Log "Found matching local IP address: $IPAddress on interface $($adapter.InterfaceIndex)"
|
||||
|
||||
# Get the corresponding network adapter
|
||||
$netAdapter = Get-NetAdapter -InterfaceIndex $adapter.InterfaceIndex -ErrorAction SilentlyContinue
|
||||
if ($netAdapter -and $netAdapter.MacAddress) {
|
||||
$cleanMac = $netAdapter.MacAddress.Replace('-', ':').ToLower()
|
||||
Write-Log "Found local MAC address: $cleanMac for IP $IPAddress"
|
||||
return $cleanMac
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Method 2: Use WMI to find network adapters with the matching IP
|
||||
$wmiAdapters = Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where-Object {
|
||||
$_.IPEnabled -eq $true -and $_.IPAddress
|
||||
}
|
||||
|
||||
foreach ($wmiAdapter in $wmiAdapters) {
|
||||
if ($wmiAdapter.IPAddress -contains $IPAddress) {
|
||||
if ($wmiAdapter.MACAddress) {
|
||||
$cleanMac = $wmiAdapter.MACAddress.Replace('-', ':').ToLower()
|
||||
Write-Log "Found local MAC via WMI: $cleanMac for IP $IPAddress"
|
||||
return $cleanMac
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Method 3: Check if IP is in the same subnet and try to find the default adapter
|
||||
$localIPs = $localAdapters | ForEach-Object { $_.IPAddress }
|
||||
foreach ($localIP in $localIPs) {
|
||||
$localParts = $localIP -split '\.'
|
||||
$targetParts = $IPAddress -split '\.'
|
||||
|
||||
# Simple check for same /24 network
|
||||
if ($localParts.Length -eq 4 -and $targetParts.Length -eq 4) {
|
||||
if ($localParts[0] -eq $targetParts[0] -and
|
||||
$localParts[1] -eq $targetParts[1] -and
|
||||
$localParts[2] -eq $targetParts[2]) {
|
||||
|
||||
# This IP is in the same subnet, find the adapter for the local IP
|
||||
$matchingAdapter = $localAdapters | Where-Object { $_.IPAddress -eq $localIP }
|
||||
if ($matchingAdapter) {
|
||||
$netAdapter = Get-NetAdapter -InterfaceIndex $matchingAdapter.InterfaceIndex -ErrorAction SilentlyContinue
|
||||
if ($netAdapter -and $netAdapter.MacAddress) {
|
||||
$cleanMac = $netAdapter.MacAddress.Replace('-', ':').ToLower()
|
||||
Write-Log "Found MAC for same subnet adapter: $cleanMac (local IP: $localIP, target: $IPAddress)"
|
||||
return $cleanMac
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "Could not determine MAC address for IP: $IPAddress"
|
||||
return ""
|
||||
}
|
||||
catch {
|
||||
Write-Log "Error finding local MAC address: $($_.Exception.Message)"
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
} elseif ($prefixLength -eq 20) {
|
||||
# For /20 networks (like 10.0.20.0/20), scan the specific /24 subnet where the network is
|
||||
$thirdOctet = [int]$networkParts[2]
|
||||
$baseIP = "$($networkParts[0]).$($networkParts[1]).$thirdOctet"
|
||||
$startIP = 1
|
||||
$endIP = 254
|
||||
Write-Log "Scanning /20 network as /24 subnet: $baseIP.0/24"
|
||||
} elseif ($prefixLength -eq 16) {
|
||||
# For /16 networks, we need to scan multiple /24 subnets
|
||||
# For now, just scan the specific /24 subnet where the network address is
|
||||
$thirdOctet = [int]$networkParts[2]
|
||||
$baseIP = "$($networkParts[0]).$($networkParts[1]).$thirdOctet"
|
||||
$startIP = 1
|
||||
$endIP = 254
|
||||
Write-Log "Scanning /16 network as /24 subnet: $baseIP.0/24"
|
||||
} else {
|
||||
Write-Log "Unsupported prefix length /$prefixLength. Supported: /16, /20, /24"
|
||||
return @()
|
||||
}
|
||||
|
||||
$devices = @()
|
||||
$totalIPs = $endIP - $startIP + 1
|
||||
|
||||
# STEP 1: Get ARP table first to find devices that might not respond to ping
|
||||
Write-Log "Step 1: Retrieving devices from ARP table..."
|
||||
$arpEntries = @{}
|
||||
$arpDevices = @{}
|
||||
|
||||
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") {
|
||||
$cleanMac = $neighbor.LinkLayerAddress.Replace('-', ':').ToLower()
|
||||
$arpEntries[$neighbor.IPAddress] = $cleanMac
|
||||
|
||||
# Check if this IP is in our scan range
|
||||
$ipParts = $neighbor.IPAddress -split '\.'
|
||||
if ($ipParts.Length -eq 4) {
|
||||
$currentBaseIP = "$($ipParts[0]).$($ipParts[1]).$($ipParts[2])"
|
||||
$lastOctet = [int]$ipParts[3]
|
||||
|
||||
if ($currentBaseIP -eq $baseIP -and $lastOctet -ge $startIP -and $lastOctet -le $endIP) {
|
||||
$arpDevices[$neighbor.IPAddress] = @{
|
||||
ip = $neighbor.IPAddress
|
||||
mac = $cleanMac
|
||||
hostname = ""
|
||||
status = "arp"
|
||||
}
|
||||
Write-Log "Found in ARP: $($neighbor.IPAddress) -> $cleanMac"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
# Check if this IP is in our scan range
|
||||
$ipParts = $ip -split '\.'
|
||||
if ($ipParts.Length -eq 4) {
|
||||
$currentBaseIP = "$($ipParts[0]).$($ipParts[1]).$($ipParts[2])"
|
||||
$lastOctet = [int]$ipParts[3]
|
||||
|
||||
if ($currentBaseIP -eq $baseIP -and $lastOctet -ge $startIP -and $lastOctet -le $endIP) {
|
||||
$arpDevices[$ip] = @{
|
||||
ip = $ip
|
||||
mac = $mac
|
||||
hostname = ""
|
||||
status = "arp"
|
||||
}
|
||||
Write-Log "Found in ARP: $ip -> $mac"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log "Warning: Could not retrieve ARP table"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "Found $($arpDevices.Count) devices in ARP table for this network"
|
||||
|
||||
# STEP 2: Ping sweep for remaining IPs not found in ARP
|
||||
Write-Log "Step 2: Starting ping sweep for remaining addresses..."
|
||||
|
||||
$aliveIPs = @()
|
||||
$processed = 0
|
||||
$skippedCount = 0
|
||||
|
||||
# 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 (skip IPs already found in ARP)
|
||||
for ($i = $batch; $i -le $batchEnd; $i++) {
|
||||
$ip = "$baseIP.$i"
|
||||
|
||||
if ($arpDevices.ContainsKey($ip)) {
|
||||
$processed++
|
||||
$skippedCount++
|
||||
continue # Skip IPs already found in ARP
|
||||
}
|
||||
|
||||
$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 via ping: $($result.IP) ($($result.ResponseTime)ms)"
|
||||
}
|
||||
|
||||
$processed++
|
||||
}
|
||||
|
||||
# Show progress more frequently
|
||||
$progressPercent = [Math]::Floor($processed * 100 / $totalIPs)
|
||||
Write-Log "Progress: $processed/$totalIPs addresses scanned ($progressPercent%) - Skipped $skippedCount (already in ARP)"
|
||||
|
||||
# Small delay between batches to prevent system overload
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
Write-Log "Found $($aliveIPs.Count) additional hosts via ping"
|
||||
|
||||
# STEP 3: Combine results and resolve hostnames
|
||||
Write-Log "Step 3: Building final device list..."
|
||||
|
||||
# Add devices found in ARP
|
||||
foreach ($arpDevice in $arpDevices.Values) {
|
||||
$arpDevice.status = "online" # Change from "arp" to "online"
|
||||
$devices += $arpDevice
|
||||
}
|
||||
|
||||
# Add devices found via ping (and try to get their MAC from ARP if available)
|
||||
foreach ($ip in $aliveIPs) {
|
||||
$device = @{
|
||||
ip = $ip
|
||||
mac = if ($arpEntries.ContainsKey($ip)) { $arpEntries[$ip] } else { "" }
|
||||
hostname = ""
|
||||
status = "online"
|
||||
}
|
||||
$devices += $device
|
||||
}
|
||||
|
||||
# Resolve hostnames and fix MAC addresses for local host
|
||||
foreach ($device in $devices) {
|
||||
try {
|
||||
$hostname = [System.Net.Dns]::GetHostEntry($device.ip).HostName
|
||||
if ($hostname -and $hostname -ne $device.ip) {
|
||||
$device.hostname = $hostname
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Hostname resolution failed, leave empty
|
||||
}
|
||||
|
||||
# Special handling for local host (Windows machine running the script)
|
||||
if (-not $device.mac -or $device.mac -eq "") {
|
||||
$device.mac = Get-LocalHostMacAddress -IPAddress $device.ip
|
||||
if ($device.mac) {
|
||||
Write-Log "Found local host MAC: $($device.ip) -> $($device.mac)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($device.mac) {
|
||||
Write-Log "Final device: $($device.ip) -> $($device.mac) ($($device.hostname))"
|
||||
} else {
|
||||
Write-Log "Final device: $($device.ip) (no MAC address available) ($($device.hostname))"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user