From 9fb42b4d7eca3d656e7b53c2beff7ecbcf427cd8 Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Wed, 16 Sep 2026 12:23:10 +0200 Subject: [PATCH] =?UTF-8?q?Policies:=20Zuweisungen=20sichtbar,=20Ergebnis-?= =?UTF-8?q?Report,=20Post-Import-Parit=C3=A4t=20(#1=20#4=20#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #1 Spalte "Zuweisungen" in der Policy-Liste (Anzahl); Klick öffnet eine Detailansicht mit den konkreten Include-/Exclude-Gruppen und Alle Geräte/ Benutzer. Get-GraphPolicyList lädt jetzt $expand=assignments und löst Gruppennamen in einem Bulk-Lookup (Resolve-GroupNamesBulk) auf. - #4 Bulk-Zuweisung zeigt statt nur eines Toasts einen Ergebnis-Report pro Policy (zugewiesen / übersprungen / Fehler) und lädt die Liste neu. - #5 Post-Import-Zuweisung erhält Alle Geräte/Alle Benutzer (Parität zur Bulk-Zuweisung) und nutzt dieselbe Ergebnisansicht. Co-Authored-By: Claude Opus 4.8 --- src/PolicyIO.ps1 | 44 ++++++++++++++++++++- www/app.js | 101 +++++++++++++++++++++++++++++++++++++---------- www/index.html | 39 +++++++++++++++++- www/styles.css | 21 ++++++++++ 4 files changed, 182 insertions(+), 23 deletions(-) diff --git a/src/PolicyIO.ps1 b/src/PolicyIO.ps1 index 618cc12..09bf75d 100644 --- a/src/PolicyIO.ps1 +++ b/src/PolicyIO.ps1 @@ -154,14 +154,35 @@ function Get-GraphPolicyList { param([Parameter(Mandatory=$true)][string]$Type) $cfg = Get-PolicyTypeConfig $Type if (-not $cfg) { throw "Unbekannter Policy-Typ: $Type" } - $raw = Get-GraphPaged -Uri "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)" - $items = @() + # Zuweisungen gleich mitladen ($expand=assignments), damit die Liste je Policy + # Anzahl + aufgeloeste Ziele zeigen kann. + $raw = Get-GraphPaged -Uri "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)?`$expand=assignments" + $items = @() + $needGroups = @{} # eindeutige Gruppen-Ids ueber alle Policies (fuer 1 Bulk-Lookup) foreach ($r in $raw) { $id = Get-PolicyProp $r 'id' if (-not $id) { continue } $name = Get-PolicyProp $r $cfg.NameField if (-not $name) { $name = Get-PolicyProp $r 'displayName' } if (-not $name) { $name = Get-PolicyProp $r 'name' } + + $asg = @() + foreach ($a in @(Get-PolicyProp $r 'assignments')) { + $t = Get-PolicyProp $a 'target' + if (-not $t) { continue } + $ot = [string](Get-PolicyProp $t '@odata.type') + $gid = [string](Get-PolicyProp $t 'groupId') + $entry = $null + if ($ot -like '*exclusionGroupAssignmentTarget') { $entry = [ordered]@{ mode = 'exclude'; kind = 'group'; groupId = $gid } } + elseif ($ot -like '*groupAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'group'; groupId = $gid } } + elseif ($ot -like '*allDevicesAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'allDevices'; groupId = '' } } + elseif ($ot -like '*allLicensedUsersAssignmentTarget') { $entry = [ordered]@{ mode = 'include'; kind = 'allUsers'; groupId = '' } } + if ($entry) { + if ($gid) { $needGroups[$gid] = $true } + $asg += $entry + } + } + $items += [ordered]@{ id = [string]$id name = [string]$name @@ -170,6 +191,25 @@ function Get-GraphPolicyList { platform = Get-PolicyPlatformLabel -Raw $r -Type $cfg.Key odataType = [string](Get-PolicyProp $r '@odata.type') lastModifiedDateTime = Get-PolicyProp $r 'lastModifiedDateTime' + assignments = $asg + assignmentCount = @($asg).Count + } + } + + # Gruppennamen in EINEM Bulk-Lookup aufloesen (bereits bekannte aus dem Cache). + $lookup = @{} + try { + foreach ($g in @($script:State.Groups)) { if ($g.Id) { $lookup[[string]$g.Id] = [string]$g.DisplayName } } + foreach ($g in @($script:State.RpaGroups)) { if ($g.Id) { $lookup[[string]$g.Id] = [string]$g.DisplayName } } + } catch {} + $unknown = [string[]]@($needGroups.Keys | ForEach-Object { [string]$_ } | Where-Object { $_ -and -not $lookup.ContainsKey($_) }) + if ($unknown.Count -gt 0) { try { Resolve-GroupNamesBulk -Ids $unknown -Lookup $lookup } catch {} } + foreach ($it in $items) { + foreach ($a in @($it.assignments)) { + if ($a.kind -eq 'group') { + $gid = [string]$a.groupId + $a.groupName = if ($lookup.ContainsKey($gid)) { $lookup[$gid] } else { $gid } + } } } return $items diff --git a/www/app.js b/www/app.js index 5fa54c4..c79d42d 100644 --- a/www/app.js +++ b/www/app.js @@ -6321,7 +6321,7 @@ function polFmtDate(iso) { async function loadPolicies() { const body = document.getElementById('polBody'); - body.innerHTML = ` Lade Policies…`; + body.innerHTML = ` Lade Policies…`; // Drei Typen parallel laden; ein fehlschlagender Typ (z.B. fehlender Scope) // soll die anderen nicht blockieren. const endpoints = [ @@ -6368,7 +6368,7 @@ function renderPolicies() { const list = polFiltered(); document.getElementById('polCount').textContent = list.length; if (!list.length) { - body.innerHTML = `${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'}`; + body.innerHTML = `${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'}`; polUpdateSelCount(); return; } @@ -6380,9 +6380,19 @@ function renderPolicies() { ${escapeHtml(p.name || '—')} ${escapeHtml(p.typeLabel || p.type)} ${escapeHtml(p.platform || '—')} + ${p.assignmentCount > 0 + ? `` + : '—'} ${polFmtDate(p.lastModifiedDateTime)} `; }).join(''); + body.querySelectorAll('.pol-asg-badge').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const p = PolState.items.find(x => polKey(x) === btn.dataset.key); + if (p) openPolAssignView(p); + }); + }); body.querySelectorAll('.pol-row-check').forEach(cb => { cb.addEventListener('change', () => { if (cb.checked) PolState.selected.add(cb.dataset.key); @@ -6702,6 +6712,8 @@ function openPolAssignModal(assignable) { exportType: r.exportType || '', include: [], exclude: [], + allDevices: false, + allUsers: false, })); renderPolAssignList(); openModal('modalPolAssign'); @@ -6728,6 +6740,10 @@ function renderPolAssignList() { `).join('')} +
+ + +
`).join(''); PolAssignState.policies.forEach((_, i) => { @@ -6753,10 +6769,18 @@ function renderPolAssignChips(idx, kind) { function updatePolAssignInfo() { const info = document.getElementById('polAssignInfo'); if (!info) return; - const n = PolAssignState.policies.filter(p => p.include.length || p.exclude.length).length; + const n = PolAssignState.policies.filter(p => p.include.length || p.exclude.length || p.allDevices || p.allUsers).length; info.textContent = n ? `${n} Policy(s) mit Zuweisung` : 'Keine Zuweisung gewählt'; } +// Integrierte Ziele (Alle Geräte/Benutzer) pro Policy im Post-Import-Modal. +document.getElementById('polAssignList')?.addEventListener('change', e => { + const dev = e.target.closest('.pol-assign-alldev'); + const usr = e.target.closest('.pol-assign-allusr'); + if (dev) { PolAssignState.policies[+dev.dataset.idx].allDevices = dev.checked; updatePolAssignInfo(); } + else if (usr) { PolAssignState.policies[+usr.dataset.idx].allUsers = usr.checked; updatePolAssignInfo(); } +}); + let _polAssignSearchTimer = null; function polAssignHideDropdowns() { document.querySelectorAll('.pol-assign-dropdown').forEach(d => { d.classList.add('hidden'); d.innerHTML = ''; }); @@ -6821,27 +6845,24 @@ document.addEventListener('click', e => { document.getElementById('polAssignApply')?.addEventListener('click', async () => { const items = PolAssignState.policies - .filter(p => p.include.length || p.exclude.length) + .filter(p => p.include.length || p.exclude.length || p.allDevices || p.allUsers) .map(p => ({ exportType: p.exportType, id: p.id, policyName: p.name, include: p.include.map(g => g.id), exclude: p.exclude.map(g => g.id), + allDevices: !!p.allDevices, + allUsers: !!p.allUsers, + mode: 'replace', // frisch importierte Policy hat noch keine Zuweisungen })); if (!items.length) { closeModal('modalPolAssign'); return; } setLoading('Weise Gruppen zu…'); try { const res = await api('/api/policies/assign', { method: 'POST', body: { items }, timeoutMs: 120000 }); - const results = res.results || []; - const fail = results.filter(r => !r.success); - if (fail.length) { - const first = fail[0]; - toast(`${res.assignedCount || 0} zugewiesen, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Zuweisung'); - } else { - toast(`${res.assignedCount || 0} Policy(s) zugewiesen.`, 'ok', 'Zuweisung'); - } closeModal('modalPolAssign'); + showPolAssignResult(res.results || [], 'replace'); + await loadPolicies(); } catch (e) { toast('Zuweisung fehlgeschlagen: ' + e.message, 'err'); } finally { @@ -6861,6 +6882,30 @@ const POL_TYPE_TO_EXPORT = { const PolBulkAssign = { policies: [], include: [], exclude: [] }; let _polBulkSearchTimer = null; +// Zuweisungen einer einzelnen Policy ansehen (aus der Listen-Spalte). +function openPolAssignView(p) { + document.getElementById('polAssignViewName').innerHTML = + `${escapeHtml(p.name || '—')} ${escapeHtml(p.typeLabel || p.type)}`; + const body = document.getElementById('polAssignViewBody'); + const asg = p.assignments || []; + if (!asg.length) { + body.innerHTML = '
Keine Zuweisungen.
'; + } else { + const label = a => a.kind === 'allDevices' ? 'Alle Geräte' + : a.kind === 'allUsers' ? 'Alle Benutzer' + : (a.groupName || a.groupId); + const inc = asg.filter(a => a.mode === 'include'); + const exc = asg.filter(a => a.mode === 'exclude'); + const chip = (a, cls) => `${escapeHtml(label(a))}`; + body.innerHTML = + `
` + + `
${inc.length ? inc.map(a => chip(a, '')).join('') : '—'}
` + + `
` + + `
${exc.length ? exc.map(a => chip(a, 'exclude')).join('') : '—'}
`; + } + openModal('modalPolAssignView'); +} + function openPolBulkAssign() { const sel = PolState.items.filter(p => PolState.selected.has(polKey(p))); if (!sel.length) return; @@ -6993,15 +7038,9 @@ document.getElementById('polBulkApply')?.addEventListener('click', async () => { setLoading('Weise Gruppen zu…'); try { const res = await api('/api/policies/assign', { method: 'POST', body: { items }, timeoutMs: 180000 }); - const results = res.results || []; - const fail = results.filter(r => !r.success); - if (fail.length) { - const first = fail[0]; - toast(`${res.assignedCount || 0} zugewiesen, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Zuweisung'); - } else { - toast(`Gruppen ${mode === 'add' ? 'hinzugefügt' : 'ersetzt'}: ${res.assignedCount || 0} Policy(s).`, 'ok', 'Zuweisung'); - } closeModal('modalPolBulkAssign'); + showPolAssignResult(res.results || [], mode); + await loadPolicies(); // Zuweisungs-Zähler in der Liste aktualisieren } catch (e) { toast('Zuweisung fehlgeschlagen: ' + e.message, 'err'); } finally { @@ -7009,6 +7048,28 @@ document.getElementById('polBulkApply')?.addEventListener('click', async () => { } }); +// Ergebnis einer (Bulk-)Zuweisung pro Policy anzeigen. +function showPolAssignResult(results, mode) { + const ok = results.filter(r => r.success && !r.skipped).length; + const skip = results.filter(r => r.success && r.skipped).length; + const fail = results.filter(r => !r.success).length; + document.getElementById('polBulkResultSummary').innerHTML = + `Modus: ${mode === 'add' ? 'Hinzufügen' : 'Ersetzen'} — ` + + `${ok} zugewiesen` + + (skip ? ` · ${skip} übersprungen` : '') + + (fail ? ` · ${fail} fehlgeschlagen` : ''); + document.getElementById('polBulkResultBody').innerHTML = results.map(r => { + let badge, detail = ''; + if (!r.success) { badge = 'Fehler'; detail = `${escapeHtml(r.error || 'Unbekannter Fehler')}`; } + else if (r.skipped) { badge = 'Übersprungen'; } + else { badge = 'Zugewiesen'; } + return `
${badge}${escapeHtml(r.policyName || r.id || '—')}${detail}
`; + }).join(''); + openModal('modalPolBulkResult'); + const anyFail = fail > 0; + toast(anyFail ? `${ok} zugewiesen, ${fail} fehlgeschlagen.` : `${ok} Policy(s) zugewiesen.`, anyFail ? 'warn' : 'ok', 'Zuweisung'); +} + document.getElementById('btnPolAssignGroups')?.addEventListener('click', openPolBulkAssign); // Alle Policies als Snapshot in den konfigurierten Git-Ordner schreiben + committen. diff --git a/www/index.html b/www/index.html index f5566e8..dfb5d5c 100644 --- a/www/index.html +++ b/www/index.html @@ -640,11 +640,12 @@ Name Typ Plattform + Zuweisungen Geändert - Klicke auf „Neu laden" um Policies zu laden. + Klicke auf „Neu laden" um Policies zu laden. @@ -1040,6 +1041,42 @@ + + + + + +