Geräte-Tab: Verwaltungsart-Spalte und Autopilot-Zeitstempel

- Neue Spalte "Verwaltung" (Intune vs. Co-Managed) in der Geräteliste,
  abgeleitet aus managementAgent; farbige Badges, sortierbar, im CSV-Export
  und im Detail-Panel.
- Detail-Panel zeigt Autopilot-Daten (Registriert, Profil zugewiesen am,
  letzter Kontakt) per Lookup über die Seriennummer. Ein echtes Importdatum
  liefert Graph nicht; verfügbar sind nur diese Zeitstempel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 07:49:14 +02:00
co-authored by Claude Opus 4.8
parent e4efb0ac32
commit dfc1128f31
4 changed files with 64 additions and 5 deletions
+46 -3
View File
@@ -1710,7 +1710,7 @@ function Get-GroupDevicesExportEndpoint {
return @{ ok = $true; groupId = $GroupId; groupName = $groupName; userCount = 0; items = @(); count = 0; resolvedMs = [int]$sw.ElapsedMilliseconds } return @{ ok = $true; groupId = $GroupId; groupName = $groupName; userCount = 0; items = @(); count = 0; resolvedMs = [int]$sw.ElapsedMilliseconds }
} }
$select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,serialNumber,model,manufacturer,lastSyncDateTime,enrolledDateTime' $select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,serialNumber,model,manufacturer,lastSyncDateTime,enrolledDateTime,managementAgent'
$batchSize = 20 $batchSize = 20
$devSeen = @{} $devSeen = @{}
$items = [System.Collections.Generic.List[object]]::new() $items = [System.Collections.Generic.List[object]]::new()
@@ -1757,6 +1757,8 @@ function Get-GroupDevicesExportEndpoint {
OS = [string]$d.operatingSystem OS = [string]$d.operatingSystem
OSVersion = [string]$d.osVersion OSVersion = [string]$d.osVersion
ComplianceState = [string]$d.complianceState ComplianceState = [string]$d.complianceState
ManagementAgent = [string]$d.managementAgent
ManagementType = Get-ManagementType ([string]$d.managementAgent)
SerialNumber = [string]$d.serialNumber SerialNumber = [string]$d.serialNumber
Model = [string]$d.model Model = [string]$d.model
Manufacturer = [string]$d.manufacturer Manufacturer = [string]$d.manufacturer
@@ -1840,6 +1842,15 @@ function Get-UserMemberOfEndpoint {
# Devices # Devices
# ============================================================ # ============================================================
# Verwaltungsart aus dem Graph-Feld 'managementAgent' ableiten.
# 'configurationManagerClientMdm' / 'configurationManagerClientMdmEas' -> Co-Managed
# (ConfigMgr + Intune), alles andere -> reines Intune (MDM).
function Get-ManagementType {
param([string]$Agent)
if ($Agent -match 'configurationManager') { return 'Co-Managed' }
return 'Intune'
}
function Search-DevicesEndpoint { function Search-DevicesEndpoint {
param($Query) param($Query)
$err = Test-Connected $err = Test-Connected
@@ -1852,7 +1863,7 @@ function Search-DevicesEndpoint {
# HINWEIS: 'managementState' ist KEIN gueltiges $select-Feld auf managedDevices # HINWEIS: 'managementState' ist KEIN gueltiges $select-Feld auf managedDevices
# -> fuehrt zu 400 BadRequest. Bewusst weggelassen. # -> fuehrt zu 400 BadRequest. Bewusst weggelassen.
$select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,serialNumber,model,manufacturer,enrolledDateTime' $select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,serialNumber,model,manufacturer,enrolledDateTime,managementAgent'
# Ohne Suchbegriff: alle Geräte (erste Seite) # Ohne Suchbegriff: alle Geräte (erste Seite)
# Mit Suchbegriff: Graph unterstuetzt startswith nur auf deviceName/userDisplayName/userPrincipalName. # Mit Suchbegriff: Graph unterstuetzt startswith nur auf deviceName/userDisplayName/userPrincipalName.
@@ -1924,6 +1935,8 @@ function Search-DevicesEndpoint {
ComplianceState = $d.complianceState ComplianceState = $d.complianceState
LastSync = ConvertTo-IsoDate $d.lastSyncDateTime LastSync = ConvertTo-IsoDate $d.lastSyncDateTime
ManagementState = $d.managementState ManagementState = $d.managementState
ManagementAgent = [string]$d.managementAgent
ManagementType = Get-ManagementType ([string]$d.managementAgent)
SerialNumber = $d.serialNumber SerialNumber = $d.serialNumber
Model = $d.model Model = $d.model
Manufacturer = $d.manufacturer Manufacturer = $d.manufacturer
@@ -1939,12 +1952,37 @@ function Get-DeviceEndpoint {
if ($err) { return $err } if ($err) { return $err }
# 'managementState' entfernt: kein gueltiges $select-Feld auf managedDevices (400). # 'managementState' entfernt: kein gueltiges $select-Feld auf managedDevices (400).
$select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,serialNumber,model,manufacturer,enrolledDateTime,imei,wiFiMacAddress,azureADDeviceId,joinType,deviceEnrollmentType,managedDeviceOwnerType,totalStorageSpaceInBytes,freeStorageSpaceInBytes' $select = 'id,deviceName,userDisplayName,userPrincipalName,operatingSystem,osVersion,complianceState,lastSyncDateTime,serialNumber,model,manufacturer,enrolledDateTime,imei,wiFiMacAddress,azureADDeviceId,joinType,deviceEnrollmentType,managedDeviceOwnerType,managementAgent,totalStorageSpaceInBytes,freeStorageSpaceInBytes'
$d = Invoke-MgGraphRequestRetry -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$DeviceId`?`$select=$select" -Method GET $d = Invoke-MgGraphRequestRetry -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$DeviceId`?`$select=$select" -Method GET
$totalGB = if ($d.totalStorageSpaceInBytes) { [math]::Round($d.totalStorageSpaceInBytes / 1GB, 1) } else { $null } $totalGB = if ($d.totalStorageSpaceInBytes) { [math]::Round($d.totalStorageSpaceInBytes / 1GB, 1) } else { $null }
$freeGB = if ($d.freeStorageSpaceInBytes) { [math]::Round($d.freeStorageSpaceInBytes / 1GB, 1) } else { $null } $freeGB = if ($d.freeStorageSpaceInBytes) { [math]::Round($d.freeStorageSpaceInBytes / 1GB, 1) } else { $null }
# Autopilot-Identity per Seriennummer nachladen (nur Windows, fehlertolerant).
# HINWEIS: Ein echtes "Import-/Registrierungsdatum" liefert Graph nicht; verfuegbar
# sind nur Profil-Zuweisung (deploymentProfileAssignedDateTime) und letzter Kontakt.
$inAutopilot = $false
$apProfileAssign = $null
$apLastContact = $null
$sn = [string]$d.serialNumber
if ($sn -and ([string]$d.operatingSystem).ToLower() -eq 'windows') {
try {
$snEsc = $sn -replace "'", "''"
$apFilt = [Uri]::EscapeDataString("contains(serialNumber,'$snEsc')")
$apSel = 'id,serialNumber,deploymentProfileAssignedDateTime,lastContactedDateTime,enrollmentState'
$apUri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$filter=$apFilt&`$select=$apSel&`$top=1"
$apResp = Invoke-MgGraphRequestRetry -Uri $apUri -Method GET
$autop = @($apResp.value)[0]
if ($autop) {
$inAutopilot = $true
$apProfileAssign = ConvertTo-IsoDate $autop.deploymentProfileAssignedDateTime
$apLastContact = ConvertTo-IsoDate $autop.lastContactedDateTime
}
} catch {
Write-Host " [DEVICE] Autopilot-Lookup (SN=$sn): $($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
return @{ return @{
Id = $d.id Id = $d.id
DeviceName = $d.deviceName DeviceName = $d.deviceName
@@ -1955,6 +1993,8 @@ function Get-DeviceEndpoint {
ComplianceState = $d.complianceState ComplianceState = $d.complianceState
LastSync = ConvertTo-IsoDate $d.lastSyncDateTime LastSync = ConvertTo-IsoDate $d.lastSyncDateTime
ManagementState = $d.managementState ManagementState = $d.managementState
ManagementAgent = [string]$d.managementAgent
ManagementType = Get-ManagementType ([string]$d.managementAgent)
SerialNumber = $d.serialNumber SerialNumber = $d.serialNumber
Model = $d.model Model = $d.model
Manufacturer = $d.manufacturer Manufacturer = $d.manufacturer
@@ -1967,6 +2007,9 @@ function Get-DeviceEndpoint {
OwnerType = $d.managedDeviceOwnerType OwnerType = $d.managedDeviceOwnerType
TotalStorageGB = $totalGB TotalStorageGB = $totalGB
FreeStorageGB = $freeGB FreeStorageGB = $freeGB
InAutopilot = $inAutopilot
AutopilotProfileAssigned = $apProfileAssign
AutopilotLastContacted = $apLastContact
} }
} }
+15 -2
View File
@@ -5872,12 +5872,20 @@ const DEV_COL_MAP = {
deviceName: 'DeviceName', deviceName: 'DeviceName',
userDisplayName: 'UserDisplayName', userDisplayName: 'UserDisplayName',
operatingSystem: 'OS', operatingSystem: 'OS',
managementType: 'ManagementType',
complianceState: 'ComplianceState', complianceState: 'ComplianceState',
lastSyncDateTime: 'LastSync', lastSyncDateTime: 'LastSync',
enrolledDateTime: 'EnrolledDateTime', enrolledDateTime: 'EnrolledDateTime',
}; };
const DEV_DATE_COLS = new Set(['LastSync', 'EnrolledDateTime']); const DEV_DATE_COLS = new Set(['LastSync', 'EnrolledDateTime']);
// Verwaltungsart als Badge: Co-Managed (ConfigMgr + Intune) vs. reines Intune.
function devMgmtBadge(type) {
if (type === 'Co-Managed') return '<span class="dev-badge dev-badge-comgmt">Co-Managed</span>';
if (type === 'Intune') return '<span class="dev-badge dev-badge-intune">Intune</span>';
return '<span class="dev-badge dev-badge-unk">—</span>';
}
function devFmtDate(iso) { function devFmtDate(iso) {
if (!iso) return '—'; if (!iso) return '—';
const d = new Date(iso); const d = new Date(iso);
@@ -5941,6 +5949,7 @@ function devRender() {
<td>${escapeHtml(d.DeviceName || '—')}</td> <td>${escapeHtml(d.DeviceName || '—')}</td>
<td>${escapeHtml(d.UserDisplayName || '—')}</td> <td>${escapeHtml(d.UserDisplayName || '—')}</td>
<td>${escapeHtml(d.OS || '—')}</td> <td>${escapeHtml(d.OS || '—')}</td>
<td>${devMgmtBadge(d.ManagementType)}</td>
<td><span class="dev-badge ${cmpCls}">${escapeHtml(d.ComplianceState || '—')}</span></td> <td><span class="dev-badge ${cmpCls}">${escapeHtml(d.ComplianceState || '—')}</span></td>
<td>${sync}</td> <td>${sync}</td>
</tr>`; </tr>`;
@@ -6018,12 +6027,16 @@ function devRenderDetail(d) {
['Benutzer', d.UserDisplayName], ['Benutzer', d.UserDisplayName],
['UPN', d.UserPrincipalName], ['UPN', d.UserPrincipalName],
['OS', d.OS ? `${d.OS} ${d.OSVersion || ''}`.trim() : null], ['OS', d.OS ? `${d.OS} ${d.OSVersion || ''}`.trim() : null],
['Verwaltung', d.ManagementType],
['Compliance', d.ComplianceState], ['Compliance', d.ComplianceState],
['Seriennummer', d.SerialNumber], ['Seriennummer', d.SerialNumber],
['IMEI', d.Imei], ['IMEI', d.Imei],
['Wi-Fi MAC', d.WiFiMac], ['Wi-Fi MAC', d.WiFiMac],
['Speicher', storage], ['Speicher', storage],
['Eingeschrieben', enrolled], ['Eingeschrieben', enrolled],
['Autopilot', d.InAutopilot ? 'Registriert' : (d.InAutopilot === false ? 'Nicht in Autopilot' : null)],
['Autopilot-Profil zugewiesen', d.AutopilotProfileAssigned ? devFmtDate(d.AutopilotProfileAssigned) : null],
['Autopilot letzter Kontakt', d.AutopilotLastContacted ? devFmtDate(d.AutopilotLastContacted) : null],
['Letzter Sync', sync], ['Letzter Sync', sync],
['Join-Typ', d.JoinType], ['Join-Typ', d.JoinType],
['Besitzertyp', d.OwnerType], ['Besitzertyp', d.OwnerType],
@@ -6222,12 +6235,12 @@ document.addEventListener('click', e => {
function devExportCsv() { function devExportCsv() {
const items = DevState.filtered || []; const items = DevState.filtered || [];
if (items.length === 0) { toast('Keine Geräte zum Exportieren.', 'warn'); return; } if (items.length === 0) { toast('Keine Geräte zum Exportieren.', 'warn'); return; }
const header = ['DeviceName','UserDisplayName','UserPrincipalName','OS','OSVersion','ComplianceState','SerialNumber','Model','Manufacturer','LastSync','EnrolledDateTime']; const header = ['DeviceName','UserDisplayName','UserPrincipalName','OS','OSVersion','ManagementType','ComplianceState','SerialNumber','Model','Manufacturer','LastSync','EnrolledDateTime'];
const rows = [header]; const rows = [header];
for (const d of items) { for (const d of items) {
rows.push([ rows.push([
d.DeviceName || '', d.UserDisplayName || '', d.UserPrincipalName || '', d.DeviceName || '', d.UserDisplayName || '', d.UserPrincipalName || '',
d.OS || '', d.OSVersion || '', d.ComplianceState || '', d.OS || '', d.OSVersion || '', d.ManagementType || '', d.ComplianceState || '',
d.SerialNumber || '', d.Model || '', d.Manufacturer || '', d.SerialNumber || '', d.Model || '', d.Manufacturer || '',
d.LastSync ? devFmtDate(d.LastSync) : '', d.EnrolledDateTime ? devFmtDate(d.EnrolledDateTime) : '', d.LastSync ? devFmtDate(d.LastSync) : '', d.EnrolledDateTime ? devFmtDate(d.EnrolledDateTime) : '',
]); ]);
+1
View File
@@ -527,6 +527,7 @@
<th class="sortable" data-col="deviceName">Gerät <span class="sort-ico"></span></th> <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="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="operatingSystem">OS <span class="sort-ico"></span></th>
<th class="sortable" data-col="managementType">Verwaltung <span class="sort-ico"></span></th>
<th class="sortable" data-col="complianceState">Compliance <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> <th class="sortable" data-col="lastSyncDateTime">Letzter Sync <span class="sort-ico"></span></th>
</tr> </tr>
+2
View File
@@ -5427,6 +5427,8 @@ body.read-only .app-row {
.dev-badge-ok { background: rgba(74,222,128,.15); color: #4ade80; } .dev-badge-ok { background: rgba(74,222,128,.15); color: #4ade80; }
.dev-badge-err { background: rgba(248,113,113,.15); color: #f87171; } .dev-badge-err { background: rgba(248,113,113,.15); color: #f87171; }
.dev-badge-unk { background: var(--hairline-soft); color: var(--muted); } .dev-badge-unk { background: var(--hairline-soft); color: var(--muted); }
.dev-badge-intune { background: rgba(96,165,250,.15); color: #60a5fa; }
.dev-badge-comgmt { background: rgba(251,191,36,.15); color: #fbbf24; }
/* Detail-Panel */ /* Detail-Panel */
.dev-detail { .dev-detail {