Policies: Zuweisungen sichtbar, Ergebnis-Report, Post-Import-Parität (#1 #4 #5)

- #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 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 12:23:10 +02:00
co-authored by Claude Opus 4.8
parent a7c8e88793
commit 9fb42b4d7e
4 changed files with 182 additions and 23 deletions
+81 -20
View File
@@ -6321,7 +6321,7 @@ function polFmtDate(iso) {
async function loadPolicies() {
const body = document.getElementById('polBody');
body.innerHTML = `<tr><td colspan="5" class="pol-hint"><span class="spinner" style="width:14px;height:14px;border-width:2px;"></span> Lade Policies…</td></tr>`;
body.innerHTML = `<tr><td colspan="6" class="pol-hint"><span class="spinner" style="width:14px;height:14px;border-width:2px;"></span> Lade Policies…</td></tr>`;
// 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 = `<tr><td colspan="5" class="pol-empty">${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'}</td></tr>`;
body.innerHTML = `<tr><td colspan="6" class="pol-empty">${PolState.items.length ? 'Keine Policy passt zum Filter.' : 'Keine Policies gefunden.'}</td></tr>`;
polUpdateSelCount();
return;
}
@@ -6380,9 +6380,19 @@ function renderPolicies() {
<td class="pol-name" title="${escapeHtml(p.name || '')}">${escapeHtml(p.name || '—')}</td>
<td><span class="pol-type-badge">${escapeHtml(p.typeLabel || p.type)}</span></td>
<td>${escapeHtml(p.platform || '—')}</td>
<td class="pol-col-asg">${p.assignmentCount > 0
? `<button type="button" class="pol-asg-badge" data-key="${escapeHtml(key)}" title="Zuweisungen anzeigen">${p.assignmentCount}</button>`
: '<span class="muted">—</span>'}</td>
<td>${polFmtDate(p.lastModifiedDateTime)}</td>
</tr>`;
}).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() {
</div>
`).join('')}
</div>
<div class="pol-assign-builtin">
<label class="pol-bulk-check"><input type="checkbox" class="pol-assign-alldev" data-idx="${i}" ${p.allDevices ? 'checked' : ''}> Alle Geräte</label>
<label class="pol-bulk-check"><input type="checkbox" class="pol-assign-allusr" data-idx="${i}" ${p.allUsers ? 'checked' : ''}> Alle Benutzer</label>
</div>
</div>
`).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 =
`<b>${escapeHtml(p.name || '—')}</b> <span class="pol-type-badge">${escapeHtml(p.typeLabel || p.type)}</span>`;
const body = document.getElementById('polAssignViewBody');
const asg = p.assignments || [];
if (!asg.length) {
body.innerHTML = '<div class="muted">Keine Zuweisungen.</div>';
} 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) => `<span class="pol-assign-chip ${cls}">${escapeHtml(label(a))}</span>`;
body.innerHTML =
`<div class="pol-av-group"><label class="lbl">Include (${inc.length})</label>`
+ `<div class="pol-assign-chips">${inc.length ? inc.map(a => chip(a, '')).join('') : '<span class="muted">—</span>'}</div></div>`
+ `<div class="pol-av-group" style="margin-top:12px;"><label class="lbl">Exclude (${exc.length})</label>`
+ `<div class="pol-assign-chips">${exc.length ? exc.map(a => chip(a, 'exclude')).join('') : '<span class="muted">—</span>'}</div></div>`;
}
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: <b>${mode === 'add' ? 'Hinzufügen' : 'Ersetzen'}</b> — `
+ `<span class="pol-res-ok">${ok} zugewiesen</span>`
+ (skip ? ` · <span class="muted">${skip} übersprungen</span>` : '')
+ (fail ? ` · <span class="pol-res-err">${fail} fehlgeschlagen</span>` : '');
document.getElementById('polBulkResultBody').innerHTML = results.map(r => {
let badge, detail = '';
if (!r.success) { badge = '<span class="pol-res-badge err">Fehler</span>'; detail = `<span class="pol-res-msg">${escapeHtml(r.error || 'Unbekannter Fehler')}</span>`; }
else if (r.skipped) { badge = '<span class="pol-res-badge skip">Übersprungen</span>'; }
else { badge = '<span class="pol-res-badge ok">Zugewiesen</span>'; }
return `<div class="pol-res-row">${badge}<span class="pol-res-name">${escapeHtml(r.policyName || r.id || '—')}</span>${detail}</div>`;
}).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.