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>
This commit is contained in:
17
app/app.py
17
app/app.py
@@ -360,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"
|
||||||
@@ -377,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
|
||||||
@@ -392,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,
|
||||||
@@ -408,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()
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 */
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,9 @@ function displayComputers(computers) {
|
|||||||
🗑️
|
🗑️
|
||||||
</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 +181,23 @@ function scanNetwork() {
|
|||||||
const network = getSelectedNetwork();
|
const network = getSelectedNetwork();
|
||||||
const requestData = network ? { network: network } : {};
|
const requestData = network ? { network: network } : {};
|
||||||
|
|
||||||
fetch('/api/scan', {
|
// Get current computers list to compare with scan results
|
||||||
method: 'POST',
|
Promise.all([
|
||||||
headers: {
|
fetch('/api/computers').then(response => response.json()),
|
||||||
'Content-Type': 'application/json',
|
fetch('/api/scan', {
|
||||||
},
|
method: 'POST',
|
||||||
body: JSON.stringify(requestData)
|
headers: {
|
||||||
})
|
'Content-Type': 'application/json',
|
||||||
.then(response => response.json())
|
},
|
||||||
.then(result => {
|
body: JSON.stringify(requestData)
|
||||||
|
}).then(response => response.json())
|
||||||
|
])
|
||||||
|
.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 +230,23 @@ function triggerWindowsScan() {
|
|||||||
|
|
||||||
showMessage('Declanșând scanul Windows...', 'success');
|
showMessage('Declanșând scanul Windows...', 'success');
|
||||||
|
|
||||||
fetch('/api/scan/windows', {
|
// Get current computers list to compare with scan results
|
||||||
method: 'POST',
|
Promise.all([
|
||||||
headers: {
|
fetch('/api/computers').then(response => response.json()),
|
||||||
'Content-Type': 'application/json',
|
fetch('/api/scan/windows', {
|
||||||
},
|
method: 'POST',
|
||||||
body: JSON.stringify(requestData)
|
headers: {
|
||||||
})
|
'Content-Type': 'application/json',
|
||||||
.then(response => response.json())
|
},
|
||||||
.then(data => {
|
body: JSON.stringify(requestData)
|
||||||
|
}).then(response => response.json())
|
||||||
|
])
|
||||||
|
.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 +286,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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">
|
||||||
@@ -313,22 +321,31 @@ function displayScanResults(computers) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
computers.forEach((computer, index) => {
|
computers.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 +358,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 +368,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 +380,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 +395,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 +501,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: {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user