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:
2025-09-07 15:32:32 +03:00
parent 3c14094650
commit 43e997ba47
4 changed files with 151 additions and 38 deletions

View File

@@ -360,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"
@@ -377,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
@@ -392,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,
@@ -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.'
}
# 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()

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;
@@ -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 */
@@ -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

@@ -53,10 +53,9 @@ function displayComputers(computers) {
🗑️
</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 +181,23 @@ function scanNetwork() {
const network = getSelectedNetwork();
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(result => {
}).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 +230,23 @@ function triggerWindowsScan() {
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
}).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 +286,16 @@ 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;
}
// 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">
@@ -313,22 +321,31 @@ function displayScanResults(computers) {
`;
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 += `
<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 +358,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 +368,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 +380,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 +395,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 +501,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: {

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