Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-06-17 22:00:49 +02:00
parent 48cb0f6c39
commit c1b9bc7848
5 changed files with 3025 additions and 0 deletions

8
src/editor-preload.js Normal file
View File

@@ -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))
});

1013
src/editor.html Normal file

File diff suppressed because it is too large Load Diff

1335
src/index.html Normal file

File diff suppressed because it is too large Load Diff

625
src/main.js Normal file
View File

@@ -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<ips.length;j+=batch) {
var slice = ips.slice(j,j+batch);
var pings = await Promise.all(slice.map(pingHost));
for (var k=0;k<pings.length;k++) {
if (pings[k].alive) {
if (sendProgress) mainWindow.webContents.send('device-found', { ip:pings[k].ip, ms:pings[k].ms });
found.push(pings[k]);
}
}
if (sendProgress) mainWindow.webContents.send('scan-progress', Math.min(Math.round(((j+batch)/ips.length)*70),70));
}
if (DEBUG_SCAN) console.log('[performScan] ping phase done. Alive devices found:', found.length, found.map(function(f){return f.ip;}));
// IPv6 neighbors are a nice-to-have enrichment — never let a failure here
// block the main scan results from being returned.
var ipv6Neighbors = [];
try {
ipv6Neighbors = await getIPv6Neighbors();
if (DEBUG_SCAN) console.log('[performScan] IPv6 neighbors found:', ipv6Neighbors.length);
} catch(e) {
if (DEBUG_SCAN) console.log('[performScan] getIPv6Neighbors threw (ignored):', e.message);
ipv6Neighbors = [];
}
var enriched = [];
for (var d=0;d<found.length;d++) {
var ip = found[d].ip;
try {
var mac = await getMacFromArp(ip);
var hostname = await getHostname(ip);
var vendor = await lookupVendor(mac);
var ipv6Match = ipv6Neighbors.find(function(n) { return mac && n.mac === mac; });
enriched.push({
ip:ip, ms:found[d].ms, mac:mac||'', hostname:hostname||ip, vendor:vendor,
os:found[d].os, ttl:found[d].ttl, ipv6: ipv6Match ? ipv6Match.ip : null,
firstSeen: Date.now(), lastSeen: Date.now()
});
recordPingHistory(ip, found[d].ms);
updateUptime(ip, true);
} catch(e) {
if (DEBUG_SCAN) console.log('[performScan] enrichment failed for', ip, '-', e.message, '— including with basic info anyway');
// Even if enrichment fails for this IP, still include it with basic info
enriched.push({ ip:ip, ms:found[d].ms, mac:'', hostname:ip, vendor:'Unbekannt', os:found[d].os, ttl:found[d].ttl, ipv6:null, firstSeen: Date.now(), lastSeen: Date.now() });
}
if (sendProgress) mainWindow.webContents.send('scan-progress', 70+Math.round((d/Math.max(found.length,1))*20));
}
if (DEBUG_SCAN) console.log('[performScan] DONE. Enriched device count:', enriched.length);
return enriched;
}
ipcMain.handle('scan-network', async function(event, subnet) {
if (DEBUG_SCAN) console.log('[scan-network] handler invoked with subnet:', subnet);
var enriched = [];
try {
enriched = await performScan(subnet, true);
// Compare with last scan detect new/offline devices
var settings = readJson('settings.json', { notifyNew:true, notifyOffline:true });
var prevIps = lastScanResults.map(function(d){return d.ip;});
var currIps = enriched.map(function(d){return d.ip;});
enriched.forEach(function(d) {
if (prevIps.length > 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 };
});

44
src/preload.js Normal file
View File

@@ -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')
});