Compare commits
6 Commits
acf234c600
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb712ae0b | |||
| 43e997ba47 | |||
| 3c14094650 | |||
| 072553953e | |||
| 86931a091e | |||
| 48db3a11d0 |
1
.env
1
.env
@@ -1,4 +1,5 @@
|
|||||||
# WOL Manager Environment Configuration
|
# WOL Manager Environment Configuration
|
||||||
WOL_NETWORK_MODE=bridge
|
WOL_NETWORK_MODE=bridge
|
||||||
WOL_EXTERNAL_PORT=5000
|
WOL_EXTERNAL_PORT=5000
|
||||||
|
FLASK_PORT=5000
|
||||||
FLASK_DEBUG=true
|
FLASK_DEBUG=true
|
||||||
@@ -10,6 +10,9 @@ WOL_NETWORK_MODE=bridge
|
|||||||
# For Linux with host mode: this setting is ignored
|
# For Linux with host mode: this setting is ignored
|
||||||
WOL_EXTERNAL_PORT=5000
|
WOL_EXTERNAL_PORT=5000
|
||||||
|
|
||||||
|
# Flask internal port (port on which Flask app runs inside container)
|
||||||
|
FLASK_PORT=5000
|
||||||
|
|
||||||
# Examples:
|
# Examples:
|
||||||
# Windows Docker Desktop:
|
# Windows Docker Desktop:
|
||||||
# WOL_NETWORK_MODE=bridge
|
# WOL_NETWORK_MODE=bridge
|
||||||
|
|||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
data/network-scan-results.json
|
||||||
|
data/wol-computers.conf
|
||||||
102
app/app.py
102
app/app.py
@@ -198,6 +198,76 @@ class WOLManager:
|
|||||||
|
|
||||||
return {'success': True, 'message': f'Computerul {deleted_name} a fost șters!'}
|
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):
|
def scan_network(self, custom_network=None):
|
||||||
try:
|
try:
|
||||||
# Încearcă să citească rezultatele scanului Windows mai întâi
|
# Încearcă să citească rezultatele scanului Windows mai întâi
|
||||||
@@ -290,6 +360,9 @@ class WOLManager:
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
gateway = parts[1]
|
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
|
# Construiește rețeaua bazată pe gateway
|
||||||
network_parts = gateway.split('.')
|
network_parts = gateway.split('.')
|
||||||
network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24"
|
network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24"
|
||||||
@@ -307,6 +380,10 @@ class WOLManager:
|
|||||||
mac = match.group(2)
|
mac = match.group(2)
|
||||||
hostname = line.split()[0] if line.split() else '?'
|
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ă
|
# Verifică dacă IP-ul este în rețeaua specificată
|
||||||
if custom_network:
|
if custom_network:
|
||||||
import ipaddress
|
import ipaddress
|
||||||
@@ -322,7 +399,7 @@ class WOLManager:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Autodetectare - adaugă toate intrările ARP
|
# Autodetectare - adaugă toate intrările ARP (fără Docker bridge)
|
||||||
scanned.append({
|
scanned.append({
|
||||||
'ip': ip,
|
'ip': ip,
|
||||||
'mac': mac,
|
'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.'
|
'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}
|
return {'success': True, 'computers': scanned}
|
||||||
|
|
||||||
wol_manager = WOLManager()
|
wol_manager = WOLManager()
|
||||||
@@ -398,6 +483,17 @@ def rename_computer():
|
|||||||
)
|
)
|
||||||
return jsonify(result)
|
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'])
|
@app.route('/api/delete', methods=['POST'])
|
||||||
def delete_computer():
|
def delete_computer():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -485,5 +581,7 @@ def trigger_windows_scan():
|
|||||||
})
|
})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
# 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)
|
||||||
@@ -129,6 +129,23 @@ body {
|
|||||||
background-color: #E0E0E0;
|
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 {
|
.status {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
@@ -166,7 +183,7 @@ body {
|
|||||||
border: 1px inset #D4D0C8;
|
border: 1px inset #D4D0C8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rename-btn {
|
.edit-btn {
|
||||||
background-color: #F0F0F0;
|
background-color: #F0F0F0;
|
||||||
border: 1px outset #D4D0C8;
|
border: 1px outset #D4D0C8;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
@@ -177,11 +194,11 @@ body {
|
|||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rename-btn:hover {
|
.edit-btn:hover {
|
||||||
background-color: #E8E5E2;
|
background-color: #E8E5E2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rename-btn:active {
|
.edit-btn:active {
|
||||||
border: 1px inset #D4D0C8;
|
border: 1px inset #D4D0C8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,9 +496,45 @@ body {
|
|||||||
margin: 0;
|
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 {
|
#scanModal .modal-content {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scanModal .modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
/* Mobile responsiveness */
|
||||||
@@ -564,7 +617,7 @@ body {
|
|||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rename-btn,
|
.edit-btn,
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@@ -633,6 +686,12 @@ body {
|
|||||||
#scanModal .modal-content {
|
#scanModal .modal-content {
|
||||||
width: calc(100vw - 20px);
|
width: calc(100vw - 20px);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scanModal .modal-body {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-computers {
|
.no-computers {
|
||||||
@@ -646,6 +705,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.computers-table {
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// WOL Manager JavaScript
|
// WOL Manager JavaScript
|
||||||
let scanModal, addModal, renameModal;
|
let scanModal, addModal, editModal;
|
||||||
|
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
scanModal = document.getElementById('scanModal');
|
scanModal = document.getElementById('scanModal');
|
||||||
addModal = document.getElementById('addModal');
|
addModal = document.getElementById('addModal');
|
||||||
renameModal = document.getElementById('renameModal');
|
editModal = document.getElementById('editModal');
|
||||||
refreshComputers();
|
refreshComputers();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,23 +40,38 @@ function displayComputers(computers) {
|
|||||||
|
|
||||||
noComputersDiv.style.display = 'none';
|
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>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button class="wake-btn" onclick="wakeComputer('${computer.mac}', '${computer.name}', '${computer.ip || ''}')" title="Trezește calculatorul">
|
<button class="wake-btn" onclick="wakeComputer('${computer.mac}', '${computer.name}', '${computer.ip || ''}')" title="Trezește calculatorul">
|
||||||
⚡
|
⚡
|
||||||
</button>
|
</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>
|
||||||
<button class="delete-btn" onclick="deleteComputer('${computer.name}', '${computer.mac}')" title="Șterge calculatorul">
|
<button class="delete-btn" onclick="deleteComputer('${computer.name}', '${computer.mac}')" title="Șterge calculatorul">
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>${computer.name}</td>
|
<td class="computer-name ${computer.status}">${computer.name}</td>
|
||||||
<td style="font-family: monospace;">${computer.mac}</td>
|
|
||||||
<td>${computer.ip || '-'}</td>
|
<td>${computer.ip || '-'}</td>
|
||||||
<td><span class="status ${computer.status}">${computer.status}</span></td>
|
<td style="font-family: monospace;">${computer.mac}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
@@ -182,20 +197,23 @@ function scanNetwork() {
|
|||||||
const network = getSelectedNetwork();
|
const network = getSelectedNetwork();
|
||||||
const requestData = network ? { network: network } : {};
|
const requestData = network ? { network: network } : {};
|
||||||
|
|
||||||
|
// Get current computers list to compare with scan results
|
||||||
|
Promise.all([
|
||||||
|
fetch('/api/computers').then(response => response.json()),
|
||||||
fetch('/api/scan', {
|
fetch('/api/scan', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
}).then(response => response.json())
|
||||||
.then(response => response.json())
|
])
|
||||||
.then(result => {
|
.then(([existingComputers, result]) => {
|
||||||
document.getElementById('scan-loading').style.display = 'none';
|
document.getElementById('scan-loading').style.display = 'none';
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (result.computers && result.computers.length > 0) {
|
if (result.computers && result.computers.length > 0) {
|
||||||
displayScanResults(result.computers);
|
displayScanResults(result.computers, existingComputers);
|
||||||
if (result.message) {
|
if (result.message) {
|
||||||
// Afișează mesajul deasupra tabelului
|
// Afișează mesajul deasupra tabelului
|
||||||
document.getElementById('scan-results').innerHTML =
|
document.getElementById('scan-results').innerHTML =
|
||||||
@@ -228,20 +246,23 @@ function triggerWindowsScan() {
|
|||||||
|
|
||||||
showMessage('Declanșând scanul Windows...', 'success');
|
showMessage('Declanșând scanul Windows...', 'success');
|
||||||
|
|
||||||
|
// Get current computers list to compare with scan results
|
||||||
|
Promise.all([
|
||||||
|
fetch('/api/computers').then(response => response.json()),
|
||||||
fetch('/api/scan/windows', {
|
fetch('/api/scan/windows', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
}).then(response => response.json())
|
||||||
.then(response => response.json())
|
])
|
||||||
.then(data => {
|
.then(([existingComputers, data]) => {
|
||||||
document.getElementById('scan-loading').style.display = 'none';
|
document.getElementById('scan-loading').style.display = 'none';
|
||||||
|
|
||||||
if (data.success && data.computers) {
|
if (data.success && data.computers) {
|
||||||
showMessage(data.message || 'Scan Windows completat cu succes!', 'success');
|
showMessage(data.message || 'Scan Windows completat cu succes!', 'success');
|
||||||
displayScanResults(data.computers);
|
displayScanResults(data.computers, existingComputers);
|
||||||
} else {
|
} else {
|
||||||
let message = data.message || 'Scanul Windows a eșuat';
|
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) {
|
if (computers.length === 0) {
|
||||||
document.getElementById('scan-results').innerHTML =
|
document.getElementById('scan-results').innerHTML =
|
||||||
'<div class="message error">Nu s-au găsit calculatoare în rețea</div>';
|
'<div class="message error">Nu s-au găsit calculatoare în rețea</div>';
|
||||||
return;
|
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 = `
|
let html = `
|
||||||
<div class="scan-controls">
|
<div class="scan-controls">
|
||||||
<label class="select-all-container">
|
<label class="select-all-container">
|
||||||
@@ -312,23 +352,32 @@ function displayScanResults(computers) {
|
|||||||
<tbody>
|
<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 += `
|
html += `
|
||||||
<tr>
|
<tr class="${rowClass}">
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="device-checkbox"
|
<input type="checkbox" class="device-checkbox"
|
||||||
data-hostname="${computer.hostname}"
|
data-hostname="${computer.hostname}"
|
||||||
data-mac="${computer.mac}"
|
data-mac="${computer.mac}"
|
||||||
data-ip="${computer.ip}"
|
data-ip="${computer.ip}"
|
||||||
onchange="updateAddButton()">
|
onchange="updateAddButton()"
|
||||||
|
${checkboxDisabled}>
|
||||||
</td>
|
</td>
|
||||||
<td>${computer.ip}</td>
|
<td>${computer.ip}</td>
|
||||||
<td style="font-family: monospace;">${computer.mac}</td>
|
<td style="font-family: monospace;">${computer.mac}</td>
|
||||||
<td>${computer.hostname}</td>
|
<td>${computer.hostname}</td>
|
||||||
<td><span class="status ${computer.status}">${computer.status}</span></td>
|
<td><span class="status ${computer.status}">${computer.status}</span></td>
|
||||||
<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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -341,7 +390,7 @@ function displayScanResults(computers) {
|
|||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
const selectAllCheckbox = document.getElementById('selectAll');
|
const selectAllCheckbox = document.getElementById('selectAll');
|
||||||
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
|
const deviceCheckboxes = document.querySelectorAll('.device-checkbox:not(:disabled)');
|
||||||
|
|
||||||
deviceCheckboxes.forEach(checkbox => {
|
deviceCheckboxes.forEach(checkbox => {
|
||||||
checkbox.checked = selectAllCheckbox.checked;
|
checkbox.checked = selectAllCheckbox.checked;
|
||||||
@@ -351,8 +400,8 @@ function toggleSelectAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAddButton() {
|
function updateAddButton() {
|
||||||
const deviceCheckboxes = document.querySelectorAll('.device-checkbox');
|
const deviceCheckboxes = document.querySelectorAll('.device-checkbox:not(:disabled)');
|
||||||
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
|
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked:not(:disabled)');
|
||||||
const addButton = document.querySelector('.add-selected-btn');
|
const addButton = document.querySelector('.add-selected-btn');
|
||||||
const selectAllCheckbox = document.getElementById('selectAll');
|
const selectAllCheckbox = document.getElementById('selectAll');
|
||||||
|
|
||||||
@@ -363,7 +412,7 @@ function updateAddButton() {
|
|||||||
`➕ Adaugă Selectate (${checkedBoxes.length})` : '➕ Adaugă Selectate';
|
`➕ Adaugă Selectate (${checkedBoxes.length})` : '➕ Adaugă Selectate';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update "Select All" checkbox state
|
// Update "Select All" checkbox state (only consider enabled checkboxes)
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
if (checkedBoxes.length === 0) {
|
if (checkedBoxes.length === 0) {
|
||||||
selectAllCheckbox.indeterminate = false;
|
selectAllCheckbox.indeterminate = false;
|
||||||
@@ -378,7 +427,7 @@ function updateAddButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addSelectedFromScan() {
|
function addSelectedFromScan() {
|
||||||
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked');
|
const checkedBoxes = document.querySelectorAll('.device-checkbox:checked:not(:disabled)');
|
||||||
|
|
||||||
if (checkedBoxes.length === 0) {
|
if (checkedBoxes.length === 0) {
|
||||||
showMessage('Nu ai selectat niciun dispozitiv!', 'error');
|
showMessage('Nu ai selectat niciun dispozitiv!', 'error');
|
||||||
@@ -484,6 +533,12 @@ function addSelectedFromScan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addFromScan(hostname, mac, ip) {
|
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', {
|
fetch('/api/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -510,52 +565,69 @@ function closeScanModal() {
|
|||||||
scanModal.style.display = 'none';
|
scanModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRenameModal(currentName) {
|
function openEditModal(currentName, currentMac, currentIp) {
|
||||||
document.getElementById('currentName').value = currentName;
|
document.getElementById('editName').value = currentName;
|
||||||
document.getElementById('newName').value = '';
|
document.getElementById('editName').dataset.originalName = currentName;
|
||||||
renameModal.style.display = 'block';
|
document.getElementById('editMac').value = currentMac;
|
||||||
document.getElementById('newName').focus();
|
document.getElementById('editIp').value = currentIp || '';
|
||||||
|
editModal.style.display = 'block';
|
||||||
|
document.getElementById('editName').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeRenameModal() {
|
function closeEditModal() {
|
||||||
renameModal.style.display = 'none';
|
editModal.style.display = 'none';
|
||||||
document.getElementById('currentName').value = '';
|
document.getElementById('editName').value = '';
|
||||||
document.getElementById('newName').value = '';
|
document.getElementById('editMac').value = '';
|
||||||
|
document.getElementById('editIp').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function performRename() {
|
function performEdit() {
|
||||||
const oldName = document.getElementById('currentName').value;
|
const oldName = document.getElementById('editName').dataset.originalName || document.getElementById('editName').value;
|
||||||
const newName = document.getElementById('newName').value.trim();
|
const newName = document.getElementById('editName').value.trim();
|
||||||
|
const newMac = document.getElementById('editMac').value.trim();
|
||||||
|
const newIp = document.getElementById('editIp').value.trim();
|
||||||
|
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
showMessage('Numele nou nu poate fi gol!', 'error');
|
showMessage('Numele nu poate fi gol!', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldName === newName) {
|
if (!newMac) {
|
||||||
showMessage('Numele nou trebuie să fie diferit de cel actual!', 'error');
|
showMessage('Adresa MAC nu poate fi goală!', 'error');
|
||||||
return;
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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(response => response.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showMessage(result.message, 'success');
|
showMessage(result.message, 'success');
|
||||||
closeRenameModal();
|
closeEditModal();
|
||||||
refreshComputers();
|
refreshComputers();
|
||||||
} else {
|
} else {
|
||||||
showMessage(result.message, 'error');
|
showMessage(result.message, 'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(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) {
|
if (event.target == scanModal) {
|
||||||
closeScanModal();
|
closeScanModal();
|
||||||
}
|
}
|
||||||
if (event.target == renameModal) {
|
if (event.target == editModal) {
|
||||||
closeRenameModal();
|
closeEditModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow Enter key to perform rename
|
// Allow Enter key to perform edit
|
||||||
document.addEventListener('keydown', function(event) {
|
document.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Enter' && renameModal.style.display === 'block') {
|
if (event.key === 'Enter' && editModal.style.display === 'block') {
|
||||||
performRename();
|
performEdit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -40,9 +40,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Acțiuni</th>
|
<th>Acțiuni</th>
|
||||||
<th>Nume Calculator</th>
|
<th>Nume Calculator</th>
|
||||||
<th>Adresa MAC</th>
|
|
||||||
<th>Adresa IP</th>
|
<th>Adresa IP</th>
|
||||||
<th>Status</th>
|
<th>Adresa MAC</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="computers-tbody">
|
<tbody id="computers-tbody">
|
||||||
@@ -100,26 +99,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal pentru redenumirea calculatoarelor -->
|
<!-- Modal pentru editarea calculatoarelor -->
|
||||||
<div id="renameModal" class="modal">
|
<div id="editModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Redenumește Calculator</h2>
|
<h2>Editează Calculator</h2>
|
||||||
<span class="close" onclick="closeRenameModal()">×</span>
|
<span class="close" onclick="closeEditModal()">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="currentName">Nume Actual:</label>
|
<label for="editName">Nume Calculator:</label>
|
||||||
<input type="text" id="currentName" readonly>
|
<input type="text" id="editName" placeholder="ex: PC Birou">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="newName">Nume Nou:</label>
|
<label for="editMac">Adresa MAC:</label>
|
||||||
<input type="text" id="newName" placeholder="Introdu noul nume">
|
<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>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" onclick="closeRenameModal()">Anulează</button>
|
<button type="button" onclick="closeEditModal()">Anulează</button>
|
||||||
<button type="button" onclick="performRename()">Redenumește</button>
|
<button type="button" onclick="performEdit()">Salvează</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -15,6 +15,7 @@ services:
|
|||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- WSL_INTEROP=${WSL_INTEROP}
|
- WSL_INTEROP=${WSL_INTEROP}
|
||||||
- FLASK_DEBUG=${FLASK_DEBUG:-false}
|
- FLASK_DEBUG=${FLASK_DEBUG:-false}
|
||||||
|
- FLASK_PORT=${FLASK_PORT:-5000}
|
||||||
network_mode: "${WOL_NETWORK_MODE:-bridge}"
|
network_mode: "${WOL_NETWORK_MODE:-bridge}"
|
||||||
privileged: true
|
privileged: true
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|||||||
@@ -30,25 +30,34 @@ function Get-LocalNetworks {
|
|||||||
$networkAddr = $adapter.IPAddress
|
$networkAddr = $adapter.IPAddress
|
||||||
$prefixLength = $adapter.PrefixLength
|
$prefixLength = $adapter.PrefixLength
|
||||||
|
|
||||||
# Simple network calculation for /24 networks (most common)
|
# Network calculation for different prefix lengths
|
||||||
if ($prefixLength -eq 24) {
|
|
||||||
$parts = $networkAddr.Split('.')
|
$parts = $networkAddr.Split('.')
|
||||||
if ($parts.Length -eq 4) {
|
if ($parts.Length -eq 4) {
|
||||||
|
if ($prefixLength -eq 24) {
|
||||||
$networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0"
|
$networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0"
|
||||||
$networks += "$networkAddress/24"
|
$networks += "$networkAddress/24"
|
||||||
Write-Log "Found network: $networkAddress/24 from adapter $($adapter.IPAddress)"
|
Write-Log "Found network: $networkAddress/24 from adapter $($adapter.IPAddress)"
|
||||||
}
|
}
|
||||||
|
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) {
|
elseif ($prefixLength -eq 16) {
|
||||||
$parts = $networkAddr.Split('.')
|
|
||||||
if ($parts.Length -eq 4) {
|
|
||||||
$networkAddress = "$($parts[0]).$($parts[1]).0.0"
|
$networkAddress = "$($parts[0]).$($parts[1]).0.0"
|
||||||
$networks += "$networkAddress/16"
|
$networks += "$networkAddress/16"
|
||||||
Write-Log "Found network: $networkAddress/16 from adapter $($adapter.IPAddress)"
|
Write-Log "Found network: $networkAddress/16 from adapter $($adapter.IPAddress)"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
Write-Log "Skipping unsupported prefix length /$prefixLength for $networkAddr"
|
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) {
|
foreach ($adapter in $adapters) {
|
||||||
$parts = $adapter.IPAddress.Split('.')
|
$parts = $adapter.IPAddress.Split('.')
|
||||||
if ($parts.Length -eq 4) {
|
if ($parts.Length -eq 4) {
|
||||||
|
$prefixLength = $adapter.PrefixLength
|
||||||
|
|
||||||
|
if ($prefixLength -eq 24) {
|
||||||
$networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/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) {
|
if ($detectedNetworks -notcontains $networkAddr) {
|
||||||
$detectedNetworks += $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 {
|
function Get-NetworkDevices {
|
||||||
param([string]$NetworkCIDR)
|
param([string]$NetworkCIDR)
|
||||||
|
|
||||||
@@ -188,22 +286,107 @@ function Get-NetworkDevices {
|
|||||||
if ($prefixLength -eq 24) {
|
if ($prefixLength -eq 24) {
|
||||||
$startIP = 1
|
$startIP = 1
|
||||||
$endIP = 254
|
$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 {
|
} else {
|
||||||
Write-Log "Only /24 networks are supported in this version"
|
Write-Log "Unsupported prefix length /$prefixLength. Supported: /16, /20, /24"
|
||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
|
|
||||||
$devices = @()
|
$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
|
$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
|
# Process IPs in smaller batches to avoid overwhelming the system
|
||||||
for ($batch = $startIP; $batch -le $endIP; $batch += $BatchSize) {
|
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"
|
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++) {
|
for ($i = $batch; $i -le $batchEnd; $i++) {
|
||||||
$ip = "$baseIP.$i"
|
$ip = "$baseIP.$i"
|
||||||
|
|
||||||
|
if ($arpDevices.ContainsKey($ip)) {
|
||||||
|
$processed++
|
||||||
|
$skippedCount++
|
||||||
|
continue # Skip IPs already found in ARP
|
||||||
|
}
|
||||||
|
|
||||||
$job = Start-Job -ScriptBlock {
|
$job = Start-Job -ScriptBlock {
|
||||||
param($IPAddress, $TimeoutMs)
|
param($IPAddress, $TimeoutMs)
|
||||||
try {
|
try {
|
||||||
@@ -246,7 +436,7 @@ function Get-NetworkDevices {
|
|||||||
|
|
||||||
if ($result -and $result.Success) {
|
if ($result -and $result.Success) {
|
||||||
$aliveIPs += $result.IP
|
$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++
|
$processed++
|
||||||
@@ -254,49 +444,24 @@ function Get-NetworkDevices {
|
|||||||
|
|
||||||
# Show progress more frequently
|
# Show progress more frequently
|
||||||
$progressPercent = [Math]::Floor($processed * 100 / $totalIPs)
|
$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
|
# Small delay between batches to prevent system overload
|
||||||
Start-Sleep -Milliseconds 100
|
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
|
# STEP 3: Combine results and resolve hostnames
|
||||||
Write-Log "Retrieving MAC addresses from ARP table..."
|
Write-Log "Step 3: Building final device list..."
|
||||||
$arpEntries = @{}
|
|
||||||
|
|
||||||
try {
|
# Add devices found in ARP
|
||||||
# Use Get-NetNeighbor for Windows 8/Server 2012 and later
|
foreach ($arpDevice in $arpDevices.Values) {
|
||||||
$neighbors = Get-NetNeighbor -AddressFamily IPv4 | Where-Object { $_.State -ne "Unreachable" }
|
$arpDevice.status = "online" # Change from "arp" to "online"
|
||||||
foreach ($neighbor in $neighbors) {
|
$devices += $arpDevice
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build device list
|
# Add devices found via ping (and try to get their MAC from ARP if available)
|
||||||
foreach ($ip in $aliveIPs) {
|
foreach ($ip in $aliveIPs) {
|
||||||
$device = @{
|
$device = @{
|
||||||
ip = $ip
|
ip = $ip
|
||||||
@@ -304,11 +469,14 @@ function Get-NetworkDevices {
|
|||||||
hostname = ""
|
hostname = ""
|
||||||
status = "online"
|
status = "online"
|
||||||
}
|
}
|
||||||
|
$devices += $device
|
||||||
|
}
|
||||||
|
|
||||||
# Try to resolve hostname
|
# Resolve hostnames and fix MAC addresses for local host
|
||||||
|
foreach ($device in $devices) {
|
||||||
try {
|
try {
|
||||||
$hostname = [System.Net.Dns]::GetHostEntry($ip).HostName
|
$hostname = [System.Net.Dns]::GetHostEntry($device.ip).HostName
|
||||||
if ($hostname -and $hostname -ne $ip) {
|
if ($hostname -and $hostname -ne $device.ip) {
|
||||||
$device.hostname = $hostname
|
$device.hostname = $hostname
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,12 +484,18 @@ function Get-NetworkDevices {
|
|||||||
# Hostname resolution failed, leave empty
|
# 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) {
|
if ($device.mac) {
|
||||||
Write-Log "Device found: $ip -> $($device.mac) ($($device.hostname))"
|
Write-Log "Final device: $($device.ip) -> $($device.mac) ($($device.hostname))"
|
||||||
} else {
|
} else {
|
||||||
Write-Log "Device found: $ip (no MAC address available)"
|
Write-Log "Final device: $($device.ip) (no MAC address available) ($($device.hostname))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user