diff --git a/src/editor-preload.js b/src/editor-preload.js
new file mode 100644
index 0000000..6c9f0f4
--- /dev/null
+++ b/src/editor-preload.js
@@ -0,0 +1,8 @@
+'use strict';
+const { contextBridge, ipcRenderer } = require('electron');
+
+contextBridge.exposeInMainWorld('editorApi', {
+ exportPdf: (html) => ipcRenderer.invoke('export-pdf', html),
+ pingDevice: (ip) => ipcRenderer.invoke('ping-device', ip),
+ onPingResult: (cb) => ipcRenderer.on('ping-result', (e, d) => cb(d))
+});
diff --git a/src/editor.html b/src/editor.html
new file mode 100644
index 0000000..2cc80ff
--- /dev/null
+++ b/src/editor.html
@@ -0,0 +1,1013 @@
+
+
+
+
+Netzwerk-Editor
+
+
+
+
+
+
+
+
Ziel-Gerรคt klicken ยท ESC = Abbrechen
+
+
+
+
+
+
+
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..c09963f
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,1335 @@
+
+
+
+
+
+NetScanner
+
+
+
+
+
+
+
+
N
+
NetScanner
+
Netzwerk-Analyse
+
โณ Auto-Scan aktiv
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gerรคt aus der Liste auswรคhlen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Auto-Scan
Netzwerk automatisch neu scannen
+
+
+
+
Neue Gerรคte melden
Windows-Benachrichtigung wenn ein neues Gerรคt erscheint
+
+
+
+
Offline-Gerรคte melden
Benachrichtigung wenn ein Gerรคt nicht mehr erreichbar ist
+
+
+
+
Beim Start automatisch scannen
Startet den Scan direkt beim รffnen
+
+
+
+
In den Tray minimieren
App lรคuft beim Schlieรen im Hintergrund weiter (Live-Ping bleibt aktiv)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IPNameNotiz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Zusรคtzliche Subnetze werden beim Scan im Hintergrund mitgescannt (z.B. fรผr VLANs oder Gรคste-Netze).
+
+
+
+
+
+
+
+
+
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..9811887
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,625 @@
+'use strict';
+
+const { app, BrowserWindow, ipcMain, Notification, shell, Tray, Menu, nativeImage } = require('electron');
+const path = require('path');
+const { execFile, exec } = require('child_process');
+const os = require('os');
+const net = require('net');
+const fs = require('fs');
+
+let mainWindow;
+var DEBUG_SCAN = false; // verbose console logging for scan diagnostics โ set to true if troubleshooting again
+let tray = null;
+var autoScanTimer = null;
+var autoScanInterval = 0; // minutes, 0 = off
+var isQuitting = false;
+
+// โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function dataPath(file) { return path.join(app.getPath('userData'), file); }
+function readJson(file, fallback) {
+ try { var d = fs.readFileSync(dataPath(file), 'utf8'); return JSON.parse(d); } catch(e) { return fallback; }
+}
+function writeJson(file, data) {
+ try { fs.writeFileSync(dataPath(file), JSON.stringify(data, null, 2), 'utf8'); return true; } catch(e) { return false; }
+}
+
+// โโ Port config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-ports', () => readJson('custom-ports.json', null) || DEFAULT_PORTS);
+ipcMain.handle('save-ports', (e, ports) => { COMMON_PORTS = ports; return writeJson('custom-ports.json', ports); });
+
+// โโ Device names โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-device-names', () => readJson('device-names.json', {}));
+ipcMain.handle('save-device-names', (e, names) => writeJson('device-names.json', names));
+
+// โโ Favorites โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-favorites', () => readJson('favorites.json', []));
+ipcMain.handle('save-favorites', (e, favs) => writeJson('favorites.json', favs));
+
+// โโ Settings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-settings', () => readJson('settings.json', { theme: 'dark', autoScan: 0, notifyNew: true, notifyOffline: true }));
+ipcMain.handle('save-settings', (e, s) => {
+ writeJson('settings.json', s);
+ autoScanInterval = s.autoScan || 0;
+ resetAutoScan();
+ return true;
+});
+
+// โโ Scan history โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function loadHistory() { return readJson('scan-history.json', []); }
+function saveHistory(h) { writeJson('scan-history.json', h.slice(0, 200)); }
+
+ipcMain.handle('get-history', () => loadHistory());
+ipcMain.handle('clear-history', () => { writeJson('scan-history.json', []); return true; });
+
+function addHistoryEntry(entry) {
+ var h = loadHistory();
+ h.unshift(entry);
+ saveHistory(h);
+}
+
+// โโ Uptime tracking โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+// Records per-IP: { onlineSince: timestamp|null, lastSeen: timestamp }
+function loadUptimeData() { return readJson('uptime.json', {}); }
+function saveUptimeData(d) { writeJson('uptime.json', d); }
+
+function updateUptime(ip, isOnline) {
+ var data = loadUptimeData();
+ var now = Date.now();
+ if (!data[ip]) data[ip] = { onlineSince: null, lastSeen: null, lastOffline: null };
+ if (isOnline) {
+ if (!data[ip].onlineSince) data[ip].onlineSince = now;
+ data[ip].lastSeen = now;
+ } else {
+ data[ip].onlineSince = null;
+ data[ip].lastOffline = now;
+ }
+ saveUptimeData(data);
+ return data[ip];
+}
+
+ipcMain.handle('get-uptime', (e, ip) => {
+ var data = loadUptimeData();
+ return data[ip] || { onlineSince: null, lastSeen: null, lastOffline: null };
+});
+
+// โโ Ping history for sparkline graphs โโโโโโโโโโโโโโโโโโโโโ
+// In-memory only (resets on restart) โ keeps last N points per IP
+var pingHistory = {}; // { ip: [{t,ms}, ...] }
+var PING_HISTORY_MAX = 40;
+
+function recordPingHistory(ip, ms) {
+ if (!pingHistory[ip]) pingHistory[ip] = [];
+ pingHistory[ip].push({ t: Date.now(), ms: ms });
+ if (pingHistory[ip].length > PING_HISTORY_MAX) pingHistory[ip].shift();
+}
+
+ipcMain.handle('get-ping-history', (e, ip) => pingHistory[ip] || []);
+
+// โโ Named network plans (multiple saved schaltplans) โโโโโโ
+ipcMain.handle('get-plans', () => readJson('plans.json', {}));
+ipcMain.handle('save-plan', (e, { name, data }) => {
+ var plans = readJson('plans.json', {});
+ plans[name] = { data: data, savedAt: Date.now() };
+ return writeJson('plans.json', plans);
+});
+ipcMain.handle('delete-plan', (e, name) => {
+ var plans = readJson('plans.json', {});
+ delete plans[name];
+ return writeJson('plans.json', plans);
+});
+
+// โโ Multi-subnet monitoring โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-monitored-subnets', () => readJson('monitored-subnets.json', []));
+ipcMain.handle('save-monitored-subnets', (e, subnets) => writeJson('monitored-subnets.json', subnets));
+
+// โโ Window โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 1200, height: 780, minWidth: 900, minHeight: 600,
+ frame: false, backgroundColor: '#0d1117',
+ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false },
+ icon: path.join(__dirname, '../assets/icon.png')
+ });
+ mainWindow.loadFile(path.join(__dirname, 'index.html'));
+
+ // F12 opens DevTools even though the window is frameless (no menu bar)
+ mainWindow.webContents.on('before-input-event', function(event, input) {
+ if (input.key === 'F12') mainWindow.webContents.toggleDevTools();
+ });
+
+ // Mirror every renderer console.log/error/warn into this terminal so
+ // frontend errors are visible without needing to open DevTools manually.
+ mainWindow.webContents.on('console-message', function(event, level, message, line, sourceId) {
+ var levelNames = ['LOG','WARN','ERROR','DEBUG'];
+ console.log('[renderer:' + (levelNames[level] || level) + ']', message, '(line ' + line + ')');
+ });
+
+ mainWindow.on('close', function(e) {
+ var settings = readJson('settings.json', {});
+ if (settings.closeToTray && !isQuitting) {
+ e.preventDefault();
+ mainWindow.hide();
+ }
+ });
+}
+
+function createTray() {
+ try {
+ var iconPath = path.join(__dirname, '../assets/icon.png');
+ var img = fs.existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty();
+ tray = new Tray(img.isEmpty() ? nativeImage.createEmpty() : img.resize({ width: 16, height: 16 }));
+ tray.setToolTip('NetScanner โ lรคuft im Hintergrund');
+ var contextMenu = Menu.buildFromTemplate([
+ { label: 'รffnen', click: function() { mainWindow.show(); } },
+ { label: 'Jetzt scannen', click: function() { mainWindow.show(); mainWindow.webContents.send('auto-scan-trigger'); } },
+ { type: 'separator' },
+ { label: 'Beenden', click: function() { isQuitting = true; app.quit(); } }
+ ]);
+ tray.setContextMenu(contextMenu);
+ tray.on('click', function() { mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); });
+ } catch(e) { /* tray not supported on this platform/config */ }
+}
+
+process.on('uncaughtException', function(err) {
+ console.error('[UNCAUGHT EXCEPTION]', err);
+});
+process.on('unhandledRejection', function(reason) {
+ console.error('[UNHANDLED REJECTION]', reason);
+});
+
+app.whenReady().then(function() {
+ var saved = readJson('custom-ports.json', null);
+ if (saved) COMMON_PORTS = saved;
+ var settings = readJson('settings.json', { autoScan: 0 });
+ autoScanInterval = settings.autoScan || 0;
+ createWindow();
+ createTray();
+ resetAutoScan();
+});
+app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
+app.on('before-quit', () => { isQuitting = true; });
+
+// โโ Window controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.on('win-minimize', () => mainWindow.minimize());
+ipcMain.on('win-maximize', () => { mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize(); });
+ipcMain.on('win-close', () => mainWindow.close());
+ipcMain.on('quit-app', () => { isQuitting = true; app.quit(); });
+
+// โโ IPv6 neighbor discovery โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function getIPv6Neighbors() {
+ return new Promise(function(resolve) {
+ var cmd = process.platform === 'win32' ? 'netsh interface ipv6 show neighbors' : 'ip -6 neigh show';
+ exec(cmd, { timeout: 3000 }, function(err, stdout) {
+ if (err || !stdout) return resolve([]);
+ var results = [];
+ var lines = stdout.split('\n');
+ lines.forEach(function(line) {
+ // Match IPv6 address (not link-local fe80:: unless specifically wanted) + MAC
+ var ipMatch = line.match(/([0-9a-f]{0,4}:[0-9a-f:]+:[0-9a-f]{0,4})/i);
+ var macMatch = line.match(/([0-9a-f]{2}[:-]){5}[0-9a-f]{2}/i);
+ if (ipMatch && macMatch && !ipMatch[1].startsWith('fe80')) {
+ results.push({ ip: ipMatch[1], mac: macMatch[0].toUpperCase() });
+ }
+ });
+ resolve(results);
+ });
+ });
+}
+
+ipcMain.handle('get-ipv6-neighbors', async () => await getIPv6Neighbors());
+
+// โโ Auto scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function resetAutoScan() {
+ if (autoScanTimer) { clearInterval(autoScanTimer); autoScanTimer = null; }
+ if (autoScanInterval > 0) {
+ autoScanTimer = setInterval(function() {
+ if (mainWindow) mainWindow.webContents.send('auto-scan-trigger');
+ }, autoScanInterval * 60 * 1000);
+ }
+}
+
+// โโ Notifications โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.on('notify', (e, { title, body, type }) => {
+ var settings = readJson('settings.json', { notifyNew: true, notifyOffline: true });
+ if (type === 'new' && !settings.notifyNew) return;
+ if (type === 'offline' && !settings.notifyOffline) return;
+ if (Notification.isSupported()) {
+ new Notification({ title: title, body: body, silent: false }).show();
+ }
+});
+
+// โโ Vendor lookup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('lookup-vendor', async (e, mac) => await lookupVendor(mac));
+
+// โโ Network info โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('get-network-info', () => {
+ var ifaces = os.networkInterfaces();
+ var results = [];
+ Object.keys(ifaces).forEach(function(name) {
+ ifaces[name].forEach(function(iface) {
+ if (iface.family === 'IPv4' && !iface.internal) {
+ var parts = iface.address.split('.');
+ results.push({ name: name, address: iface.address, subnet: parts[0]+'.'+parts[1]+'.'+parts[2]+'.', mac: iface.mac });
+ }
+ });
+ });
+ return results;
+});
+
+// โโ Ping โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+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) {
+ stdout = stdout || '';
+ var alive = !err && (stdout.includes('TTL=') || stdout.includes('ttl=') || stdout.includes('1 received'));
+ var ttl = null, os_guess = null;
+ var ttlMatch = stdout.match(/TTL=(\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';
+ }
+ 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);
+ }
+ 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 });
+ });
+ });
+}
+
+ipcMain.handle('ping-device', async (e, ip) => await pingHost(ip));
+
+// โโ Ping multiple devices at once (for live updates) โโโโโโ
+// Pings are chunked instead of all-at-once: spawning 30+ ping processes
+// simultaneously creates OS scheduling contention that inflates wall-clock
+// timings across the board. Chunks of 12 keep this from happening.
+ipcMain.handle('ping-devices-batch', async function(event, ips) {
+ var results = [];
+ var chunkSize = 12;
+ for (var i = 0; i < ips.length; i += chunkSize) {
+ var chunk = ips.slice(i, i + chunkSize);
+ var chunkResults = await Promise.all(chunk.map(function(ip) { return pingHost(ip); }));
+ results = results.concat(chunkResults);
+ }
+ results.forEach(function(r) {
+ recordPingHistory(r.ip, r.alive ? r.ms : null);
+ updateUptime(r.ip, r.alive);
+ });
+ return results;
+});
+
+// โโ Hostname โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function getHostname(ip) {
+ return new Promise(function(resolve) {
+ exec('nslookup ' + ip, { timeout: 2000 }, function(err, stdout) {
+ if (!err && stdout) { var m = stdout.match(/Name:\s+(.+)/i); if (m) return resolve(m[1].trim().split('.')[0]); }
+ resolve(null);
+ });
+ });
+}
+
+// โโ MAC from ARP โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+function getMacFromArp(ip) {
+ return new Promise(function(resolve) {
+ exec('arp -a ' + ip, { timeout: 2000 }, function(err, stdout) {
+ if (!err && stdout) { var m = stdout.match(/([0-9a-f]{2}[:-]){5}[0-9a-f]{2}/i); if (m) return resolve(m[0].toUpperCase()); }
+ resolve(null);
+ });
+ });
+}
+
+// โโ OUI lookup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+var OUI = {
+ '00:50:56':'VMware','00:0C:29':'VMware','00:1A:4B':'VMware','00:05:69':'VMware',
+ '52:54:00':'QEMU/KVM','00:16:3E':'Xen',
+ 'B8:27:EB':'Raspberry Pi','DC:A6:32':'Raspberry Pi','E4:5F:01':'Raspberry Pi','28:CD:C1':'Raspberry Pi',
+ '00:1B:21':'Intel','8C:8D:28':'Intel','00:21:6A':'Intel','A4:C3:F0':'Intel','00:1F:3C':'Intel','A0:36:9F':'Intel',
+ 'AC:22:05':'ASUS','04:D4:C4':'ASUS','14:DA:E9':'ASUS','2C:4D:54':'ASUS','BC:AE:C5':'ASUS','74:D0:2B':'ASUS',
+ '18:31:BF':'Apple','3C:06:30':'Apple','F0:18:98':'Apple','50:1A:C5':'Apple','A4:CF:99':'Apple',
+ 'AC:BC:32':'Apple','F4:F1:5A':'Apple','DC:2B:2A':'Apple','A8:51:AB':'Apple','3C:22:FB':'Apple',
+ 'AC:E2:D3':'Hewlett Packard','00:17:08':'Hewlett Packard','00:1F:29':'Hewlett Packard',
+ '3C:D9:2B':'Hewlett Packard','94:57:A5':'Hewlett Packard','B4:B5:2F':'Hewlett Packard',
+ '00:14:22':'Dell','F8:DB:88':'Dell','BC:30:5B':'Dell','18:FB:7B':'Dell','B8:AC:6F':'Dell',
+ '00:23:AE':'Lenovo','54:EE:75':'Lenovo','E8:6A:64':'Lenovo','28:D2:44':'Lenovo',
+ 'FC:AA:14':'Synology','00:11:32':'Synology','BC:5F:F4':'Synology',
+ 'CC:9F:06':'TP-Link','F4:F2:6D':'TP-Link','18:D6:C7':'TP-Link','50:C7:BF':'TP-Link','AC:84:C6':'TP-Link',
+ 'B4:FB:E4':'AVM (Fritz!Box)','00:04:0E':'AVM (Fritz!Box)','C4:86:E9':'AVM (Fritz!Box)',
+ 'AC:16:2D':'AVM (Fritz!Box)','3C:37:12':'AVM (Fritz!Box)','A0:63:91':'AVM (Fritz!Box)',
+ '08:96:D7':'AVM (Fritz!Box)','1C:4B:D6':'AVM (Fritz!Box)','34:31:C4':'AVM (Fritz!Box)',
+ '7C:FF:4D':'AVM (Fritz!Box)','9C:C7:A6':'AVM (Fritz!Box)','D4:21:22':'AVM (Fritz!Box)',
+ 'E0:14:C8':'AVM (Fritz!Box)','F8:1A:67':'AVM (Fritz!Box)','BC:05:43':'AVM (Fritz!Box)',
+ 'D8:61:62':'Ubiquiti','24:A4:3C':'Ubiquiti','80:2A:A8':'Ubiquiti','F0:9F:C2':'Ubiquiti',
+ '00:1E:67':'Cisco','00:50:0F':'Cisco','AC:F2:C5':'Cisco','00:00:0C':'Cisco',
+ 'C8:D3:A3':'Huawei','00:E0:FC':'Huawei','28:6E:D4':'Huawei',
+ '00:12:47':'Samsung','08:D4:0C':'Samsung','F4:7B:5E':'Samsung','CC:07:AB':'Samsung','8C:77:12':'Samsung',
+ '98:DA:C4':'Xiaomi','64:09:80':'Xiaomi','50:64:2B':'Xiaomi',
+ '00:E0:4C':'Realtek',
+ '00:17:9A':'D-Link','1C:7E:E5':'D-Link','C8:BE:19':'D-Link',
+ '00:14:6C':'Netgear','20:E5:2A':'Netgear','A0:21:B7':'Netgear',
+ '4C:5E:0C':'MikroTik','B8:69:F4':'MikroTik','CC:2D:E0':'MikroTik',
+ 'AC:1F:6B':'Supermicro','0C:C4:7A':'Supermicro',
+ '00:08:9B':'QNAP','24:5E:BE':'QNAP',
+ '00:09:BF':'Nintendo','00:17:AB':'Nintendo','58:BD:A3':'Nintendo',
+ '00:13:A9':'Sony','F8:DA:0C':'Sony','30:17:C8':'Sony',
+ 'BC:24:11':'Proxmox Server Solutions GmbH','00:23:24':'G-PRO COMPUTER',
+ '8C:11:CB':'ABUS Security-Center GmbH','94:B3:F7':'Hui Zhou Gaoshengda Technology',
+ 'E0:03:6B':'Samsung Electronics Co.,Ltd','F4:B8:5E':'Texas Instruments',
+ '00:1B:A9':'Brother Industries','E0:D3:62':'TP-Link Systems Inc.',
+ '12:C5:74':'Private/Unknown'
+};
+
+var vendorCache = {};
+function lookupVendorApi(mac) {
+ return new Promise(function(resolve) {
+ var normalized = mac.replace(/[:\-]/g,'').toUpperCase().match(/.{2}/g).join(':');
+ var oui = normalized.substring(0,8);
+ if (vendorCache[oui]) return resolve(vendorCache[oui]);
+ var url = 'https://api.maclookup.app/v2/macs/' + normalized;
+ var https = require('https');
+ var req = https.get(url, { timeout: 3000 }, function(res) {
+ var data = '';
+ res.on('data', function(c) { data += c; });
+ res.on('end', function() {
+ try {
+ var json = JSON.parse(data);
+ var vendor = (json.company && json.company !== 'Private') ? json.company : 'Unbekannt';
+ vendorCache[oui] = vendor; resolve(vendor);
+ } catch(e) { resolve('Unbekannt'); }
+ });
+ });
+ req.on('error', () => resolve('Unbekannt'));
+ req.on('timeout', () => { req.destroy(); resolve('Unbekannt'); });
+ });
+}
+
+async function lookupVendor(mac) {
+ if (!mac || mac === 'โ') return 'Unbekannt';
+ var normalized = mac.replace(/-/g,':').toUpperCase();
+ var oui = normalized.substring(0,8);
+ if (OUI[oui]) return OUI[oui];
+ return await lookupVendorApi(normalized);
+}
+
+// โโ Ports โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+var DEFAULT_PORTS = [
+ { port:21,name:'FTP',desc:'Dateiรผbertragung (File Transfer Protocol)' },
+ { 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:53,name:'DNS',desc:'Domain Name System โ Namensauflรถsung' },
+ { 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' },
+ { port:139,name:'NetBIOS',desc:'Windows Netzwerkfreigabe (NetBIOS)' },
+ { 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:1883,name:'MQTT',desc:'Smart-Home Nachrichtenprotokoll (IoT)' },
+ { port:3306,name:'MySQL',desc:'MySQL/MariaDB Datenbank-Server' },
+ { port:3389,name:'RDP',desc:'Windows Remote Desktop โ Fernsteuerung' },
+ { 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:8006,name:'Proxmox',desc:'Proxmox VE Web-Oberflรคche' },
+ { port:8080,name:'HTTP-Alt',desc:'Alternativer HTTP-Port (oft Web-Apps)' },
+ { 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: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)' },
+ { port:32400,name:'Plex',desc:'Plex Media Server' }
+];
+var COMMON_PORTS = DEFAULT_PORTS;
+
+function scanPort(ip, port) {
+ return new Promise(function(resolve) {
+ var socket = new net.Socket(); var done = false; socket.setTimeout(600);
+ socket.on('connect', function() { done=true; socket.destroy(); resolve(true); });
+ socket.on('timeout', function() { if(!done){done=true;socket.destroy();resolve(false);} });
+ socket.on('error', function() { if(!done){done=true;resolve(false);} });
+ socket.connect(port, ip);
+ });
+}
+
+async function scanPorts(ip) {
+ var results = []; var batch = 5;
+ for (var i = 0; i < COMMON_PORTS.length; i += batch) {
+ var slice = COMMON_PORTS.slice(i, i+batch);
+ var checks = await Promise.all(slice.map(function(p) { return scanPort(ip,p.port).then(function(open){return open?p:null;}); }));
+ checks.forEach(function(r) { if(r) results.push(r); });
+ }
+ return results;
+}
+
+// โโ Security warnings: flag insecure/risky open ports โโโโโ
+var INSECURE_PORTS = {
+ 21: 'FTP รผbermittelt Zugangsdaten unverschlรผsselt',
+ 23: 'Telnet ist komplett unverschlรผsselt โ Fernzugriff vermeiden',
+ 139: 'NetBIOS ist ein hรคufiges Angriffsziel im LAN',
+ 445: 'SMB war Ziel diverser Wรผrmer (z.B. WannaCry) โ nur intern erlauben',
+ 3389: 'RDP sollte nie direkt ins Internet exponiert werden',
+ 5900: 'VNC ist oft unverschlรผsselt โ Passwortschutz prรผfen'
+};
+
+function getSecurityWarnings(openPorts) {
+ var warnings = [];
+ openPorts.forEach(function(p) {
+ if (INSECURE_PORTS[p.port]) warnings.push({ port: p.port, name: p.name, warning: INSECURE_PORTS[p.port] });
+ });
+ return warnings;
+}
+
+ipcMain.handle('get-security-warnings', (e, openPorts) => getSecurityWarnings(openPorts));
+
+// โโ Full scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+var lastScanResults = [];
+
+async function performScan(subnet, sendProgress) {
+ if (DEBUG_SCAN) console.log('[performScan] START subnet:', subnet, 'sendProgress:', sendProgress);
+ var found = []; var batch = 50; var ips = [];
+ for (var i=1;i<=254;i++) ips.push(subnet+i);
+
+ for (var j=0;j 0 && prevIps.indexOf(d.ip) < 0) {
+ mainWindow.webContents.send('device-new', d);
+ if (settings.notifyNew && Notification.isSupported()) {
+ new Notification({ title:'NetScanner โ Neues Gerรคt', body: (d.hostname||d.ip) + ' (' + d.ip + ') ist dem Netzwerk beigetreten.', silent:false }).show();
+ }
+ }
+ });
+
+ lastScanResults.forEach(function(d) {
+ if (currIps.indexOf(d.ip) < 0) {
+ mainWindow.webContents.send('device-offline', d);
+ updateUptime(d.ip, false);
+ if (settings.notifyOffline && Notification.isSupported()) {
+ new Notification({ title:'NetScanner โ Gerรคt offline', body: (d.hostname||d.ip) + ' (' + d.ip + ') ist nicht mehr erreichbar.', silent:false }).show();
+ }
+ }
+ });
+
+ lastScanResults = enriched;
+ addHistoryEntry({ time: Date.now(), subnet: subnet, count: enriched.length, devices: enriched.map(function(d){return {ip:d.ip,hostname:d.hostname,ms:d.ms,os:d.os};}) });
+ } catch(e) {
+ console.error('scan-network error:', e);
+ }
+ try { mainWindow.webContents.send('scan-progress', 100); } catch(e2) {}
+ if (DEBUG_SCAN) console.log('[scan-network] returning', enriched.length, 'devices to renderer');
+ return enriched;
+});
+
+// โโ Scan an additional subnet (multi-subnet monitoring) โโโ
+ipcMain.handle('scan-subnet-silent', async function(event, subnet) {
+ return await performScan(subnet, false);
+});
+
+ipcMain.handle('scan-ports', async (e,ip) => await scanPorts(ip));
+
+// โโ Export device list as CSV/JSON โโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('export-device-list', async function(event, { devices, format }) {
+ var { dialog } = require('electron');
+ var ext = format === 'json' ? 'json' : 'csv';
+ var result = await dialog.showSaveDialog(mainWindow, {
+ title: 'Gerรคteliste exportieren',
+ defaultPath: 'NetScanner-Geraete-' + new Date().toISOString().slice(0,10) + '.' + ext,
+ filters: [{ name: ext.toUpperCase(), extensions: [ext] }]
+ });
+ if (result.canceled || !result.filePath) return { success: false };
+
+ var content;
+ if (format === 'json') {
+ content = JSON.stringify(devices, null, 2);
+ } else {
+ var header = 'IP,Name,Hostname,MAC,Hersteller,Ping(ms),OS,IPv6\n';
+ var rows = devices.map(function(d) {
+ return [d.ip, (d.displayName||''), (d.hostname||''), (d.mac||''), (d.vendor||''), (d.ms!=null?d.ms:''), (d.os||''), (d.ipv6||'')]
+ .map(function(v){ return '"' + String(v).replace(/"/g,'""') + '"'; }).join(',');
+ });
+ content = header + rows.join('\n');
+ }
+ fs.writeFileSync(result.filePath, content, 'utf8');
+ shell.showItemInFolder(result.filePath);
+ return { success: true };
+});
+ipcMain.handle('open-editor', async function(event, layoutData) {
+ var { BrowserWindow: BW } = require('electron');
+ var editorWin = new BW({ width:1300, height:800, title:'Netzwerk-Editor', frame:true, backgroundColor:'#0d1117',
+ webPreferences:{ nodeIntegration:false, contextIsolation:true, preload:path.join(__dirname,'editor-preload.js') }
+ });
+ editorWin.loadFile(path.join(__dirname,'editor.html'));
+ editorWin.webContents.on('did-finish-load', function() {
+ editorWin.webContents.executeJavaScript('window.postMessage('+JSON.stringify({type:'init',nodes:layoutData.nodes,connections:layoutData.connections,groups:layoutData.groups||[]})+', "*")');
+ });
+ return { success:true };
+});
+
+// โโ PDF Export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ipcMain.handle('export-pdf', async function(event, htmlContent) {
+ var { BrowserWindow: BW, dialog } = require('electron');
+ var result = await dialog.showSaveDialog(mainWindow, {
+ title:'Netzwerk-Schaltplan speichern',
+ defaultPath:'NetScanner-Schaltplan-'+new Date().toISOString().slice(0,10)+'.pdf',
+ filters:[{name:'PDF',extensions:['pdf']}]
+ });
+ if (result.canceled || !result.filePath) return { success:false };
+ var tmpHtml = path.join(os.tmpdir(),'netscanner-export-'+Date.now()+'.html');
+ fs.writeFileSync(tmpHtml, htmlContent, 'utf8');
+ var win = new BW({ show:false, width:1200, height:900, webPreferences:{nodeIntegration:false,contextIsolation:true} });
+ await win.loadFile(tmpHtml);
+ await new Promise(function(r){setTimeout(r,2000);});
+ var pdfData = await win.webContents.printToPDF({ printBackground:true, pageSize:'A4', landscape:true, margins:{top:0.4,bottom:0.4,left:0.4,right:0.4} });
+ win.close();
+ try { fs.unlinkSync(tmpHtml); } catch(e) {}
+ fs.writeFileSync(result.filePath, pdfData);
+ shell.openPath(result.filePath);
+ return { success:true };
+});
diff --git a/src/preload.js b/src/preload.js
new file mode 100644
index 0000000..e91debd
--- /dev/null
+++ b/src/preload.js
@@ -0,0 +1,44 @@
+'use strict';
+const { contextBridge, ipcRenderer } = require('electron');
+
+contextBridge.exposeInMainWorld('api', {
+ getNetworkInfo: () => ipcRenderer.invoke('get-network-info'),
+ scanNetwork: (s) => ipcRenderer.invoke('scan-network', s),
+ scanSubnetSilent: (s) => ipcRenderer.invoke('scan-subnet-silent', s),
+ scanPorts: (ip) => ipcRenderer.invoke('scan-ports', ip),
+ pingDevice: (ip) => ipcRenderer.invoke('ping-device', ip),
+ pingDevicesBatch: (ips) => ipcRenderer.invoke('ping-devices-batch', ips),
+ lookupVendor: (mac) => ipcRenderer.invoke('lookup-vendor', mac),
+ getPorts: () => ipcRenderer.invoke('get-ports'),
+ savePorts: (p) => ipcRenderer.invoke('save-ports', p),
+ getDeviceNames: () => ipcRenderer.invoke('get-device-names'),
+ saveDeviceNames: (n) => ipcRenderer.invoke('save-device-names', n),
+ getFavorites: () => ipcRenderer.invoke('get-favorites'),
+ saveFavorites: (f) => ipcRenderer.invoke('save-favorites', f),
+ getSettings: () => ipcRenderer.invoke('get-settings'),
+ saveSettings: (s) => ipcRenderer.invoke('save-settings', s),
+ getHistory: () => ipcRenderer.invoke('get-history'),
+ clearHistory: () => ipcRenderer.invoke('clear-history'),
+ getUptime: (ip) => ipcRenderer.invoke('get-uptime', ip),
+ getPingHistory: (ip) => ipcRenderer.invoke('get-ping-history', ip),
+ getSecurityWarnings: (ports) => ipcRenderer.invoke('get-security-warnings', ports),
+ getPlans: () => ipcRenderer.invoke('get-plans'),
+ savePlan: (n,d) => ipcRenderer.invoke('save-plan', { name:n, data:d }),
+ deletePlan: (n) => ipcRenderer.invoke('delete-plan', n),
+ getMonitoredSubnets: () => ipcRenderer.invoke('get-monitored-subnets'),
+ 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 }),
+ openEditor: (l) => ipcRenderer.invoke('open-editor', l),
+ exportPdf: (h) => ipcRenderer.invoke('export-pdf', h),
+ notify: (d) => ipcRenderer.send('notify', d),
+ onDeviceFound: (cb) => ipcRenderer.on('device-found', (e,d) => cb(d)),
+ onScanProgress: (cb) => ipcRenderer.on('scan-progress', (e,p) => cb(p)),
+ onDeviceNew: (cb) => ipcRenderer.on('device-new', (e,d) => cb(d)),
+ onDeviceOffline: (cb) => ipcRenderer.on('device-offline', (e,d) => cb(d)),
+ onAutoScan: (cb) => ipcRenderer.on('auto-scan-trigger', () => cb()),
+ winMinimize: () => ipcRenderer.send('win-minimize'),
+ winMaximize: () => ipcRenderer.send('win-maximize'),
+ winClose: () => ipcRenderer.send('win-close'),
+ quitApp: () => ipcRenderer.send('quit-app')
+});