Compare commits

..

6 Commits

Author SHA1 Message Date
3fb712ae0b Improve UI consistency and device list organization
- Style edit button to match wake/delete button appearance
- Sort device lists by IP address in main table and scan results
- Ensure consistent button styling across desktop and mobile views

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 16:36:55 +03:00
43e997ba47 Enhance mobile UI and network scanning experience
- Fix modal scroll issues on mobile devices with proper viewport constraints
- Remove status column and apply status colors directly to device names
- Reorder columns: IP before MAC for better logical flow
- Optimize table layout for mobile with reduced padding and text ellipsis
- Implement visual disabled state for existing devices in scan modal
- Add intelligent device comparison to prevent duplicate additions
- Exclude Docker bridge networks from Linux scanning fallback
- Improve scan result handling with better error messaging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 15:32:32 +03:00
3c14094650 ignore 2025-09-07 14:45:26 +03:00
072553953e Transform rename button to full edit functionality
- Replace rename modal with comprehensive edit modal supporting name, MAC, and IP changes
- Add edit_computer() method with full validation (MAC format, duplicates)
- Create new /api/edit endpoint accepting all computer attributes
- Update frontend JavaScript for multi-field editing with client-side validation
- Rename functions from openRenameModal/performRename to openEditModal/performEdit
- Pre-populate edit form with current values and validate MAC address format

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 00:10:21 +03:00
86931a091e Add flexible Flask port configuration via environment variable
- Add FLASK_PORT environment variable support in docker-compose.yml
- Update Flask app to read port from FLASK_PORT environment variable
- Add FLASK_PORT configuration to .env and .env.example files
- Enables running Flask on custom ports to avoid port conflicts in containerized environments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 23:14:24 +03:00
48db3a11d0 Enhance Windows network scanning with improved device discovery
- Add ARP-first scanning approach to find devices that don't respond to ping
- Implement local host MAC address detection for Windows host machines
- Support multiple network prefix lengths (/16, /20, /24) with proper network calculation
- Add Get-LocalHostMacAddress function with 3 detection methods:
  * Direct IP-to-adapter matching via Get-NetIPAddress/Get-NetAdapter
  * WMI-based detection via Win32_NetworkAdapterConfiguration
  * Same-subnet fallback detection
- Skip ping for devices already found in ARP table (performance improvement)
- Improved detection of host.docker.internal MAC addresses

This resolves issues where local Windows host devices were found without MAC addresses,
enabling proper Wake-on-LAN functionality for all network devices including the host machine.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 22:42:41 +03:00
11 changed files with 576 additions and 245 deletions

1
.env
View File

@@ -1,4 +1,5 @@
# WOL Manager Environment Configuration
WOL_NETWORK_MODE=bridge
WOL_EXTERNAL_PORT=5000
FLASK_PORT=5000
FLASK_DEBUG=true

View File

@@ -10,6 +10,9 @@ WOL_NETWORK_MODE=bridge
# 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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
data/network-scan-results.json
data/wol-computers.conf

View File

@@ -198,6 +198,76 @@ class WOLManager:
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:
# Încearcă să citească rezultatele scanului Windows mai întâi
@@ -290,6 +360,9 @@ class WOLManager:
parts = line.split()
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"
@@ -307,6 +380,10 @@ class WOLManager:
mac = match.group(2)
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
@@ -322,7 +399,7 @@ class WOLManager:
except:
pass
else:
# Autodetectare - adaugă toate intrările ARP
# Autodetectare - adaugă toate intrările ARP (fără Docker bridge)
scanned.append({
'ip': ip,
'mac': mac,
@@ -338,6 +415,14 @@ class WOLManager:
'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}
wol_manager = WOLManager()
@@ -398,6 +483,17 @@ def rename_computer():
)
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()
@@ -485,5 +581,7 @@ def trigger_windows_scan():
})
if __name__ == '__main__':
# 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=5000, debug=True, use_reloader=True)
app.run(host='0.0.0.0', port=port, debug=True, use_reloader=True)

View File

