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:
+113
@@ -70,6 +70,18 @@ function Invoke-ApiHandler {
|
||||
return Get-UserMemberOfEndpoint -UserId $matches[1]
|
||||
}
|
||||
|
||||
if ($Method -eq "GET" -and $Path -eq "/api/devices") {
|
||||
return Search-DevicesEndpoint -Query $Query
|
||||
}
|
||||
|
||||
if ($Method -eq "GET" -and $Path -match "^/api/devices/([^/]+)$") {
|
||||
return Get-DeviceEndpoint -DeviceId $matches[1]
|
||||
}
|
||||
|
||||
if ($Method -eq "POST" -and $Path -match "^/api/devices/([^/]+)/action$") {
|
||||
return Invoke-DeviceActionEndpoint -DeviceId $matches[1] -Body $Body
|
||||
}
|
||||
|
||||
if ($Method -eq "GET" -and $Path -match "^/api/apps/([^/]+)/details$") {
|
||||
return Get-AppDetailsEndpoint -AppId $matches[1]
|
||||
}
|
||||
@@ -1520,6 +1532,107 @@ function Get-UserMemberOfEndpoint {
|
||||
return @{ groups = $groups; count = $groups.Count }
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Devices
|
||||
# ============================================================
|
||||
|
||||
function Search-DevicesEndpoint {
|
||||
param($Query)
|
||||
$err = Test-Connected
|
||||
if ($err) { return $err }
|
||||
|
||||
$q = [string]$Query.q
|
||||
$os = [string]$Query.os
|
||||
$compliance = [string]$Query.compliance
|
||||
$top = 100
|
||||
|
||||
$filters = @()
|
||||
if ($q -and $q.Length -ge 2) {
|
||||
$qEnc = [uri]::EscapeDataString($q)
|
||||
$filters += "(startswith(deviceName,'$q') or startswith(userDisplayName,'$q') or startswith(serialNumber,'$q'))"
|
||||
}
|
||||
if ($os) { $filters += "operatingSystem eq '$os'" }
|
||||
if ($compliance) { $filters += "complianceState eq '$compliance'" }
|
||||
|
||||
$filterStr = if ($filters.Count -gt 0) { '$filter=' + [uri]::EscapeDataString(($filters -join ' and ')) + '&' } else { '' }
|
||||
$select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,managementState,serialNumber,model,manufacturer,enrolledDateTime'
|
||||
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?${filterStr}`$select=$select&`$top=$top&`$orderby=deviceName"
|
||||
|
||||
$items = @()
|
||||
$resp = Invoke-MgGraphRequestRetry -Uri $uri -Method GET
|
||||
foreach ($d in $resp.value) {
|
||||
$items += [pscustomobject]@{
|
||||
Id = $d.id
|
||||
DeviceName = $d.deviceName
|
||||
UserDisplayName = $d.userDisplayName
|
||||
UserPrincipalName= $d.userPrincipalName
|
||||
OS = $d.operatingSystem
|
||||
OSVersion = $d.osVersion
|
||||
ComplianceState = $d.complianceState
|
||||
LastSync = $d.lastSyncDateTime
|
||||
ManagementState = $d.managementState
|
||||
SerialNumber = $d.serialNumber
|
||||
Model = $d.model
|
||||
Manufacturer = $d.manufacturer
|
||||
EnrolledDateTime = $d.enrolledDateTime
|
||||
}
|
||||
}
|
||||
return @{ items = $items; count = $items.Count }
|
||||
}
|
||||
|
||||
function Get-DeviceEndpoint {
|
||||
param($DeviceId)
|
||||
$err = Test-Connected
|
||||
if ($err) { return $err }
|
||||
|
||||
$select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,managementState,serialNumber,model,manufacturer,enrolledDateTime,imei,wiFiMacAddress,azureADDeviceId,joinType,deviceEnrollmentType,managedDeviceOwnerType,totalStorageSpaceInBytes,freeStorageSpaceInBytes'
|
||||
$d = Invoke-MgGraphRequestRetry -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId`?`$select=$select" -Method GET
|
||||
|
||||
$totalGB = if ($d.totalStorageSpaceInBytes) { [math]::Round($d.totalStorageSpaceInBytes / 1GB, 1) } else { $null }
|
||||
$freeGB = if ($d.freeStorageSpaceInBytes) { [math]::Round($d.freeStorageSpaceInBytes / 1GB, 1) } else { $null }
|
||||
|
||||
return @{
|
||||
Id = $d.id
|
||||
DeviceName = $d.deviceName
|
||||
UserDisplayName = $d.userDisplayName
|
||||
UserPrincipalName= $d.userPrincipalName
|
||||
OS = $d.operatingSystem
|
||||
OSVersion = $d.osVersion
|
||||
ComplianceState = $d.complianceState
|
||||
LastSync = $d.lastSyncDateTime
|
||||
ManagementState = $d.managementState
|
||||
SerialNumber = $d.serialNumber
|
||||
Model = $d.model
|
||||
Manufacturer = $d.manufacturer
|
||||
EnrolledDateTime = $d.enrolledDateTime
|
||||
Imei = $d.imei
|
||||
WiFiMac = $d.wiFiMacAddress
|
||||
AzureADDeviceId = $d.azureADDeviceId
|
||||
JoinType = $d.joinType
|
||||
EnrollmentType = $d.deviceEnrollmentType
|
||||
OwnerType = $d.managedDeviceOwnerType
|
||||
TotalStorageGB = $totalGB
|
||||
FreeStorageGB = $freeGB
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DeviceActionEndpoint {
|
||||
param($DeviceId, $Body)
|
||||
$err = Test-Connected
|
||||
if ($err) { return $err }
|
||||
|
||||
$action = [string]$Body.action
|
||||
$allowed = @('syncDevice','rebootNow','remoteLock','collectDiagnostics','rotateBitLockerKeys','retire')
|
||||
if ($action -notin $allowed) {
|
||||
return @{ statusCode = 400; error = "Unbekannte Aktion: $action" }
|
||||
}
|
||||
|
||||
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId/$action"
|
||||
$bodyJson = '{}'
|
||||
Invoke-MgGraphRequestRetry -Uri $uri -Method POST -Body $bodyJson -ContentType 'application/json' | Out-Null
|
||||
return @{ success = $true; action = $action }
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Apps
|
||||
# ============================================================
|
||||
|
||||
+240
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user