Geräte-Tab: Suche, Detail-Panel und Aktionen

Neuer Haupttab "Geräte" mit Geräteliste (filterbar nach OS/Compliance),
Detail-Panel mit vollständigen Gerätedaten und Aktions-Buttons:
Sync, Neustart, Sperren, Diagnose, BitLocker-Rotation, Retire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 14:26:46 +02:00
co-authored by Claude Sonnet 4.6
parent ec130031cb
commit 78dc8d5c01
4 changed files with 566 additions and 1 deletions
+240 -1
View File
@@ -4390,6 +4390,7 @@ function switchMainView(view) {
const layout = document.getElementById('layout');
const groupView = document.getElementById('groupMgmtView');
const reportView = document.getElementById('appReportView');
const devView = document.getElementById('devView');
document.querySelectorAll('#topbarNav .nav-tab').forEach(t => {
t.classList.toggle('active', t.dataset.view === view);
t.setAttribute('aria-selected', t.dataset.view === view ? 'true' : 'false');
@@ -4397,18 +4398,20 @@ function switchMainView(view) {
layout .classList.toggle('hidden', view !== 'apps');
groupView .classList.toggle('hidden', view !== 'groups');
reportView.classList.toggle('hidden', view !== 'report');
devView .classList.toggle('hidden', view !== 'devices');
}
document.querySelectorAll('#topbarNav .nav-tab').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
if ((view === 'groups' || view === 'report') && !State.connected) {
if ((view === 'groups' || view === 'report' || view === 'devices') && !State.connected) {
toast('Bitte zuerst verbinden.', 'warn');
return;
}
switchMainView(view);
if (view === 'groups') openGroupExportModal();
if (view === 'report' && ReportState.items.length === 0) loadAppReport();
if (view === 'devices') { devLoadAll(); setTimeout(() => document.getElementById('devSearch').focus(), 80); }
});
});
@@ -5222,3 +5225,239 @@ function notifyCsvImportGroupSelected(groupId, groupName) {
if (CsvImport.resolved.length > 0) renderCsvPreview();
else updateCsvImportNote();
}
// ============================================================
// Geräte-Tab
// ============================================================
const DevState = {
items: [],
filtered: [],
selected: null,
sortCol: 'deviceName',
sortAsc: true,
loaded: false
};
async function devLoadAll() {
if (DevState.loaded) return;
devShowLoading(true);
try {
const data = await api('/api/devices');
DevState.items = data.items || [];
DevState.loaded = true;
devApplyFilters();
} catch (e) {
toast('Geräte konnten nicht geladen werden: ' + e, 'error');
} finally {
devShowLoading(false);
}
}
function devShowLoading(on) {
document.getElementById('devLoading').classList.toggle('hidden', !on);
document.getElementById('devEmpty').classList.add('hidden');
}
function devApplyFilters() {
const q = document.getElementById('devSearch').value.toLowerCase();
const os = document.getElementById('devOsFilter').value;
const cmp = document.getElementById('devComplianceFilter').value;
DevState.filtered = DevState.items.filter(d => {
if (os && d.OS !== os) return false;
if (cmp && d.ComplianceState !== cmp) return false;
if (q) {
const hay = `${d.DeviceName} ${d.UserDisplayName} ${d.UserPrincipalName} ${d.SerialNumber}`.toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
devSort();
}
function devSort() {
const col = DevState.sortCol;
const asc = DevState.sortAsc;
DevState.filtered.sort((a, b) => {
const av = (a[col] || '').toString().toLowerCase();
const bv = (b[col] || '').toString().toLowerCase();
return asc ? av.localeCompare(bv) : bv.localeCompare(av);
});
devRender();
}
function devRender() {
const tbody = document.getElementById('devTableBody');
const empty = document.getElementById('devEmpty');
document.getElementById('devCount').textContent = DevState.filtered.length;
if (DevState.filtered.length === 0) {
tbody.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
// Sort-Icons
document.querySelectorAll('#devTable th.sortable').forEach(th => {
const ico = th.querySelector('.sort-ico');
if (th.dataset.col === DevState.sortCol) {
ico.textContent = DevState.sortAsc ? ' ▲' : ' ▼';
} else {
ico.textContent = '';
}
});
let html = '';
for (const d of DevState.filtered) {
const sync = d.LastSync ? new Date(d.LastSync).toLocaleDateString('de-DE') : '—';
const cmpCls = d.ComplianceState === 'compliant' ? 'dev-badge-ok'
: d.ComplianceState === 'noncompliant' ? 'dev-badge-err' : 'dev-badge-unk';
const sel = DevState.selected && DevState.selected.Id === d.Id ? ' class="dev-row-selected"' : '';
html += `<tr data-id="${escapeHtml(d.Id)}"${sel}>
<td>${escapeHtml(d.DeviceName || '—')}</td>
<td>${escapeHtml(d.UserDisplayName || '—')}</td>
<td>${escapeHtml(d.OS || '—')}</td>
<td><span class="dev-badge ${cmpCls}">${escapeHtml(d.ComplianceState || '—')}</span></td>
<td>${sync}</td>
</tr>`;
}
tbody.innerHTML = html;
tbody.querySelectorAll('tr').forEach(tr => {
tr.addEventListener('click', () => {
const id = tr.dataset.id;
const device = DevState.filtered.find(d => d.Id === id);
if (device) devSelectDevice(device);
});
});
}
async function devSelectDevice(device) {
DevState.selected = device;
// Auswahl visuell
document.querySelectorAll('#devTableBody tr').forEach(tr =>
tr.classList.toggle('dev-row-selected', tr.dataset.id === device.Id));
// Detail-Panel anzeigen mit Basis-Infos sofort
devRenderDetail(device);
// Vollständige Details nachladen
try {
const full = await api(`/api/devices/${encodeURIComponent(device.Id)}`);
devRenderDetail(full);
} catch (e) {
// Basis-Infos bleiben
}
}
function devRenderDetail(d) {
const panel = document.getElementById('devDetail');
panel.classList.remove('hidden');
document.getElementById('devDetailName').textContent = d.DeviceName || '—';
document.getElementById('devDetailSub').textContent = `${d.Manufacturer || ''} ${d.Model || ''}`.trim() || '—';
const sync = d.LastSync ? new Date(d.LastSync).toLocaleString('de-DE') : '—';
const enrolled = d.EnrolledDateTime ? new Date(d.EnrolledDateTime).toLocaleDateString('de-DE') : '—';
const storage = (d.TotalStorageGB != null)
? `${d.FreeStorageGB} GB frei / ${d.TotalStorageGB} GB gesamt` : null;
const rows = [
['Benutzer', d.UserDisplayName],
['UPN', d.UserPrincipalName],
['OS', d.OS ? `${d.OS} ${d.OSVersion || ''}`.trim() : null],
['Compliance', d.ComplianceState],
['Seriennummer', d.SerialNumber],
['IMEI', d.Imei],
['Wi-Fi MAC', d.WiFiMac],
['Speicher', storage],
['Eingeschrieben', enrolled],
['Letzter Sync', sync],
['Join-Typ', d.JoinType],
['Besitzertyp', d.OwnerType],
['Azure AD ID', d.AzureADDeviceId],
];
let html = '';
for (const [label, val] of rows) {
if (!val) continue;
html += `<dt>${escapeHtml(label)}</dt><dd>${escapeHtml(String(val))}</dd>`;
}
document.getElementById('devInfoGrid').innerHTML = html;
// Aktionen: BitLocker nur für Windows
const isWin = (d.OS || '').toLowerCase() === 'windows';
const bitlockerBtn = document.getElementById('devBtnBitlocker');
if (bitlockerBtn) bitlockerBtn.style.display = isWin ? '' : 'none';
}
// Aktion ausführen
document.getElementById('devActions')?.addEventListener('click', async (e) => {
const btn = e.target.closest('.dev-action-btn');
if (!btn || !DevState.selected) return;
const action = btn.dataset.action;
if (!action) return;
const labels = {
syncDevice: 'Sync',
rebootNow: 'Neustart',
remoteLock: 'Sperren',
collectDiagnostics: 'Diagnose sammeln',
rotateBitLockerKeys: 'BitLocker-Schlüssel rotieren',
retire: 'Retire (MDM entfernen)'
};
const isDangerous = action === 'retire';
const devName = DevState.selected.DeviceName || DevState.selected.Id;
const msg = isDangerous
? `ACHTUNG: "${labels[action]}" auf "${devName}" ausführen?\n\nDas MDM-Profil wird entfernt — Gerät wird nicht mehr verwaltet.`
: `"${labels[action]}" auf "${devName}" ausführen?`;
if (!confirm(msg)) return;
btn.disabled = true;
const origText = btn.textContent.trim();
btn.textContent = '…';
try {
await api(`/api/devices/${encodeURIComponent(DevState.selected.Id)}/action`, {
method: 'POST',
body: JSON.stringify({ action })
});
toast(`${labels[action]}: Aktion wurde gesendet`, 'ok');
if (action === 'retire') {
// Gerät aus Liste entfernen
DevState.items = DevState.items.filter(d => d.Id !== DevState.selected.Id);
DevState.selected = null;
document.getElementById('devDetail').classList.add('hidden');
devApplyFilters();
}
} catch (e) {
toast(`Fehler bei ${labels[action]}: ${e}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = origText;
}
});
// Such- und Filter-Events
let devSearchDebounce = null;
document.getElementById('devSearch').addEventListener('input', () => {
clearTimeout(devSearchDebounce);
devSearchDebounce = setTimeout(devApplyFilters, 250);
});
document.getElementById('devOsFilter').addEventListener('change', devApplyFilters);
document.getElementById('devComplianceFilter').addEventListener('change', devApplyFilters);
// Sortierung
document.querySelectorAll('#devTable th.sortable').forEach(th => {
th.addEventListener('click', () => {
if (DevState.sortCol === th.dataset.col) {
DevState.sortAsc = !DevState.sortAsc;
} else {
DevState.sortCol = th.dataset.col;
DevState.sortAsc = true;
}
devSort();
});
});