1336 lines
72 KiB
HTML
1336 lines
72 KiB
HTML
<!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 & 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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
function getCustomName(ip){return deviceNames[ip]?deviceNames[ip].name:null;}
|
||
function getDeviceIcon(vendor) {
|
||
if (!vendor) return '🖥️';
|
||
var v=vendor.toLowerCase();
|
||
if(v.includes('apple')) return '🍎';
|
||
if(v.includes('synology')) return '🗄️';
|
||
if(v.includes('raspberry')) return '🍓';
|
||
if(v.includes('fritz')||v.includes('avm')) return '📡';
|
||
if(v.includes('tp-link')) return '📡';
|
||
if(v.includes('ubiquiti')) return '📶';
|
||
if(v.includes('vmware')||v.includes('qemu')||v.includes('proxmox')) return '💻';
|
||
if(v.includes('cisco')) return '🔀';
|
||
if(v.includes('samsung')) return '📺';
|
||
if(v.includes('nintendo')) return '🎮';
|
||
if(v.includes('brother')) return '🖨️';
|
||
if(v.includes('abus')) return '🔒';
|
||
return '🖥️';
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|