Files
wol/app/templates/index.html
Marius Mutu f7b0c28d1a 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>
2025-09-04 16:19:09 +03:00

639 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>