Policy-Import haerten, Admin-Vorlagen, Zuweisung nach Import, Git-Snapshot
Neu: - Administrative Vorlagen (ADMX / groupPolicyConfigurations) als vierter Policy-Typ fuer Export/Import (GET /api/policies/administrativetemplate) - Zuweisung direkt nach Import: pro Policy Include-/Exclude-Gruppen (POST /api/policies/assign, typ-spezifische /assign-Action) - Git-Snapshot: versioniertes Policy-Backup in lokalen Git-Ordner (Settings-Sektion policyBackup) - Import akzeptiert Git-Snapshot-Dateien (policyType neben exportType) Behoben: - Settings-Catalog-Import schema-konform: Collection-Properties immer als Array (repariert PS-5.1-Roundtrip), null -> [], Wrapper-@odata.type, read-only id entfernt - Import-Phantom-Fehler (@($null)-Geisterdurchlauf im Archiv-Pfad) - Frontend-Cache-Reset beim Tenant-Wechsel (resetClientState) - WAM-Anmeldefenster zuverlaessig im Vordergrund (AttachThreadInput) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+237
-15
@@ -194,9 +194,11 @@ document.getElementById('tenantSwitch')?.addEventListener('change', async e => {
|
||||
State.readOnly = !!s.readOnly;
|
||||
applyReadOnlyMode();
|
||||
renderConnection();
|
||||
// Frontend-Caches anderer Tabs verwerfen (anderer Tenant = andere Daten).
|
||||
if (typeof ReportState !== 'undefined') ReportState.items = [];
|
||||
if (typeof PolState !== 'undefined') { PolState.items = []; PolState.selected?.clear?.(); }
|
||||
// Anderer Tenant = andere Daten: ALLE datentragenden Frontend-Caches
|
||||
// verwerfen, sonst ueberspringen die Load-Guards (appsLoaded/loadedModes)
|
||||
// das Neuladen und die UI zeigt weiter den vorherigen Tenant.
|
||||
if (typeof stopDevicePolling === 'function') stopDevicePolling();
|
||||
resetClientState();
|
||||
switchMainView('apps');
|
||||
if (s.connected) {
|
||||
toast('Verbunden mit ' + (s.tenantLabel || s.account || 'Mandant'), 'ok', 'Mandant gewechselt');
|
||||
@@ -345,17 +347,8 @@ document.getElementById('btnDisconnect').addEventListener('click', async () => {
|
||||
connectInFlight = false;
|
||||
try {
|
||||
await api('/api/disconnect', { method: 'POST' });
|
||||
Object.assign(State, {
|
||||
connected: false, account: null,
|
||||
targets: [], apps: [], session: [],
|
||||
appsLoaded: false,
|
||||
});
|
||||
State.selectedTargetIds.clear();
|
||||
State.membershipCache.clear();
|
||||
State.loadedModes.clear();
|
||||
State.appDetails.clear();
|
||||
State.expandedApps.clear();
|
||||
State.modeCache.clear();
|
||||
Object.assign(State, { connected: false, account: null });
|
||||
resetClientState();
|
||||
renderConnection();
|
||||
renderTargets();
|
||||
renderPinnedTargets();
|
||||
@@ -368,6 +361,25 @@ document.getElementById('btnDisconnect').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Alle datentragenden Frontend-Caches verwerfen (Apps, Targets, Gruppen-
|
||||
// Mitglieder, Report, Policies, Devices). Beruehrt NICHT connected/account —
|
||||
// das setzt der jeweilige Aufrufer (Disconnect vs. Tenant-Wechsel) selbst.
|
||||
// Wird beim Tenant-Wechsel benoetigt, sonst zeigen die Load-Guards
|
||||
// (appsLoaded / loadedModes) noch die Daten des vorherigen Tenants.
|
||||
function resetClientState() {
|
||||
Object.assign(State, { targets: [], apps: [], session: [], appsLoaded: false });
|
||||
State.selectedTargetIds.clear();
|
||||
State.membershipCache.clear();
|
||||
State.loadedModes.clear();
|
||||
State.appDetails.clear();
|
||||
State.expandedApps.clear();
|
||||
State.modeCache.clear();
|
||||
State.expandedGroups = new Map();
|
||||
if (typeof ReportState !== 'undefined') ReportState.items = [];
|
||||
if (typeof PolState !== 'undefined') { PolState.items = []; PolState.selected?.clear?.(); }
|
||||
if (typeof DevState !== 'undefined') { DevState.items = []; DevState.filtered = []; DevState.selected = null; }
|
||||
}
|
||||
|
||||
async function autoLoadAfterConnect() {
|
||||
// Load default mode targets + apps in parallel
|
||||
const tasks = [];
|
||||
@@ -4119,6 +4131,11 @@ function fillSettingsForm(s) {
|
||||
document.getElementById('setAvailSuffix').value = av.suffix || '';
|
||||
updateAvailPreview();
|
||||
|
||||
// Policy-Backup (Git)
|
||||
const pb = s.policyBackup || {};
|
||||
document.getElementById('setPolicyGitPath').value = pb.gitRepoPath || '';
|
||||
document.getElementById('setPolicyGitPush').checked = !!pb.push;
|
||||
|
||||
// Branding-Logo Preview
|
||||
fillLogoPreview(s.branding);
|
||||
|
||||
@@ -4219,6 +4236,10 @@ function readSettingsForm() {
|
||||
detailPanel: document.getElementById('setColDetailPanelHex').value.trim() || ThemeDefaults.detailPanel,
|
||||
},
|
||||
},
|
||||
policyBackup: {
|
||||
gitRepoPath: document.getElementById('setPolicyGitPath').value.trim(),
|
||||
push: document.getElementById('setPolicyGitPush').checked,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5751,6 +5772,7 @@ async function loadPolicies() {
|
||||
['/api/policies/settingscatalog', 'Settings Catalog'],
|
||||
['/api/policies/compliance', 'Compliance'],
|
||||
['/api/policies/configuration', 'Konfigurationsprofile'],
|
||||
['/api/policies/administrativetemplate', 'Administrative Vorlagen'],
|
||||
];
|
||||
const settled = await Promise.allSettled(endpoints.map(([url]) => api(url)));
|
||||
let items = [];
|
||||
@@ -5891,7 +5913,8 @@ async function polExportSelected() {
|
||||
// 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];
|
||||
// Beide Typ-Felder akzeptieren: exportType (Export-Download) + policyType (Git-Snapshot).
|
||||
if (parsed && (parsed.exportType || parsed.policyType) && parsed.policy) return [parsed];
|
||||
if (parsed && parsed.policy) return [parsed]; // nachsichtig
|
||||
toast(`${fileName}: unbekanntes Policy-Format.`, 'err');
|
||||
return [];
|
||||
@@ -5922,6 +5945,9 @@ async function polHandleImportFiles(fileList) {
|
||||
toast(`${ok.length} Policy(s) importiert.`, 'ok', 'Import');
|
||||
}
|
||||
await loadPolicies();
|
||||
// Nach erfolgreichem Import optional Gruppen zuweisen (pro Policy).
|
||||
const assignable = ok.filter(r => r.newId);
|
||||
if (assignable.length && !State.readOnly) openPolAssignModal(assignable);
|
||||
} catch (e) {
|
||||
toast('Import fehlgeschlagen: ' + e.message, 'err');
|
||||
} finally {
|
||||
@@ -5929,8 +5955,204 @@ async function polHandleImportFiles(fileList) {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Post-Import: Policies pro Stück Include-/Exclude-Gruppen zuweisen
|
||||
// =============================================================
|
||||
const PolAssignState = { policies: [] };
|
||||
const POL_TYPE_LABELS = {
|
||||
SettingsCatalog: 'Settings Catalog',
|
||||
CompliancePolicy: 'Compliance',
|
||||
ConfigurationProfile: 'Konfigurationsprofil',
|
||||
AdministrativeTemplate: 'Administrative Vorlage',
|
||||
};
|
||||
|
||||
function openPolAssignModal(assignable) {
|
||||
PolAssignState.policies = assignable.map(r => ({
|
||||
id: r.newId,
|
||||
name: r.policyName || '(ohne Name)',
|
||||
exportType: r.exportType || '',
|
||||
include: [],
|
||||
exclude: [],
|
||||
}));
|
||||
renderPolAssignList();
|
||||
openModal('modalPolAssign');
|
||||
}
|
||||
|
||||
function renderPolAssignList() {
|
||||
const list = document.getElementById('polAssignList');
|
||||
if (!list) return;
|
||||
list.innerHTML = PolAssignState.policies.map((p, i) => `
|
||||
<div class="pol-assign-row" data-idx="${i}">
|
||||
<div class="pol-assign-head">
|
||||
<span class="pol-name">${escapeHtml(p.name)}</span>
|
||||
<span class="pol-type-badge">${escapeHtml(POL_TYPE_LABELS[p.exportType] || p.exportType || 'Policy')}</span>
|
||||
</div>
|
||||
<div class="pol-assign-pickers">
|
||||
${['include', 'exclude'].map(kind => `
|
||||
<div class="pol-assign-picker" data-kind="${kind}">
|
||||
<label class="lbl">${kind === 'include' ? 'Include-Gruppen' : 'Exclude-Gruppen'}</label>
|
||||
<div class="pol-assign-chips" data-idx="${i}" data-kind="${kind}"></div>
|
||||
<div class="pol-assign-search-wrap">
|
||||
<input type="text" class="input pol-assign-search" data-idx="${i}" data-kind="${kind}" placeholder="Gruppe suchen…" autocomplete="off">
|
||||
<div class="pol-assign-dropdown hidden" data-idx="${i}" data-kind="${kind}"></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
PolAssignState.policies.forEach((_, i) => {
|
||||
renderPolAssignChips(i, 'include');
|
||||
renderPolAssignChips(i, 'exclude');
|
||||
});
|
||||
updatePolAssignInfo();
|
||||
}
|
||||
|
||||
function renderPolAssignChips(idx, kind) {
|
||||
const box = document.querySelector(`.pol-assign-chips[data-idx="${idx}"][data-kind="${kind}"]`);
|
||||
if (!box) return;
|
||||
const arr = PolAssignState.policies[idx][kind];
|
||||
box.innerHTML = arr.map((g, j) => `
|
||||
<span class="pol-assign-chip ${kind === 'exclude' ? 'exclude' : ''}">
|
||||
${escapeHtml(g.name)}
|
||||
<button type="button" data-idx="${idx}" data-kind="${kind}" data-j="${j}" title="Entfernen">×</button>
|
||||
</span>
|
||||
`).join('');
|
||||
updatePolAssignInfo();
|
||||
}
|
||||
|
||||
function updatePolAssignInfo() {
|
||||
const info = document.getElementById('polAssignInfo');
|
||||
if (!info) return;
|
||||
const n = PolAssignState.policies.filter(p => p.include.length || p.exclude.length).length;
|
||||
info.textContent = n ? `${n} Policy(s) mit Zuweisung` : 'Keine Zuweisung gewählt';
|
||||
}
|
||||
|
||||
let _polAssignSearchTimer = null;
|
||||
function polAssignHideDropdowns() {
|
||||
document.querySelectorAll('.pol-assign-dropdown').forEach(d => { d.classList.add('hidden'); d.innerHTML = ''; });
|
||||
}
|
||||
|
||||
// Suche (debounced) — Event-Delegation auf der Liste.
|
||||
document.getElementById('polAssignList')?.addEventListener('input', e => {
|
||||
const input = e.target.closest('.pol-assign-search');
|
||||
if (!input) return;
|
||||
const idx = input.dataset.idx, kind = input.dataset.kind;
|
||||
const dd = document.querySelector(`.pol-assign-dropdown[data-idx="${idx}"][data-kind="${kind}"]`);
|
||||
const q = input.value.trim();
|
||||
clearTimeout(_polAssignSearchTimer);
|
||||
if (q.length < 2) { if (dd) { dd.classList.add('hidden'); dd.innerHTML = ''; } return; }
|
||||
if (dd) { dd.classList.remove('hidden'); dd.innerHTML = '<div class="msg">Suche…</div>'; }
|
||||
_polAssignSearchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const data = await api(`/api/groups/search?q=${encodeURIComponent(q)}`);
|
||||
const items = (data.items || []).slice(0, 25);
|
||||
if (!dd) return;
|
||||
if (!items.length) { dd.innerHTML = '<div class="msg">Keine Gruppen gefunden.</div>'; return; }
|
||||
dd.innerHTML = items.map(g => `
|
||||
<div class="opt" data-id="${escapeHtml(g.Id)}" data-name="${escapeHtml(g.DisplayName)}">
|
||||
<div>${escapeHtml(g.DisplayName)}</div>
|
||||
${g.Description ? `<div class="desc">${escapeHtml(g.Description)}</div>` : ''}
|
||||
</div>`).join('');
|
||||
} catch (err) {
|
||||
if (dd) dd.innerHTML = `<div class="msg">Suche fehlgeschlagen: ${escapeHtml(err.message)}</div>`;
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Klick auf ein Suchergebnis -> als Chip hinzufügen. Klick auf Chip-× -> entfernen.
|
||||
document.getElementById('polAssignList')?.addEventListener('click', e => {
|
||||
const opt = e.target.closest('.pol-assign-dropdown .opt');
|
||||
if (opt) {
|
||||
const dd = opt.closest('.pol-assign-dropdown');
|
||||
const idx = +dd.dataset.idx, kind = dd.dataset.kind;
|
||||
const id = opt.dataset.id, name = opt.dataset.name;
|
||||
const p = PolAssignState.policies[idx];
|
||||
const other = kind === 'include' ? 'exclude' : 'include';
|
||||
// Nicht doppelt, und nicht gleichzeitig include+exclude derselben Gruppe.
|
||||
if (!p[kind].some(g => g.id === id) && !p[other].some(g => g.id === id)) {
|
||||
p[kind].push({ id, name });
|
||||
renderPolAssignChips(idx, kind);
|
||||
}
|
||||
const input = document.querySelector(`.pol-assign-search[data-idx="${idx}"][data-kind="${kind}"]`);
|
||||
if (input) input.value = '';
|
||||
dd.classList.add('hidden'); dd.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const rm = e.target.closest('.pol-assign-chip button');
|
||||
if (rm) {
|
||||
const idx = +rm.dataset.idx, kind = rm.dataset.kind, j = +rm.dataset.j;
|
||||
PolAssignState.policies[idx][kind].splice(j, 1);
|
||||
renderPolAssignChips(idx, kind);
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.pol-assign-search-wrap')) polAssignHideDropdowns();
|
||||
});
|
||||
|
||||
document.getElementById('polAssignApply')?.addEventListener('click', async () => {
|
||||
const items = PolAssignState.policies
|
||||
.filter(p => p.include.length || p.exclude.length)
|
||||
.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),
|
||||
}));
|
||||
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');
|
||||
} catch (e) {
|
||||
toast('Zuweisung fehlgeschlagen: ' + e.message, 'err');
|
||||
} finally {
|
||||
clearLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// Alle Policies als Snapshot in den konfigurierten Git-Ordner schreiben + committen.
|
||||
async function polGitSnapshot() {
|
||||
setLoading('Erstelle Policy-Snapshot & committe…');
|
||||
try {
|
||||
const res = await api('/api/policies/git-snapshot', { method: 'POST', timeoutMs: 300000 });
|
||||
if (res.changed === false) {
|
||||
toast(res.message || 'Keine Änderungen seit dem letzten Snapshot.', 'info', 'Git-Snapshot');
|
||||
} else {
|
||||
let msg = res.message || `Snapshot committet (${res.total} Policies).`;
|
||||
if (res.pushError) msg += ` — Push-Fehler: ${res.pushError}`;
|
||||
toast(msg, res.pushError ? 'warn' : 'ok', 'Git-Snapshot');
|
||||
}
|
||||
} catch (e) {
|
||||
toast('Snapshot fehlgeschlagen: ' + e.message, 'err');
|
||||
} finally {
|
||||
clearLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Ordner-Auswahl für den Git-Repo-Pfad (Einstellungen).
|
||||
async function pickPolicyGitFolder() {
|
||||
try {
|
||||
const res = await api('/api/pickfolder', { method: 'POST', body: { title: 'Git-Repo-Ordner für Policy-Backup wählen' } });
|
||||
if (res && res.path) document.getElementById('setPolicyGitPath').value = res.path;
|
||||
} catch (e) {
|
||||
toast('Ordner-Auswahl fehlgeschlagen: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
document.getElementById('btnPickPolicyGit')?.addEventListener('click', pickPolicyGitFolder);
|
||||
|
||||
// --- Policies: Event-Wiring ---
|
||||
document.getElementById('btnPolRefresh')?.addEventListener('click', loadPolicies);
|
||||
document.getElementById('btnPolGitSnapshot')?.addEventListener('click', polGitSnapshot);
|
||||
document.getElementById('btnPolExport')?.addEventListener('click', polExportSelected);
|
||||
document.getElementById('btnPolImport')?.addEventListener('click', () => document.getElementById('polImportFile').click());
|
||||
document.getElementById('polImportFile')?.addEventListener('change', e => {
|
||||
|
||||
Reference in New Issue
Block a user