@@ -129,6 +129,23 @@ body {
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;
@@ -166,7 +183,7 @@ body {
border: 1px inset #D4D0C8;
}
.rename-btn {
.edit-btn {
background-color: #F0F0F0;
border: 1px outset #D4D0C8;
padding: 6px 8px;
@@ -177,11 +194,11 @@ body {
min-width: 32px;
}
.rename-btn:hover {
.edit-btn:hover {
background-color: #E8E5E2;
}
.rename-btn:active {
.edit-btn:active {
border: 1px inset #D4D0C8;
}
@@ -479,9 +496,45 @@ body {
margin: 0;
}
/* Wider scan modal for the additional column */
.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 */
@@ -564,7 +617,7 @@ body {
min-width: 40px;
}
.rename-btn,
.edit-btn,
.delete-btn {
font-size: 16px;
padding: 8px 10px;
@@ -633,6 +686,12 @@ body {
#scanModal .modal-content {
width: calc(100vw - 20px);
max-width: 100%;
max-height: 90vh;
}
#scanModal .modal-body {
max-height: 60vh;
overflow-y: auto;
}
.no-computers {
@@ -646,6 +705,23 @@ body {
}
.computers-table {
min-width: 600px;
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;
}
}

View File

@@ -1,11 +1,11 @@
// WOL Manager JavaScript
let scanModal, addModal, renameModal;
let scanModal, addModal, editModal;
// Initialize on page load
window.onload = function() {
scanModal = document.getElementById('scanModal');
addModal = document.getElementById('addModal');
renameModal = document.getElementById('renameModal');
editModal = document.getElementById('editModal');
refreshComputers();
};
@@ -40,23 +40,38 @@ function displayComputers(computers) {
noComputersDiv.style.display = 'none';
tbody.innerHTML = computers.map(computer => `
// 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="rename-btn" onclick="openRenameModal('${computer.name}')" title="Redenumește calculatorul">
<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>${computer.name}</td>
<td style="font-family: monospace;">${computer.mac}</td>
<td class="computer-name ${computer.status}">${computer.name}</td>
<td>${computer.ip || '-'}</td>
<td><span class="status ${computer.status}">${computer.status}</span></td>
<td style="font-family: monospace;">${computer.mac}</td>
</tr>
`).join('');
}
@@ -182,20 +197,23 @@ function scanNetwork() {
const network = getSelectedNetwork();
const requestData = network ? { network: network } : {};
fetch('/api/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(result => {
// 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);
displayScanResults(result.computers, existingComputers);
if (result.message) {
// Afișează mesajul deasupra tabelului
document.getElementById('scan-results').innerHTML =
@@ -228,20 +246,23 @@ function triggerWindowsScan() {
showMessage('Declanșând scanul Windows...', 'success');
fetch('/api/scan/windows', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
// 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);
displayScanResults(data.computers, existingComputers);
} else {
let message = data.message || 'Scanul Windows a eșuat';
@@ -281,13 +302,32 @@ function triggerWindowsScan() {
});
}
function displayScanResults(computers) {
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">
@@ -312,23 +352,32 @@ function displayScanResults(computers) {
<tbody>
`;
computers.forEach((computer, index) => {
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>
<tr class="${rowClass}">
<td>
<input type="checkbox" class="device-checkbox"
data-hostname="${computer.hostname}"
data-mac="${computer.mac}"
data-ip="${computer.ip}"
onchange="updateAddButton()">
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="Adaugă calculatorul">
<button class="add-btn" onclick="addFromScan('${computer.hostname}', '${computer.mac}', '${computer.ip}')"
title="${buttonTitle}" ${buttonDisabled}>
${deviceExists ? '✓' : ''}
</button>
</td>
</tr>
@@ -341,7 +390,7 @@ function displayScanResults(computers) {
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAll');
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
const deviceCheckboxes = document.querySelectorAll('.device-checkbox:not(:disabled)');
deviceCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
@@ -351,8 +400,8 @@ function toggleSelectAll() {
}
function updateAddButton() {
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
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');
@@ -363,7 +412,7 @@ function updateAddButton() {
` Adaugă Selectate (${checkedBoxes.length})` : ' Adaugă Selectate';
}
// Update "Select All" checkbox state
// Update "Select All" checkbox state (only consider enabled checkboxes)
if (selectAllCheckbox) {
if (checkedBoxes.length === 0) {
selectAllCheckbox.indeterminate = false;
@@ -378,7 +427,7 @@ function updateAddButton() {
}
function addSelectedFromScan() {
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked:not(:disabled)');
if (checkedBoxes.length === 0) {
showMessage('Nu ai selectat niciun dispozitiv!', 'error');
@@ -484,6 +533,12 @@ function addSelectedFromScan() {
}
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: {
@@ -510,52 +565,69 @@ function closeScanModal() {
scanModal.style.display = 'none';
}
function openRenameModal(currentName) {
document.getElementById('currentName').value = currentName;
document.getElementById('newName').value = '';
renameModal.style.display = 'block';
document.getElementById('newName').focus();
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 closeRenameModal() {
renameModal.style.display = 'none';
document.getElementById('currentName').value = '';
document.getElementById('newName').value = '';
function closeEditModal() {
editModal.style.display = 'none';
document.getElementById('editName').value = '';
document.getElementById('editMac').value = '';
document.getElementById('editIp').value = '';
}
function performRename() {
const oldName = document.getElementById('currentName').value;
const newName = document.getElementById('newName').value.trim();
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 nou nu poate fi gol!', 'error');
showMessage('Numele nu poate fi gol!', 'error');
return;
}
if (oldName === newName) {
showMessage('Numele nou trebuie să fie diferit de cel actual!', 'error');
if (!newMac) {
showMessage('Adresa MAC nu poate fi goală!', 'error');
return;
}
fetch('/api/rename', {
// 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})
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');
closeRenameModal();
closeEditModal();
refreshComputers();
} else {
showMessage(result.message, 'error');
}
})
.catch(error => {
showMessage('Eroare la redenumirea calculatorului: ' + error.message, 'error');
showMessage('Eroare la editarea calculatorului: ' + error.message, 'error');
});
}
@@ -595,14 +667,14 @@ window.onclick = function(event) {
if (event.target == scanModal) {
closeScanModal();
}
if (event.target == renameModal) {
closeRenameModal();
if (event.target == editModal) {
closeEditModal();
}
}
// Allow Enter key to perform rename
// Allow Enter key to perform edit
document.addEventListener('keydown', function(event) {
if (event.key === 'Enter' && renameModal.style.display === 'block') {
performRename();
if (event.key === 'Enter' && editModal.style.display === 'block') {
performEdit();
}
});

View File

@@ -40,9 +40,8 @@
<tr>
<th>Acțiuni</th>
<th>Nume Calculator</th>
<th>Adresa MAC</th>
<th>Adresa IP</th>
<th>Status</th>
<th>Adresa MAC</th>
</tr>
</thead>
<tbody id="computers-tbody">
@@ -100,26 +99,30 @@
</div>
</div>
<!-- Modal pentru redenumirea calculatoarelor -->
<div id="renameModal" class="modal">
<!-- Modal pentru editarea calculatoarelor -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Redenumește Calculator</h2>
<span class="close" onclick="closeRenameModal()">&times;</span>
<h2>Editează Calculator</h2>
<span class="close" onclick="closeEditModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="currentName">Nume Actual:</label>
<input type="text" id="currentName" readonly>
<label for="editName">Nume Calculator:</label>
<input type="text" id="editName" placeholder="ex: PC Birou">
</div>
<div class="form-group">
<label for="newName">Nume Nou:</label>
<input type="text" id="newName" placeholder="Introdu noul nume">
<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="closeRenameModal()">Anulează</button>
<button type="button" onclick="performRename()">Redenumește</button>
<button type="button" onclick="closeEditModal()">Anulează</button>
<button type="button" onclick="performEdit()">Salvează</button>
</div>
</div>
</div>

View File

@@ -1,86 +0,0 @@
{
"message": "Scanare completata cu succes. Gasite 13 dispozitive.",
"networks_scanned": "10.0.20.0/24",
"timestamp": "2025-09-05T17:03:30.367Z",
"computers": [
{
"status": "online",
"ip": "10.0.20.1",
"mac": "6c:5a:b0:20:ff:7c",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.36",
"mac": "9c:6b:00:18:5f:23",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.122",
"mac": "d4:3d:7e:de:05:f7",
"hostname": "svnroa"
},
{
"status": "online",
"ip": "10.0.20.144",
"mac": "",
"hostname": "host.docker.internal"
},
{
"status": "online",
"ip": "10.0.20.161",
"mac": "de:ad:be:ef:10:04",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.162",
"mac": "bc:24:11:05:58:5c",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.163",
"mac": "bc:24:11:3e:9d:70",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.164",
"mac": "bc:24:11:90:b4:04",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.165",
"mac": "bc:24:11:da:c2:63",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.170",
"mac": "de:ad:be:ef:10:00",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.200",
"mac": "fc:3f:db:0a:0d:d8",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.201",
"mac": "58:47:ca:7d:51:3b",
"hostname": ""
},
{
"status": "online",
"ip": "10.0.20.210",
"mac": "b0:a7:b9:4c:28:2c",
"hostname": ""
}
],
"success": true
}

View File

@@ -1,13 +0,0 @@
# Format: name|mac|ip
ROUTER|6c:5a:b0:20:ff:7c|10.0.20.1
ROA|9c:6b:00:18:5f:23|10.0.20.36
SVNROA|d4:3d:7e:de:05:f7|10.0.20.122
PVEMINI-1|de:ad:be:ef:10:04|10.0.20.161
PVEMINI-2|bc:24:11:3e:9d:70|10.0.20.163
PVEMINI-3|bc:24:11:05:58:5c|10.0.20.162
PVEMINI-4|bc:24:11:da:c2:63|10.0.20.165
PVEMINI-5|bc:24:11:90:b4:04|10.0.20.164
PVEMINI-PORTAINER|de:ad:be:ef:10:00|10.0.20.170
PVE1|fc:3f:db:0a:0d:d8|10.0.20.200
PVEMINI|74:38:b7:fd:0b:b6|10.0.20.221
|c2:f5:a3:17:a2:dc|10.0.20.229

View File

@@ -15,6 +15,7 @@ services:
- PYTHONUNBUFFERED=1
- WSL_INTEROP=${WSL_INTEROP}
- FLASK_DEBUG=${FLASK_DEBUG:-false}
- FLASK_PORT=${FLASK_PORT:-5000}
network_mode: "${WOL_NETWORK_MODE:-bridge}"
privileged: true
cap_add:

View File

@@ -30,25 +30,34 @@ function Get-LocalNetworks {
$networkAddr = $adapter.IPAddress
$prefixLength = $adapter.PrefixLength
# Simple network calculation for /24 networks (most common)
if ($prefixLength -eq 24) {
$parts = $networkAddr.Split('.')
if ($parts.Length -eq 4) {
# 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 16) {
$parts = $networkAddr.Split('.')
if ($parts.Length -eq 4) {
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 "Skipping unsupported prefix length /$prefixLength for $networkAddr"
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)"
}
}
}
@@ -74,7 +83,21 @@ function Show-NetworkSelectionMenu {
foreach ($adapter in $adapters) {
$parts = $adapter.IPAddress.Split('.')
if ($parts.Length -eq 4) {
$networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24"
$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
}
@@ -170,6 +193,81 @@ function Test-NetworkAddress {
}
}
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)
@@ -188,22 +286,107 @@ function Get-NetworkDevices {
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 "Only /24 networks are supported in this version"
Write-Log "Unsupported prefix length /$prefixLength. Supported: /16, /20, /24"
return @()
}
$devices = @()
$aliveIPs = @()
# Simplified ping sweep with progress indication
Write-Log "Starting ping sweep for $($endIP - $startIP + 1) addresses..."
# Use the configurable batch size parameter
$totalIPs = $endIP - $startIP + 1
$processed = 0
Write-Log "Scanning $totalIPs addresses in batches of $BatchSize..."
# 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) {
@@ -212,9 +395,16 @@ function Get-NetworkDevices {
Write-Log "Batch progress: $([Math]::Floor(($batch - $startIP) * 100 / $totalIPs))% - Scanning IPs $batch to $batchEnd"
# Create jobs for current batch
# 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 {
@@ -246,7 +436,7 @@ function Get-NetworkDevices {
if ($result -and $result.Success) {
$aliveIPs += $result.IP
Write-Log "Found alive host: $($result.IP) ($($result.ResponseTime)ms)"
Write-Log "Found alive host via ping: $($result.IP) ($($result.ResponseTime)ms)"
}
$processed++
@@ -254,49 +444,24 @@ function Get-NetworkDevices {
# Show progress more frequently
$progressPercent = [Math]::Floor($processed * 100 / $totalIPs)
Write-Log "Progress: $processed/$totalIPs addresses scanned ($progressPercent%)"
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) alive hosts"
Write-Log "Found $($aliveIPs.Count) additional hosts via ping"
# Get ARP table to find MAC addresses
Write-Log "Retrieving MAC addresses from ARP table..."
$arpEntries = @{}
# STEP 3: Combine results and resolve hostnames
Write-Log "Step 3: Building final device list..."
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") {
$arpEntries[$neighbor.IPAddress] = $neighbor.LinkLayerAddress.Replace('-', ':').ToLower()
}
}
}
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
}
}
}
}
catch {
Write-Log "Warning: Could not retrieve ARP table"
}
# Add devices found in ARP
foreach ($arpDevice in $arpDevices.Values) {
$arpDevice.status = "online" # Change from "arp" to "online"
$devices += $arpDevice
}
# Build device list
# Add devices found via ping (and try to get their MAC from ARP if available)
foreach ($ip in $aliveIPs) {
$device = @{
ip = $ip
@@ -304,11 +469,14 @@ function Get-NetworkDevices {
hostname = ""
status = "online"
}
$devices += $device
}
# Try to resolve hostname
# Resolve hostnames and fix MAC addresses for local host
foreach ($device in $devices) {
try {
$hostname = [System.Net.Dns]::GetHostEntry($ip).HostName
if ($hostname -and $hostname -ne $ip) {
$hostname = [System.Net.Dns]::GetHostEntry($device.ip).HostName
if ($hostname -and $hostname -ne $device.ip) {
$device.hostname = $hostname
}
}
@@ -316,12 +484,18 @@ function Get-NetworkDevices {
# Hostname resolution failed, leave empty
}
$devices += $device
# 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 "Device found: $ip -> $($device.mac) ($($device.hostname))"
Write-Log "Final device: $($device.ip) -> $($device.mac) ($($device.hostname))"
} else {
Write-Log "Device found: $ip (no MAC address available)"
Write-Log "Final device: $($device.ip) (no MAC address available) ($($device.hostname))"
}
}