Upload folder via GUI - src
This commit is contained in:
@@ -379,7 +379,6 @@ function draw() {
|
||||
var isSel=selNode&&selNode.id===n.id;
|
||||
var isCF=connFrom&&connFrom.id===n.id;
|
||||
var isMultiSel=multiSel.indexOf(n)>=0;
|
||||
var isMultiSel=multiSel.indexOf(n)>=0;
|
||||
if(isSel||isMultiSel){ctx.shadowColor=n.color||'#00d4aa';ctx.shadowBlur=16;}
|
||||
ctx.fillStyle='#161b22';
|
||||
ctx.strokeStyle=isCF?'#ffa502':(isMultiSel?'#00d4aa':(isSel?(n.color||'#00d4aa'):'#30363d'));
|
||||
|
||||
384
src/index.html
384
src/index.html
@@ -10,6 +10,50 @@
|
||||
--accent:#00d4aa; --accent2:#0099ff; --danger:#ff4757; --warn:#ffa502;
|
||||
--text:#e6edf3; --muted:#7d8590; --online:#26de81; --purple:#a78bfa;
|
||||
}
|
||||
var DEVICE_TYPE_OPTIONS = [
|
||||
'Windows-PC',
|
||||
'Linux-Server',
|
||||
'NAS/Storage',
|
||||
'Kamera/CCTV',
|
||||
'Alarmanlage/Security',
|
||||
'Router/Access Point',
|
||||
'Drucker',
|
||||
'IoT/Smart-Device',
|
||||
'Virtualisierungs-Host',
|
||||
'Konsole/Mediengeraet'
|
||||
];
|
||||
|
||||
function getForcedDeviceType(ip) {
|
||||
var p = deviceProfiles[ip];
|
||||
if (!p || !p.forcedType) return 'auto';
|
||||
return p.forcedType;
|
||||
}
|
||||
|
||||
function buildDeviceTypeOptions(currentType) {
|
||||
var html = '<option value="auto"'+(currentType==='auto'?' selected':'')+'>Auto</option>';
|
||||
DEVICE_TYPE_OPTIONS.forEach(function(t) {
|
||||
html += '<option value="'+esc(t)+'"'+(currentType===t?' selected':'')+'>'+esc(t)+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function setDeviceClass(ip, selectedType) {
|
||||
if (!ip) return;
|
||||
if (selectedType === 'auto') {
|
||||
delete deviceProfiles[ip];
|
||||
} else {
|
||||
deviceProfiles[ip] = deviceProfiles[ip] || {};
|
||||
deviceProfiles[ip].forcedType = selectedType;
|
||||
}
|
||||
await api.saveDeviceProfiles(deviceProfiles);
|
||||
|
||||
var activeItem = document.querySelector('.device-item.active');
|
||||
if (activeItem) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
if (d) selectDevice(d, activeItem);
|
||||
}
|
||||
showToast('Geräteklasse gespeichert', selectedType==='auto' ? 'Auto-Erkennung aktiv' : selectedType+' manuell gesetzt', 'info');
|
||||
}
|
||||
.light {
|
||||
--bg:#f6f8fa; --surface:#ffffff; --surface2:#f0f2f5; --border:#d0d7de;
|
||||
--text:#24292f; --muted:#57606a; --surface2:#eaeef2;
|
||||
@@ -323,8 +367,8 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<div class="stat-box"><div class="stat-num" id="statOnline">0</div><div class="stat-label">Online</div></div>
|
||||
</div>
|
||||
|
||||
<div id="liveStatusBar" style="display:none;padding:5px 12px;font-size:10px;color:var(--accent);display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--border)">
|
||||
<span style="animation:livepulse 2s ease-in-out infinite">●</span> Live-Ping aktiv – aktualisiert alle 4s
|
||||
<div id="liveStatusBar" style="display:none;padding:5px 12px;font-size:10px;color:var(--accent);align-items:center;gap:6px;border-bottom:1px solid var(--border)">
|
||||
<span style="animation:livepulse 2s ease-in-out infinite">●</span> Live-Ping aktiv – aktualisiert alle 6s
|
||||
</div>
|
||||
|
||||
<div class="search-wrap">
|
||||
@@ -415,6 +459,15 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<div><div class="setting-label">Offline-Geräte melden</div><div class="setting-sub">Benachrichtigung wenn ein Gerät nicht mehr erreichbar ist</div></div>
|
||||
<label class="toggle"><input type="checkbox" id="settingNotifyOffline"><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div><div class="setting-label">Offline-Empfindlichkeit</div><div class="setting-sub">Jede Prüfung sendet bereits 3 Ping-Pakete (gilt nur als Fehlschlag, wenn alle 3 verloren gehen). Hier stellst du ein, wie viele solcher Prüfungen in Folge fehlschlagen müssen, bevor ein Gerät als offline gilt.</div></div>
|
||||
<select class="setting-select" id="settingFailThreshold">
|
||||
<option value="1">Sofort (1 Prüfung)</option>
|
||||
<option value="2">Normal (2 Prüfungen)</option>
|
||||
<option value="3">Tolerant (3 Prüfungen)</option>
|
||||
<option value="5">Sehr tolerant (5 Prüfungen)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div><div class="setting-label">Beim Start automatisch scannen</div><div class="setting-sub">Startet den Scan direkt beim Öffnen</div></div>
|
||||
<label class="toggle"><input type="checkbox" id="settingAutoStart"><span class="toggle-slider"></span></label>
|
||||
@@ -532,6 +585,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<script>
|
||||
var devices = [], totalPorts = 0, scanning = false;
|
||||
var currentPorts = [], deviceNames = {}, favorites = [], settings = {};
|
||||
var deviceProfiles = {};
|
||||
var monitoredSubnets = [];
|
||||
var extraSubnetDevices = {}; // { subnet: [devices] }
|
||||
|
||||
@@ -539,6 +593,7 @@ var extraSubnetDevices = {}; // { subnet: [devices] }
|
||||
async function init() {
|
||||
currentPorts = await api.getPorts();
|
||||
deviceNames = await api.getDeviceNames();
|
||||
deviceProfiles = await api.getDeviceProfiles();
|
||||
favorites = await api.getFavorites();
|
||||
settings = await api.getSettings();
|
||||
applyTheme(settings.theme || 'dark');
|
||||
@@ -546,6 +601,8 @@ async function init() {
|
||||
document.getElementById('settingAutoScan').value = settings.autoScan || 0;
|
||||
document.getElementById('settingNotifyNew').checked = settings.notifyNew !== false;
|
||||
document.getElementById('settingNotifyOffline').checked = settings.notifyOffline !== false;
|
||||
document.getElementById('settingFailThreshold').value = settings.failThreshold || 2;
|
||||
FAIL_THRESHOLD = settings.failThreshold || 2;
|
||||
document.getElementById('settingAutoStart').checked = settings.autoStart || false;
|
||||
document.getElementById('settingCloseToTray').checked = settings.closeToTray || false;
|
||||
if (settings.autoScan > 0) document.getElementById('autoscanBadge').classList.add('visible');
|
||||
@@ -669,12 +726,15 @@ async function scanExtraSubnets() {
|
||||
// ── Live Ping Loop ────────────────────────────────────────
|
||||
var livePingTimer = null;
|
||||
var livePingBusy = false;
|
||||
var LIVE_PING_INTERVAL = 4000; // ms
|
||||
var LIVE_PING_INTERVAL = 6000; // ms — increased from 4s since each check now sends 3 packets per device (takes a bit longer, but is far more reliable)
|
||||
var FAIL_THRESHOLD = 2; // consecutive FAILED CHECKS before flagging offline. Note: each "check" already sends 3 ICMP packets and only fails if ALL 3 are lost, so this is now a much stronger guarantee than it sounds.
|
||||
var consecutiveFails = {}; // { ip: count }
|
||||
|
||||
function startLivePing() {
|
||||
stopLivePing();
|
||||
if (!devices.length) return;
|
||||
document.getElementById('liveStatusBar').style.display = 'flex';
|
||||
consecutiveFails = {};
|
||||
livePingTimer = setInterval(runLivePingCycle, LIVE_PING_INTERVAL);
|
||||
runLivePingCycle();
|
||||
}
|
||||
@@ -694,16 +754,27 @@ async function runLivePingCycle() {
|
||||
results.forEach(function(res) {
|
||||
var d = devices.find(function(x) { return x.ip === res.ip; });
|
||||
if (!d) return;
|
||||
var liveMs = res.alive && res.ms !== null ? parseInt(res.ms) : null;
|
||||
d.ms = liveMs;
|
||||
if (res.alive) onlineCount++;
|
||||
|
||||
if (res.alive) {
|
||||
consecutiveFails[res.ip] = 0;
|
||||
} else {
|
||||
consecutiveFails[res.ip] = (consecutiveFails[res.ip] || 0) + 1;
|
||||
}
|
||||
// Only treat as truly offline after FAIL_THRESHOLD consecutive misses —
|
||||
// a single dropped ping on a healthy LAN is normal and not worth alerting on.
|
||||
var confirmedOffline = consecutiveFails[res.ip] >= FAIL_THRESHOLD;
|
||||
var liveMs = (res.alive && res.ms !== null) ? parseInt(res.ms) : null;
|
||||
|
||||
if (res.alive) { d.ms = liveMs; onlineCount++; }
|
||||
else if (!confirmedOffline) { onlineCount++; } // still counted online while within tolerance
|
||||
|
||||
// Update list row
|
||||
var item = document.getElementById('dev-' + res.ip.replace(/\./g, '-'));
|
||||
if (item) {
|
||||
var dot = item.querySelector('.device-dot');
|
||||
var pingEl = item.querySelector('.device-ping');
|
||||
var wasOffline = item.classList.contains('is-offline');
|
||||
var wasConfirmedOffline = item.classList.contains('is-offline');
|
||||
|
||||
if (res.alive) {
|
||||
item.classList.remove('is-offline');
|
||||
var dotColor = liveMs<=50?'var(--online)':liveMs<=100?'var(--warn)':'var(--danger)';
|
||||
@@ -713,12 +784,13 @@ async function runLivePingCycle() {
|
||||
pingEl.className = 'device-ping ' + lpc;
|
||||
pingEl.textContent = liveMs + ' ms';
|
||||
}
|
||||
if (wasOffline) showToast('Gerät wieder online', (getCustomName(res.ip)||d.hostname||res.ip), 'new');
|
||||
} else {
|
||||
if (!wasOffline) { item.classList.add('is-offline'); showToast('Gerät offline', (getCustomName(res.ip)||d.hostname||res.ip)+' antwortet nicht mehr', 'offline'); }
|
||||
if (wasConfirmedOffline) showToast('Gerät wieder online', (getCustomName(res.ip)||d.hostname||res.ip), 'new');
|
||||
} else if (confirmedOffline) {
|
||||
if (!wasConfirmedOffline) { item.classList.add('is-offline'); showToast('Gerät offline', (getCustomName(res.ip)||d.hostname||res.ip)+' antwortet seit '+FAIL_THRESHOLD+' Prüfungen (je 3 Pings) nicht mehr', 'offline'); }
|
||||
if (dot) dot.style.background = 'var(--danger)';
|
||||
if (pingEl) { pingEl.className='device-ping slow'; pingEl.textContent='offline'; }
|
||||
}
|
||||
// else: single missed ping within tolerance — leave display as-is, no alert
|
||||
}
|
||||
|
||||
// Update detail panel if this device is currently selected
|
||||
@@ -730,7 +802,7 @@ async function runLivePingCycle() {
|
||||
var pc = liveMs<=50?'green':liveMs<=100?'warn':'danger';
|
||||
pingVal.className = 'info-card-val ' + pc;
|
||||
pingVal.textContent = liveMs + ' ms';
|
||||
} else {
|
||||
} else if (confirmedOffline) {
|
||||
pingVal.className = 'info-card-val danger';
|
||||
pingVal.textContent = 'Offline';
|
||||
}
|
||||
@@ -805,9 +877,13 @@ function toggleFavorite(ip, btn, e) {
|
||||
|
||||
function renderFavorites() {
|
||||
var panel = document.getElementById('panelFavorites');
|
||||
var empty = document.getElementById('emptyFavs');
|
||||
if (!favorites.length) { empty.style.display=''; return; }
|
||||
empty.style.display = 'none';
|
||||
if (!favorites.length) {
|
||||
panel.innerHTML = '<div class="empty-list" id="emptyFavs">'+
|
||||
'<div class="empty-icon">⭐</div>'+
|
||||
'Noch keine Favoriten.<br>Klick auf ☆ beim Gerät<br>um es zu pinnen.'+
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
favorites.forEach(function(ip) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
@@ -832,15 +908,17 @@ function selectDeviceByIp(ip) {
|
||||
|
||||
// ── Device detail ─────────────────────────────────────────
|
||||
async function selectDevice(d, item) {
|
||||
if (watchIp && watchIp !== d.ip) stopWatchMode();
|
||||
document.querySelectorAll('.device-item').forEach(function(el){el.classList.remove('active');});
|
||||
if(item) item.classList.add('active');
|
||||
document.getElementById('placeholder').style.display='none';
|
||||
var detail = document.getElementById('deviceDetail');
|
||||
detail.classList.add('visible');
|
||||
var icon = getDeviceIcon(d.vendor);
|
||||
var icon = getDeviceIcon(d);
|
||||
var customName = getCustomName(d.ip);
|
||||
var customNote = deviceNames[d.ip]&&deviceNames[d.ip].note?deviceNames[d.ip].note:null;
|
||||
var displayName = customName||d.hostname||d.ip;
|
||||
var forcedType = getForcedDeviceType(d.ip);
|
||||
var isUnknown = !customName && (!d.vendor || d.vendor==='Unbekannt') && !confirmedDevices[d.ip];
|
||||
|
||||
detail.innerHTML =
|
||||
@@ -853,6 +931,7 @@ async function selectDevice(d, item) {
|
||||
(d.ipv6?'<div class="ipv6-badge">IPv6: '+esc(d.ipv6)+'</div>':'')+
|
||||
(customNote?'<div style="font-size:11px;color:var(--muted);margin-top:2px">📝 '+esc(customNote)+'</div>':'')+
|
||||
'<div class="detail-vendor" id="detailVendor">'+(d.vendor&&d.vendor!=='Unbekannt'?esc(d.vendor):'…')+'</div>'+
|
||||
'<div style="font-size:11px;color:var(--muted);margin-top:2px">🧠 Klasse: <span style="color:var(--accent)">'+(forcedType!=='auto'?esc(forcedType)+' (manuell)':'Auto-Erkennung')+'</span></div>'+
|
||||
(d.os?'<div class="detail-os">🖥 Erkanntes OS: '+esc(d.os)+(d.ttl?' (TTL '+d.ttl+')':'')+'</div>':'')+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
@@ -860,7 +939,16 @@ async function selectDevice(d, item) {
|
||||
'<button class="btn-action" onclick="reping(\''+d.ip+'\')">⚡ Sofort aktualisieren</button>'+
|
||||
'<button class="btn-action" onclick="rescanPorts(\''+d.ip+'\')">🔍 Ports scannen</button>'+
|
||||
'<button class="btn-action" onclick="quickEditDevice(\''+d.ip+'\')">✏️ Umbenennen</button>'+
|
||||
'<button class="btn-action" id="btnWatch" onclick="toggleWatchMode(\''+d.ip+'\')" style="color:#a78bfa;border-color:#a78bfa">🔭 Beobachten (Stecker-Test)</button>'+
|
||||
'<button class="btn-action" onclick="identifyDevice(\''+d.ip+'\',\''+(d.mac||'')+'\')" style="color:#ffa502;border-color:#ffa502">🔬 Identifizieren</button>'+
|
||||
'</div>'+
|
||||
'<div style="margin:-8px 0 16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">'+
|
||||
'<span style="font-size:11px;color:var(--muted)">Geräteklasse:</span>'+
|
||||
'<select class="setting-select" onchange="setDeviceClass(\''+d.ip+'\', this.value)">'+buildDeviceTypeOptions(forcedType)+'</select>'+
|
||||
'<span style="font-size:10px;color:var(--muted)">Auto nutzt Ports, Hersteller und Hostname.</span>'+
|
||||
'</div>'+
|
||||
'<div id="watchPanel" style="display:none"></div>'+
|
||||
'<div id="identifyPanel" style="display:none"></div>'+
|
||||
'<div class="info-grid">'+
|
||||
'<div class="info-card"><div class="info-card-label">IP-Adresse</div><div class="info-card-val teal">'+d.ip+'</div></div>'+
|
||||
'<div class="info-card"><div class="info-card-label">MAC-Adresse</div><div class="info-card-val blue" id="detailMac">'+(d.mac||'–')+'</div></div>'+
|
||||
@@ -1018,6 +1106,158 @@ async function reping(ip) {
|
||||
if (lpe&&liveMs!==null) { var lpc=liveMs<=50?'fast':liveMs<=100?'mid':'slow'; lpe.className='device-ping '+lpc; lpe.textContent=liveMs+' ms'; }
|
||||
}
|
||||
|
||||
// ── Watch Mode: fast continuous ping for the unplug/replug identification test ──
|
||||
var watchTimer = null;
|
||||
var watchIp = null;
|
||||
var watchHistory = []; // sequence of true/false results shown as a timeline
|
||||
|
||||
function toggleWatchMode(ip) {
|
||||
if (watchTimer && watchIp === ip) { stopWatchMode(); return; }
|
||||
stopWatchMode();
|
||||
watchIp = ip;
|
||||
watchHistory = [];
|
||||
document.getElementById('watchPanel').style.display = 'block';
|
||||
document.getElementById('btnWatch').textContent = '⏹ Beobachtung stoppen';
|
||||
document.getElementById('btnWatch').style.background = '#a78bfa';
|
||||
document.getElementById('btnWatch').style.color = '#000';
|
||||
renderWatchPanel('Starte Beobachtung…', null);
|
||||
watchTick(); // run immediately, then every 1.5s
|
||||
watchTimer = setInterval(watchTick, 1500);
|
||||
}
|
||||
|
||||
function stopWatchMode() {
|
||||
if (watchTimer) { clearInterval(watchTimer); watchTimer = null; }
|
||||
watchIp = null;
|
||||
var btn = document.getElementById('btnWatch');
|
||||
if (btn) { btn.textContent = '🔭 Beobachten (Stecker-Test)'; btn.style.background=''; btn.style.color='#a78bfa'; }
|
||||
var panel = document.getElementById('watchPanel');
|
||||
if (panel) panel.style.display = 'none';
|
||||
}
|
||||
|
||||
async function watchTick() {
|
||||
if (!watchIp) return;
|
||||
var ip = watchIp;
|
||||
try {
|
||||
var res = await api.pingDevice(ip);
|
||||
if (watchIp !== ip) return; // user switched device mid-flight, ignore stale result
|
||||
watchHistory.push(res.alive);
|
||||
if (watchHistory.length > 40) watchHistory.shift();
|
||||
var statusText = res.alive ? ('Online – '+(res.ms!=null?res.ms+' ms':'')) : 'Keine Antwort';
|
||||
renderWatchPanel(statusText, res.alive);
|
||||
} catch(e) {
|
||||
watchHistory.push(false);
|
||||
renderWatchPanel('Fehler bei der Abfrage', false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWatchPanel(statusText, alive) {
|
||||
var panel = document.getElementById('watchPanel');
|
||||
if (!panel) return;
|
||||
var dots = watchHistory.map(function(a) {
|
||||
return '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:3px;background:'+(a?'#26de81':'#ff4757')+'"></span>';
|
||||
}).join('');
|
||||
var bigColor = alive===null ? 'var(--muted)' : (alive ? '#26de81' : '#ff4757');
|
||||
var bigText = alive===null ? '⏳' : (alive ? '🟢 ONLINE' : '🔴 KEINE ANTWORT');
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:2px solid '+bigColor+';border-radius:10px;padding:16px;margin-bottom:16px;text-align:center">'+
|
||||
'<div style="font-size:11px;color:var(--muted);margin-bottom:6px">🔭 Live-Beobachtung – jetzt Gerät vom Strom/Kabel trennen und beobachten</div>'+
|
||||
'<div style="font-size:26px;font-weight:800;color:'+bigColor+'">'+bigText+'</div>'+
|
||||
'<div style="font-size:12px;color:var(--muted);margin-top:4px">'+esc(statusText)+'</div>'+
|
||||
'<div style="margin-top:10px;line-height:1.8">'+dots+'</div>'+
|
||||
'<div style="font-size:10px;color:var(--muted);margin-top:6px">Jeder Punkt = 1 Prüfung (alle 1,5s) · grün = Antwort · rot = keine Antwort</div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ── Device identification: combines NetBIOS, reverse-DNS, WoL-probe, and ports ──
|
||||
async function identifyDevice(ip, mac) {
|
||||
var panel = document.getElementById('identifyPanel');
|
||||
if (!panel) return;
|
||||
panel.style.display = 'block';
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:1px solid var(--warn);border-radius:10px;padding:16px;margin-bottom:16px">'+
|
||||
'<div style="font-size:12px;color:var(--warn);display:flex;align-items:center;gap:8px"><div class="spinner"></div> Identifiziere Gerät – prüfe NetBIOS, Hostname und Wake-on-LAN…</div>'+
|
||||
'</div>';
|
||||
|
||||
var result;
|
||||
try {
|
||||
result = await api.identifyDevice(ip, mac);
|
||||
} catch(e) {
|
||||
panel.innerHTML = '<div style="background:var(--surface);border:1px solid var(--danger);border-radius:10px;padding:16px;margin-bottom:16px;color:var(--danger);font-size:12px">Identifikation fehlgeschlagen: '+esc(String(e.message||e))+'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If we resolved a fresher MAC than what was previously known (e.g. ARP
|
||||
// cache had expired), update the displayed MAC and the in-memory device record.
|
||||
if (result.mac && result.mac !== '–') {
|
||||
var macEl = document.getElementById('detailMac');
|
||||
if (macEl) macEl.textContent = result.mac;
|
||||
var d2 = devices.find(function(x) { return x.ip === ip; });
|
||||
if (d2) d2.mac = result.mac;
|
||||
}
|
||||
|
||||
var rows = [];
|
||||
rows.push({
|
||||
label: 'NetBIOS-Name',
|
||||
value: result.netbiosName || 'Nicht ermittelbar',
|
||||
hint: result.netbiosName ? 'Computername über NetBIOS gefunden — starker Hinweis auf einen Windows-PC oder ein NAS mit aktiviertem NetBIOS.' : 'Kein NetBIOS verfügbar (deaktiviert, kein Windows-Gerät, oder nicht im selben Subnetz erreichbar).',
|
||||
ok: !!result.netbiosName
|
||||
});
|
||||
rows.push({
|
||||
label: 'Reverse-DNS / Hostname',
|
||||
value: result.reverseDns || 'Nicht ermittelbar',
|
||||
hint: result.reverseDns ? 'Hostname über DNS-Auflösung gefunden.' : 'Router/DNS liefert keinen Namen für diese IP zurück.',
|
||||
ok: !!result.reverseDns
|
||||
});
|
||||
rows.push({
|
||||
label: 'Wake-on-LAN',
|
||||
value: result.wakeOnLan && result.wakeOnLan.sent ? 'Paket gesendet' : 'Nicht möglich',
|
||||
hint: result.wakeOnLan ? result.wakeOnLan.reason : '',
|
||||
ok: result.wakeOnLan && result.wakeOnLan.sent
|
||||
});
|
||||
|
||||
var portsHint = '';
|
||||
if (result.openPorts && result.openPorts.length) {
|
||||
portsHint = '<div style="margin-top:10px;font-size:12px;color:var(--text)"><strong>Offene Ports gefunden:</strong> ' +
|
||||
result.openPorts.map(function(p){return esc(p.name)+' ('+p.port+')';}).join(', ') + '</div>';
|
||||
} else {
|
||||
portsHint = '<div style="margin-top:10px;font-size:12px;color:var(--muted)">Keine bekannten Ports offen — typisch für einfache IoT-Geräte, Smart-Plugs oder Bridges ohne eigene Web-Oberfläche.</div>';
|
||||
}
|
||||
|
||||
var identity = result.identity || null;
|
||||
var identityBlock = '';
|
||||
if (identity) {
|
||||
var confidenceColor = identity.confidence === 'manuell' ? 'var(--accent2)' : (identity.confidence === 'hoch' ? 'var(--online)' : (identity.confidence === 'mittel' ? 'var(--warn)' : 'var(--danger)'));
|
||||
var ranking = (identity.ranking || []).map(function(r) {
|
||||
return '<div style="font-size:11px;color:var(--muted)">• '+esc(r.type)+' ('+r.score+' Punkte)</div>';
|
||||
}).join('');
|
||||
var reasons = (identity.reasons || []).map(function(x) {
|
||||
return '<div style="font-size:11px;color:var(--muted)">• '+esc(x)+'</div>';
|
||||
}).join('');
|
||||
identityBlock =
|
||||
'<div style="margin-top:10px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--surface2)">'+
|
||||
'<div style="font-size:12px"><strong>Wahrscheinlicher Gerätetyp:</strong> '+esc(identity.likelyType)+'</div>'+
|
||||
'<div style="font-size:11px;margin-top:3px;color:'+confidenceColor+'"><strong>Confidence:</strong> '+esc(identity.confidence)+'</div>'+
|
||||
(ranking ? '<div style="margin-top:6px"><div style="font-size:11px;color:var(--text);font-weight:600">Top-Kandidaten</div>'+ranking+'</div>' : '')+
|
||||
(reasons ? '<div style="margin-top:6px"><div style="font-size:11px;color:var(--text);font-weight:600">Signale</div>'+reasons+'</div>' : '')+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:1px solid var(--warn);border-radius:10px;padding:16px;margin-bottom:16px">'+
|
||||
'<div style="font-size:12px;font-weight:700;color:var(--warn);margin-bottom:10px">🔬 Identifikations-Ergebnis</div>'+
|
||||
rows.map(function(r) {
|
||||
return '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">'+
|
||||
'<span style="flex-shrink:0;margin-top:1px">'+(r.ok?'✅':'⚪')+'</span>'+
|
||||
'<div><div style="font-size:12px"><strong>'+esc(r.label)+':</strong> '+esc(r.value)+'</div>'+
|
||||
(r.hint?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+esc(r.hint)+'</div>':'')+
|
||||
'</div></div>';
|
||||
}).join('') +
|
||||
identityBlock +
|
||||
portsHint +
|
||||
'<div style="font-size:10px;color:var(--muted);margin-top:10px;border-top:1px solid var(--border);padding-top:8px">Tipp: Wake-on-LAN funktioniert nur, wenn das Gerät ausgeschaltet (nicht getrennt) ist und WoL in dessen Netzwerkeinstellungen aktiviert ist. Beobachte, ob es kurz danach wieder online geht.</div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
async function rescanPorts(ip) {
|
||||
var pa = document.getElementById('portsArea');
|
||||
if (!pa) return;
|
||||
@@ -1082,11 +1322,13 @@ async function saveSettings() {
|
||||
autoScan: parseInt(document.getElementById('settingAutoScan').value),
|
||||
notifyNew: document.getElementById('settingNotifyNew').checked,
|
||||
notifyOffline: document.getElementById('settingNotifyOffline').checked,
|
||||
failThreshold: parseInt(document.getElementById('settingFailThreshold').value),
|
||||
autoStart: document.getElementById('settingAutoStart').checked,
|
||||
closeToTray: document.getElementById('settingCloseToTray').checked
|
||||
};
|
||||
await api.saveSettings(settings);
|
||||
applyTheme(settings.theme);
|
||||
FAIL_THRESHOLD = settings.failThreshold;
|
||||
document.getElementById('autoscanBadge').classList.toggle('visible', settings.autoScan > 0);
|
||||
closeModal('settingsModal');
|
||||
showToast('Einstellungen gespeichert','','info');
|
||||
@@ -1138,7 +1380,13 @@ async function resetPorts() {
|
||||
function openDeviceManager() { renderDeviceList(); document.getElementById('deviceModal').classList.add('open'); }
|
||||
function renderDeviceList() {
|
||||
var list=document.getElementById('deviceModalList'); list.innerHTML='';
|
||||
var ips=Object.keys(deviceNames).sort(function(a,b){return a.split('.').map(Number).join('')-b.split('.').map(Number).join('');});
|
||||
var ips=Object.keys(deviceNames).sort(function(a,b){
|
||||
var aa=a.split('.').map(Number), bb=b.split('.').map(Number);
|
||||
for (var i=0;i<4;i++) {
|
||||
if ((aa[i]||0)!==(bb[i]||0)) return (aa[i]||0)-(bb[i]||0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
ips.forEach(function(ip) {
|
||||
var d=deviceNames[ip], row=document.createElement('div'); row.className='device-name-row';
|
||||
row.innerHTML='<span class="dn-ip">'+ip+'</span>'+
|
||||
@@ -1308,24 +1556,96 @@ async function exportDeviceList(format) {
|
||||
if (result.success) showToast('Export erfolgreich', 'Geräteliste als '+format.toUpperCase()+' gespeichert', 'info');
|
||||
}
|
||||
|
||||
var DEVICE_TYPE_OPTIONS = [
|
||||
'Windows-PC',
|
||||
'Linux-Server',
|
||||
'NAS/Storage',
|
||||
'Kamera/CCTV',
|
||||
'Alarmanlage/Security',
|
||||
'Router/Access Point',
|
||||
'Drucker',
|
||||
'IoT/Smart-Device',
|
||||
'Virtualisierungs-Host',
|
||||
'Konsole/Mediengeraet'
|
||||
];
|
||||
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
function getForcedDeviceType(ip) {
|
||||
var p = deviceProfiles[ip];
|
||||
if (!p || !p.forcedType) return 'auto';
|
||||
return p.forcedType;
|
||||
}
|
||||
|
||||
function buildDeviceTypeOptions(currentType) {
|
||||
var html = '<option value="auto"'+(currentType==='auto'?' selected':'')+'>Auto</option>';
|
||||
DEVICE_TYPE_OPTIONS.forEach(function(t) {
|
||||
html += '<option value="'+esc(t)+'"'+(currentType===t?' selected':'')+'>'+esc(t)+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function setDeviceClass(ip, selectedType) {
|
||||
if (!ip) return;
|
||||
if (selectedType === 'auto') {
|
||||
delete deviceProfiles[ip];
|
||||
} else {
|
||||
deviceProfiles[ip] = deviceProfiles[ip] || {};
|
||||
deviceProfiles[ip].forcedType = selectedType;
|
||||
}
|
||||
await api.saveDeviceProfiles(deviceProfiles);
|
||||
|
||||
var activeItem = document.querySelector('.device-item.active');
|
||||
if (activeItem) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
if (d) selectDevice(d, activeItem);
|
||||
}
|
||||
showToast('Geräteklasse gespeichert', selectedType==='auto' ? 'Auto-Erkennung aktiv' : selectedType+' manuell gesetzt', 'info');
|
||||
}
|
||||
|
||||
|
||||
function esc(s){
|
||||
return String(s||'')
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"')
|
||||
.replace(/'/g,''');
|
||||
}
|
||||
function getCustomName(ip){return deviceNames[ip]?deviceNames[ip].name:null;}
|
||||
function getDeviceIcon(vendor) {
|
||||
if (!vendor) return '🖥️';
|
||||
var v=vendor.toLowerCase();
|
||||
if(v.includes('apple')) return '🍎';
|
||||
if(v.includes('synology')) return '🗄️';
|
||||
if(v.includes('raspberry')) return '🍓';
|
||||
if(v.includes('fritz')||v.includes('avm')) return '📡';
|
||||
if(v.includes('tp-link')) return '📡';
|
||||
if(v.includes('ubiquiti')) return '📶';
|
||||
if(v.includes('vmware')||v.includes('qemu')||v.includes('proxmox')) return '💻';
|
||||
if(v.includes('cisco')) return '🔀';
|
||||
if(v.includes('samsung')) return '📺';
|
||||
if(v.includes('nintendo')) return '🎮';
|
||||
if(v.includes('brother')) return '🖨️';
|
||||
if(v.includes('abus')) return '🔒';
|
||||
function getDeviceIcon(deviceOrVendor) {
|
||||
var vendor = '';
|
||||
var nameHints = '';
|
||||
var forcedType = null;
|
||||
|
||||
if (typeof deviceOrVendor === 'string') {
|
||||
vendor = deviceOrVendor.toLowerCase();
|
||||
} else if (deviceOrVendor && typeof deviceOrVendor === 'object') {
|
||||
vendor = String(deviceOrVendor.vendor || '').toLowerCase();
|
||||
nameHints = String(deviceOrVendor.hostname || deviceOrVendor.displayName || '').toLowerCase();
|
||||
if (deviceOrVendor.ip) forcedType = getForcedDeviceType(deviceOrVendor.ip);
|
||||
}
|
||||
|
||||
if (forcedType && forcedType !== 'auto') {
|
||||
if (forcedType === 'NAS/Storage') return '🗄️';
|
||||
if (forcedType === 'Kamera/CCTV') return '📷';
|
||||
if (forcedType === 'Alarmanlage/Security') return '🚨';
|
||||
if (forcedType === 'Drucker') return '🖨️';
|
||||
if (forcedType === 'Router/Access Point') return '📡';
|
||||
if (forcedType === 'Virtualisierungs-Host') return '💻';
|
||||
}
|
||||
|
||||
if (vendor.includes('synology') || vendor.includes('qnap') || vendor.includes('asustor') || nameHints.includes('nas') || nameHints.includes('diskstation')) return '🗄️';
|
||||
if (vendor.includes('hikvision') || vendor.includes('dahua') || vendor.includes('reolink') || vendor.includes('axis') || nameHints.includes('cam') || nameHints.includes('cctv') || nameHints.includes('nvr')) return '📷';
|
||||
if (vendor.includes('abus') || vendor.includes('ajax') || vendor.includes('honeywell') || nameHints.includes('alarm') || nameHints.includes('security')) return '🚨';
|
||||
if (vendor.includes('brother') || vendor.includes('hewlett') || vendor.includes('hp')) return '🖨️';
|
||||
if (vendor.includes('apple')) return '🍎';
|
||||
if (vendor.includes('raspberry')) return '🍓';
|
||||
if (vendor.includes('fritz') || vendor.includes('avm')) return '📡';
|
||||
if (vendor.includes('tp-link')) return '📡';
|
||||
if (vendor.includes('ubiquiti')) return '📶';
|
||||
if (vendor.includes('vmware') || vendor.includes('qemu') || vendor.includes('proxmox')) return '💻';
|
||||
if (vendor.includes('cisco')) return '🔀';
|
||||
if (vendor.includes('samsung')) return '📺';
|
||||
if (vendor.includes('nintendo')) return '🎮';
|
||||
return '🖥️';
|
||||
}
|
||||
|
||||
|
||||
355
src/main.js
355
src/main.js
@@ -31,6 +31,54 @@ ipcMain.handle('save-ports', (e, ports) => { COMMON_PORTS = ports; return writeJ
|
||||
ipcMain.handle('get-device-names', () => readJson('device-names.json', {}));
|
||||
ipcMain.handle('save-device-names', (e, names) => writeJson('device-names.json', names));
|
||||
|
||||
// ── Device profiles (manual class override per IP) ───────
|
||||
function loadDeviceProfiles() { return readJson('device-profiles.json', {}); }
|
||||
function saveDeviceProfiles(p) { return writeJson('device-profiles.json', p); }
|
||||
ipcMain.handle('get-device-profiles', () => loadDeviceProfiles());
|
||||
ipcMain.handle('save-device-profiles', (e, profiles) => saveDeviceProfiles(profiles));
|
||||
|
||||
// ── Identity keyword catalog (vendor + hostname hints) ───
|
||||
var DEFAULT_IDENTITY_KEYWORDS = {
|
||||
vendor: {
|
||||
nas: ['synology', 'qnap', 'asustor', 'terramaster', 'western digital', 'wd', 'seagate'],
|
||||
camera: ['hikvision', 'dahua', 'reolink', 'axis', 'foscam', 'vivotek', 'ezviz', 'imou', 'uniview'],
|
||||
alarm: ['abus', 'ajax', 'honeywell', 'bosch security', 'jablotron', 'somfy', 'satel', 'paradox', 'risco'],
|
||||
router: ['avm', 'fritz', 'tp-link', 'ubiquiti', 'cisco', 'mikrotik', 'netgear'],
|
||||
virtualization: ['vmware', 'proxmox', 'xen', 'qemu'],
|
||||
printer: ['brother', 'hewlett', 'hp']
|
||||
},
|
||||
hostname: {
|
||||
nas: ['nas', 'diskstation', 'storage', 'fileserver'],
|
||||
camera: ['cam', 'camera', 'cctv', 'nvr', 'dvr', 'ipc', 'doorbell'],
|
||||
alarm: ['alarm', 'security', 'siren', 'panel', 'zentrale'],
|
||||
router: ['fritz', 'router', 'gateway', 'access point', 'ap-']
|
||||
}
|
||||
};
|
||||
|
||||
function mergeStringArrays(baseArr, extraArr) {
|
||||
var out = (baseArr || []).slice();
|
||||
(extraArr || []).forEach(function(v) {
|
||||
var val = String(v || '').toLowerCase().trim();
|
||||
if (val && out.indexOf(val) < 0) out.push(val);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadIdentityKeywords() {
|
||||
var custom = readJson('identity-keywords.json', {});
|
||||
var merged = { vendor: {}, hostname: {} };
|
||||
Object.keys(DEFAULT_IDENTITY_KEYWORDS.vendor).forEach(function(k) {
|
||||
merged.vendor[k] = mergeStringArrays(DEFAULT_IDENTITY_KEYWORDS.vendor[k], custom.vendor && custom.vendor[k]);
|
||||
});
|
||||
Object.keys(DEFAULT_IDENTITY_KEYWORDS.hostname).forEach(function(k) {
|
||||
merged.hostname[k] = mergeStringArrays(DEFAULT_IDENTITY_KEYWORDS.hostname[k], custom.hostname && custom.hostname[k]);
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
ipcMain.handle('get-identity-keywords', () => loadIdentityKeywords());
|
||||
ipcMain.handle('save-identity-keywords', (e, customKeywords) => writeJson('identity-keywords.json', customKeywords || {}));
|
||||
|
||||
// ── Favorites ─────────────────────────────────────────────
|
||||
ipcMain.handle('get-favorites', () => readJson('favorites.json', []));
|
||||
ipcMain.handle('save-favorites', (e, favs) => writeJson('favorites.json', favs));
|
||||
@@ -250,25 +298,44 @@ ipcMain.handle('get-network-info', () => {
|
||||
function pingHost(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
var start = Date.now();
|
||||
var args = process.platform === 'win32' ? ['-n','1','-w','500',ip] : ['-c','1','-W','1',ip];
|
||||
execFile('ping', args, { timeout: 2000 }, function(err, stdout) {
|
||||
// Send 3 ICMP packets per check (like a real "ping" command does) instead
|
||||
// of just 1. A single dropped packet is normal on any real network and
|
||||
// does NOT mean the host is down — only treat it as unreachable if ALL
|
||||
// packets in this check fail to get a reply.
|
||||
var args = process.platform === 'win32' ? ['-n','3','-w','1000',ip] : ['-c','3','-W','1',ip];
|
||||
execFile('ping', args, { timeout: 4000 }, function(err, stdout) {
|
||||
stdout = stdout || '';
|
||||
var alive = !err && (stdout.includes('TTL=') || stdout.includes('ttl=') || stdout.includes('1 received'));
|
||||
// Count how many replies actually came back, rather than treating the
|
||||
// whole check as binary alive/dead based on the process exit code.
|
||||
var ttlMatches = stdout.match(/ttl[=:]\s*\d+/gi) || [];
|
||||
var repliesReceived = ttlMatches.length;
|
||||
var alive = repliesReceived > 0;
|
||||
|
||||
var ttl = null, os_guess = null;
|
||||
var ttlMatch = stdout.match(/TTL=(\d+)/i);
|
||||
var ttlMatch = stdout.match(/ttl[=:]\s*(\d+)/i);
|
||||
if (ttlMatch) {
|
||||
ttl = parseInt(ttlMatch[1]);
|
||||
if (ttl >= 128) os_guess = 'Windows';
|
||||
else if (ttl >= 64) os_guess = 'Linux/macOS';
|
||||
else if (ttl >= 32) os_guess = 'Network Device';
|
||||
}
|
||||
|
||||
// Average the RTT across all replies actually received, for a more
|
||||
// representative number than a single sample.
|
||||
var ms = null;
|
||||
if (alive) {
|
||||
var msMatch = stdout.match(/(\d+(?:\.\d+)?)\s*ms\b/i);
|
||||
ms = msMatch ? Math.round(parseFloat(msMatch[1])) : (Date.now() - start);
|
||||
var msMatches = stdout.match(/(\d+(?:\.\d+)?)\s*ms\b/gi) || [];
|
||||
var msValues = msMatches.map(function(m) { return parseFloat(m); }).filter(function(n) { return !isNaN(n); });
|
||||
if (msValues.length) {
|
||||
var sum = msValues.reduce(function(a,b){return a+b;}, 0);
|
||||
ms = Math.round(sum / msValues.length);
|
||||
} else {
|
||||
ms = Date.now() - start;
|
||||
}
|
||||
}
|
||||
if (DEBUG_SCAN) console.log('[pingHost]', ip, '-> alive:', alive, 'err:', err ? err.message : null, 'stdout snippet:', stdout.substring(0,120).replace(/\n/g,' | '));
|
||||
resolve({ ip: ip, alive: alive, ms: ms, ttl: ttl, os: os_guess });
|
||||
|
||||
if (DEBUG_SCAN) console.log('[pingHost]', ip, '-> alive:', alive, 'replies:', repliesReceived+'/3', 'ms:', ms);
|
||||
resolve({ ip: ip, alive: alive, ms: ms, ttl: ttl, os: os_guess, repliesReceived: repliesReceived });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -314,6 +381,265 @@ function getMacFromArp(ip) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Deep device identification (on-demand, not run during normal scans) ──
|
||||
// Combines three independent techniques so that even if one fails (e.g. a
|
||||
// device has NetBIOS disabled), the others may still reveal something useful.
|
||||
|
||||
// 1. NetBIOS name via nbtstat — works for Windows PCs and many NAS/printers
|
||||
// that still respond to legacy NetBIOS name queries even when mDNS/UPnP is off.
|
||||
function getNetbiosName(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
if (process.platform !== 'win32') return resolve(null);
|
||||
exec('nbtstat -A ' + ip, { timeout: 3000 }, function(err, stdout) {
|
||||
if (err || !stdout) return resolve(null);
|
||||
// Look for the <00> UNIQUE entry, which is typically the actual computer name
|
||||
var lines = stdout.split('\n');
|
||||
var nameLine = lines.find(function(l) { return /<00>\s*UNIQUE/i.test(l); });
|
||||
if (nameLine) {
|
||||
var m = nameLine.trim().match(/^([^\s]+)/);
|
||||
if (m) return resolve(m[1].trim());
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Reverse-DNS / mDNS-style hostname — tries both nslookup (works if router's
|
||||
// DNS resolves DHCP hostnames, similar to how Fritz!Box names devices) and a
|
||||
// raw PTR-style attempt for broader compatibility.
|
||||
function getReverseDns(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
exec('nslookup ' + ip, { timeout: 2500 }, function(err, stdout) {
|
||||
if (!err && stdout) {
|
||||
var m = stdout.match(/Name:\s+(.+)/i);
|
||||
if (m) return resolve(m[1].trim());
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Wake-on-LAN capability probe — sends a harmless WoL magic packet and
|
||||
// reports whether the device's network port is configured to listen for one.
|
||||
// This itself doesn't reveal a name, but tells you whether it's a PC/server
|
||||
// (WoL-capable NICs almost always belong to Windows/Linux machines, never
|
||||
// to simple IoT bulbs, dryers, or basic network bridges).
|
||||
function sendWakeOnLanProbe(mac) {
|
||||
return new Promise(function(resolve) {
|
||||
if (!mac || mac === '–') return resolve({ sent: false, reason: 'Keine MAC-Adresse bekannt' });
|
||||
try {
|
||||
var macBytes = mac.replace(/[:-]/g, '').match(/.{2}/g).map(function(h) { return parseInt(h, 16); });
|
||||
if (macBytes.length !== 6 || macBytes.some(isNaN)) return resolve({ sent: false, reason: 'Ungültige MAC-Adresse' });
|
||||
var magicPacket = Buffer.alloc(102);
|
||||
for (var i = 0; i < 6; i++) magicPacket[i] = 0xFF;
|
||||
for (var j = 0; j < 16; j++) macBytes.forEach(function(b, k) { magicPacket[6 + j*6 + k] = b; });
|
||||
var dgram = require('dgram');
|
||||
var socket = dgram.createSocket('udp4');
|
||||
socket.on('error', function() { try { socket.close(); } catch(e){}; resolve({ sent: false, reason: 'Senden fehlgeschlagen' }); });
|
||||
socket.send(magicPacket, 0, magicPacket.length, 9, '255.255.255.255', function(err) {
|
||||
socket.close();
|
||||
if (err) return resolve({ sent: false, reason: 'Senden fehlgeschlagen' });
|
||||
resolve({ sent: true, reason: 'Wake-on-LAN-Paket gesendet (Port 9, Broadcast)' });
|
||||
});
|
||||
} catch(e) {
|
||||
resolve({ sent: false, reason: 'Fehler: ' + e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('identify-device', async function(event, { ip, mac }) {
|
||||
var results = { ip: ip, mac: mac };
|
||||
|
||||
// Ping first — this forces the OS to send an ARP request for the IP if no
|
||||
// cache entry exists, which is required before "arp -a" can return anything.
|
||||
// Without this, a stale/expired ARP cache entry silently makes the MAC
|
||||
// lookup (and therefore Wake-on-LAN) fail even though the device is online.
|
||||
try { await pingHost(ip); } catch(e) {}
|
||||
|
||||
// Re-resolve the MAC fresh rather than trusting whatever was passed in from
|
||||
// the renderer's possibly-stale device object.
|
||||
try {
|
||||
var freshMac = await getMacFromArp(ip);
|
||||
if (freshMac) { mac = freshMac; results.mac = freshMac; }
|
||||
} catch(e) {}
|
||||
|
||||
try {
|
||||
results.vendor = await lookupVendor(results.mac);
|
||||
} catch(e) { results.vendor = 'Unbekannt'; }
|
||||
|
||||
try {
|
||||
var netbios = await getNetbiosName(ip);
|
||||
results.netbiosName = netbios;
|
||||
} catch(e) { results.netbiosName = null; }
|
||||
|
||||
try {
|
||||
var rdns = await getReverseDns(ip);
|
||||
results.reverseDns = rdns;
|
||||
} catch(e) { results.reverseDns = null; }
|
||||
|
||||
try {
|
||||
var wol = await sendWakeOnLanProbe(mac);
|
||||
results.wakeOnLan = wol;
|
||||
} catch(e) { results.wakeOnLan = { sent: false, reason: 'Unbekannter Fehler' }; }
|
||||
|
||||
// Re-check ports with a slightly wider net for identification purposes —
|
||||
// reuse the existing scanner, results are already useful context.
|
||||
try {
|
||||
results.openPorts = await scanPorts(ip);
|
||||
} catch(e) { results.openPorts = []; }
|
||||
|
||||
var profiles = loadDeviceProfiles();
|
||||
var profile = profiles[ip] || {};
|
||||
var identityKeywords = loadIdentityKeywords();
|
||||
results.identity = inferIdentityProfile(results, profile, identityKeywords);
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
function inferIdentityProfile(results, profile, keywords) {
|
||||
var scores = {
|
||||
'Windows-PC': 0,
|
||||
'Linux-Server': 0,
|
||||
'NAS/Storage': 0,
|
||||
'Kamera/CCTV': 0,
|
||||
'Alarmanlage/Security': 0,
|
||||
'Router/Access Point': 0,
|
||||
'Drucker': 0,
|
||||
'IoT/Smart-Device': 0,
|
||||
'Virtualisierungs-Host': 0,
|
||||
'Konsole/Mediengeraet': 0
|
||||
};
|
||||
var reasons = [];
|
||||
|
||||
var vendor = (results.vendor || '').toLowerCase();
|
||||
var nameHints = [results.netbiosName, results.reverseDns].filter(Boolean).join(' ').toLowerCase();
|
||||
var ports = (results.openPorts || []).map(function(p) { return p.port; });
|
||||
|
||||
function hasPort(p) { return ports.indexOf(p) >= 0; }
|
||||
function hasAnyPort(list) { return list.some(function(p) { return hasPort(p); }); }
|
||||
function hasKeyword(haystack, words) {
|
||||
return words.some(function(w) { return haystack.indexOf(w) >= 0; });
|
||||
}
|
||||
|
||||
function hasConfiguredKeyword(scope, category, haystack) {
|
||||
var list = ((keywords || {})[scope] || {})[category] || [];
|
||||
return hasKeyword(haystack, list);
|
||||
}
|
||||
|
||||
function add(category, points, reason) {
|
||||
scores[category] = (scores[category] || 0) + points;
|
||||
reasons.push(reason);
|
||||
}
|
||||
|
||||
if (hasPort(3389)) add('Windows-PC', 4, 'Port 3389 (RDP) ist offen');
|
||||
if (hasPort(445) || hasPort(139)) add('Windows-PC', 3, 'SMB/NetBIOS-Port ist offen');
|
||||
if (hasPort(22)) add('Linux-Server', 3, 'Port 22 (SSH) ist offen');
|
||||
if (hasAnyPort([9090, 19999])) add('Linux-Server', 2, 'Monitoring/Server-Port ist offen');
|
||||
if (hasPort(8006)) add('Virtualisierungs-Host', 5, 'Port 8006 (Proxmox) ist offen');
|
||||
if (hasAnyPort([25565, 25575])) add('Linux-Server', 2, 'Gaming-Server-Port ist offen');
|
||||
if (hasPort(53)) add('Router/Access Point', 2, 'Port 53 (DNS) ist offen');
|
||||
if (hasAnyPort([80, 443, 8080, 8443])) {
|
||||
add('Router/Access Point', 1, 'Web-Management-Port ist offen');
|
||||
}
|
||||
|
||||
// NAS/Storage
|
||||
if (hasAnyPort([5000, 5001, 32400])) add('NAS/Storage', 4, 'Typischer NAS/Media-Port ist offen');
|
||||
if (hasAnyPort([111, 2049, 873])) add('NAS/Storage', 3, 'NFS/Rsync-Port ist offen');
|
||||
if (hasPort(445) && hasPort(5001)) add('NAS/Storage', 2, 'SMB + NAS-Webinterface Kombination erkannt');
|
||||
|
||||
// Camera / CCTV / NVR
|
||||
if (hasAnyPort([554, 8554])) add('Kamera/CCTV', 5, 'RTSP-Port ist offen (Video-Stream)');
|
||||
if (hasAnyPort([37777, 37778])) add('Kamera/CCTV', 4, 'Dahua-CCTV-Port ist offen');
|
||||
if (hasPort(8000)) add('Kamera/CCTV', 3, 'Typischer Kamera/NVR-Port ist offen');
|
||||
if (hasPort(7443)) add('Kamera/CCTV', 2, 'Video-Management Port ist offen');
|
||||
|
||||
// Alarm / security panels
|
||||
if (hasPort(10001)) add('Alarmanlage/Security', 3, 'Typischer Alarm/Controller-Port ist offen');
|
||||
if (hasPort(1883) || hasPort(5683)) add('IoT/Smart-Device', 3, 'IoT-Protokoll-Port ist offen');
|
||||
if (hasPort(9100) || hasPort(515) || hasPort(631)) add('Drucker', 4, 'Typischer Drucker-Port ist offen');
|
||||
|
||||
if (hasConfiguredKeyword('vendor', 'router', vendor)) {
|
||||
add('Router/Access Point', 3, 'Hersteller deutet auf Netzwerkhardware hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('vendor', 'nas', vendor)) {
|
||||
add('NAS/Storage', 4, 'Hersteller deutet auf NAS/Storage hin');
|
||||
}
|
||||
if (vendor.indexOf('supermicro') >= 0) {
|
||||
add('Linux-Server', 3, 'Hersteller deutet auf Server-Hardware hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('vendor', 'camera', vendor)) {
|
||||
add('Kamera/CCTV', 5, 'Hersteller deutet auf Kamera/CCTV hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('vendor', 'alarm', vendor)) {
|
||||
add('Alarmanlage/Security', 5, 'Hersteller deutet auf Alarm/Security hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('vendor', 'virtualization', vendor)) {
|
||||
add('Virtualisierungs-Host', 4, 'Hersteller deutet auf Virtualisierung hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('vendor', 'printer', vendor)) {
|
||||
add('Drucker', 2, 'Hersteller passt zu Drucker/Office-Hardware');
|
||||
}
|
||||
if (vendor.indexOf('nintendo') >= 0 || vendor.indexOf('sony') >= 0) {
|
||||
add('Konsole/Mediengeraet', 4, 'Hersteller passt zu Konsole/Media');
|
||||
}
|
||||
|
||||
if (hasConfiguredKeyword('hostname', 'router', nameHints)) {
|
||||
add('Router/Access Point', 2, 'Hostname deutet auf Router/AP hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('hostname', 'nas', nameHints)) {
|
||||
add('NAS/Storage', 3, 'Hostname deutet auf NAS/Storage hin');
|
||||
}
|
||||
if (nameHints.indexOf('server') >= 0 || nameHints.indexOf('plex') >= 0) {
|
||||
add('Linux-Server', 2, 'Hostname deutet auf Server hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('hostname', 'camera', nameHints)) {
|
||||
add('Kamera/CCTV', 4, 'Hostname deutet auf Kamera/CCTV hin');
|
||||
}
|
||||
if (hasConfiguredKeyword('hostname', 'alarm', nameHints)) {
|
||||
add('Alarmanlage/Security', 4, 'Hostname deutet auf Alarm/Security hin');
|
||||
}
|
||||
if (results.netbiosName) {
|
||||
add('Windows-PC', 2, 'NetBIOS-Name wurde aufgeloest');
|
||||
}
|
||||
|
||||
// Cross-signal boosters to improve confidence for your use-cases.
|
||||
if (scores['Kamera/CCTV'] > 0 && (scores['Alarmanlage/Security'] > 0 || hasPort(554))) {
|
||||
add('Kamera/CCTV', 1, 'Mehrere Kamera-/Security-Signale kombiniert');
|
||||
}
|
||||
if (scores['NAS/Storage'] > 0 && (hasPort(445) || hasPort(5001) || hasPort(32400))) {
|
||||
add('NAS/Storage', 1, 'NAS-Signale aus Ports und Hersteller/Name kombiniert');
|
||||
}
|
||||
|
||||
var top = Object.keys(scores)
|
||||
.map(function(k) { return { type: k, score: scores[k] }; })
|
||||
.sort(function(a, b) { return b.score - a.score; });
|
||||
|
||||
var best = top[0];
|
||||
var second = top[1] || { score: 0 };
|
||||
var confidence;
|
||||
if (!best || best.score <= 0) confidence = 'niedrig';
|
||||
else if (best.score >= 8 || (best.score >= 6 && (best.score - second.score) >= 3)) confidence = 'hoch';
|
||||
else if (best.score >= 4) confidence = 'mittel';
|
||||
else confidence = 'niedrig';
|
||||
|
||||
var identity = {
|
||||
likelyType: (best && best.score > 0) ? best.type : 'Unbekannt',
|
||||
confidence: confidence,
|
||||
ranking: top.filter(function(x) { return x.score > 0; }).slice(0, 3),
|
||||
reasons: reasons.slice(0, 8)
|
||||
};
|
||||
|
||||
if (profile && profile.forcedType && profile.forcedType !== 'auto') {
|
||||
identity.likelyType = profile.forcedType;
|
||||
identity.confidence = 'manuell';
|
||||
identity.ranking = [{ type: profile.forcedType, score: 999 }].concat(identity.ranking).slice(0, 3);
|
||||
identity.reasons = ['Manuelle Geräteklasse gesetzt'].concat(identity.reasons).slice(0, 8);
|
||||
identity.manualOverride = true;
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
// ── OUI lookup ────────────────────────────────────────────
|
||||
var OUI = {
|
||||
'00:50:56':'VMware','00:0C:29':'VMware','00:1A:4B':'VMware','00:05:69':'VMware',
|
||||
@@ -392,7 +718,12 @@ var DEFAULT_PORTS = [
|
||||
{ port:22,name:'SSH',desc:'Sichere Fernsteuerung / Terminal-Zugriff' },
|
||||
{ port:23,name:'Telnet',desc:'Unverschlüsselter Fernzugriff (veraltet)' },
|
||||
{ port:25,name:'SMTP',desc:'E-Mail-Versand (Simple Mail Transfer Protocol)' },
|
||||
{ port:111,name:'RPCbind',desc:'NFS/RPC Dienst (NAS/Linux)' },
|
||||
{ port:53,name:'DNS',desc:'Domain Name System – Namensauflösung' },
|
||||
{ port:515,name:'LPD',desc:'Legacy Druckerwarteschlange (Line Printer Daemon)' },
|
||||
{ port:554,name:'RTSP',desc:'Video-Stream (IP-Kamera/NVR)' },
|
||||
{ port:631,name:'IPP',desc:'Internet Printing Protocol (Drucker)' },
|
||||
{ port:873,name:'Rsync',desc:'Dateisynchronisierung (NAS/Server)' },
|
||||
{ port:80,name:'HTTP',desc:'Unverschlüsselter Web-Server' },
|
||||
{ port:110,name:'POP3',desc:'E-Mail-Empfang (Post Office Protocol)' },
|
||||
{ port:135,name:'RPC',desc:'Windows Remote Procedure Call' },
|
||||
@@ -400,18 +731,26 @@ var DEFAULT_PORTS = [
|
||||
{ port:143,name:'IMAP',desc:'E-Mail-Empfang (Internet Message Access Protocol)' },
|
||||
{ port:443,name:'HTTPS',desc:'Verschlüsselter Web-Server (SSL/TLS)' },
|
||||
{ port:445,name:'SMB',desc:'Windows Dateifreigabe / Netzlaufwerke' },
|
||||
{ port:5683,name:'CoAP',desc:'IoT-Protokoll (Constrained Application Protocol)' },
|
||||
{ port:1883,name:'MQTT',desc:'Smart-Home Nachrichtenprotokoll (IoT)' },
|
||||
{ port:2049,name:'NFS',desc:'Network File System (NAS/Linux)' },
|
||||
{ port:3306,name:'MySQL',desc:'MySQL/MariaDB Datenbank-Server' },
|
||||
{ port:3389,name:'RDP',desc:'Windows Remote Desktop – Fernsteuerung' },
|
||||
{ port:37777,name:'Dahua',desc:'Dahua CCTV Service Port' },
|
||||
{ port:37778,name:'Dahua-Alt',desc:'Dahua CCTV Service Port (Alternative)' },
|
||||
{ port:5000,name:'DSM HTTP',desc:'Synology DiskStation Manager (HTTP)' },
|
||||
{ port:5001,name:'DSM HTTPS',desc:'Synology DiskStation Manager (HTTPS)' },
|
||||
{ port:5900,name:'VNC',desc:'Virtual Network Computing – Desktop-Fernzugriff' },
|
||||
{ port:7443,name:'Video-Management',desc:'Video-Management / NVR Web-UI' },
|
||||
{ port:8000,name:'CCTV-API',desc:'Hikvision/NVR Service Port' },
|
||||
{ port:8006,name:'Proxmox',desc:'Proxmox VE Web-Oberfläche' },
|
||||
{ port:8080,name:'HTTP-Alt',desc:'Alternativer HTTP-Port (oft Web-Apps)' },
|
||||
{ port:8554,name:'RTSP-Alt',desc:'Alternativer RTSP Video-Stream Port' },
|
||||
{ port:8443,name:'HTTPS-Alt',desc:'Alternativer HTTPS-Port (oft Web-Apps)' },
|
||||
{ port:9000,name:'Portainer',desc:'Portainer – Docker-Verwaltung' },
|
||||
{ port:9090,name:'Cockpit',desc:'Linux Cockpit Server-Verwaltung' },
|
||||
{ port:9100,name:'Prometheus',desc:'Prometheus Node Exporter (Monitoring)' },
|
||||
{ port:10001,name:'Alarm-Controller',desc:'Haeufig bei Alarm-/Security-Steuergeraeten' },
|
||||
{ port:19999,name:'Netdata',desc:'Netdata Echtzeit-Monitoring Dashboard' },
|
||||
{ port:25565,name:'Minecraft',desc:'Minecraft Java Edition Server' },
|
||||
{ port:25575,name:'MC RCON',desc:'Minecraft Remote Console (RCON)' },
|
||||
|
||||
@@ -13,8 +13,12 @@ contextBridge.exposeInMainWorld('api', {
|
||||
savePorts: (p) => ipcRenderer.invoke('save-ports', p),
|
||||
getDeviceNames: () => ipcRenderer.invoke('get-device-names'),
|
||||
saveDeviceNames: (n) => ipcRenderer.invoke('save-device-names', n),
|
||||
getDeviceProfiles: () => ipcRenderer.invoke('get-device-profiles'),
|
||||
saveDeviceProfiles: (p) => ipcRenderer.invoke('save-device-profiles', p),
|
||||
getFavorites: () => ipcRenderer.invoke('get-favorites'),
|
||||
saveFavorites: (f) => ipcRenderer.invoke('save-favorites', f),
|
||||
getIdentityKeywords: () => ipcRenderer.invoke('get-identity-keywords'),
|
||||
saveIdentityKeywords:(k) => ipcRenderer.invoke('save-identity-keywords', k),
|
||||
getSettings: () => ipcRenderer.invoke('get-settings'),
|
||||
saveSettings: (s) => ipcRenderer.invoke('save-settings', s),
|
||||
getHistory: () => ipcRenderer.invoke('get-history'),
|
||||
@@ -29,6 +33,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
saveMonitoredSubnets:(s) => ipcRenderer.invoke('save-monitored-subnets', s),
|
||||
getIpv6Neighbors: () => ipcRenderer.invoke('get-ipv6-neighbors'),
|
||||
exportDeviceList: (d,f) => ipcRenderer.invoke('export-device-list', { devices:d, format:f }),
|
||||
identifyDevice: (ip,mac)=> ipcRenderer.invoke('identify-device', { ip:ip, mac:mac }),
|
||||
openEditor: (l) => ipcRenderer.invoke('open-editor', l),
|
||||
exportPdf: (h) => ipcRenderer.invoke('export-pdf', h),
|
||||
notify: (d) => ipcRenderer.send('notify', d),
|
||||
|
||||
Reference in New Issue
Block a user