Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-06-18 20:56:23 +02:00
parent c1b9bc7848
commit 1b5719d716
4 changed files with 704 additions and 41 deletions

View File

@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
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,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
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 '🖥️';
}