Files
netscanner/src/index.html
2026-06-17 22:00:49 +02:00

1336 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';">
<title>NetScanner</title>
<style>
:root {
--bg:#0d1117; --surface:#161b22; --surface2:#1c2330; --border:#30363d;
--accent:#00d4aa; --accent2:#0099ff; --danger:#ff4757; --warn:#ffa502;
--text:#e6edf3; --muted:#7d8590; --online:#26de81; --purple:#a78bfa;
}
.light {
--bg:#f6f8fa; --surface:#ffffff; --surface2:#f0f2f5; --border:#d0d7de;
--text:#24292f; --muted:#57606a; --surface2:#eaeef2;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden;user-select:none;}
/* Titlebar */
.titlebar{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 12px;background:var(--surface);border-bottom:1px solid var(--border);-webkit-app-region:drag;flex-shrink:0;}
.titlebar-left{display:flex;align-items:center;gap:10px;}
.titlebar-logo{width:20px;height:20px;background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#000;}
.titlebar-title{font-size:13px;font-weight:600;color:var(--text);}
.titlebar-sub{font-size:11px;color:var(--muted);}
.win-controls{display:flex;gap:4px;-webkit-app-region:no-drag;}
.win-btn{width:32px;height:24px;border:none;border-radius:4px;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s;}
.win-btn:hover{background:var(--surface2);color:var(--text);}
.win-btn.close:hover{background:var(--danger);color:#fff;}
/* Toast notifications */
.toast-wrap{position:fixed;top:50px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;}
.toast{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:12px;max-width:280px;opacity:0;transform:translateX(20px);transition:all .3s;pointer-events:none;}
.toast.show{opacity:1;transform:none;}
.toast.new{border-left:3px solid var(--online);}
.toast.offline{border-left:3px solid var(--danger);}
.toast.info{border-left:3px solid var(--accent);}
.toast-title{font-weight:600;margin-bottom:2px;}
.toast-body{color:var(--muted);font-size:11px;}
/* Layout */
.layout{display:flex;flex:1;overflow:hidden;}
/* Sidebar */
.sidebar{width:290px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;}
.sidebar-header{padding:14px;border-bottom:1px solid var(--border);}
.sidebar-header h2{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;}
.net-info{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 11px;margin-bottom:10px;font-size:11px;}
.net-info-row{display:flex;justify-content:space-between;padding:2px 0;}
.net-info-label{color:var(--muted);}
.net-info-val{color:var(--accent);font-family:monospace;font-size:10px;}
.subnet-select{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:7px 10px;font-size:12px;margin-bottom:8px;outline:none;cursor:pointer;}
.subnet-select:focus{border-color:var(--accent);}
.btn-scan{width:100%;padding:10px;background:linear-gradient(135deg,var(--accent),#00b891);color:#000;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:opacity .2s,transform .1s;}
.btn-scan:hover:not(:disabled){opacity:.9;}
.btn-scan:active:not(:disabled){transform:scale(.98);}
.btn-scan:disabled{opacity:.5;cursor:not-allowed;}
.progress-wrap{margin-top:10px;}
.progress-bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;}
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));width:0%;transition:width .3s;border-radius:2px;}
.progress-label{font-size:10px;color:var(--muted);margin-top:5px;text-align:center;}
/* Tabs */
.sidebar-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;}
.sidebar-tab{flex:1;padding:8px 4px;text-align:center;font-size:11px;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;}
.sidebar-tab.active{color:var(--accent);border-bottom-color:var(--accent);}
.sidebar-tab:hover:not(.active){color:var(--text);}
/* Stats */
.stats{display:flex;gap:6px;padding:10px 14px;border-bottom:1px solid var(--border);}
.stat-box{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:7px;text-align:center;}
.stat-num{font-size:20px;font-weight:700;color:var(--accent);line-height:1;}
.stat-label{font-size:9px;color:var(--muted);margin-top:2px;text-transform:uppercase;letter-spacing:.05em;}
/* Search */
.search-wrap{padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0;}
.search-input{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:6px 10px;font-size:12px;outline:none;}
.search-input:focus{border-color:var(--accent);}
/* Device list */
.tab-panel{display:none;flex:1;overflow-y:auto;padding:6px;}
.tab-panel.active{display:block;}
.tab-panel::-webkit-scrollbar{width:4px;}
.tab-panel::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
.device-item{display:flex;align-items:center;gap:9px;padding:8px 9px;border-radius:7px;cursor:pointer;border:1px solid transparent;margin-bottom:2px;transition:background .15s;}
.device-item:hover{background:var(--surface2);}
.device-item.active{background:var(--surface2);border-color:var(--accent);}
.device-item.favorite .star-btn{color:var(--warn);}
.device-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;}
.device-info{flex:1;min-width:0;}
.device-hostname{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.device-ip{font-size:10px;color:var(--muted);font-family:monospace;}
.device-os{font-size:9px;color:var(--purple);margin-top:1px;}
.device-ping{font-size:10px;font-family:monospace;flex-shrink:0;}
.device-ping.fast{color:var(--online);}
.device-ping.mid{color:var(--warn);}
.device-ping.slow{color:var(--danger);}
.star-btn{background:none;border:none;color:var(--border);cursor:pointer;font-size:14px;flex-shrink:0;padding:0 2px;transition:color .15s;}
.star-btn:hover{color:var(--warn);}
.empty-list{text-align:center;color:var(--muted);font-size:12px;padding:30px 16px;line-height:1.7;}
.empty-icon{font-size:28px;margin-bottom:8px;}
/* History list */
.history-item{padding:8px 10px;border-radius:7px;margin-bottom:4px;background:var(--surface2);border:1px solid var(--border);font-size:11px;}
.history-time{color:var(--muted);font-size:10px;margin-bottom:3px;}
.history-info{color:var(--text);}
.history-clear{width:100%;padding:7px;background:transparent;border:1px solid var(--border);border-radius:6px;color:var(--muted);font-size:11px;cursor:pointer;margin-bottom:8px;transition:border-color .15s;}
.history-clear:hover{border-color:var(--danger);color:var(--danger);}
/* Footer buttons */
.sidebar-footer{padding:8px 10px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px;flex-shrink:0;}
.btn-footer{width:100%;padding:7px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--muted);font-size:11px;cursor:pointer;text-align:left;transition:border-color .15s,color .15s;}
.btn-footer:hover{border-color:var(--accent);color:var(--text);}
.btn-footer.accent{color:var(--accent);border-color:var(--accent);}
/* Main */
.main-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;}
.detail-placeholder{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--muted);font-size:13px;gap:10px;}
.detail-placeholder svg{opacity:.2;}
.device-detail{flex:1;overflow-y:auto;padding:22px;display:none;}
.device-detail.visible{display:block;}
.device-detail::-webkit-scrollbar{width:4px;}
.device-detail::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
.detail-header{display:flex;align-items:flex-start;gap:14px;margin-bottom:20px;}
.detail-icon{width:50px;height:50px;background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:13px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;}
.detail-title{flex:1;}
.detail-hostname{font-size:20px;font-weight:700;}
.detail-ip{font-size:13px;color:var(--muted);font-family:monospace;margin-top:2px;}
.detail-vendor{font-size:12px;color:var(--accent);margin-top:3px;}
.detail-os{font-size:11px;color:var(--purple);margin-top:2px;}
.detail-actions{display:flex;gap:7px;margin-bottom:20px;flex-wrap:wrap;}
.btn-action{padding:6px 12px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:12px;cursor:pointer;transition:border-color .15s,background .15s;display:flex;align-items:center;gap:5px;}
.btn-action:hover{border-color:var(--accent);background:var(--surface2);}
.info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:20px;}
.info-card{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:12px;}
.info-card-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:5px;}
.info-card-val{font-size:14px;font-weight:600;font-family:monospace;}
.info-card-val.green{color:var(--online);}
.info-card-val.blue{color:var(--accent2);}
.info-card-val.teal{color:var(--accent);}
.info-card-val.warn{color:var(--warn);}
.info-card-val.danger{color:var(--danger);}
.section-title{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;}
.ports-grid{display:flex;flex-direction:column;gap:5px;margin-bottom:20px;}
.port-tag{padding:7px 11px;background:var(--surface);border:1px solid var(--border);border-radius:7px;display:flex;align-items:center;gap:9px;transition:border-color .15s;}
.port-tag:hover{border-color:var(--accent);}
.port-tag .port-dot{width:6px;height:6px;border-radius:50%;background:var(--online);flex-shrink:0;}
.port-tag .port-num{font-family:monospace;color:var(--accent);font-weight:700;min-width:46px;font-size:12px;}
.port-tag .port-name{color:var(--text);font-weight:600;min-width:90px;font-size:12px;}
.port-tag .port-desc{color:var(--muted);font-size:11px;}
.port-scanning{color:var(--muted);font-size:12px;display:flex;align-items:center;gap:8px;margin-bottom:20px;}
.no-ports{color:var(--muted);font-size:12px;margin-bottom:20px;}
.spinner{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0;}
@keyframes spin{to{transform:rotate(360deg);}}
@keyframes livepulse{0%,100%{opacity:1;}50%{opacity:.25;}}
#liveDot{animation:livepulse 2s ease-in-out infinite;}
/* Quick edit inline */
.quick-edit-form{background:var(--surface2);border:1px solid var(--accent);border-radius:9px;padding:12px 14px;margin-bottom:16px;display:flex;flex-direction:column;gap:9px;}
.qe-input{background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:6px 9px;font-size:12px;outline:none;width:100%;}
.qe-input:focus{border-color:var(--accent);}
.qe-row{display:flex;gap:7px;}
.btn-save{padding:7px 14px;background:linear-gradient(135deg,var(--accent),#00b891);border:none;border-radius:6px;color:#000;font-size:12px;font-weight:700;cursor:pointer;transition:opacity .15s;}
.btn-save:hover{opacity:.85;}
/* Settings modal */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;align-items:center;justify-content:center;}
.modal-overlay.open{display:flex;}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:12px;width:500px;max-width:95vw;box-shadow:0 24px 60px rgba(0,0,0,.5);}
.modal-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border);}
.modal-title{font-size:14px;font-weight:700;}
.modal-close{width:26px;height:26px;border:none;background:transparent;color:var(--muted);cursor:pointer;font-size:13px;border-radius:5px;}
.modal-close:hover{background:var(--danger);color:#fff;}
.modal-body{padding:18px;}
.setting-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);}
.setting-row:last-child{border-bottom:none;}
.setting-label{font-size:13px;}
.setting-sub{font-size:11px;color:var(--muted);margin-top:2px;}
.toggle{position:relative;width:38px;height:22px;flex-shrink:0;}
.toggle input{opacity:0;width:0;height:0;}
.toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:22px;cursor:pointer;transition:background .2s;}
.toggle input:checked + .toggle-slider{background:var(--accent);}
.toggle-slider:before{content:'';position:absolute;width:16px;height:16px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .2s;}
.toggle input:checked + .toggle-slider:before{transform:translateX(16px);}
.setting-select{background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:5px 8px;font-size:12px;outline:none;}
/* Port manager modal */
.add-row{display:flex;gap:7px;align-items:center;}
.add-input{background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:6px 9px;font-size:12px;outline:none;}
.add-input:focus{border-color:var(--accent);}
.modal-list{max-height:340px;overflow-y:auto;padding:8px 18px;}
.modal-list::-webkit-scrollbar{width:4px;}
.modal-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
.port-row{display:flex;align-items:center;gap:7px;padding:5px 0;border-bottom:1px solid var(--border);}
.port-row:last-child{border-bottom:none;}
.pr-port{font-family:monospace;color:var(--accent);font-weight:700;min-width:48px;font-size:12px;}
.port-row input{background:transparent;border:1px solid transparent;border-radius:5px;color:var(--text);padding:3px 6px;font-size:12px;outline:none;}
.port-row input:focus{border-color:var(--accent);background:var(--surface2);}
.pr-name{width:110px;}
.pr-desc{flex:1;color:var(--muted);}
.pr-desc:focus{color:var(--text);}
.btn-delete{width:24px;height:24px;border:none;background:transparent;color:var(--muted);cursor:pointer;font-size:13px;border-radius:4px;flex-shrink:0;}
.btn-delete:hover{background:var(--danger);color:#fff;}
.modal-footer{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;border-top:1px solid var(--border);}
.btn-reset{padding:6px 12px;background:transparent;border:1px solid var(--border);border-radius:6px;color:var(--muted);font-size:12px;cursor:pointer;}
.btn-reset:hover{border-color:var(--warn);color:var(--warn);}
.modal-add{padding:12px 18px;border-bottom:1px solid var(--border);}
/* Device manager modal */
.device-name-row{display:grid;grid-template-columns:120px 1fr 1fr auto;gap:5px;align-items:center;padding:4px 0;border-bottom:1px solid var(--border);}
.device-name-row:last-child{border-bottom:none;}
.dn-ip{font-family:monospace;color:var(--accent);font-size:11px;padding:0 3px;}
.device-name-row input{background:transparent;border:1px solid transparent;border-radius:5px;color:var(--text);padding:3px 6px;font-size:12px;outline:none;width:100%;}
.device-name-row input:focus{border-color:var(--accent);background:var(--surface2);}
.sidebar-footer-top{display:flex;gap:6px;padding:8px 10px 0;border-top:1px solid var(--border);}
/* Auto-scan indicator */
.autoscan-badge{display:none;font-size:10px;color:var(--accent);background:var(--surface2);border:1px solid var(--accent);border-radius:4px;padding:2px 6px;margin-left:6px;-webkit-app-region:no-drag;}
.autoscan-badge.visible{display:inline-block;}
/* New/offline alerts in list */
.device-item.is-new .device-hostname::after{content:' 🆕';font-size:9px;}
.device-item.is-offline{opacity:.5;}
.device-item.is-offline .device-dot{background:var(--danger)!important;}
/* Uptime */
.uptime-badge{font-size:11px;color:var(--online);font-family:monospace;}
/* Sparkline */
.sparkline-wrap{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:12px;margin-bottom:20px;}
.sparkline-svg{width:100%;height:50px;display:block;}
/* Security warnings */
.sec-warning{display:flex;align-items:flex-start;gap:8px;background:rgba(255,71,87,.1);border:1px solid var(--danger);border-radius:7px;padding:8px 10px;margin-bottom:6px;font-size:11px;}
.sec-warning-icon{color:var(--danger);flex-shrink:0;}
.sec-warning-title{font-weight:700;color:var(--danger);}
.sec-warning-text{color:var(--muted);margin-top:2px;}
/* Unknown device confirm banner */
.unknown-banner{display:flex;align-items:center;justify-content:space-between;gap:10px;background:rgba(255,165,2,.1);border:1px solid var(--warn);border-radius:8px;padding:10px 12px;margin-bottom:16px;font-size:12px;}
.unknown-banner-text{color:var(--warn);font-weight:600;}
.btn-confirm{padding:5px 12px;background:var(--warn);color:#000;border:none;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;}
.btn-confirm:hover{opacity:.85;}
/* Export dropdown */
.export-dropdown{position:relative;}
.export-menu{display:none;position:absolute;bottom:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:7px;margin-bottom:4px;overflow:hidden;box-shadow:0 -8px 20px rgba(0,0,0,.3);z-index:10;}
.export-menu.open{display:block;}
.export-menu-item{padding:8px 12px;font-size:11px;cursor:pointer;color:var(--text);transition:background .15s;}
.export-menu-item:hover{background:var(--surface2);color:var(--accent);}
/* Plan manager */
.plan-row{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:7px;margin-bottom:6px;}
.plan-name{flex:1;font-size:12px;font-weight:600;}
.plan-date{font-size:10px;color:var(--muted);}
.plan-btn{padding:4px 9px;background:var(--surface);border:1px solid var(--border);border-radius:5px;font-size:11px;cursor:pointer;color:var(--text);}
.plan-btn:hover{border-color:var(--accent);color:var(--accent);}
.plan-btn.danger:hover{border-color:var(--danger);color:var(--danger);}
/* Multi-subnet monitor */
.subnet-tag{display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:5px 9px;font-size:11px;font-family:monospace;margin:0 6px 6px 0;}
.subnet-tag .subnet-status{width:6px;height:6px;border-radius:50%;background:var(--online);}
.subnet-tag .subnet-remove{cursor:pointer;color:var(--muted);margin-left:4px;}
.subnet-tag .subnet-remove:hover{color:var(--danger);}
/* IPv6 badge */
.ipv6-badge{font-size:9px;color:var(--purple);font-family:monospace;margin-top:1px;word-break:break-all;}
</style>
</head>
<body>
<!-- Titlebar -->
<div class="titlebar">
<div class="titlebar-left">
<div class="titlebar-logo">N</div>
<span class="titlebar-title">NetScanner</span>
<span class="titlebar-sub">Netzwerk-Analyse</span>
<span class="autoscan-badge" id="autoscanBadge">⟳ Auto-Scan aktiv</span>
</div>
<div class="win-controls">
<button class="win-btn" onclick="openSettings()" title="Einstellungen"></button>
<button class="win-btn" onclick="api.winMinimize()"></button>
<button class="win-btn" onclick="api.winMaximize()"></button>
<button class="win-btn close" onclick="api.winClose()"></button>
</div>
</div>
<!-- Toast container -->
<div class="toast-wrap" id="toastWrap"></div>
<!-- Layout -->
<div class="layout">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h2>Netzwerk</h2>
<div class="net-info" id="netInfo">
<div class="net-info-row"><span class="net-info-label">Interface</span><span class="net-info-val" id="ifaceName"></span></div>
<div class="net-info-row"><span class="net-info-label">Eigene IP</span><span class="net-info-val" id="localIp"></span></div>
<div class="net-info-row"><span class="net-info-label">Subnetz</span><span class="net-info-val" id="subnetShow"></span></div>
</div>
<select class="subnet-select" id="subnetSelect"></select>
<button class="btn-scan" id="btnScan" onclick="startScan()">
<span>🔍</span> Netzwerk scannen
</button>
<div class="progress-wrap" id="progressWrap" style="display:none">
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-label" id="progressLabel">Scanne…</div>
</div>
</div>
<!-- Tabs -->
<div class="sidebar-tabs">
<div class="sidebar-tab active" id="tabDevices" onclick="switchTab('devices')">Geräte</div>
<div class="sidebar-tab" id="tabFavorites" onclick="switchTab('favorites')">⭐ Favoriten</div>
<div class="sidebar-tab" id="tabHistory" onclick="switchTab('history')">Verlauf</div>
</div>
<div class="stats">
<div class="stat-box"><div class="stat-num" id="statDevices">0</div><div class="stat-label">Geräte</div></div>
<div class="stat-box"><div class="stat-num" id="statPorts">0</div><div class="stat-label">Ports</div></div>
<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>
<div class="search-wrap">
<input class="search-input" id="searchInput" placeholder="🔎 Suche nach Name, IP, Hersteller…" oninput="filterDevices()">
</div>
<!-- Devices tab -->
<div class="tab-panel active" id="panelDevices"></div>
<!-- Favorites tab -->
<div class="tab-panel" id="panelFavorites">
<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>
</div>
<!-- History tab -->
<div class="tab-panel" id="panelHistory">
<div style="padding:8px 6px 0">
<button class="history-clear" onclick="clearHistory()">🗑 Verlauf löschen</button>
</div>
<div id="historyList" style="padding:0 6px 6px"></div>
</div>
<div class="sidebar-footer">
<div style="display:flex;gap:5px">
<button class="btn-footer" style="flex:1" onclick="openPortManager()">⚙️ Ports</button>
<button class="btn-footer" style="flex:1" onclick="openDeviceManager()">🏷️ Namen</button>
</div>
<div style="display:flex;gap:5px">
<button class="btn-footer" style="flex:1" onclick="openPlanManager()">📁 Pläne</button>
<button class="btn-footer" style="flex:1" onclick="openSubnetManager()">🌐 Subnetze</button>
</div>
<button class="btn-footer accent" onclick="openNetworkEditor()">🗺️ Schaltplan bearbeiten &amp; PDF</button>
<div class="export-dropdown">
<button class="btn-footer" style="width:100%" onclick="toggleExportMenu()">📤 Geräteliste exportieren ▾</button>
<div class="export-menu" id="exportMenu">
<div class="export-menu-item" onclick="exportDeviceList('csv')">📄 Als CSV exportieren</div>
<div class="export-menu-item" onclick="exportDeviceList('json')">🧾 Als JSON exportieren</div>
</div>
</div>
</div>
</div>
<!-- Main panel -->
<div class="main-panel">
<div class="detail-placeholder" id="placeholder">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/>
</svg>
<span>Gerät aus der Liste auswählen</span>
</div>
<div class="device-detail" id="deviceDetail"></div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal" onclick="closeModal('settingsModal',event)">
<div class="modal">
<div class="modal-header">
<span class="modal-title">⚙️ Einstellungen</span>
<button class="modal-close" onclick="closeModal('settingsModal')"></button>
</div>
<div class="modal-body">
<div class="setting-row">
<div><div class="setting-label">Design</div><div class="setting-sub">Hell oder dunkel</div></div>
<select class="setting-select" id="settingTheme" onchange="applyTheme(this.value)">
<option value="dark">🌙 Dunkel</option>
<option value="light">☀️ Hell</option>
</select>
</div>
<div class="setting-row">
<div><div class="setting-label">Auto-Scan</div><div class="setting-sub">Netzwerk automatisch neu scannen</div></div>
<select class="setting-select" id="settingAutoScan">
<option value="0">Aus</option>
<option value="1">Jede Minute</option>
<option value="5">Alle 5 Minuten</option>
<option value="10">Alle 10 Minuten</option>
<option value="30">Alle 30 Minuten</option>
</select>
</div>
<div class="setting-row">
<div><div class="setting-label">Neue Geräte melden</div><div class="setting-sub">Windows-Benachrichtigung wenn ein neues Gerät erscheint</div></div>
<label class="toggle"><input type="checkbox" id="settingNotifyNew"><span class="toggle-slider"></span></label>
</div>
<div class="setting-row">
<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">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>
</div>
<div class="setting-row">
<div><div class="setting-label">In den Tray minimieren</div><div class="setting-sub">App läuft beim Schließen im Hintergrund weiter (Live-Ping bleibt aktiv)</div></div>
<label class="toggle"><input type="checkbox" id="settingCloseToTray"><span class="toggle-slider"></span></label>
</div>
</div>
<div class="modal-footer">
<span></span>
<button class="btn-save" onclick="saveSettings()">💾 Speichern</button>
</div>
</div>
</div>
<!-- Port Manager Modal -->
<div class="modal-overlay" id="portModal" onclick="closeModal('portModal',event)">
<div class="modal" style="width:680px">
<div class="modal-header">
<span class="modal-title">⚙️ Ports verwalten</span>
<button class="modal-close" onclick="closeModal('portModal')"></button>
</div>
<div class="modal-add">
<div class="add-row">
<input class="add-input" id="newPort" type="number" placeholder="Port" min="1" max="65535" style="width:110px">
<input class="add-input" id="newName" type="text" placeholder="Name" style="width:120px">
<input class="add-input" id="newDesc" type="text" placeholder="Beschreibung" style="flex:1">
<button class="btn-save" onclick="addPort()"> Hinzufügen</button>
</div>
</div>
<div class="modal-list" id="portModalList"></div>
<div class="modal-footer">
<button class="btn-reset" onclick="resetPorts()">↺ Zurücksetzen</button>
<button class="btn-save" onclick="savePorts()">💾 Speichern</button>
</div>
</div>
</div>
<!-- Device Manager Modal -->
<div class="modal-overlay" id="deviceModal" onclick="closeModal('deviceModal',event)">
<div class="modal" style="width:720px">
<div class="modal-header">
<span class="modal-title">🏷️ Geräte benennen</span>
<button class="modal-close" onclick="closeModal('deviceModal')"></button>
</div>
<div class="modal-add">
<div class="add-row">
<input class="add-input" id="newDeviceIp" type="text" placeholder="IP" style="width:150px">
<input class="add-input" id="newDeviceName" type="text" placeholder="Name" style="width:150px">
<input class="add-input" id="newDeviceNote" type="text" placeholder="Notiz" style="flex:1">
<button class="btn-save" onclick="addDeviceName()"> Hinzufügen</button>
</div>
<div style="margin-top:8px;display:flex;gap:8px;align-items:center">
<label style="font-size:11px;color:var(--muted)">📥 CSV importieren:</label>
<label class="btn-action" style="font-size:11px;cursor:pointer">📂 Datei wählen<input type="file" id="csvImport" accept=".csv,.txt" onchange="importCSV(this)" style="display:none"></label>
<span id="csvFileName" style="font-size:11px;color:var(--muted)">Keine Datei</span>
<button class="btn-action" onclick="exportCSV()" style="font-size:11px;margin-left:auto">📤 Export</button>
</div>
</div>
<div style="display:grid;grid-template-columns:130px 1fr 1fr auto;gap:0;padding:5px 18px 0;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border)">
<span>IP</span><span>Name</span><span>Notiz</span><span></span>
</div>
<div class="modal-list" id="deviceModalList"></div>
<div class="modal-footer">
<span style="font-size:11px;color:var(--muted)" id="deviceCount">0 Einträge</span>
<button class="btn-save" onclick="saveDeviceNames()">💾 Speichern</button>
</div>
</div>
</div>
<!-- Plan Manager Modal -->
<div class="modal-overlay" id="planModal" onclick="closeModal('planModal',event)">
<div class="modal" style="width:480px">
<div class="modal-header">
<span class="modal-title">📁 Gespeicherte Schaltpläne</span>
<button class="modal-close" onclick="closeModal('planModal')"></button>
</div>
<div class="modal-add">
<div class="add-row">
<input class="add-input" id="newPlanName" type="text" placeholder="Name (z.B. 'Vor Umzug')" style="flex:1">
<button class="btn-save" onclick="saveCurrentAsPlan()">💾 Aktuellen Plan speichern</button>
</div>
</div>
<div class="modal-list" id="planModalList" style="max-height:300px"></div>
<div class="modal-footer">
<span style="font-size:11px;color:var(--muted)">Pläne öffnen sich im Editor</span>
<span></span>
</div>
</div>
</div>
<!-- Subnet Manager Modal -->
<div class="modal-overlay" id="subnetModal" onclick="closeModal('subnetModal',event)">
<div class="modal" style="width:480px">
<div class="modal-header">
<span class="modal-title">🌐 Mehrere Subnetze überwachen</span>
<button class="modal-close" onclick="closeModal('subnetModal')"></button>
</div>
<div class="modal-add">
<div class="add-row">
<input class="add-input" id="newSubnetInput" type="text" placeholder="z.B. 192.168.1. oder 10.0.0." style="flex:1">
<button class="btn-save" onclick="addMonitoredSubnet()"> Hinzufügen</button>
</div>
<div style="font-size:11px;color:var(--muted);margin-top:8px">Zusätzliche Subnetze werden beim Scan im Hintergrund mitgescannt (z.B. für VLANs oder Gäste-Netze).</div>
</div>
<div class="modal-list" id="subnetModalList" style="padding:14px 18px"></div>
<div class="modal-footer">
<span style="font-size:11px;color:var(--muted)" id="subnetCount">0 zusätzliche Subnetze</span>
<button class="btn-save" onclick="closeModal('subnetModal')">Fertig</button>
</div>
</div>
</div>
<script>
var devices = [], totalPorts = 0, scanning = false;
var currentPorts = [], deviceNames = {}, favorites = [], settings = {};
var monitoredSubnets = [];
var extraSubnetDevices = {}; // { subnet: [devices] }
// ── Init ──────────────────────────────────────────────────
async function init() {
currentPorts = await api.getPorts();
deviceNames = await api.getDeviceNames();
favorites = await api.getFavorites();
settings = await api.getSettings();
applyTheme(settings.theme || 'dark');
document.getElementById('settingTheme').value = settings.theme || 'dark';
document.getElementById('settingAutoScan').value = settings.autoScan || 0;
document.getElementById('settingNotifyNew').checked = settings.notifyNew !== false;
document.getElementById('settingNotifyOffline').checked = settings.notifyOffline !== false;
document.getElementById('settingAutoStart').checked = settings.autoStart || false;
document.getElementById('settingCloseToTray').checked = settings.closeToTray || false;
if (settings.autoScan > 0) document.getElementById('autoscanBadge').classList.add('visible');
monitoredSubnets = await api.getMonitoredSubnets();
var ifaces = await api.getNetworkInfo();
var sel = document.getElementById('subnetSelect');
sel.innerHTML = '';
if (!ifaces.length) { sel.innerHTML = '<option>Kein Netzwerk</option>'; return; }
ifaces.forEach(function(i) {
var o = document.createElement('option');
o.value = i.subnet; o.textContent = i.subnet+'0/24 ('+i.name+')';
o.dataset.name = i.name; o.dataset.ip = i.address;
sel.appendChild(o);
});
updateNetInfo();
sel.addEventListener('change', updateNetInfo);
// Listeners
api.onDeviceFound(function(d) { addDeviceToList(d, true); });
api.onScanProgress(function(p) {
document.getElementById('progressFill').style.width = p+'%';
document.getElementById('progressLabel').textContent = p < 70 ? 'Ping-Scan: '+p+'%' : p < 100 ? 'Geräte anreichern…' : 'Fertig';
});
api.onDeviceNew(function(d) { showToast('Neues Gerät', (getCustomName(d.ip)||d.hostname||d.ip)+' ist online', 'new'); markDeviceNew(d.ip); });
api.onDeviceOffline(function(d) { showToast('Gerät offline', (getCustomName(d.ip)||d.hostname||d.ip)+' nicht erreichbar', 'offline'); markDeviceOffline(d.ip); });
api.onAutoScan(function() { if (!scanning) startScan(); });
loadHistory();
if (settings.autoStart) setTimeout(startScan, 800);
}
function updateNetInfo() {
var sel = document.getElementById('subnetSelect');
var opt = sel.options[sel.selectedIndex];
if (!opt) return;
document.getElementById('ifaceName').textContent = opt.dataset.name || '';
document.getElementById('localIp').textContent = opt.dataset.ip || '';
document.getElementById('subnetShow').textContent = opt.value+'0/24';
}
function applyTheme(t) {
document.body.classList.toggle('light', t === 'light');
}
// ── Toast ─────────────────────────────────────────────────
function showToast(title, body, type) {
var wrap = document.getElementById('toastWrap');
var t = document.createElement('div');
t.className = 'toast ' + (type||'info');
t.innerHTML = '<div class="toast-title">'+esc(title)+'</div><div class="toast-body">'+esc(body)+'</div>';
wrap.appendChild(t);
setTimeout(function(){t.classList.add('show');},10);
setTimeout(function(){t.classList.remove('show');setTimeout(function(){t.remove();},400);},4000);
}
// ── Tabs ──────────────────────────────────────────────────
function switchTab(tab) {
['devices','favorites','history'].forEach(function(t) {
document.getElementById('tab'+t.charAt(0).toUpperCase()+t.slice(1)).classList.toggle('active',t===tab);
document.getElementById('panel'+t.charAt(0).toUpperCase()+t.slice(1)).classList.toggle('active',t===tab);
});
if (tab === 'history') loadHistory();
if (tab === 'favorites') renderFavorites();
}
// ── Scan ──────────────────────────────────────────────────
async function startScan() {
if (scanning) return;
stopLivePing();
scanning = true; devices = []; totalPorts = 0;
document.getElementById('statDevices').textContent = '0';
document.getElementById('statPorts').textContent = '0';
document.getElementById('statOnline').textContent = '0';
document.getElementById('panelDevices').innerHTML = '';
document.getElementById('deviceDetail').classList.remove('visible');
document.getElementById('placeholder').style.display = 'flex';
document.getElementById('btnScan').disabled = true;
document.getElementById('btnScan').innerHTML = '<div class="spinner"></div> Scanne…';
document.getElementById('progressWrap').style.display = 'block';
document.getElementById('searchInput').value = '';
var subnet = document.getElementById('subnetSelect').value;
console.log('[startScan] requesting scan for subnet:', subnet);
var result = [];
try {
result = await api.scanNetwork(subnet);
console.log('[startScan] api.scanNetwork resolved with', result.length, 'devices:', result);
} catch(e) {
console.error('[startScan] api.scanNetwork threw:', e);
showToast('Scan-Fehler', String(e.message||e), 'offline');
}
devices = result;
document.getElementById('panelDevices').innerHTML = '';
result.forEach(function(d) { addDeviceToList(d, false); });
document.getElementById('statDevices').textContent = result.length;
document.getElementById('statOnline').textContent = result.length;
document.getElementById('btnScan').disabled = false;
document.getElementById('btnScan').innerHTML = '<span>🔍</span> Erneut scannen';
scanning = false;
showToast('Scan abgeschlossen', result.length+' Geräte gefunden', 'info');
startLivePing();
// Scan additional monitored subnets silently in the background
if (monitoredSubnets.length) scanExtraSubnets();
}
async function scanExtraSubnets() {
for (var i = 0; i < monitoredSubnets.length; i++) {
var subnet = monitoredSubnets[i];
try {
var extraResult = await api.scanSubnetSilent(subnet);
extraSubnetDevices[subnet] = extraResult;
showToast('Subnetz '+subnet+'0/24', extraResult.length+' Geräte gefunden', 'info');
} catch(e) {}
}
}
// ── Live Ping Loop ────────────────────────────────────────
var livePingTimer = null;
var livePingBusy = false;
var LIVE_PING_INTERVAL = 4000; // ms
function startLivePing() {
stopLivePing();
if (!devices.length) return;
document.getElementById('liveStatusBar').style.display = 'flex';
livePingTimer = setInterval(runLivePingCycle, LIVE_PING_INTERVAL);
runLivePingCycle();
}
function stopLivePing() {
if (livePingTimer) { clearInterval(livePingTimer); livePingTimer = null; }
document.getElementById('liveStatusBar').style.display = 'none';
}
async function runLivePingCycle() {
if (livePingBusy || !devices.length) return;
livePingBusy = true;
try {
var ips = devices.map(function(d) { return d.ip; });
var results = await api.pingDevicesBatch(ips);
var onlineCount = 0;
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++;
// 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');
if (res.alive) {
item.classList.remove('is-offline');
var dotColor = liveMs<=50?'var(--online)':liveMs<=100?'var(--warn)':'var(--danger)';
if (dot) dot.style.background = dotColor;
if (pingEl) {
var lpc = liveMs<=50?'fast':liveMs<=100?'mid':'slow';
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 (dot) dot.style.background = 'var(--danger)';
if (pingEl) { pingEl.className='device-ping slow'; pingEl.textContent='offline'; }
}
}
// Update detail panel if this device is currently selected
var activeItem = document.querySelector('.device-item.active');
if (activeItem && activeItem.id === 'dev-' + res.ip.replace(/\./g, '-')) {
var pingVal = document.getElementById('pingVal');
if (pingVal) {
if (res.alive) {
var pc = liveMs<=50?'green':liveMs<=100?'warn':'danger';
pingVal.className = 'info-card-val ' + pc;
pingVal.textContent = liveMs + ' ms';
} else {
pingVal.className = 'info-card-val danger';
pingVal.textContent = 'Offline';
}
}
loadSparkline(res.ip);
loadUptimeDisplay(res.ip);
}
});
document.getElementById('statOnline').textContent = onlineCount;
// Refresh favorites tab if visible (ping values shown there too)
if (document.getElementById('panelFavorites').classList.contains('active')) renderFavorites();
} catch(e) {
// Silent fail - network hiccup, try again next cycle
}
livePingBusy = false;
}
// ── Device list ───────────────────────────────────────────
function addDeviceToList(d, live) {
var list = document.getElementById('panelDevices');
var id = 'dev-'+d.ip.replace(/\./g,'-');
var ex = document.getElementById(id); if (ex) ex.remove();
var ms = d.ms !== null ? parseInt(d.ms) : null;
var pingClass = ms === null ? '' : ms<=50?'fast':ms<=100?'mid':'slow';
var pingText = ms !== null ? ms+' ms' : '';
var isFav = favorites.indexOf(d.ip) >= 0;
var name = getCustomName(d.ip) || d.hostname || d.ip;
var dot = ms!==null?(ms<=50?'var(--online)':ms<=100?'var(--warn)':'var(--danger)'):'var(--border)';
var item = document.createElement('div');
item.className = 'device-item' + (isFav?' favorite':'');
item.id = id;
item.innerHTML =
'<div class="device-dot" style="background:'+dot+'"></div>'+
'<div class="device-info">'+
'<div class="device-hostname">'+esc(name)+'</div>'+
'<div class="device-ip">'+d.ip+'</div>'+
(d.os?'<div class="device-os">'+esc(d.os)+'</div>':'')+
'</div>'+
'<div class="device-ping '+pingClass+'">'+pingText+'</div>'+
'<button class="star-btn" onclick="toggleFavorite(\''+d.ip+'\',this,event)" title="Favorit">'+(isFav?'⭐':'☆')+'</button>';
item.addEventListener('click', function(){selectDevice(d, item);});
if (favorites.indexOf(d.ip) >= 0) list.prepend(item);
else list.appendChild(item);
}
function filterDevices() {
var q = document.getElementById('searchInput').value.toLowerCase();
document.querySelectorAll('.device-item').forEach(function(el) {
var text = el.textContent.toLowerCase();
el.style.display = q===''||text.includes(q)?'':'none';
});
}
function markDeviceNew(ip) {
var el = document.getElementById('dev-'+ip.replace(/\./g,'-'));
if (el) { el.classList.add('is-new'); setTimeout(function(){el.classList.remove('is-new');},30000); }
}
function markDeviceOffline(ip) {
var el = document.getElementById('dev-'+ip.replace(/\./g,'-'));
if (el) el.classList.add('is-offline');
}
// ── Favorites ─────────────────────────────────────────────
function toggleFavorite(ip, btn, e) {
e.stopPropagation();
var idx = favorites.indexOf(ip);
if (idx>=0) { favorites.splice(idx,1); btn.textContent='☆'; btn.closest('.device-item').classList.remove('favorite'); }
else { favorites.push(ip); btn.textContent='⭐'; btn.closest('.device-item').classList.add('favorite'); }
api.saveFavorites(favorites);
renderFavorites();
}
function renderFavorites() {
var panel = document.getElementById('panelFavorites');
var empty = document.getElementById('emptyFavs');
if (!favorites.length) { empty.style.display=''; return; }
empty.style.display = 'none';
var html = '';
favorites.forEach(function(ip) {
var d = devices.find(function(x){return x.ip===ip;});
var name = getCustomName(ip) || (d&&d.hostname) || ip;
var ms = d&&d.ms!==null?parseInt(d.ms):null;
var dot = ms!==null?(ms<=50?'var(--online)':ms<=100?'var(--warn)':'var(--danger)'):'var(--border)';
html += '<div class="device-item" onclick="selectDeviceByIp(\''+ip+'\')">'+
'<div class="device-dot" style="background:'+dot+'"></div>'+
'<div class="device-info"><div class="device-hostname">'+esc(name)+'</div><div class="device-ip">'+ip+'</div></div>'+
'<div class="device-ping">'+(ms!==null?ms+' ms':'')+'</div></div>';
});
panel.innerHTML = html;
}
function selectDeviceByIp(ip) {
var d = devices.find(function(x){return x.ip===ip;});
if (!d) return;
switchTab('devices');
var item = document.getElementById('dev-'+ip.replace(/\./g,'-'));
if (item) selectDevice(d, item);
}
// ── Device detail ─────────────────────────────────────────
async function selectDevice(d, item) {
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 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 isUnknown = !customName && (!d.vendor || d.vendor==='Unbekannt') && !confirmedDevices[d.ip];
detail.innerHTML =
(isUnknown?'<div class="unknown-banner"><span class="unknown-banner-text">⚠️ Unbekanntes Gerät ist das deins?</span><button class="btn-confirm" onclick="confirmDevice(\''+d.ip+'\')">✓ Bestätigen</button></div>':'')+
'<div class="detail-header">'+
'<div class="detail-icon">'+icon+'</div>'+
'<div class="detail-title">'+
'<div class="detail-hostname">'+esc(displayName)+'</div>'+
(customName&&d.hostname&&d.hostname!==d.ip?'<div class="detail-ip">'+d.ip+' · '+esc(d.hostname)+'</div>':'<div class="detail-ip">'+d.ip+'</div>')+
(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>'+
(d.os?'<div class="detail-os">🖥 Erkanntes OS: '+esc(d.os)+(d.ttl?' (TTL '+d.ttl+')':'')+'</div>':'')+
'</div>'+
'</div>'+
'<div class="detail-actions">'+
'<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>'+
'</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>'+
'<div class="info-card"><div class="info-card-label">Ping <span style="font-size:9px;color:var(--accent)" id="liveDot">●</span></div><div class="info-card-val" id="pingVal">'+
(d.ms!==null&&d.ms!==undefined?'<span class="'+(d.ms<=50?'green':d.ms<=100?'warn':'danger')+'" style="color:inherit">'+d.ms+' ms</span>':'<div class="spinner" style="margin:auto"></div>')+
'</div></div>'+
'</div>'+
'<div class="section-title">Uptime</div>'+
'<div class="info-grid" id="uptimeArea">'+
'<div class="info-card"><div class="info-card-label">Online seit</div><div class="info-card-val green" id="uptimeSince">…</div></div>'+
'<div class="info-card"><div class="info-card-label">Zuletzt gesehen</div><div class="info-card-val teal" id="uptimeLastSeen">…</div></div>'+
'<div class="info-card"><div class="info-card-label">Status</div><div class="info-card-val" id="uptimeStatus">…</div></div>'+
'</div>'+
'<div class="section-title">Latenz-Verlauf</div>'+
'<div class="sparkline-wrap" id="sparklineWrap"><svg class="sparkline-svg"><text x="50%" y="50%" text-anchor="middle" fill="var(--muted)" font-size="11">Sammle Daten…</text></svg></div>'+
'<div class="section-title">Offene Ports</div>'+
'<div id="securityWarnings"></div>'+
'<div id="portsArea"><div class="port-scanning"><div class="spinner"></div> Ports werden gescannt…</div></div>';
// Live ping
var pingRes = await api.pingDevice(d.ip);
var liveMs = pingRes.ms!==null?parseInt(pingRes.ms):null;
var pingEl = document.getElementById('pingVal');
if (pingEl) {
var pc = liveMs!==null?(liveMs<=50?'green':liveMs<=100?'warn':'danger'):'danger';
pingEl.className='info-card-val '+pc;
pingEl.textContent = liveMs!==null?liveMs+' ms':'Offline';
}
// Update OS if available from fresh ping
if (pingRes.os && !d.os) { d.os = pingRes.os; d.ttl = pingRes.ttl; }
// Update list item ping
var listPingEl = document.querySelector('#dev-'+d.ip.replace(/\./g,'-')+' .device-ping');
if (listPingEl&&liveMs!==null) {
var lpc=liveMs<=50?'fast':liveMs<=100?'mid':'slow';
listPingEl.className='device-ping '+lpc;
listPingEl.textContent=liveMs+' ms';
d.ms=liveMs;
}
// Vendor live lookup
if (d.mac&&d.mac!==''&&(!d.vendor||d.vendor==='Unbekannt')) {
api.lookupVendor(d.mac).then(function(v) { d.vendor=v; var el=document.getElementById('detailVendor'); if(el)el.textContent=v||'Unbekannt'; });
}
// Uptime data
loadUptimeDisplay(d.ip);
// Sparkline
loadSparkline(d.ip);
// Port scan
var ports = await api.scanPorts(d.ip);
totalPorts = ports.length;
document.getElementById('statPorts').textContent = totalPorts;
var pa = document.getElementById('portsArea');
if (!ports.length) { pa.innerHTML='<div class="no-ports">Keine bekannten Ports offen gefunden.</div>'; }
else {
var html='<div class="ports-grid">';
ports.forEach(function(p) {
html+='<div class="port-tag"><div class="port-dot"></div><span class="port-num">'+p.port+'</span><span class="port-name">'+p.name+'</span><span class="port-desc">'+(p.desc||'')+'</span></div>';
});
html+='</div>'; pa.innerHTML=html;
}
// Security warnings
var warnings = await api.getSecurityWarnings(ports);
var wEl = document.getElementById('securityWarnings');
if (wEl) {
if (warnings.length) {
wEl.innerHTML = warnings.map(function(w) {
return '<div class="sec-warning"><span class="sec-warning-icon">⚠️</span><div><div class="sec-warning-title">Port '+w.port+' ('+w.name+') Sicherheitsrisiko</div><div class="sec-warning-text">'+esc(w.warning)+'</div></div></div>';
}).join('');
} else { wEl.innerHTML=''; }
}
}
// ── Uptime display ────────────────────────────────────────
async function loadUptimeDisplay(ip) {
var data = await api.getUptime(ip);
var sinceEl = document.getElementById('uptimeSince');
var lastEl = document.getElementById('uptimeLastSeen');
var statusEl = document.getElementById('uptimeStatus');
if (!sinceEl) return;
if (data.onlineSince) {
sinceEl.textContent = formatDuration(Date.now()-data.onlineSince);
statusEl.textContent = '🟢 Online';
statusEl.className = 'info-card-val green';
} else {
sinceEl.textContent = '';
statusEl.textContent = '🔴 Offline';
statusEl.className = 'info-card-val danger';
}
lastEl.textContent = data.lastSeen ? formatDuration(Date.now()-data.lastSeen)+' her' : '';
}
function formatDuration(ms) {
var sec = Math.floor(ms/1000);
if (sec < 60) return sec+'s';
var min = Math.floor(sec/60);
if (min < 60) return min+'min';
var h = Math.floor(min/60);
if (h < 24) return h+'h '+(min%60)+'min';
var days = Math.floor(h/24);
return days+'d '+(h%24)+'h';
}
// ── Sparkline ─────────────────────────────────────────────
async function loadSparkline(ip) {
var history = await api.getPingHistory(ip);
renderSparkline(history);
}
function renderSparkline(history) {
var wrap = document.getElementById('sparklineWrap');
if (!wrap) return;
if (!history || history.length < 2) {
wrap.innerHTML = '<svg class="sparkline-svg"><text x="50%" y="50%" text-anchor="middle" fill="var(--muted)" font-size="11">Sammle Daten… (erscheint nach ein paar Sekunden Live-Ping)</text></svg>';
return;
}
var w = 600, h = 50, pad = 4;
var validPoints = history.filter(function(p){return p.ms!==null;});
var maxMs = Math.max.apply(null, validPoints.map(function(p){return p.ms;}).concat([10]));
var stepX = (w-pad*2) / Math.max(history.length-1,1);
var points = history.map(function(p,i) {
var x = pad + i*stepX;
var y = p.ms===null ? h-pad : (h-pad) - ((p.ms/maxMs) * (h-pad*2));
return {x:x,y:y,ms:p.ms};
});
var pathD = points.map(function(p,i){return (i===0?'M':'L')+p.x.toFixed(1)+','+p.y.toFixed(1);}).join(' ');
var dots = points.map(function(p) {
var color = p.ms===null?'var(--danger)':p.ms<=50?'var(--online)':p.ms<=100?'var(--warn)':'var(--danger)';
return '<circle cx="'+p.x.toFixed(1)+'" cy="'+p.y.toFixed(1)+'" r="2" fill="'+color+'"/>';
}).join('');
wrap.innerHTML = '<svg class="sparkline-svg" viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none">'+
'<path d="'+pathD+'" fill="none" stroke="var(--accent)" stroke-width="1.5"/>'+dots+'</svg>'+
'<div style="font-size:10px;color:var(--muted);margin-top:4px">Letzte '+history.length+' Messungen · max '+maxMs+'ms</div>';
}
// ── Unknown device confirmation ───────────────────────────
var confirmedDevices = {};
function confirmDevice(ip) {
confirmedDevices[ip] = true;
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ät bestätigt', ip+' wird nicht mehr als unbekannt markiert', 'info');
}
async function reping(ip) {
var res = await api.pingDevice(ip);
var liveMs = res.ms!==null?parseInt(res.ms):null;
var el = document.getElementById('pingVal');
if (el) { var pc=liveMs!==null?(liveMs<=50?'green':liveMs<=100?'warn':'danger'):'danger'; el.className='info-card-val '+pc; el.textContent=liveMs!==null?liveMs+' ms':'Offline'; }
var lpe = document.querySelector('#dev-'+ip.replace(/\./g,'-')+' .device-ping');
if (lpe&&liveMs!==null) { var lpc=liveMs<=50?'fast':liveMs<=100?'mid':'slow'; lpe.className='device-ping '+lpc; lpe.textContent=liveMs+' ms'; }
}
async function rescanPorts(ip) {
var pa = document.getElementById('portsArea');
if (!pa) return;
pa.innerHTML='<div class="port-scanning"><div class="spinner"></div> Ports werden gescannt…</div>';
var ports = await api.scanPorts(ip);
if (!ports.length) { pa.innerHTML='<div class="no-ports">Keine bekannten Ports offen gefunden.</div>'; }
else {
var html='<div class="ports-grid">';
ports.forEach(function(p){html+='<div class="port-tag"><div class="port-dot"></div><span class="port-num">'+p.port+'</span><span class="port-name">'+p.name+'</span><span class="port-desc">'+(p.desc||'')+'</span></div>';});
html+='</div>'; pa.innerHTML=html;
}
}
// ── Quick edit ────────────────────────────────────────────
function quickEditDevice(ip) {
var ex = document.getElementById('quickEditForm'); if(ex){ex.remove();return;}
var cur = getCustomName(ip)||''; var note=(deviceNames[ip]&&deviceNames[ip].note)||'';
var form = document.createElement('div'); form.id='quickEditForm'; form.className='quick-edit-form';
form.innerHTML='<div style="font-size:11px;font-weight:600;color:var(--accent)">✏️ Umbenennen · '+ip+'</div>'+
'<div class="qe-row"><input class="qe-input" id="qeName" placeholder="Name" value="'+esc(cur)+'"><input class="qe-input" id="qeNote" placeholder="Notiz" value="'+esc(note)+'"></div>'+
'<div class="qe-row" style="justify-content:flex-end"><button class="btn-action" onclick="document.getElementById(\'quickEditForm\').remove()">Abbrechen</button><button class="btn-save" onclick="applyQuickEdit(\''+ip+'\')">💾 Speichern</button></div>';
var actions = document.querySelector('.detail-actions');
if (actions) actions.after(form);
document.getElementById('qeName').focus(); document.getElementById('qeName').select();
}
async function applyQuickEdit(ip) {
var name=document.getElementById('qeName').value.trim(), note=document.getElementById('qeNote').value.trim();
if (name) deviceNames[ip]={name:name,note:note}; else delete deviceNames[ip];
await api.saveDeviceNames(deviceNames);
document.getElementById('quickEditForm').remove();
var el=document.getElementById('dev-'+ip.replace(/\./g,'-'));
if (el) { el.querySelector('.device-hostname').textContent=getCustomName(ip)||(devices.find(function(x){return x.ip===ip;})||{}).hostname||ip; }
var activeItem = document.querySelector('.device-item.active');
if (activeItem) { var d=devices.find(function(x){return x.ip===ip;}); if(d)selectDevice(d,activeItem); }
}
// ── History ───────────────────────────────────────────────
async function loadHistory() {
var hist = await api.getHistory();
var list = document.getElementById('historyList');
if (!hist.length) { list.innerHTML='<div class="empty-list"><div class="empty-icon">📋</div>Noch kein Scan-Verlauf.</div>'; return; }
list.innerHTML = hist.slice(0,30).map(function(h) {
var d = new Date(h.time);
var ds = d.toLocaleDateString('de-DE')+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
return '<div class="history-item"><div class="history-time">'+ds+'</div><div class="history-info">'+h.count+' Geräte · '+h.subnet+'0/24</div></div>';
}).join('');
}
async function clearHistory() {
if (!confirm('Scan-Verlauf löschen?')) return;
await api.clearHistory();
loadHistory();
showToast('Verlauf gelöscht','Scan-Verlauf wurde geleert','info');
}
// ── Settings ──────────────────────────────────────────────
function openSettings() { document.getElementById('settingsModal').classList.add('open'); }
async function saveSettings() {
settings = {
theme: document.getElementById('settingTheme').value,
autoScan: parseInt(document.getElementById('settingAutoScan').value),
notifyNew: document.getElementById('settingNotifyNew').checked,
notifyOffline: document.getElementById('settingNotifyOffline').checked,
autoStart: document.getElementById('settingAutoStart').checked,
closeToTray: document.getElementById('settingCloseToTray').checked
};
await api.saveSettings(settings);
applyTheme(settings.theme);
document.getElementById('autoscanBadge').classList.toggle('visible', settings.autoScan > 0);
closeModal('settingsModal');
showToast('Einstellungen gespeichert','','info');
}
// ── Modals ────────────────────────────────────────────────
function closeModal(id, e) {
if (e && e.target !== document.getElementById(id)) return;
document.getElementById(id).classList.remove('open');
}
// ── Port Manager ──────────────────────────────────────────
function openPortManager() { renderPortList(); document.getElementById('portModal').classList.add('open'); }
function renderPortList() {
var list = document.getElementById('portModalList');
list.innerHTML = '';
currentPorts.slice().sort(function(a,b){return a.port-b.port;}).forEach(function(p,i) {
var idx = currentPorts.indexOf(p);
var row = document.createElement('div'); row.className='port-row';
row.innerHTML='<span class="pr-port">'+p.port+'</span>'+
'<input class="pr-name" type="text" value="'+esc(p.name)+'" placeholder="Name" oninput="currentPorts['+idx+'].name=this.value">'+
'<input class="pr-desc" type="text" value="'+esc(p.desc||'')+'" placeholder="Beschreibung" oninput="currentPorts['+idx+'].desc=this.value">'+
'<button class="btn-delete" onclick="currentPorts.splice('+idx+',1);renderPortList()">✕</button>';
list.appendChild(row);
});
}
function addPort() {
var port=parseInt(document.getElementById('newPort').value), name=document.getElementById('newName').value.trim(), desc=document.getElementById('newDesc').value.trim();
if (!port||port<1||port>65535) return alert('Ungültiger Port.');
if (!name) return alert('Bitte Namen eingeben.');
if (currentPorts.find(function(p){return p.port===port;})) return alert('Port bereits vorhanden.');
currentPorts.push({port:port,name:name,desc:desc});
currentPorts.sort(function(a,b){return a.port-b.port;});
document.getElementById('newPort').value=''; document.getElementById('newName').value=''; document.getElementById('newDesc').value='';
renderPortList();
}
async function savePorts() {
await api.savePorts(currentPorts);
var btn=document.querySelector('#portModal .btn-save:last-child'); btn.textContent='✓ Gespeichert!'; btn.style.background='var(--online)';
setTimeout(function(){btn.textContent='💾 Speichern';btn.style.background='';},1800);
}
async function resetPorts() {
if (!confirm('Standard-Ports wiederherstellen?')) return;
currentPorts = await api.getPorts();
renderPortList();
}
// ── Device Manager ────────────────────────────────────────
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('');});
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>'+
'<input type="text" value="'+esc(d.name||'')+'" placeholder="Name" oninput="deviceNames[\''+ip+'\'].name=this.value">'+
'<input type="text" value="'+esc(d.note||'')+'" placeholder="Notiz" oninput="deviceNames[\''+ip+'\'].note=this.value">'+
'<button class="btn-delete" onclick="delete deviceNames[\''+ip+'\'];renderDeviceList()">✕</button>';
list.appendChild(row);
});
document.getElementById('deviceCount').textContent=ips.length+' Einträge';
}
function addDeviceName() {
var ip=document.getElementById('newDeviceIp').value.trim(), name=document.getElementById('newDeviceName').value.trim(), note=document.getElementById('newDeviceNote').value.trim();
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) return alert('Ungültige IP.');
if (!name) return alert('Name eingeben.');
deviceNames[ip]={name:name,note:note};
document.getElementById('newDeviceIp').value=''; document.getElementById('newDeviceName').value=''; document.getElementById('newDeviceNote').value='';
renderDeviceList();
}
async function saveDeviceNames() {
await api.saveDeviceNames(deviceNames);
var btn=document.querySelector('#deviceModal .btn-save'); btn.textContent='✓ Gespeichert!'; btn.style.background='var(--online)';
setTimeout(function(){btn.textContent='💾 Speichern';btn.style.background='';},1800);
}
function importCSV(input) {
var file=input.files[0]; if(!file) return;
document.getElementById('csvFileName').textContent=file.name;
var r=new FileReader();
r.onload=function(e) {
var lines=e.target.result.split('\n'); var count=0;
lines.forEach(function(line) {
line=line.trim(); if(!line||line.startsWith('#')) return;
var parts=line.split(','); var ip=(parts[0]||'').trim(), name=(parts[1]||'').trim(), note=(parts[2]||'').trim();
if(ip.match(/^\d+\.\d+\.\d+\.\d+$/)&&name){deviceNames[ip]={name:name,note:note};count++;}
});
renderDeviceList(); alert(count+' Geräte importiert.');
};
r.readAsText(file); input.value='';
}
function exportCSV() {
var lines=['# NetScanner Geräte-Export','# IP,Name,Notiz'];
Object.keys(deviceNames).sort().forEach(function(ip){var d=deviceNames[ip];lines.push(ip+','+(d.name||'')+','+(d.note||''));});
var blob=new Blob([lines.join('\n')],{type:'text/csv'});
var url=URL.createObjectURL(blob); var a=document.createElement('a');
a.href=url; a.download='netscanner-geraete.csv'; a.click(); URL.revokeObjectURL(url);
}
// ── Network Editor & PDF ──────────────────────────────────
async function openNetworkEditor() {
if (!devices.length) return alert('Bitte zuerst einen Scan durchführen.');
var cols=5,cardW=175,cardH=65,gapX=30,gapY=40,startX=60,startY=160;
var initNodes=[{id:1,type:'router',name:'Router',ip:document.getElementById('subnetSelect').value+'1',x:400,y:40,w:200,h:65,color:'#9c59ff',note:''}];
devices.forEach(function(d,i){
var col=i%cols,row=Math.floor(i/cols);
initNodes.push({id:i+10,type:'device',name:getCustomName(d.ip)||d.hostname||d.ip,ip:d.ip,note:(deviceNames[d.ip]&&deviceNames[d.ip].note)||'',x:startX+col*(cardW+gapX),y:startY+row*(cardH+gapY),w:cardW,h:cardH,color:'#00d4aa',ms:d.ms!==null?parseInt(d.ms):null,mac:d.mac||'',vendor:d.vendor||''});
});
var initConns=devices.map(function(d,i){return{from:1,to:i+10,type:'lan',color:'#9c59ff',width:1,dash:true,label:''};});
await api.openEditor({nodes:initNodes,connections:initConns,groups:[]});
}
// ── Plan Manager ──────────────────────────────────────────
async function openPlanManager() {
await renderPlanList();
document.getElementById('planModal').classList.add('open');
}
async function renderPlanList() {
var plans = await api.getPlans();
var list = document.getElementById('planModalList');
var names = Object.keys(plans);
if (!names.length) { list.innerHTML = '<div class="empty-list" style="padding:20px"><div class="empty-icon">📁</div>Noch keine Pläne gespeichert.</div>'; return; }
list.innerHTML = names.map(function(name) {
var p = plans[name];
var d = new Date(p.savedAt);
var ds = d.toLocaleDateString('de-DE')+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
return '<div class="plan-row">'+
'<div style="flex:1"><div class="plan-name">'+esc(name)+'</div><div class="plan-date">'+ds+'</div></div>'+
'<button class="plan-btn" onclick="loadPlan(\''+esc(name).replace(/'/g,"\\'")+'\')">📂 Öffnen</button>'+
'<button class="plan-btn danger" onclick="deletePlanConfirm(\''+esc(name).replace(/'/g,"\\'")+'\')">🗑</button>'+
'</div>';
}).join('');
}
async function saveCurrentAsPlan() {
var name = document.getElementById('newPlanName').value.trim();
if (!name) return alert('Bitte einen Namen für den Plan eingeben.');
if (!devices.length) return alert('Bitte zuerst einen Netzwerk-Scan durchführen, damit Geräte im Plan enthalten sind.');
// Build the same layout as openNetworkEditor does, but save instead of opening editor
var cols=5,cardW=175,cardH=65,gapX=30,gapY=40,startX=60,startY=160;
var initNodes=[{id:1,type:'router',name:'Router',ip:document.getElementById('subnetSelect').value+'1',x:400,y:40,w:200,h:65,color:'#9c59ff',note:''}];
devices.forEach(function(d,i){
var col=i%cols,row=Math.floor(i/cols);
initNodes.push({id:i+10,type:'device',name:getCustomName(d.ip)||d.hostname||d.ip,ip:d.ip,note:(deviceNames[d.ip]&&deviceNames[d.ip].note)||'',x:startX+col*(cardW+gapX),y:startY+row*(cardH+gapY),w:cardW,h:cardH,color:'#00d4aa',ms:d.ms!==null?parseInt(d.ms):null,mac:d.mac||'',vendor:d.vendor||''});
});
var initConns=devices.map(function(d,i){return{from:1,to:i+10,type:'lan',color:'#9c59ff',width:1,dash:true,label:''};});
await api.savePlan(name, { nodes: initNodes, connections: initConns, groups: [] });
document.getElementById('newPlanName').value = '';
renderPlanList();
showToast('Plan gespeichert', name, 'info');
}
async function loadPlan(name) {
var plans = await api.getPlans();
var plan = plans[name];
if (!plan) return;
await api.openEditor(plan.data);
closeModal('planModal');
}
async function deletePlanConfirm(name) {
if (!confirm('Plan "'+name+'" wirklich löschen?')) return;
await api.deletePlan(name);
renderPlanList();
}
// ── Subnet Manager ────────────────────────────────────────
function openSubnetManager() { renderSubnetList(); document.getElementById('subnetModal').classList.add('open'); }
function renderSubnetList() {
var list = document.getElementById('subnetModalList');
if (!monitoredSubnets.length) { list.innerHTML = '<div style="font-size:12px;color:var(--muted)">Noch keine zusätzlichen Subnetze hinzugefügt.</div>'; }
else {
list.innerHTML = monitoredSubnets.map(function(s,i) {
var count = extraSubnetDevices[s] ? extraSubnetDevices[s].length : null;
return '<div class="subnet-tag"><span class="subnet-status"></span>'+s+'0/24'+(count!==null?' · '+count+' Geräte':'')+'<span class="subnet-remove" onclick="removeMonitoredSubnet('+i+')">✕</span></div>';
}).join('');
}
document.getElementById('subnetCount').textContent = monitoredSubnets.length+' zusätzliche Subnetze';
}
async function addMonitoredSubnet() {
var val = document.getElementById('newSubnetInput').value.trim();
if (!val) return;
if (!val.endsWith('.')) val += '.';
if (!val.match(/^\d+\.\d+\.\d+\.$/)) return alert('Format: z.B. 192.168.1. (mit Punkt am Ende)');
if (monitoredSubnets.indexOf(val) >= 0) return alert('Subnetz bereits vorhanden.');
monitoredSubnets.push(val);
await api.saveMonitoredSubnets(monitoredSubnets);
document.getElementById('newSubnetInput').value = '';
renderSubnetList();
}
async function removeMonitoredSubnet(idx) {
var removed = monitoredSubnets.splice(idx,1)[0];
delete extraSubnetDevices[removed];
await api.saveMonitoredSubnets(monitoredSubnets);
renderSubnetList();
}
// ── Export device list ────────────────────────────────────
function toggleExportMenu() {
document.getElementById('exportMenu').classList.toggle('open');
}
document.addEventListener('click', function(e) {
var menu = document.getElementById('exportMenu');
if (menu && menu.classList.contains('open') && !e.target.closest('.export-dropdown')) menu.classList.remove('open');
});
async function exportDeviceList(format) {
document.getElementById('exportMenu').classList.remove('open');
if (!devices.length) return alert('Keine Geräte zum Exportieren. Bitte zuerst scannen.');
var exportData = devices.map(function(d) {
return { ip:d.ip, displayName:getCustomName(d.ip)||d.hostname||d.ip, hostname:d.hostname, mac:d.mac, vendor:d.vendor, ms:d.ms, os:d.os, ipv6:d.ipv6 };
});
var result = await api.exportDeviceList(exportData, format);
if (result.success) showToast('Export erfolgreich', 'Geräteliste als '+format.toUpperCase()+' gespeichert', 'info');
}
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
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 '🔒';
return '🖥️';
}
init();
</script>
</body>
</html>