Upload folder via GUI - src
This commit is contained in:
384
src/index.html
384
src/index.html
@@ -10,6 +10,50 @@
|
||||
--accent:#00d4aa; --accent2:#0099ff; --danger:#ff4757; --warn:#ffa502;
|
||||
--text:#e6edf3; --muted:#7d8590; --online:#26de81; --purple:#a78bfa;
|
||||
}
|
||||
var DEVICE_TYPE_OPTIONS = [
|
||||
'Windows-PC',
|
||||
'Linux-Server',
|
||||
'NAS/Storage',
|
||||
'Kamera/CCTV',
|
||||
'Alarmanlage/Security',
|
||||
'Router/Access Point',
|
||||
'Drucker',
|
||||
'IoT/Smart-Device',
|
||||
'Virtualisierungs-Host',
|
||||
'Konsole/Mediengeraet'
|
||||
];
|
||||
|
||||
function getForcedDeviceType(ip) {
|
||||
var p = deviceProfiles[ip];
|
||||
if (!p || !p.forcedType) return 'auto';
|
||||
return p.forcedType;
|
||||
}
|
||||
|
||||
function buildDeviceTypeOptions(currentType) {
|
||||
var html = '<option value="auto"'+(currentType==='auto'?' selected':'')+'>Auto</option>';
|
||||
DEVICE_TYPE_OPTIONS.forEach(function(t) {
|
||||
html += '<option value="'+esc(t)+'"'+(currentType===t?' selected':'')+'>'+esc(t)+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function setDeviceClass(ip, selectedType) {
|
||||
if (!ip) return;
|
||||
if (selectedType === 'auto') {
|
||||
delete deviceProfiles[ip];
|
||||
} else {
|
||||
deviceProfiles[ip] = deviceProfiles[ip] || {};
|
||||
deviceProfiles[ip].forcedType = selectedType;
|
||||
}
|
||||
await api.saveDeviceProfiles(deviceProfiles);
|
||||
|
||||
var activeItem = document.querySelector('.device-item.active');
|
||||
if (activeItem) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
if (d) selectDevice(d, activeItem);
|
||||
}
|
||||
showToast('Geräteklasse gespeichert', selectedType==='auto' ? 'Auto-Erkennung aktiv' : selectedType+' manuell gesetzt', 'info');
|
||||
}
|
||||
.light {
|
||||
--bg:#f6f8fa; --surface:#ffffff; --surface2:#f0f2f5; --border:#d0d7de;
|
||||
--text:#24292f; --muted:#57606a; --surface2:#eaeef2;
|
||||
@@ -323,8 +367,8 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<div class="stat-box"><div class="stat-num" id="statOnline">0</div><div class="stat-label">Online</div></div>
|
||||
</div>
|
||||
|
||||
<div id="liveStatusBar" style="display:none;padding:5px 12px;font-size:10px;color:var(--accent);display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--border)">
|
||||
<span style="animation:livepulse 2s ease-in-out infinite">●</span> Live-Ping aktiv – aktualisiert alle 4s
|
||||
<div id="liveStatusBar" style="display:none;padding:5px 12px;font-size:10px;color:var(--accent);align-items:center;gap:6px;border-bottom:1px solid var(--border)">
|
||||
<span style="animation:livepulse 2s ease-in-out infinite">●</span> Live-Ping aktiv – aktualisiert alle 6s
|
||||
</div>
|
||||
|
||||
<div class="search-wrap">
|
||||
@@ -415,6 +459,15 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<div><div class="setting-label">Offline-Geräte melden</div><div class="setting-sub">Benachrichtigung wenn ein Gerät nicht mehr erreichbar ist</div></div>
|
||||
<label class="toggle"><input type="checkbox" id="settingNotifyOffline"><span class="toggle-slider"></span></label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div><div class="setting-label">Offline-Empfindlichkeit</div><div class="setting-sub">Jede Prüfung sendet bereits 3 Ping-Pakete (gilt nur als Fehlschlag, wenn alle 3 verloren gehen). Hier stellst du ein, wie viele solcher Prüfungen in Folge fehlschlagen müssen, bevor ein Gerät als offline gilt.</div></div>
|
||||
<select class="setting-select" id="settingFailThreshold">
|
||||
<option value="1">Sofort (1 Prüfung)</option>
|
||||
<option value="2">Normal (2 Prüfungen)</option>
|
||||
<option value="3">Tolerant (3 Prüfungen)</option>
|
||||
<option value="5">Sehr tolerant (5 Prüfungen)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div><div class="setting-label">Beim Start automatisch scannen</div><div class="setting-sub">Startet den Scan direkt beim Öffnen</div></div>
|
||||
<label class="toggle"><input type="checkbox" id="settingAutoStart"><span class="toggle-slider"></span></label>
|
||||
@@ -532,6 +585,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
<script>
|
||||
var devices = [], totalPorts = 0, scanning = false;
|
||||
var currentPorts = [], deviceNames = {}, favorites = [], settings = {};
|
||||
var deviceProfiles = {};
|
||||
var monitoredSubnets = [];
|
||||
var extraSubnetDevices = {}; // { subnet: [devices] }
|
||||
|
||||
@@ -539,6 +593,7 @@ var extraSubnetDevices = {}; // { subnet: [devices] }
|
||||
async function init() {
|
||||
currentPorts = await api.getPorts();
|
||||
deviceNames = await api.getDeviceNames();
|
||||
deviceProfiles = await api.getDeviceProfiles();
|
||||
favorites = await api.getFavorites();
|
||||
settings = await api.getSettings();
|
||||
applyTheme(settings.theme || 'dark');
|
||||
@@ -546,6 +601,8 @@ async function init() {
|
||||
document.getElementById('settingAutoScan').value = settings.autoScan || 0;
|
||||
document.getElementById('settingNotifyNew').checked = settings.notifyNew !== false;
|
||||
document.getElementById('settingNotifyOffline').checked = settings.notifyOffline !== false;
|
||||
document.getElementById('settingFailThreshold').value = settings.failThreshold || 2;
|
||||
FAIL_THRESHOLD = settings.failThreshold || 2;
|
||||
document.getElementById('settingAutoStart').checked = settings.autoStart || false;
|
||||
document.getElementById('settingCloseToTray').checked = settings.closeToTray || false;
|
||||
if (settings.autoScan > 0) document.getElementById('autoscanBadge').classList.add('visible');
|
||||
@@ -669,12 +726,15 @@ async function scanExtraSubnets() {
|
||||
// ── Live Ping Loop ────────────────────────────────────────
|
||||
var livePingTimer = null;
|
||||
var livePingBusy = false;
|
||||
var LIVE_PING_INTERVAL = 4000; // ms
|
||||
var LIVE_PING_INTERVAL = 6000; // ms — increased from 4s since each check now sends 3 packets per device (takes a bit longer, but is far more reliable)
|
||||
var FAIL_THRESHOLD = 2; // consecutive FAILED CHECKS before flagging offline. Note: each "check" already sends 3 ICMP packets and only fails if ALL 3 are lost, so this is now a much stronger guarantee than it sounds.
|
||||
var consecutiveFails = {}; // { ip: count }
|
||||
|
||||
function startLivePing() {
|
||||
stopLivePing();
|
||||
if (!devices.length) return;
|
||||
document.getElementById('liveStatusBar').style.display = 'flex';
|
||||
consecutiveFails = {};
|
||||
livePingTimer = setInterval(runLivePingCycle, LIVE_PING_INTERVAL);
|
||||
runLivePingCycle();
|
||||
}
|
||||
@@ -694,16 +754,27 @@ async function runLivePingCycle() {
|
||||
results.forEach(function(res) {
|
||||
var d = devices.find(function(x) { return x.ip === res.ip; });
|
||||
if (!d) return;
|
||||
var liveMs = res.alive && res.ms !== null ? parseInt(res.ms) : null;
|
||||
d.ms = liveMs;
|
||||
if (res.alive) onlineCount++;
|
||||
|
||||
if (res.alive) {
|
||||
consecutiveFails[res.ip] = 0;
|
||||
} else {
|
||||
consecutiveFails[res.ip] = (consecutiveFails[res.ip] || 0) + 1;
|
||||
}
|
||||
// Only treat as truly offline after FAIL_THRESHOLD consecutive misses —
|
||||
// a single dropped ping on a healthy LAN is normal and not worth alerting on.
|
||||
var confirmedOffline = consecutiveFails[res.ip] >= FAIL_THRESHOLD;
|
||||
var liveMs = (res.alive && res.ms !== null) ? parseInt(res.ms) : null;
|
||||
|
||||
if (res.alive) { d.ms = liveMs; onlineCount++; }
|
||||
else if (!confirmedOffline) { onlineCount++; } // still counted online while within tolerance
|
||||
|
||||
// Update list row
|
||||
var item = document.getElementById('dev-' + res.ip.replace(/\./g, '-'));
|
||||
if (item) {
|
||||
var dot = item.querySelector('.device-dot');
|
||||
var pingEl = item.querySelector('.device-ping');
|
||||
var wasOffline = item.classList.contains('is-offline');
|
||||
var wasConfirmedOffline = item.classList.contains('is-offline');
|
||||
|
||||
if (res.alive) {
|
||||
item.classList.remove('is-offline');
|
||||
var dotColor = liveMs<=50?'var(--online)':liveMs<=100?'var(--warn)':'var(--danger)';
|
||||
@@ -713,12 +784,13 @@ async function runLivePingCycle() {
|
||||
pingEl.className = 'device-ping ' + lpc;
|
||||
pingEl.textContent = liveMs + ' ms';
|
||||
}
|
||||
if (wasOffline) showToast('Gerät wieder online', (getCustomName(res.ip)||d.hostname||res.ip), 'new');
|
||||
} else {
|
||||
if (!wasOffline) { item.classList.add('is-offline'); showToast('Gerät offline', (getCustomName(res.ip)||d.hostname||res.ip)+' antwortet nicht mehr', 'offline'); }
|
||||
if (wasConfirmedOffline) showToast('Gerät wieder online', (getCustomName(res.ip)||d.hostname||res.ip), 'new');
|
||||
} else if (confirmedOffline) {
|
||||
if (!wasConfirmedOffline) { item.classList.add('is-offline'); showToast('Gerät offline', (getCustomName(res.ip)||d.hostname||res.ip)+' antwortet seit '+FAIL_THRESHOLD+' Prüfungen (je 3 Pings) nicht mehr', 'offline'); }
|
||||
if (dot) dot.style.background = 'var(--danger)';
|
||||
if (pingEl) { pingEl.className='device-ping slow'; pingEl.textContent='offline'; }
|
||||
}
|
||||
// else: single missed ping within tolerance — leave display as-is, no alert
|
||||
}
|
||||
|
||||
// Update detail panel if this device is currently selected
|
||||
@@ -730,7 +802,7 @@ async function runLivePingCycle() {
|
||||
var pc = liveMs<=50?'green':liveMs<=100?'warn':'danger';
|
||||
pingVal.className = 'info-card-val ' + pc;
|
||||
pingVal.textContent = liveMs + ' ms';
|
||||
} else {
|
||||
} else if (confirmedOffline) {
|
||||
pingVal.className = 'info-card-val danger';
|
||||
pingVal.textContent = 'Offline';
|
||||
}
|
||||
@@ -805,9 +877,13 @@ function toggleFavorite(ip, btn, e) {
|
||||
|
||||
function renderFavorites() {
|
||||
var panel = document.getElementById('panelFavorites');
|
||||
var empty = document.getElementById('emptyFavs');
|
||||
if (!favorites.length) { empty.style.display=''; return; }
|
||||
empty.style.display = 'none';
|
||||
if (!favorites.length) {
|
||||
panel.innerHTML = '<div class="empty-list" id="emptyFavs">'+
|
||||
'<div class="empty-icon">⭐</div>'+
|
||||
'Noch keine Favoriten.<br>Klick auf ☆ beim Gerät<br>um es zu pinnen.'+
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
favorites.forEach(function(ip) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
@@ -832,15 +908,17 @@ function selectDeviceByIp(ip) {
|
||||
|
||||
// ── Device detail ─────────────────────────────────────────
|
||||
async function selectDevice(d, item) {
|
||||
if (watchIp && watchIp !== d.ip) stopWatchMode();
|
||||
document.querySelectorAll('.device-item').forEach(function(el){el.classList.remove('active');});
|
||||
if(item) item.classList.add('active');
|
||||
document.getElementById('placeholder').style.display='none';
|
||||
var detail = document.getElementById('deviceDetail');
|
||||
detail.classList.add('visible');
|
||||
var icon = getDeviceIcon(d.vendor);
|
||||
var icon = getDeviceIcon(d);
|
||||
var customName = getCustomName(d.ip);
|
||||
var customNote = deviceNames[d.ip]&&deviceNames[d.ip].note?deviceNames[d.ip].note:null;
|
||||
var displayName = customName||d.hostname||d.ip;
|
||||
var forcedType = getForcedDeviceType(d.ip);
|
||||
var isUnknown = !customName && (!d.vendor || d.vendor==='Unbekannt') && !confirmedDevices[d.ip];
|
||||
|
||||
detail.innerHTML =
|
||||
@@ -853,6 +931,7 @@ async function selectDevice(d, item) {
|
||||
(d.ipv6?'<div class="ipv6-badge">IPv6: '+esc(d.ipv6)+'</div>':'')+
|
||||
(customNote?'<div style="font-size:11px;color:var(--muted);margin-top:2px">📝 '+esc(customNote)+'</div>':'')+
|
||||
'<div class="detail-vendor" id="detailVendor">'+(d.vendor&&d.vendor!=='Unbekannt'?esc(d.vendor):'…')+'</div>'+
|
||||
'<div style="font-size:11px;color:var(--muted);margin-top:2px">🧠 Klasse: <span style="color:var(--accent)">'+(forcedType!=='auto'?esc(forcedType)+' (manuell)':'Auto-Erkennung')+'</span></div>'+
|
||||
(d.os?'<div class="detail-os">🖥 Erkanntes OS: '+esc(d.os)+(d.ttl?' (TTL '+d.ttl+')':'')+'</div>':'')+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
@@ -860,7 +939,16 @@ async function selectDevice(d, item) {
|
||||
'<button class="btn-action" onclick="reping(\''+d.ip+'\')">⚡ Sofort aktualisieren</button>'+
|
||||
'<button class="btn-action" onclick="rescanPorts(\''+d.ip+'\')">🔍 Ports scannen</button>'+
|
||||
'<button class="btn-action" onclick="quickEditDevice(\''+d.ip+'\')">✏️ Umbenennen</button>'+
|
||||
'<button class="btn-action" id="btnWatch" onclick="toggleWatchMode(\''+d.ip+'\')" style="color:#a78bfa;border-color:#a78bfa">🔭 Beobachten (Stecker-Test)</button>'+
|
||||
'<button class="btn-action" onclick="identifyDevice(\''+d.ip+'\',\''+(d.mac||'')+'\')" style="color:#ffa502;border-color:#ffa502">🔬 Identifizieren</button>'+
|
||||
'</div>'+
|
||||
'<div style="margin:-8px 0 16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">'+
|
||||
'<span style="font-size:11px;color:var(--muted)">Geräteklasse:</span>'+
|
||||
'<select class="setting-select" onchange="setDeviceClass(\''+d.ip+'\', this.value)">'+buildDeviceTypeOptions(forcedType)+'</select>'+
|
||||
'<span style="font-size:10px;color:var(--muted)">Auto nutzt Ports, Hersteller und Hostname.</span>'+
|
||||
'</div>'+
|
||||
'<div id="watchPanel" style="display:none"></div>'+
|
||||
'<div id="identifyPanel" style="display:none"></div>'+
|
||||
'<div class="info-grid">'+
|
||||
'<div class="info-card"><div class="info-card-label">IP-Adresse</div><div class="info-card-val teal">'+d.ip+'</div></div>'+
|
||||
'<div class="info-card"><div class="info-card-label">MAC-Adresse</div><div class="info-card-val blue" id="detailMac">'+(d.mac||'–')+'</div></div>'+
|
||||
@@ -1018,6 +1106,158 @@ async function reping(ip) {
|
||||
if (lpe&&liveMs!==null) { var lpc=liveMs<=50?'fast':liveMs<=100?'mid':'slow'; lpe.className='device-ping '+lpc; lpe.textContent=liveMs+' ms'; }
|
||||
}
|
||||
|
||||
// ── Watch Mode: fast continuous ping for the unplug/replug identification test ──
|
||||
var watchTimer = null;
|
||||
var watchIp = null;
|
||||
var watchHistory = []; // sequence of true/false results shown as a timeline
|
||||
|
||||
function toggleWatchMode(ip) {
|
||||
if (watchTimer && watchIp === ip) { stopWatchMode(); return; }
|
||||
stopWatchMode();
|
||||
watchIp = ip;
|
||||
watchHistory = [];
|
||||
document.getElementById('watchPanel').style.display = 'block';
|
||||
document.getElementById('btnWatch').textContent = '⏹ Beobachtung stoppen';
|
||||
document.getElementById('btnWatch').style.background = '#a78bfa';
|
||||
document.getElementById('btnWatch').style.color = '#000';
|
||||
renderWatchPanel('Starte Beobachtung…', null);
|
||||
watchTick(); // run immediately, then every 1.5s
|
||||
watchTimer = setInterval(watchTick, 1500);
|
||||
}
|
||||
|
||||
function stopWatchMode() {
|
||||
if (watchTimer) { clearInterval(watchTimer); watchTimer = null; }
|
||||
watchIp = null;
|
||||
var btn = document.getElementById('btnWatch');
|
||||
if (btn) { btn.textContent = '🔭 Beobachten (Stecker-Test)'; btn.style.background=''; btn.style.color='#a78bfa'; }
|
||||
var panel = document.getElementById('watchPanel');
|
||||
if (panel) panel.style.display = 'none';
|
||||
}
|
||||
|
||||
async function watchTick() {
|
||||
if (!watchIp) return;
|
||||
var ip = watchIp;
|
||||
try {
|
||||
var res = await api.pingDevice(ip);
|
||||
if (watchIp !== ip) return; // user switched device mid-flight, ignore stale result
|
||||
watchHistory.push(res.alive);
|
||||
if (watchHistory.length > 40) watchHistory.shift();
|
||||
var statusText = res.alive ? ('Online – '+(res.ms!=null?res.ms+' ms':'')) : 'Keine Antwort';
|
||||
renderWatchPanel(statusText, res.alive);
|
||||
} catch(e) {
|
||||
watchHistory.push(false);
|
||||
renderWatchPanel('Fehler bei der Abfrage', false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWatchPanel(statusText, alive) {
|
||||
var panel = document.getElementById('watchPanel');
|
||||
if (!panel) return;
|
||||
var dots = watchHistory.map(function(a) {
|
||||
return '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:3px;background:'+(a?'#26de81':'#ff4757')+'"></span>';
|
||||
}).join('');
|
||||
var bigColor = alive===null ? 'var(--muted)' : (alive ? '#26de81' : '#ff4757');
|
||||
var bigText = alive===null ? '⏳' : (alive ? '🟢 ONLINE' : '🔴 KEINE ANTWORT');
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:2px solid '+bigColor+';border-radius:10px;padding:16px;margin-bottom:16px;text-align:center">'+
|
||||
'<div style="font-size:11px;color:var(--muted);margin-bottom:6px">🔭 Live-Beobachtung – jetzt Gerät vom Strom/Kabel trennen und beobachten</div>'+
|
||||
'<div style="font-size:26px;font-weight:800;color:'+bigColor+'">'+bigText+'</div>'+
|
||||
'<div style="font-size:12px;color:var(--muted);margin-top:4px">'+esc(statusText)+'</div>'+
|
||||
'<div style="margin-top:10px;line-height:1.8">'+dots+'</div>'+
|
||||
'<div style="font-size:10px;color:var(--muted);margin-top:6px">Jeder Punkt = 1 Prüfung (alle 1,5s) · grün = Antwort · rot = keine Antwort</div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ── Device identification: combines NetBIOS, reverse-DNS, WoL-probe, and ports ──
|
||||
async function identifyDevice(ip, mac) {
|
||||
var panel = document.getElementById('identifyPanel');
|
||||
if (!panel) return;
|
||||
panel.style.display = 'block';
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:1px solid var(--warn);border-radius:10px;padding:16px;margin-bottom:16px">'+
|
||||
'<div style="font-size:12px;color:var(--warn);display:flex;align-items:center;gap:8px"><div class="spinner"></div> Identifiziere Gerät – prüfe NetBIOS, Hostname und Wake-on-LAN…</div>'+
|
||||
'</div>';
|
||||
|
||||
var result;
|
||||
try {
|
||||
result = await api.identifyDevice(ip, mac);
|
||||
} catch(e) {
|
||||
panel.innerHTML = '<div style="background:var(--surface);border:1px solid var(--danger);border-radius:10px;padding:16px;margin-bottom:16px;color:var(--danger);font-size:12px">Identifikation fehlgeschlagen: '+esc(String(e.message||e))+'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If we resolved a fresher MAC than what was previously known (e.g. ARP
|
||||
// cache had expired), update the displayed MAC and the in-memory device record.
|
||||
if (result.mac && result.mac !== '–') {
|
||||
var macEl = document.getElementById('detailMac');
|
||||
if (macEl) macEl.textContent = result.mac;
|
||||
var d2 = devices.find(function(x) { return x.ip === ip; });
|
||||
if (d2) d2.mac = result.mac;
|
||||
}
|
||||
|
||||
var rows = [];
|
||||
rows.push({
|
||||
label: 'NetBIOS-Name',
|
||||
value: result.netbiosName || 'Nicht ermittelbar',
|
||||
hint: result.netbiosName ? 'Computername über NetBIOS gefunden — starker Hinweis auf einen Windows-PC oder ein NAS mit aktiviertem NetBIOS.' : 'Kein NetBIOS verfügbar (deaktiviert, kein Windows-Gerät, oder nicht im selben Subnetz erreichbar).',
|
||||
ok: !!result.netbiosName
|
||||
});
|
||||
rows.push({
|
||||
label: 'Reverse-DNS / Hostname',
|
||||
value: result.reverseDns || 'Nicht ermittelbar',
|
||||
hint: result.reverseDns ? 'Hostname über DNS-Auflösung gefunden.' : 'Router/DNS liefert keinen Namen für diese IP zurück.',
|
||||
ok: !!result.reverseDns
|
||||
});
|
||||
rows.push({
|
||||
label: 'Wake-on-LAN',
|
||||
value: result.wakeOnLan && result.wakeOnLan.sent ? 'Paket gesendet' : 'Nicht möglich',
|
||||
hint: result.wakeOnLan ? result.wakeOnLan.reason : '',
|
||||
ok: result.wakeOnLan && result.wakeOnLan.sent
|
||||
});
|
||||
|
||||
var portsHint = '';
|
||||
if (result.openPorts && result.openPorts.length) {
|
||||
portsHint = '<div style="margin-top:10px;font-size:12px;color:var(--text)"><strong>Offene Ports gefunden:</strong> ' +
|
||||
result.openPorts.map(function(p){return esc(p.name)+' ('+p.port+')';}).join(', ') + '</div>';
|
||||
} else {
|
||||
portsHint = '<div style="margin-top:10px;font-size:12px;color:var(--muted)">Keine bekannten Ports offen — typisch für einfache IoT-Geräte, Smart-Plugs oder Bridges ohne eigene Web-Oberfläche.</div>';
|
||||
}
|
||||
|
||||
var identity = result.identity || null;
|
||||
var identityBlock = '';
|
||||
if (identity) {
|
||||
var confidenceColor = identity.confidence === 'manuell' ? 'var(--accent2)' : (identity.confidence === 'hoch' ? 'var(--online)' : (identity.confidence === 'mittel' ? 'var(--warn)' : 'var(--danger)'));
|
||||
var ranking = (identity.ranking || []).map(function(r) {
|
||||
return '<div style="font-size:11px;color:var(--muted)">• '+esc(r.type)+' ('+r.score+' Punkte)</div>';
|
||||
}).join('');
|
||||
var reasons = (identity.reasons || []).map(function(x) {
|
||||
return '<div style="font-size:11px;color:var(--muted)">• '+esc(x)+'</div>';
|
||||
}).join('');
|
||||
identityBlock =
|
||||
'<div style="margin-top:10px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--surface2)">'+
|
||||
'<div style="font-size:12px"><strong>Wahrscheinlicher Gerätetyp:</strong> '+esc(identity.likelyType)+'</div>'+
|
||||
'<div style="font-size:11px;margin-top:3px;color:'+confidenceColor+'"><strong>Confidence:</strong> '+esc(identity.confidence)+'</div>'+
|
||||
(ranking ? '<div style="margin-top:6px"><div style="font-size:11px;color:var(--text);font-weight:600">Top-Kandidaten</div>'+ranking+'</div>' : '')+
|
||||
(reasons ? '<div style="margin-top:6px"><div style="font-size:11px;color:var(--text);font-weight:600">Signale</div>'+reasons+'</div>' : '')+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
panel.innerHTML =
|
||||
'<div style="background:var(--surface);border:1px solid var(--warn);border-radius:10px;padding:16px;margin-bottom:16px">'+
|
||||
'<div style="font-size:12px;font-weight:700;color:var(--warn);margin-bottom:10px">🔬 Identifikations-Ergebnis</div>'+
|
||||
rows.map(function(r) {
|
||||
return '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">'+
|
||||
'<span style="flex-shrink:0;margin-top:1px">'+(r.ok?'✅':'⚪')+'</span>'+
|
||||
'<div><div style="font-size:12px"><strong>'+esc(r.label)+':</strong> '+esc(r.value)+'</div>'+
|
||||
(r.hint?'<div style="font-size:11px;color:var(--muted);margin-top:1px">'+esc(r.hint)+'</div>':'')+
|
||||
'</div></div>';
|
||||
}).join('') +
|
||||
identityBlock +
|
||||
portsHint +
|
||||
'<div style="font-size:10px;color:var(--muted);margin-top:10px;border-top:1px solid var(--border);padding-top:8px">Tipp: Wake-on-LAN funktioniert nur, wenn das Gerät ausgeschaltet (nicht getrennt) ist und WoL in dessen Netzwerkeinstellungen aktiviert ist. Beobachte, ob es kurz danach wieder online geht.</div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
async function rescanPorts(ip) {
|
||||
var pa = document.getElementById('portsArea');
|
||||
if (!pa) return;
|
||||
@@ -1082,11 +1322,13 @@ async function saveSettings() {
|
||||
autoScan: parseInt(document.getElementById('settingAutoScan').value),
|
||||
notifyNew: document.getElementById('settingNotifyNew').checked,
|
||||
notifyOffline: document.getElementById('settingNotifyOffline').checked,
|
||||
failThreshold: parseInt(document.getElementById('settingFailThreshold').value),
|
||||
autoStart: document.getElementById('settingAutoStart').checked,
|
||||
closeToTray: document.getElementById('settingCloseToTray').checked
|
||||
};
|
||||
await api.saveSettings(settings);
|
||||
applyTheme(settings.theme);
|
||||
FAIL_THRESHOLD = settings.failThreshold;
|
||||
document.getElementById('autoscanBadge').classList.toggle('visible', settings.autoScan > 0);
|
||||
closeModal('settingsModal');
|
||||
showToast('Einstellungen gespeichert','','info');
|
||||
@@ -1138,7 +1380,13 @@ async function resetPorts() {
|
||||
function openDeviceManager() { renderDeviceList(); document.getElementById('deviceModal').classList.add('open'); }
|
||||
function renderDeviceList() {
|
||||
var list=document.getElementById('deviceModalList'); list.innerHTML='';
|
||||
var ips=Object.keys(deviceNames).sort(function(a,b){return a.split('.').map(Number).join('')-b.split('.').map(Number).join('');});
|
||||
var ips=Object.keys(deviceNames).sort(function(a,b){
|
||||
var aa=a.split('.').map(Number), bb=b.split('.').map(Number);
|
||||
for (var i=0;i<4;i++) {
|
||||
if ((aa[i]||0)!==(bb[i]||0)) return (aa[i]||0)-(bb[i]||0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
ips.forEach(function(ip) {
|
||||
var d=deviceNames[ip], row=document.createElement('div'); row.className='device-name-row';
|
||||
row.innerHTML='<span class="dn-ip">'+ip+'</span>'+
|
||||
@@ -1308,24 +1556,96 @@ async function exportDeviceList(format) {
|
||||
if (result.success) showToast('Export erfolgreich', 'Geräteliste als '+format.toUpperCase()+' gespeichert', 'info');
|
||||
}
|
||||
|
||||
var DEVICE_TYPE_OPTIONS = [
|
||||
'Windows-PC',
|
||||
'Linux-Server',
|
||||
'NAS/Storage',
|
||||
'Kamera/CCTV',
|
||||
'Alarmanlage/Security',
|
||||
'Router/Access Point',
|
||||
'Drucker',
|
||||
'IoT/Smart-Device',
|
||||
'Virtualisierungs-Host',
|
||||
'Konsole/Mediengeraet'
|
||||
];
|
||||
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
function getForcedDeviceType(ip) {
|
||||
var p = deviceProfiles[ip];
|
||||
if (!p || !p.forcedType) return 'auto';
|
||||
return p.forcedType;
|
||||
}
|
||||
|
||||
function buildDeviceTypeOptions(currentType) {
|
||||
var html = '<option value="auto"'+(currentType==='auto'?' selected':'')+'>Auto</option>';
|
||||
DEVICE_TYPE_OPTIONS.forEach(function(t) {
|
||||
html += '<option value="'+esc(t)+'"'+(currentType===t?' selected':'')+'>'+esc(t)+'</option>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function setDeviceClass(ip, selectedType) {
|
||||
if (!ip) return;
|
||||
if (selectedType === 'auto') {
|
||||
delete deviceProfiles[ip];
|
||||
} else {
|
||||
deviceProfiles[ip] = deviceProfiles[ip] || {};
|
||||
deviceProfiles[ip].forcedType = selectedType;
|
||||
}
|
||||
await api.saveDeviceProfiles(deviceProfiles);
|
||||
|
||||
var activeItem = document.querySelector('.device-item.active');
|
||||
if (activeItem) {
|
||||
var d = devices.find(function(x){return x.ip===ip;});
|
||||
if (d) selectDevice(d, activeItem);
|
||||
}
|
||||
showToast('Geräteklasse gespeichert', selectedType==='auto' ? 'Auto-Erkennung aktiv' : selectedType+' manuell gesetzt', 'info');
|
||||
}
|
||||
|
||||
|
||||
function esc(s){
|
||||
return String(s||'')
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"')
|
||||
.replace(/'/g,''');
|
||||
}
|
||||
function getCustomName(ip){return deviceNames[ip]?deviceNames[ip].name:null;}
|
||||
function getDeviceIcon(vendor) {
|
||||
if (!vendor) return '🖥️';
|
||||
var v=vendor.toLowerCase();
|
||||
if(v.includes('apple')) return '🍎';
|
||||
if(v.includes('synology')) return '🗄️';
|
||||
if(v.includes('raspberry')) return '🍓';
|
||||
if(v.includes('fritz')||v.includes('avm')) return '📡';
|
||||
if(v.includes('tp-link')) return '📡';
|
||||
if(v.includes('ubiquiti')) return '📶';
|
||||
if(v.includes('vmware')||v.includes('qemu')||v.includes('proxmox')) return '💻';
|
||||
if(v.includes('cisco')) return '🔀';
|
||||
if(v.includes('samsung')) return '📺';
|
||||
if(v.includes('nintendo')) return '🎮';
|
||||
if(v.includes('brother')) return '🖨️';
|
||||
if(v.includes('abus')) return '🔒';
|
||||
function getDeviceIcon(deviceOrVendor) {
|
||||
var vendor = '';
|
||||
var nameHints = '';
|
||||
var forcedType = null;
|
||||
|
||||
if (typeof deviceOrVendor === 'string') {
|
||||
vendor = deviceOrVendor.toLowerCase();
|
||||
} else if (deviceOrVendor && typeof deviceOrVendor === 'object') {
|
||||
vendor = String(deviceOrVendor.vendor || '').toLowerCase();
|
||||
nameHints = String(deviceOrVendor.hostname || deviceOrVendor.displayName || '').toLowerCase();
|
||||
if (deviceOrVendor.ip) forcedType = getForcedDeviceType(deviceOrVendor.ip);
|
||||
}
|
||||
|
||||
if (forcedType && forcedType !== 'auto') {
|
||||
if (forcedType === 'NAS/Storage') return '🗄️';
|
||||
if (forcedType === 'Kamera/CCTV') return '📷';
|
||||
if (forcedType === 'Alarmanlage/Security') return '🚨';
|
||||
if (forcedType === 'Drucker') return '🖨️';
|
||||
if (forcedType === 'Router/Access Point') return '📡';
|
||||
if (forcedType === 'Virtualisierungs-Host') return '💻';
|
||||
}
|
||||
|
||||
if (vendor.includes('synology') || vendor.includes('qnap') || vendor.includes('asustor') || nameHints.includes('nas') || nameHints.includes('diskstation')) return '🗄️';
|
||||
if (vendor.includes('hikvision') || vendor.includes('dahua') || vendor.includes('reolink') || vendor.includes('axis') || nameHints.includes('cam') || nameHints.includes('cctv') || nameHints.includes('nvr')) return '📷';
|
||||
if (vendor.includes('abus') || vendor.includes('ajax') || vendor.includes('honeywell') || nameHints.includes('alarm') || nameHints.includes('security')) return '🚨';
|
||||
if (vendor.includes('brother') || vendor.includes('hewlett') || vendor.includes('hp')) return '🖨️';
|
||||
if (vendor.includes('apple')) return '🍎';
|
||||
if (vendor.includes('raspberry')) return '🍓';
|
||||
if (vendor.includes('fritz') || vendor.includes('avm')) return '📡';
|
||||
if (vendor.includes('tp-link')) return '📡';
|
||||
if (vendor.includes('ubiquiti')) return '📶';
|
||||
if (vendor.includes('vmware') || vendor.includes('qemu') || vendor.includes('proxmox')) return '💻';
|
||||
if (vendor.includes('cisco')) return '🔀';
|
||||
if (vendor.includes('samsung')) return '📺';
|
||||
if (vendor.includes('nintendo')) return '🎮';
|
||||
return '🖥️';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user