- 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>
639 lines
20 KiB
HTML
639 lines
20 KiB
HTML
<!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> |