Initial commit - WOL Manager Flask application
- Added containerized Flask web application for Wake-on-LAN management - Implemented computer management with file-based configuration - Added network scanning and device discovery functionality - Included Docker setup with privileged networking for WOL operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
59
CLAUDE.md
Normal file
59
CLAUDE.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
See README.md for complete project description, installation instructions, and usage details.
|
||||||
|
|
||||||
|
This is a Wake-on-LAN (WOL) Manager - a containerized Flask web application for managing and remotely waking computers on a local network.
|
||||||
|
|
||||||
|
## Architecture & Code Structure
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
**For installation and basic usage, see README.md**
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
```bash
|
||||||
|
# Development build and run
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# View real-time logs
|
||||||
|
docker-compose logs -f wol-web
|
||||||
|
|
||||||
|
# Shell access to container
|
||||||
|
docker-compose exec wol-web bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Requirements
|
||||||
|
- Runs on port 8088 (external) → 8080 (internal)
|
||||||
|
- Requires `NET_ADMIN` and `NET_RAW` capabilities
|
||||||
|
- Uses `network_mode: host` for WOL packet transmission
|
||||||
|
- Must run with `privileged: true` for network operations
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- Application uses Romanian language in UI
|
||||||
|
- No authentication/authorization implemented
|
||||||
|
- Configuration persisted in volume-mounted `/data` directory
|
||||||
|
- Flask runs in debug=False mode in container
|
||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Instalează dependențele sistem
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wakeonlan \
|
||||||
|
nmap \
|
||||||
|
iputils-ping \
|
||||||
|
net-tools \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Setează directorul de lucru
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Instalează dependențele Python
|
||||||
|
COPY app/requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copiază aplicația
|
||||||
|
COPY app/ .
|
||||||
|
|
||||||
|
# Expune portul
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Comandă de start
|
||||||
|
CMD ["python", "app.py"]
|
||||||
200
app/app.py
Normal file
200
app/app.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import socket
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CONFIG_FILE = '/data/wol-computers.conf'
|
||||||
|
|
||||||
|
class WOLManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.ensure_config_exists()
|
||||||
|
|
||||||
|
def ensure_config_exists(self):
|
||||||
|
os.makedirs('/data', exist_ok=True)
|
||||||
|
if not os.path.exists(CONFIG_FILE):
|
||||||
|
with open(CONFIG_FILE, 'w') as f:
|
||||||
|
f.write("# Format: name|mac|ip\n")
|
||||||
|
|
||||||
|
def load_computers(self):
|
||||||
|
computers = []
|
||||||
|
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:
|
||||||
|
computer = {
|
||||||
|
'name': parts[0],
|
||||||
|
'mac': parts[1],
|
||||||
|
'ip': parts[2] if len(parts) > 2 else '',
|
||||||
|
'status': self.ping_computer(parts[2]) if len(parts) > 2 and parts[2] else 'unknown'
|
||||||
|
}
|
||||||
|
computers.append(computer)
|
||||||
|
return computers
|
||||||
|
|
||||||
|
def ping_computer(self, ip):
|
||||||
|
if not ip:
|
||||||
|
return 'unknown'
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ping', '-c', '1', '-W', '2', ip],
|
||||||
|
capture_output=True, timeout=5)
|
||||||
|
return 'online' if result.returncode == 0 else 'offline'
|
||||||
|
except:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
def wake_computer(self, mac, name, ip=''):
|
||||||
|
try:
|
||||||
|
# Trimite magic packet
|
||||||
|
result = subprocess.run(['wakeonlan', mac], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Dacă avem IP, verifică dacă s-a trezit
|
||||||
|
if ip:
|
||||||
|
for i in range(10): # 30 secunde total
|
||||||
|
if self.ping_computer(ip) == 'online':
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'{name} s-a trezit după {i*3} secunde!',
|
||||||
|
'status': 'online'
|
||||||
|
}
|
||||||
|
if i < 9: # Nu aștepta după ultima încercare
|
||||||
|
subprocess.run(['sleep', '3'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Magic packet trimis pentru {name}, dar nu răspunde la ping',
|
||||||
|
'status': 'unknown'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Magic packet trimis pentru {name}!',
|
||||||
|
'status': 'unknown'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Eroare la trimiterea magic packet: {result.stderr}',
|
||||||
|
'status': 'error'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Eroare: {str(e)}',
|
||||||
|
'status': 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_computer(self, name, mac, ip=''):
|
||||||
|
# Validare MAC
|
||||||
|
mac_pattern = r'^[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}$'
|
||||||
|
if not re.match(mac_pattern, mac):
|
||||||
|
return {'success': False, 'message': 'MAC address invalid!'}
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
try:
|
||||||
|
# Detectează rețeaua locală
|
||||||
|
result = subprocess.run(['ip', 'route'], capture_output=True, text=True)
|
||||||
|
network = '192.168.1.0/24' # default
|
||||||
|
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'default' in line:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3:
|
||||||
|
gateway = parts[2]
|
||||||
|
# Construiește rețeaua bazată pe gateway
|
||||||
|
network_parts = gateway.split('.')
|
||||||
|
network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24"
|
||||||
|
break
|
||||||
|
|
||||||
|
# Scanează rețeaua
|
||||||
|
subprocess.run(['nmap', '-sn', network], capture_output=True, timeout=30)
|
||||||
|
|
||||||
|
# Citește ARP table
|
||||||
|
result = subprocess.run(['arp', '-a'], capture_output=True, text=True)
|
||||||
|
|
||||||
|
scanned = []
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
# Regex pentru parsarea ARP
|
||||||
|
match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\).*([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})', line)
|
||||||
|
if match:
|
||||||
|
ip = match.group(1)
|
||||||
|
mac = match.group(2)
|
||||||
|
hostname = line.split()[0] if line.split() else 'unknown'
|
||||||
|
|
||||||
|
scanned.append({
|
||||||
|
'ip': ip,
|
||||||
|
'mac': mac,
|
||||||
|
'hostname': hostname,
|
||||||
|
'status': self.ping_computer(ip)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {'success': True, 'computers': scanned}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': f'Eroare la scanare: {str(e)}'}
|
||||||
|
|
||||||
|
wol_manager = WOLManager()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/api/computers')
|
||||||
|
def get_computers():
|
||||||
|
return jsonify(wol_manager.load_computers())
|
||||||
|
|
||||||
|
@app.route('/api/wake', methods=['POST'])
|
||||||
|
def wake_computer():
|
||||||
|
data = request.get_json()
|
||||||
|
result = wol_manager.wake_computer(
|
||||||
|
data.get('mac'),
|
||||||
|
data.get('name'),
|
||||||
|
data.get('ip', '')
|
||||||
|
)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@app.route('/api/wake-all', methods=['POST'])
|
||||||
|
def wake_all():
|
||||||
|
computers = wol_manager.load_computers()
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for computer in computers:
|
||||||
|
result = wol_manager.wake_computer(
|
||||||
|
computer['mac'],
|
||||||
|
computer['name'],
|
||||||
|
computer.get('ip', '')
|
||||||
|
)
|
||||||
|
results.append({
|
||||||
|
'name': computer['name'],
|
||||||
|
'result': result
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({'results': results})
|
||||||
|
|
||||||
|
@app.route('/api/add', methods=['POST'])
|
||||||
|
def add_computer():
|
||||||
|
data = request.get_json()
|
||||||
|
result = wol_manager.add_computer(
|
||||||
|
data.get('name'),
|
||||||
|
data.get('mac'),
|
||||||
|
data.get('ip', '')
|
||||||
|
)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@app.route('/api/scan')
|
||||||
|
def scan_network():
|
||||||
|
result = wol_manager.scan_network()
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=8080, debug=False)
|
||||||
2
app/requirements.txt
Normal file
2
app/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
requests==2.31.0
|
||||||
639
app/templates/index.html
Normal file
639
app/templates/index.html
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ro">
|
||||||
|
<head>
|
||||||
|
<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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🚀 Wake-on-LAN Manager</h1>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div id="computers-grid" class="computer-grid">
|
||||||
|
<!-- Calculatoarele vor fi încărcate aici -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal pentru adăugarea calculatoarelor -->
|
||||||
|
<div id="addModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeAddModal()">×</span>
|
||||||
|
<h2>Adaugă Calculator Nou</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="computerName">Nume Calculator:</label>
|
||||||
|
<input type="text" id="computerName" placeholder="ex: PC Birou">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="computerMac">Adresa MAC:</label>
|
||||||
|
<input type="text" id="computerMac" placeholder="ex: 00:11:22:33:44:55">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="computerIp">IP (opțional):</label>
|
||||||
|
<input type="text" id="computerIp" placeholder="ex: 192.168.1.100">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" onclick="closeAddModal()" class="btn-secondary">Anulează</button>
|
||||||
|
<button type="button" onclick="addComputer()">Adaugă</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal pentru scanarea rețelei -->
|
||||||
|
<div id="scanModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeScanModal()">×</span>
|
||||||
|
<h2>Calculatoare din Rețea</h2>
|
||||||
|
<div id="scan-loading" style="display: none; text-align: center;">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
wol-web:
|
||||||
|
build: .
|
||||||
|
container_name: wol-manager
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8088:8080"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
networks:
|
||||||
|
- wol-network
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Necesare pentru Wake-on-LAN
|
||||||
|
network_mode: host
|
||||||
|
privileged: true
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- NET_RAW
|
||||||
|
|
||||||
|
networks:
|
||||||
|
wol-network:
|
||||||
|
driver: bridge
|
||||||
65
readme.md
Normal file
65
readme.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Wake-on-LAN Manager
|
||||||
|
|
||||||
|
Aplicație web pentru managementul și trezirea calculatoarelor din rețeaua locală folosind Wake-on-LAN magic packets.
|
||||||
|
|
||||||
|
## Funcționalități
|
||||||
|
|
||||||
|
- 🚀 Interfață web modernă pentru gestionarea calculatoarelor
|
||||||
|
- ⚡ Trimitere magic packets Wake-on-LAN
|
||||||
|
- 🔍 Scanare automată a rețelei pentru detectarea dispozitivelor
|
||||||
|
- 📱 Design responsive (mobile-friendly)
|
||||||
|
- 🐳 Containerizată cu Docker pentru deployment ușor
|
||||||
|
|
||||||
|
## Instalare și Pornire
|
||||||
|
|
||||||
|
### Prima pornire (recomandat)
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual cu Docker Compose
|
||||||
|
```bash
|
||||||
|
# Creează directorul pentru date
|
||||||
|
mkdir -p data
|
||||||
|
|
||||||
|
# Pornește serviciile
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Vezi logs
|
||||||
|
docker-compose logs -f wol-web
|
||||||
|
|
||||||
|
# Oprește serviciile
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accesare
|
||||||
|
|
||||||
|
Aplicația va fi disponibilă pe: **http://IP_HOST:8088**
|
||||||
|
|
||||||
|
## Structura Proiectului
|
||||||
|
|
||||||
|
```
|
||||||
|
├── app/ # Directorul aplicației
|
||||||
|
│ ├── app.py # Aplicația Flask principală
|
||||||
|
│ ├── requirements.txt # Dependențele Python
|
||||||
|
│ └── templates/
|
||||||
|
│ └── index.html # Interfața web
|
||||||
|
├── Dockerfile # Configurație Docker
|
||||||
|
├── docker-compose.yml # Orchestrare servicii
|
||||||
|
├── start.sh # Script de pornire rapidă
|
||||||
|
├── CLAUDE.md # Ghid pentru Claude Code
|
||||||
|
└── data/ # Directorul pentru configurații (creat automat)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configurare
|
||||||
|
|
||||||
|
Calculatoarele sunt stocate în fișierul `data/wol-computers.conf` cu formatul:
|
||||||
|
```
|
||||||
|
nume_calculator|adresa_mac|adresa_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplu:
|
||||||
|
```
|
||||||
|
PC-Birou|00:11:22:33:44:55|192.168.1.100
|
||||||
|
Laptop-Gaming|aa:bb:cc:dd:ee:ff|192.168.1.101
|
||||||
|
```
|
||||||
13
start.sh
Normal file
13
start.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🚀 Pornesc Wake-on-LAN Manager..."
|
||||||
|
|
||||||
|
# Creează directorul pentru date
|
||||||
|
mkdir -p data
|
||||||
|
|
||||||
|
# Pornește containerele
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo "✅ Aplicația rulează pe http://IP_LXC:8088"
|
||||||
|
echo "📊 Vezi logs cu: docker-compose logs -f"
|
||||||
|
echo "⏹️ Oprește cu: docker-compose down"
|
||||||
Reference in New Issue
Block a user