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();
});
});
+87
View File
@@ -35,6 +35,7 @@
<button class="nav-tab active" data-view="apps" aria-selected="true">App Management</button>
<button class="nav-tab" data-view="groups" aria-selected="false">Group Management</button>
<button class="nav-tab" data-view="report" aria-selected="false">App Report</button>
<button class="nav-tab" data-view="devices" aria-selected="false">Geräte</button>
</nav>
<div class="header-actions">
@@ -463,6 +464,92 @@
</div>
</section>
<!-- GERÄTE VIEW -->
<section class="dev-view hidden" id="devView">
<div class="dev-toolbar">
<div class="search-wrap">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="devSearch" class="input input-search" placeholder="Gerätename, Benutzer, Seriennummer…" autocomplete="off">
</div>
<span id="devCount" class="count-badge">0</span>
<select id="devOsFilter" class="input input-select btn-sm">
<option value="">Alle OS</option>
<option value="Windows">Windows</option>
<option value="macOS">macOS</option>
<option value="iOS">iOS</option>
<option value="Android">Android</option>
</select>
<select id="devComplianceFilter" class="input input-select btn-sm">
<option value="">Alle Compliance</option>
<option value="compliant">Compliant</option>
<option value="noncompliant">Non-Compliant</option>
<option value="unknown">Unbekannt</option>
</select>
</div>
<div class="dev-layout">
<!-- Geräteliste -->
<div class="dev-list-wrap">
<table class="dev-table" id="devTable">
<thead>
<tr>
<th class="sortable" data-col="deviceName">Gerät <span class="sort-ico"></span></th>
<th class="sortable" data-col="userDisplayName">Benutzer <span class="sort-ico"></span></th>
<th class="sortable" data-col="operatingSystem">OS <span class="sort-ico"></span></th>
<th class="sortable" data-col="complianceState">Compliance <span class="sort-ico"></span></th>
<th class="sortable" data-col="lastSyncDateTime">Letzter Sync <span class="sort-ico"></span></th>
</tr>
</thead>
<tbody id="devTableBody"></tbody>
</table>
<div id="devEmpty" class="dev-empty hidden">Keine Geräte gefunden.</div>
<div id="devLoading" class="dev-loading hidden">
<span class="spinner"></span> Lade Geräte…
</div>
</div>
<!-- Detail-Panel -->
<div class="dev-detail hidden" id="devDetail">
<div class="dev-detail-header">
<div class="dev-detail-name" id="devDetailName">—</div>
<div class="dev-detail-sub" id="devDetailSub">—</div>
</div>
<div class="dev-detail-body">
<dl class="dev-info-grid" id="devInfoGrid"></dl>
</div>
<div class="dev-actions" id="devActions">
<div class="dev-actions-title">Aktionen</div>
<div class="dev-action-btns">
<button class="btn btn-secondary btn-sm dev-action-btn" data-action="syncDevice" title="Richtlinien & Apps jetzt abrufen">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
Sync
</button>
<button class="btn btn-secondary btn-sm dev-action-btn" data-action="rebootNow" title="Gerät neu starten">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
Neustart
</button>
<button class="btn btn-secondary btn-sm dev-action-btn" data-action="remoteLock" title="Gerät sperren">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Sperren
</button>
<button class="btn btn-secondary btn-sm dev-action-btn" data-action="collectDiagnostics" title="Diagnose-Daten sammeln">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
Diagnose
</button>
<button class="btn btn-secondary btn-sm dev-action-btn" data-action="rotateBitLockerKeys" title="BitLocker-Schlüssel rotieren" id="devBtnBitlocker">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/><circle cx="12" cy="16" r="1"/></svg>
BitLocker
</button>
<button class="btn btn-danger btn-sm dev-action-btn" data-action="retire" title="MDM-Profil entfernen (Retire)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
Retire
</button>
</div>
</div>
</div>
</div>
</section>
<!-- TOAST CONTAINER -->
<div id="toasts" class="toasts"></div>
+126
View File
@@ -5195,3 +5195,129 @@ body.read-only .app-row {
.csv-row-done { background: rgba(74,222,128,.05); }
.csv-row-warn { background: rgba(250,204,21,.06); opacity: 0.85; }
/* ============================================================
Geräte-View
============================================================ */
.dev-view {
display: flex;
flex-direction: column;
height: calc(100vh - 52px);
overflow: hidden;
}
.dev-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--hairline-soft);
flex-shrink: 0;
}
.dev-toolbar .search-wrap { flex: 1; max-width: 380px; }
.input-select {
padding: 5px 8px;
border: 1px solid var(--hairline);
border-radius: 6px;
background: var(--bg2);
color: var(--text);
font-size: 12.5px;
cursor: pointer;
}
.dev-layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* Tabelle */
.dev-list-wrap {
flex: 1;
overflow-y: auto;
border-right: 1px solid var(--hairline-soft);
}
.dev-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.dev-table thead th {
position: sticky;
top: 0;
background: var(--bg2);
padding: 8px 12px;
text-align: left;
font-weight: 600;
font-size: 12px;
color: var(--muted);
border-bottom: 1px solid var(--hairline-soft);
white-space: nowrap;
user-select: none;
}
.dev-table thead th.sortable { cursor: pointer; }
.dev-table thead th.sortable:hover { color: var(--text); }
.dev-table tbody tr {
cursor: pointer;
border-bottom: 1px solid var(--hairline-soft);
transition: background 0.1s;
}
.dev-table tbody tr:hover { background: var(--bg2); }
.dev-table tbody tr.dev-row-selected { background: var(--accent-faint, rgba(59,130,246,.12)); }
.dev-table tbody td {
padding: 7px 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 220px;
}
.dev-empty, .dev-loading {
padding: 32px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
.dev-loading { display: flex; align-items: center; justify-content: center; gap: 10px; }
.dev-badge {
display: inline-block;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 10px;
}
.dev-badge-ok { background: rgba(74,222,128,.15); color: #4ade80; }
.dev-badge-err { background: rgba(248,113,113,.15); color: #f87171; }
.dev-badge-unk { background: var(--hairline-soft); color: var(--muted); }
/* Detail-Panel */
.dev-detail {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
background: var(--bg2);
}
.dev-detail-header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--hairline-soft);
}
.dev-detail-name { font-weight: 700; font-size: 15px; }
.dev-detail-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
.dev-detail-body { padding: 12px 16px; flex: 1; }
.dev-info-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
font-size: 12.5px;
}
.dev-info-grid dt { color: var(--muted); white-space: nowrap; }
.dev-info-grid dd { margin: 0; word-break: break-all; }
.dev-actions { padding: 12px 16px; border-top: 1px solid var(--hairline-soft); }
.dev-actions-title { font-size: 11px; font-weight: 600; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: .04em; }
.dev-action-btns { display: flex; flex-wrap: wrap; gap: 6px; }
.dev-action-btn { display: inline-flex; align-items: center; gap: 5px; }