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:
2025-09-04 16:19:09 +03:00
commit f7b0c28d1a
8 changed files with 1028 additions and 0 deletions

200
app/app.py Normal file
View 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
View File

@@ -0,0 +1,2 @@
flask==2.3.3
requests==2.31.0

639
app/templates/index.html Normal file
View 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()">&times;</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()">&times;</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>