From e63ae28cb16911b6412ff4571c3f21302c8a75b8 Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Wed, 12 Aug 2026 14:52:42 +0200 Subject: [PATCH] =?UTF-8?q?Ger=C3=A4te-Tab:=20serverseitige=20Suche=20via?= =?UTF-8?q?=20Graph=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suche ruft bei jeder Eingabe die API auf statt lokal zu filtern. Seriennummer wird per exaktem eq-Filter parallel gesucht und mit Namens-Treffern zusammengeführt (Graph unterstützt kein startswith auf serialNumber). Co-Authored-By: Claude Sonnet 4.6 --- src/Api.ps1 | 57 +++++++++++++++++++++++++++++++++++++++++------------ www/app.js | 51 ++++++++++++++++++++++------------------------- 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/Api.ps1 b/src/Api.ps1 index 622edb2..74bfa05 100644 --- a/src/Api.ps1 +++ b/src/Api.ps1 @@ -1544,23 +1544,54 @@ function Search-DevicesEndpoint { $q = [string]$Query.q $os = [string]$Query.os $compliance = [string]$Query.compliance - $top = 100 + $top = 50 - $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" + # Ohne Suchbegriff: alle Geräte (erste Seite) + # Mit Suchbegriff: Graph unterstuetzt startswith nur auf deviceName/userDisplayName/userPrincipalName. + # Seriennummer wird separat per exaktem Filter gesucht und mit Name-Ergebnissen zusammengefuehrt. + $nameItems = @() + $snItems = @() + + if ($q -and $q.Length -ge 2) { + $filters = @() + $nameFilter = "(startswith(deviceName,'$q') or startswith(userDisplayName,'$q') or startswith(userPrincipalName,'$q'))" + $filters += $nameFilter + if ($os) { $filters += "operatingSystem eq '$os'" } + if ($compliance) { $filters += "complianceState eq '$compliance'" } + $filterStr = '$filter=' + [uri]::EscapeDataString(($filters -join ' and ')) + '&' + $uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?${filterStr}`$select=$select&`$top=$top&`$orderby=deviceName" + $resp = Invoke-MgGraphRequestRetry -Uri $uri -Method GET + $nameItems = @($resp.value) + + # Seriennummer: exakter Vergleich (Graph unterstuetzt kein startswith auf serialNumber) + $snFilters = @("serialNumber eq '$q'") + if ($os) { $snFilters += "operatingSystem eq '$os'" } + if ($compliance) { $snFilters += "complianceState eq '$compliance'" } + $snFilter = '$filter=' + [uri]::EscapeDataString(($snFilters -join ' and ')) + '&' + $snUri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?${snFilter}`$select=$select&`$top=10" + try { + $snResp = Invoke-MgGraphRequestRetry -Uri $snUri -Method GET + $snItems = @($snResp.value) + } catch { $snItems = @() } + } else { + $filters = @() + 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 { '' } + $uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?${filterStr}`$select=$select&`$top=$top&`$orderby=deviceName" + $resp = Invoke-MgGraphRequestRetry -Uri $uri -Method GET + $nameItems = @($resp.value) + } + + # Zusammenfuehren, Duplikate entfernen + $seen = @{} + $all = @($nameItems) + @($snItems) $items = @() - $resp = Invoke-MgGraphRequestRetry -Uri $uri -Method GET - foreach ($d in $resp.value) { + foreach ($d in $all) { + if (-not $d.id -or $seen[$d.id]) { continue } + $seen[$d.id] = $true $items += [pscustomobject]@{ Id = $d.id DeviceName = $d.deviceName diff --git a/www/app.js b/www/app.js index 2962669..9ae5a54 100644 --- a/www/app.js +++ b/www/app.js @@ -5239,18 +5239,7 @@ const DevState = { }; 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); - } + await devFetch(); } function devShowLoading(on) { @@ -5258,22 +5247,30 @@ function devShowLoading(on) { document.getElementById('devEmpty').classList.add('hidden'); } -function devApplyFilters() { - const q = document.getElementById('devSearch').value.toLowerCase(); +async function devFetch() { + const q = document.getElementById('devSearch').value.trim(); 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; - }); + devShowLoading(true); + try { + const params = new URLSearchParams(); + if (q) params.set('q', q); + if (os) params.set('os', os); + if (cmp) params.set('compliance', cmp); + const data = await api('/api/devices?' + params.toString()); + DevState.items = data.items || []; + DevState.filtered = DevState.items; + devSort(); + } catch (e) { + toast('Geräte konnten nicht geladen werden: ' + e, 'error'); + } finally { + devShowLoading(false); + } +} - devSort(); +function devApplyFilters() { + devFetch(); } function devSort() { @@ -5495,10 +5492,10 @@ document.getElementById('devActions')?.addEventListener('click', async (e) => { let devSearchDebounce = null; document.getElementById('devSearch').addEventListener('input', () => { clearTimeout(devSearchDebounce); - devSearchDebounce = setTimeout(devApplyFilters, 250); + devSearchDebounce = setTimeout(devFetch, 400); }); -document.getElementById('devOsFilter').addEventListener('change', devApplyFilters); -document.getElementById('devComplianceFilter').addEventListener('change', devApplyFilters); +document.getElementById('devOsFilter').addEventListener('change', devFetch); +document.getElementById('devComplianceFilter').addEventListener('change', devFetch); // Sortierung document.querySelectorAll('#devTable th.sortable').forEach(th => {