Policies-Tab: Export/Import (Settings Catalog, Compliance, Config)
Build & Release MSI / build-msi (push) Canceled after 0s

Neuer Tab "Policies" zum Sichern und Wiederherstellen von Intune-Policies.

- Backend src/PolicyIO.ps1: typ-getrieben fuer configurationPolicies
  (Settings Catalog), deviceCompliancePolicies und deviceConfigurations
- Export als JSON-Download (einzeln/Bundle) + Server-Archiv unter %APPDATA%
- Import per Datei-Upload, immer Neuanlage (nie ueberschreiben), robust fuer
  Hashtable (PS7) und PSCustomObject (PS5.1); im Read-Only-Modus ausgeblendet
- Routen in src/Api.ps1; Config-Scope DeviceManagementConfiguration.* auch
  nach Settings-Save sichergestellt
- Frontend: Tab + View, Suche/Typ-Filter/Sortierung, Auswahl, Download/Upload
- Version 0.1.25 (Start.ps1, CHANGELOG, README)
- build-local.ps1: x64-.NET-Runtime fuer wix auf ARM64-Windows + echter
  Desktop-Pfad

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 14:25:29 +02:00
co-authored by Claude Opus 4.8
parent e63ae28cb1
commit ea51bac772
9 changed files with 898 additions and 7 deletions
+243 -1
View File
@@ -4391,6 +4391,7 @@ function switchMainView(view) {
const groupView = document.getElementById('groupMgmtView');
const reportView = document.getElementById('appReportView');
const devView = document.getElementById('devView');
const polView = document.getElementById('policiesView');
document.querySelectorAll('#topbarNav .nav-tab').forEach(t => {
t.classList.toggle('active', t.dataset.view === view);
t.setAttribute('aria-selected', t.dataset.view === view ? 'true' : 'false');
@@ -4399,12 +4400,13 @@ function switchMainView(view) {
groupView .classList.toggle('hidden', view !== 'groups');
reportView.classList.toggle('hidden', view !== 'report');
devView .classList.toggle('hidden', view !== 'devices');
polView .classList.toggle('hidden', view !== 'policies');
}
document.querySelectorAll('#topbarNav .nav-tab').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
if ((view === 'groups' || view === 'report' || view === 'devices') && !State.connected) {
if ((view === 'groups' || view === 'report' || view === 'devices' || view === 'policies') && !State.connected) {
toast('Bitte zuerst verbinden.', 'warn');
return;
}
@@ -4412,6 +4414,7 @@ document.querySelectorAll('#topbarNav .nav-tab').forEach(btn => {
if (view === 'groups') openGroupExportModal();
if (view === 'report' && ReportState.items.length === 0) loadAppReport();
if (view === 'devices') { devLoadAll(); setTimeout(() => document.getElementById('devSearch').focus(), 80); }
if (view === 'policies' && PolState.items.length === 0) loadPolicies();
});
});
@@ -5509,3 +5512,242 @@ document.querySelectorAll('#devTable th.sortable').forEach(th => {
devSort();
});
});
// =============================================================
// Policies (Export / Import)
// =============================================================
const PolState = {
items: [], // alle geladenen Policies (normalisiert)
selected: new Set(), // Keys "type::id"
sortCol: 'name',
sortAsc: true,
search: '',
typeFilter: '',
};
function polKey(p) { return `${p.type}::${p.id}`; }
function polFmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d)) return '—';
return d.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
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>`;
// Drei Typen parallel laden; ein fehlschlagender Typ (z.B. fehlender Scope)
// soll die anderen nicht blockieren.
const endpoints = [
['/api/policies/settingscatalog', 'Settings Catalog'],
['/api/policies/compliance', 'Compliance'],
['/api/policies/configuration', 'Konfigurationsprofile'],
];
const settled = await Promise.allSettled(endpoints.map(([url]) => api(url)));
let items = [];
const errors = [];
settled.forEach((r, i) => {
if (r.status === 'fulfilled') {
items = items.concat(r.value.items || []);
} else {
errors.push(`${endpoints[i][1]}: ${r.reason?.message || 'Fehler'}`);
}
});
PolState.items = items;
// Auswahl auf noch existierende Policies eindampfen
const valid = new Set(items.map(polKey));
PolState.selected = new Set([...PolState.selected].filter(k => valid.has(k)));
renderPolicies();
if (errors.length) toast('Einige Typen konnten nicht geladen werden. ' + errors.join(' · '), 'warn');
}
function polFiltered() {
const q = PolState.search.toLowerCase();
const tf = PolState.typeFilter;
let list = PolState.items.filter(p => {
if (tf && p.type !== tf) return false;
if (!q) return true;
return (p.name || '').toLowerCase().includes(q)
|| (p.typeLabel || '').toLowerCase().includes(q)
|| (p.platform || '').toLowerCase().includes(q);
});
const col = PolState.sortCol, asc = PolState.sortAsc ? 1 : -1;
list.sort((a, b) => String(a[col] ?? '').localeCompare(String(b[col] ?? ''), 'de', { numeric: true }) * asc);
return list;
}
function renderPolicies() {
const body = document.getElementById('polBody');
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>`;
polUpdateSelCount();
return;
}
body.innerHTML = list.map(p => {
const key = polKey(p);
const checked = PolState.selected.has(key) ? 'checked' : '';
return `<tr data-key="${escapeHtml(key)}">
<td class="pol-col-check"><input type="checkbox" class="pol-row-check" data-key="${escapeHtml(key)}" ${checked}></td>
<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>${polFmtDate(p.lastModifiedDateTime)}</td>
</tr>`;
}).join('');
body.querySelectorAll('.pol-row-check').forEach(cb => {
cb.addEventListener('change', () => {
if (cb.checked) PolState.selected.add(cb.dataset.key);
else PolState.selected.delete(cb.dataset.key);
polUpdateSelCount();
polSyncCheckAll();
});
});
// Klick auf Zeile toggelt die Checkbox (außer direkt auf die Checkbox)
body.querySelectorAll('tr[data-key]').forEach(tr => {
tr.addEventListener('click', e => {
if (e.target.classList.contains('pol-row-check')) return;
const cb = tr.querySelector('.pol-row-check');
cb.checked = !cb.checked;
cb.dispatchEvent(new Event('change'));
});
});
polUpdateSelCount();
polSyncCheckAll();
}
function polUpdateSelCount() {
const n = PolState.selected.size;
document.getElementById('polSelCount').textContent = n;
document.getElementById('btnPolExport').disabled = n === 0;
}
function polSyncCheckAll() {
const visible = polFiltered().map(polKey);
const all = visible.length > 0 && visible.every(k => PolState.selected.has(k));
const some = visible.some(k => PolState.selected.has(k));
const cb = document.getElementById('polCheckAll');
cb.checked = all;
cb.indeterminate = !all && some;
}
function polDownloadJson(obj, filename) {
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
async function polExportSelected() {
const sel = [...PolState.selected];
if (!sel.length) return;
const byType = {};
for (const key of sel) {
const idx = key.indexOf('::');
const type = key.slice(0, idx), id = key.slice(idx + 2);
(byType[type] ||= []).push(id);
}
setLoading('Exportiere Policies…');
try {
let allExports = [];
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 (!allExports.length) { toast('Nichts exportiert.', 'warn'); return; }
if (allExports.length === 1) {
polDownloadJson(allExports[0].data, allExports[0].fileName || 'policy.json');
} else {
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '').replace(/-/g, '');
const bundle = {
type: 'IntuneManagerPolicyBundle',
exportDate: new Date().toISOString(),
count: allExports.length,
policies: allExports.map(e => e.data),
};
polDownloadJson(bundle, `intune-policies_${stamp}.json`);
}
toast(`${allExports.length} Policy(s) exportiert (auch im Server-Archiv abgelegt).`, 'ok');
} catch (e) {
toast('Export fehlgeschlagen: ' + e.message, 'err');
} finally {
clearLoading();
}
}
// Datei-Inhalt -> Liste von Envelopes { exportType, policy, ... }
function polExtractEnvelopes(parsed, fileName) {
if (parsed && Array.isArray(parsed.policies)) return parsed.policies;
if (parsed && parsed.exportType && parsed.policy) return [parsed];
if (parsed && parsed.policy) return [parsed]; // nachsichtig
toast(`${fileName}: unbekanntes Policy-Format.`, 'err');
return [];
}
async function polHandleImportFiles(fileList) {
const files = [...fileList];
if (!files.length) return;
let envelopes = [];
for (const f of files) {
let parsed = null;
try { parsed = JSON.parse(await f.text()); }
catch { toast(`${f.name}: kein gültiges JSON.`, 'err'); continue; }
envelopes = envelopes.concat(polExtractEnvelopes(parsed, f.name));
}
if (!envelopes.length) return;
if (!window.confirm(`${envelopes.length} Policy(s) als NEUE Policies im verbundenen Tenant anlegen?\n\nBestehende Policies werden dabei nicht verändert.`)) return;
setLoading('Importiere Policies…');
try {
const res = await api('/api/policies/import', { method: 'POST', body: { policies: envelopes }, timeoutMs: 180000 });
const results = res.results || [];
const ok = results.filter(r => r.success);
const fail = results.filter(r => !r.success);
if (fail.length) {
const first = fail[0];
toast(`${ok.length} importiert, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Import');
} else {
toast(`${ok.length} Policy(s) importiert.`, 'ok', 'Import');
}
await loadPolicies();
} catch (e) {
toast('Import fehlgeschlagen: ' + e.message, 'err');
} finally {
clearLoading();
}
}
// --- Policies: Event-Wiring ---
document.getElementById('btnPolRefresh')?.addEventListener('click', loadPolicies);
document.getElementById('btnPolExport')?.addEventListener('click', polExportSelected);
document.getElementById('btnPolImport')?.addEventListener('click', () => document.getElementById('polImportFile').click());
document.getElementById('polImportFile')?.addEventListener('change', e => {
polHandleImportFiles(e.target.files);
e.target.value = ''; // erlaubt erneuten Import derselben Datei
});
document.getElementById('polCheckAll')?.addEventListener('change', e => {
const visible = polFiltered().map(polKey);
if (e.target.checked) visible.forEach(k => PolState.selected.add(k));
else visible.forEach(k => PolState.selected.delete(k));
renderPolicies();
});
let _polSearchTimer = null;
document.getElementById('polSearch')?.addEventListener('input', e => {
clearTimeout(_polSearchTimer);
_polSearchTimer = setTimeout(() => { PolState.search = e.target.value.trim(); renderPolicies(); }, 200);
});
document.getElementById('polTypeFilter')?.addEventListener('change', e => {
PolState.typeFilter = e.target.value;
renderPolicies();
});
document.querySelectorAll('#polTable th.sortable').forEach(th => {
th.addEventListener('click', () => {
if (PolState.sortCol === th.dataset.col) PolState.sortAsc = !PolState.sortAsc;
else { PolState.sortCol = th.dataset.col; PolState.sortAsc = true; }
renderPolicies();
});
});