diff --git a/src/PolicyIO.ps1 b/src/PolicyIO.ps1 index 613f0fb..24552b4 100644 --- a/src/PolicyIO.ps1 +++ b/src/PolicyIO.ps1 @@ -154,8 +154,17 @@ function Get-GraphPolicyList { $cfg = Get-PolicyTypeConfig $Type if (-not $cfg) { throw "Unbekannter Policy-Typ: $Type" } # 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" + # Anzahl + aufgeloeste Ziele zeigen kann. Nicht jede deviceManagement-Collection + # unterstuetzt $expand=assignments (z.B. groupPolicyConfigurations lehnt es teils + # mit 400 ab) -> im Fehlerfall ohne Zuweisungen laden, damit der Typ ueberhaupt + # erscheint (und exportierbar bleibt). + $listBase = "https://graph.microsoft.com/beta/deviceManagement/$($cfg.Collection)" + try { + $raw = Get-GraphPaged -Uri ($listBase + '?$expand=assignments') + } catch { + Write-Host " [POLICIES] $($cfg.Key): `$expand=assignments nicht unterstuetzt -> lade ohne Zuweisungen ($($_.Exception.Message))" -ForegroundColor DarkYellow + $raw = Get-GraphPaged -Uri $listBase + } $items = @() $needGroups = @{} # eindeutige Gruppen-Ids ueber alle Policies (fuer 1 Bulk-Lookup) foreach ($r in $raw) { @@ -592,10 +601,16 @@ function Export-PoliciesEndpoint { $files = @() $exports = @() + $errors = @() foreach ($id in $ids) { $policy = $null - try { $policy = Get-GraphPolicyDetail -Type $type -Id ([string]$id) } catch {} - if (-not $policy) { continue } + try { $policy = Get-GraphPolicyDetail -Type $type -Id ([string]$id) } + catch { + $errors += @{ id = [string]$id; error = $_.Exception.Message } + Write-Host " [EXPORT] $type/$id fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor DarkYellow + continue + } + if (-not $policy) { $errors += @{ id = [string]$id; error = 'Leere Antwort von Graph' }; continue } $displayName = Get-PolicyDisplayName $policy $exportData = [ordered]@{ @@ -623,7 +638,7 @@ function Export-PoliciesEndpoint { } } - return @{ ok = $true; exportedCount = $exports.Count; files = @($files); exports = @($exports) } + return @{ ok = $true; exportedCount = $exports.Count; files = @($files); exports = @($exports); errors = @($errors) } } # Liste des Server-Archivs (fuer optionalen Server-seitigen Re-Import). diff --git a/www/app.js b/www/app.js index c79d42d..e7418d5 100644 --- a/www/app.js +++ b/www/app.js @@ -6467,11 +6467,17 @@ async function polExportSelected() { setLoading('Exportiere Policies…'); try { let allExports = []; + let allErrors = []; for (const [type, ids] of Object.entries(byType)) { const res = await api('/api/policies/export', { method: 'POST', body: { type, ids }, timeoutMs: 120000 }); if (res.exports) allExports = allExports.concat(res.exports); + if (res.errors) allErrors = allErrors.concat(res.errors); + } + if (!allExports.length) { + const hint = allErrors.length ? ` (${allErrors.length} Fehler, z.B. ${escapeHtml(allErrors[0].error || '')})` : ''; + toast('Nichts exportiert.' + hint, 'err', 'Export'); + return; } - if (!allExports.length) { toast('Nichts exportiert.', 'warn'); return; } // Jede ausgewählte Policy als eigene JSON-Datei herunterladen. const usedNames = new Set(); for (let i = 0; i < allExports.length; i++) { @@ -6484,7 +6490,11 @@ async function polExportSelected() { // Kurze Pause, damit der Browser mehrere aufeinanderfolgende Downloads nicht verwirft. if (i < allExports.length - 1) await new Promise(r => setTimeout(r, 150)); } - toast(`${allExports.length} Policy(s) als Einzeldatei(en) exportiert (auch im Server-Archiv abgelegt).`, 'ok'); + if (allErrors.length) { + toast(`${allExports.length} exportiert, ${allErrors.length} fehlgeschlagen (z.B. ${escapeHtml(allErrors[0].error || 'Fehler')}).`, 'warn', 'Export'); + } else { + toast(`${allExports.length} Policy(s) als Einzeldatei(en) exportiert (auch im Server-Archiv abgelegt).`, 'ok'); + } } catch (e) { toast('Export fehlgeschlagen: ' + e.message, 'err'); } finally {