Multi-Tenant-Umschaltung + Pro-Tenant-Vorgaben, RPA deaktiviert
- Multi-Tenant: Settings-Schalter Single/Multi, Profile mit je eigener App-Registrierung, Topbar-Umschalter mit Sofort-Reconnect; verlustfreie Migration (Get-ActiveConnection/-TenantProfile, /api/tenants[/switch]). - Pro-Tenant-Overrides mit globalem Fallback: Abteilungs-Praefixe sowie Required-/Available-Gruppen-Naming (Get-DepartmentPrefixes/Get-GroupNaming tenant-bewusst; New-GroupEndpoint nutzt Resolver). - RPA komplett deaktiviert: Mode-Tab, globaler Settings-Abschnitt und Test-Anzeige entfernt. - Setup-Zwang gelockert: nur Tenant ID + Client ID Pflicht; Abteilungs- Praefixe optional (kein harter Setup-Blocker mehr). - api(): "Failed to fetch" -> klare Meldung "Server nicht erreichbar…". - Hilfe-Footer: "Entwickelt von WendeIT – Marco Wende". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+246
-38
@@ -47,6 +47,11 @@ async function api(path, options = {}) {
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(path, opts);
|
||||
} catch (e) {
|
||||
// "Failed to fetch" = der lokale Server hat nicht geantwortet (Prozess
|
||||
// beendet/abgestürzt oder PowerShell-Fenster geschlossen). Klartext geben.
|
||||
if (e && (e.name === 'AbortError')) throw e;
|
||||
throw new Error('Server nicht erreichbar — läuft das PowerShell-Fenster (Run.cmd) noch? Bitte die App neu starten und erneut versuchen.');
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
@@ -158,6 +163,56 @@ async function refreshStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Topbar-Mandantenumschalter: nur im Multi-Tenant-Modus sichtbar.
|
||||
async function refreshTenantSwitcher() {
|
||||
const sel = document.getElementById('tenantSwitch');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const t = await api('/api/tenants');
|
||||
if (!t.multiTenant || !(t.items || []).length) {
|
||||
sel.classList.add('hidden');
|
||||
sel.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = t.items.map(it =>
|
||||
`<option value="${escapeHtml(it.id)}" ${it.id === t.activeId ? 'selected' : ''}>${escapeHtml(it.label || it.tenantId)}</option>`
|
||||
).join('');
|
||||
sel.classList.remove('hidden');
|
||||
} catch {
|
||||
sel.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('tenantSwitch')?.addEventListener('change', async e => {
|
||||
const id = e.target.value;
|
||||
setLoading('Wechsle Mandant…');
|
||||
try {
|
||||
// Löst server-seitig Disconnect + Reconnect mit dem neuen Tenant aus.
|
||||
const s = await api('/api/tenants/switch', { method: 'POST', body: { id }, timeoutMs: 120000 });
|
||||
State.connected = !!s.connected;
|
||||
State.account = s.account;
|
||||
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?.(); }
|
||||
switchMainView('apps');
|
||||
if (s.connected) {
|
||||
toast('Verbunden mit ' + (s.tenantLabel || s.account || 'Mandant'), 'ok', 'Mandant gewechselt');
|
||||
autoLoadAfterConnect();
|
||||
} else {
|
||||
toast('Umgestellt, aber nicht verbunden.', 'warn');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('Wechsel fehlgeschlagen: ' + err.message, 'err');
|
||||
await refreshStatus();
|
||||
} finally {
|
||||
clearLoading();
|
||||
refreshTenantSwitcher();
|
||||
}
|
||||
});
|
||||
|
||||
function applyReadOnlyMode() {
|
||||
document.body.classList.toggle('read-only', State.readOnly);
|
||||
// Falls ein Schreib-Tab aktiv ist beim Wechsel in Read-Only -> Export-Tab zeigen
|
||||
@@ -2554,17 +2609,31 @@ let cgCheckTimer = null;
|
||||
|
||||
// Naming-Schemas aus Settings holen. Fallback auf Defaults wenn Settings noch
|
||||
// nicht geladen sind (z.B. waehrend des allerersten Init-Renders).
|
||||
// Aktives Tenant-Profil aus einem Settings-Objekt (oder null im Single-Modus).
|
||||
function activeTenantProfileFromSettings(s) {
|
||||
const c = s && s.connection;
|
||||
if (!c || !c.multiTenant || !Array.isArray(c.tenants) || !c.tenants.length) return null;
|
||||
return c.tenants.find(t => t.id === c.activeTenantId) || c.tenants[0];
|
||||
}
|
||||
|
||||
function getNamingFor(intent) {
|
||||
const s = SettingsState && SettingsState.current;
|
||||
const block = intent === 'available'
|
||||
? (s && s.availableGroupNaming)
|
||||
: (s && s.requiredGroupNaming);
|
||||
const isAvail = intent === 'available';
|
||||
const defPrefix = 'intune-win-app-';
|
||||
const defSuffix = intent === 'available' ? '-available' : '-required';
|
||||
return {
|
||||
prefix: (block && block.prefix) || defPrefix,
|
||||
suffix: (block && block.suffix) || defSuffix,
|
||||
};
|
||||
const defSuffix = isAvail ? '-available' : '-required';
|
||||
const gBlock = s && (isAvail ? s.availableGroupNaming : s.requiredGroupNaming);
|
||||
let prefix = (gBlock && gBlock.prefix) || defPrefix;
|
||||
let suffix = (gBlock && gBlock.suffix) || defSuffix;
|
||||
// Aktives Tenant-Profil kann pro Feld überschreiben (leer = globale Vorgabe).
|
||||
const prof = activeTenantProfileFromSettings(s);
|
||||
if (prof) {
|
||||
const pBlock = isAvail ? prof.availableGroupNaming : prof.requiredGroupNaming;
|
||||
if (pBlock) {
|
||||
if (pBlock.prefix && String(pBlock.prefix).trim()) prefix = pBlock.prefix;
|
||||
if (pBlock.suffix && String(pBlock.suffix).trim()) suffix = pBlock.suffix;
|
||||
}
|
||||
}
|
||||
return { prefix, suffix };
|
||||
}
|
||||
|
||||
function appSlug(appName) {
|
||||
@@ -3405,7 +3474,7 @@ async function openHelpModal() {
|
||||
try {
|
||||
const v = await api('/api/version');
|
||||
const el = document.getElementById('helpVersion');
|
||||
if (el) el.textContent = `Version ${v.version} · Build ${v.build}`;
|
||||
if (el) el.textContent = `Version ${v.version} · Build ${v.build} · Entwickelt von WendeIT – Marco Wende`;
|
||||
} catch {}
|
||||
if (helpLoaded) return;
|
||||
const contentEl = document.getElementById('helpContent');
|
||||
@@ -3756,17 +3825,12 @@ function rebuildCategoryFilterOptions() {
|
||||
function isSetupIncomplete(s) {
|
||||
if (!s) return true;
|
||||
const c = s.connection || {};
|
||||
const d = s.departments || {};
|
||||
const tenant = (c.tenantId || '').trim();
|
||||
const client = (c.clientId || '').trim();
|
||||
// Praefixe: 'prefixes'-Array bevorzugt, sonst alter Single-String 'prefix'.
|
||||
// Setup ist komplett, wenn mind. ein nicht-leerer Praefix (>= 2 Zeichen) existiert.
|
||||
let hasPrefix = false;
|
||||
if (Array.isArray(d.prefixes)) {
|
||||
for (const p of d.prefixes) { if (p && String(p).trim().length >= 2) { hasPrefix = true; break; } }
|
||||
}
|
||||
if (!hasPrefix && d.prefix && String(d.prefix).trim().length >= 2) { hasPrefix = true; }
|
||||
return !tenant || !client || !hasPrefix;
|
||||
// Nur Tenant ID + Client ID sind Pflicht fuer eine Verbindung. Abteilungs-
|
||||
// Praefixe und RPA-Gruppen sind optional — fehlen sie, zeigen die jeweiligen
|
||||
// Tabs lediglich einen Konfigurations-Hinweis (kein harter Setup-Blocker).
|
||||
return !tenant || !client;
|
||||
}
|
||||
|
||||
function applySetupNeededUI(needed) {
|
||||
@@ -3856,15 +3920,7 @@ function renderSettingsTestResult(res) {
|
||||
} else {
|
||||
lines.push(`<div class="set-test-line set-test-fail">${fail} Abteilungs-Gruppen-Lookup fehlgeschlagen: ${escapeHtml(d.error || '')}</div>`);
|
||||
}
|
||||
const r = res.rpa || {};
|
||||
if (r.ok) {
|
||||
lines.push(`<div class="set-test-line">${ok} RPA-Gruppen: <strong>${(r.found || []).length}</strong> von <strong>${r.expected}</strong> gefunden</div>`);
|
||||
} else if (r.reason === 'not_configured') {
|
||||
lines.push(`<div class="set-test-line set-test-warn">${fail} Keine RPA-Gruppen konfiguriert (optional — der RPA-Tab bleibt dann leer).</div>`);
|
||||
} else {
|
||||
const miss = (r.missing || []).map(escapeHtml).join(', ');
|
||||
lines.push(`<div class="set-test-line set-test-fail">${fail} RPA-Gruppen: ${(r.found || []).length} von ${r.expected} gefunden. Fehlt: <code>${miss}</code></div>`);
|
||||
}
|
||||
// RPA-Funktion ist deaktiviert — Test-Ergebnis dazu wird nicht angezeigt.
|
||||
const u = res.userSearch || {};
|
||||
if (u.ok) {
|
||||
lines.push(`<div class="set-test-line">${ok} Benutzer-Suche: ok (${(u.fields || []).join(', ')})</div>`);
|
||||
@@ -3887,6 +3943,136 @@ function renderSettingsTestResult(res) {
|
||||
return lines.join('');
|
||||
}
|
||||
|
||||
// --- Multi-Tenant Profil-Editor (Einstellungen) ---
|
||||
function tenantNewId() {
|
||||
return (window.crypto && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: 't-' + Date.now() + '-' + Math.random().toString(16).slice(2, 8);
|
||||
}
|
||||
|
||||
function applyConnModeUI(mode) {
|
||||
const multi = mode === 'multi';
|
||||
document.getElementById('setSingleConn').classList.toggle('hidden', multi);
|
||||
document.getElementById('setMultiConn').classList.toggle('hidden', !multi);
|
||||
}
|
||||
|
||||
function tenantRowHtml(t, active) {
|
||||
const id = t.id || tenantNewId();
|
||||
return `<div class="tenant-row" data-id="${escapeHtml(id)}">
|
||||
<div class="tenant-row-head">
|
||||
<label class="tenant-active"><input type="radio" name="tenantActive" value="${escapeHtml(id)}" ${active ? 'checked' : ''}><span>Aktiv</span></label>
|
||||
<input type="text" class="input tenant-label" placeholder="Bezeichnung (z.B. Kunde A)" value="${escapeHtml(t.label || '')}" autocomplete="off">
|
||||
<button type="button" class="btn btn-danger btn-sm tenant-remove" title="Profil entfernen">✕</button>
|
||||
</div>
|
||||
<div class="tenant-row-grid">
|
||||
<input type="text" class="input mono tenant-tid" placeholder="Tenant ID (GUID)" value="${escapeHtml(t.tenantId || '')}" spellcheck="false" autocomplete="off">
|
||||
<input type="text" class="input mono tenant-cid" placeholder="Client ID Read/Write (GUID)" value="${escapeHtml(t.clientId || '')}" spellcheck="false" autocomplete="off">
|
||||
<input type="text" class="input mono tenant-cidro" placeholder="Client ID Read-Only (optional)" value="${escapeHtml(t.clientIdRo || '')}" spellcheck="false" autocomplete="off">
|
||||
</div>
|
||||
<div class="tenant-row-overrides">
|
||||
<label class="tenant-sub-lbl">Abteilungs-Präfixe <span class="tenant-sub-hint">(leer = globale Vorgabe)</span></label>
|
||||
<textarea class="input mono tenant-prefixes" rows="2" spellcheck="false" placeholder="eine pro Zeile, z.B. abt-hm">${escapeHtml((t.prefixes || []).join('\n'))}</textarea>
|
||||
<label class="tenant-sub-lbl">Required-Gruppen-Naming <span class="tenant-sub-hint">(leer = globale Vorgabe)</span></label>
|
||||
<div class="tenant-naming-grid">
|
||||
<input type="text" class="input mono tenant-req-prefix" placeholder="Präfix" value="${escapeHtml((t.requiredGroupNaming && t.requiredGroupNaming.prefix) || '')}" spellcheck="false" autocomplete="off">
|
||||
<input type="text" class="input mono tenant-req-suffix" placeholder="Suffix" value="${escapeHtml((t.requiredGroupNaming && t.requiredGroupNaming.suffix) || '')}" spellcheck="false" autocomplete="off">
|
||||
</div>
|
||||
<label class="tenant-sub-lbl">Available-Gruppen-Naming <span class="tenant-sub-hint">(leer = globale Vorgabe)</span></label>
|
||||
<div class="tenant-naming-grid">
|
||||
<input type="text" class="input mono tenant-avail-prefix" placeholder="Präfix" value="${escapeHtml((t.availableGroupNaming && t.availableGroupNaming.prefix) || '')}" spellcheck="false" autocomplete="off">
|
||||
<input type="text" class="input mono tenant-avail-suffix" placeholder="Suffix" value="${escapeHtml((t.availableGroupNaming && t.availableGroupNaming.suffix) || '')}" spellcheck="false" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const TENANT_EMPTY_HINT = '<div class="form-hint tenant-empty">Noch keine Tenants. Füge unten einen hinzu.</div>';
|
||||
|
||||
function renderTenantRows(tenants, activeId) {
|
||||
const el = document.getElementById('setTenantList');
|
||||
const list = Array.isArray(tenants) ? tenants.filter(Boolean) : [];
|
||||
let active = activeId;
|
||||
if (!list.some(t => t.id === active) && list.length) active = list[0].id;
|
||||
el.innerHTML = list.length ? list.map(t => tenantRowHtml(t, t.id === active)).join('') : TENANT_EMPTY_HINT;
|
||||
wireTenantRowEvents();
|
||||
}
|
||||
|
||||
function wireTenantRowEvents() {
|
||||
document.querySelectorAll('#setTenantList .tenant-remove').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const row = btn.closest('.tenant-row');
|
||||
const wasActive = row.querySelector('input[name="tenantActive"]').checked;
|
||||
row.remove();
|
||||
if (wasActive) {
|
||||
const first = document.querySelector('#setTenantList input[name="tenantActive"]');
|
||||
if (first) first.checked = true;
|
||||
}
|
||||
if (!document.querySelector('#setTenantList .tenant-row')) {
|
||||
document.getElementById('setTenantList').innerHTML = TENANT_EMPTY_HINT;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addTenantRow() {
|
||||
const el = document.getElementById('setTenantList');
|
||||
if (!el.querySelector('.tenant-row')) el.innerHTML = '';
|
||||
const id = tenantNewId();
|
||||
const isFirst = !el.querySelector('.tenant-row');
|
||||
el.insertAdjacentHTML('beforeend', tenantRowHtml({ id }, isFirst));
|
||||
wireTenantRowEvents();
|
||||
el.querySelector(`.tenant-row[data-id="${CSS.escape(id)}"] .tenant-label`)?.focus();
|
||||
}
|
||||
|
||||
function tenantSplitLines(txt) {
|
||||
return (txt || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function readTenantRows() {
|
||||
const tenants = [];
|
||||
let activeTenantId = '';
|
||||
document.querySelectorAll('#setTenantList .tenant-row').forEach(row => {
|
||||
const id = row.dataset.id;
|
||||
tenants.push({
|
||||
id,
|
||||
label: row.querySelector('.tenant-label').value.trim(),
|
||||
tenantId: row.querySelector('.tenant-tid').value.trim(),
|
||||
clientId: row.querySelector('.tenant-cid').value.trim(),
|
||||
clientIdRo: row.querySelector('.tenant-cidro').value.trim(),
|
||||
// Pro-Tenant-Overrides; leer = globale Vorgabe gilt.
|
||||
prefixes: tenantSplitLines(row.querySelector('.tenant-prefixes').value),
|
||||
requiredGroupNaming: {
|
||||
prefix: row.querySelector('.tenant-req-prefix').value.trim(),
|
||||
suffix: row.querySelector('.tenant-req-suffix').value.trim(),
|
||||
},
|
||||
availableGroupNaming: {
|
||||
prefix: row.querySelector('.tenant-avail-prefix').value.trim(),
|
||||
suffix: row.querySelector('.tenant-avail-suffix').value.trim(),
|
||||
},
|
||||
});
|
||||
if (row.querySelector('input[name="tenantActive"]').checked) activeTenantId = id;
|
||||
});
|
||||
if (!activeTenantId && tenants.length) activeTenantId = tenants[0].id;
|
||||
return { tenants, activeTenantId };
|
||||
}
|
||||
|
||||
document.getElementById('setConnMode')?.addEventListener('change', e => {
|
||||
applyConnModeUI(e.target.value);
|
||||
// Beim erstmaligen Wechsel auf Multi ohne Profile: aus den Single-Feldern ein Profil vorbefüllen
|
||||
if (e.target.value === 'multi' && !document.querySelector('#setTenantList .tenant-row')) {
|
||||
const tid = document.getElementById('setTenantId').value.trim();
|
||||
const cid = document.getElementById('setClientId').value.trim();
|
||||
if (tid || cid) {
|
||||
renderTenantRows([{
|
||||
id: tenantNewId(), label: 'Standard',
|
||||
tenantId: tid, clientId: cid,
|
||||
clientIdRo: document.getElementById('setClientIdRo').value.trim(),
|
||||
}], null);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById('btnAddTenant')?.addEventListener('click', addTenantRow);
|
||||
|
||||
function fillSettingsForm(s) {
|
||||
const c = s.connection || {};
|
||||
document.getElementById('setTenantId').value = c.tenantId || '';
|
||||
@@ -3895,6 +4081,12 @@ function fillSettingsForm(s) {
|
||||
document.getElementById('setScopes').value = (c.scopes || []).join('\n');
|
||||
document.getElementById('setScopesRo').value = (c.scopesRo || []).join('\n');
|
||||
|
||||
// Verbindungsmodus + Multi-Tenant-Profile
|
||||
const mode = c.multiTenant ? 'multi' : 'single';
|
||||
document.getElementById('setConnMode').value = mode;
|
||||
renderTenantRows(Array.isArray(c.tenants) ? c.tenants : [], c.activeTenantId || '');
|
||||
applyConnModeUI(mode);
|
||||
|
||||
// Backward-compat: lese 'prefixes' (Array) bevorzugt, falle sonst auf
|
||||
// alten Single-String 'prefix' zurueck. Beide werden in die Textarea
|
||||
// gemerged (gleiche Logik wie Get-DepartmentPrefixes im Backend).
|
||||
@@ -3911,7 +4103,6 @@ function fillSettingsForm(s) {
|
||||
}
|
||||
document.getElementById('setDeptPrefixes').value = out.join('\n');
|
||||
})();
|
||||
document.getElementById('setRpaGroups').value = ((s.rpa && s.rpa.groupNames) || []).join('\n');
|
||||
|
||||
const fields = (s.userSearch && s.userSearch.fields) || [];
|
||||
document.getElementById('setUsfDisplayName').checked = fields.includes('displayName');
|
||||
@@ -3972,14 +4163,32 @@ function readSettingsForm() {
|
||||
if (document.getElementById('setUsfMail').checked) fields.push('mail');
|
||||
if (document.getElementById('setUsfDepartment').checked) fields.push('department');
|
||||
|
||||
const mode = document.getElementById('setConnMode').value;
|
||||
const multiTenant = mode === 'multi';
|
||||
const connection = {
|
||||
multiTenant,
|
||||
scopes: splitLines(document.getElementById('setScopes').value),
|
||||
scopesRo: splitLines(document.getElementById('setScopesRo').value),
|
||||
};
|
||||
if (multiTenant) {
|
||||
const { tenants, activeTenantId } = readTenantRows();
|
||||
connection.tenants = tenants;
|
||||
connection.activeTenantId = activeTenantId;
|
||||
// Aktives Profil in die flachen Felder spiegeln (Rückwärts-Kompatibilität;
|
||||
// Status-Anzeige und evtl. Alt-Code lesen weiter connection.tenantId/clientId).
|
||||
const active = tenants.find(t => t.id === activeTenantId) || tenants[0] || {};
|
||||
connection.tenantId = active.tenantId || '';
|
||||
connection.clientId = active.clientId || '';
|
||||
connection.clientIdRo = active.clientIdRo || '';
|
||||
} else {
|
||||
connection.tenantId = document.getElementById('setTenantId').value.trim();
|
||||
connection.clientId = document.getElementById('setClientId').value.trim();
|
||||
connection.clientIdRo = document.getElementById('setClientIdRo').value.trim();
|
||||
// tenants/activeTenantId nicht mitschicken -> Merge behält vorhandene Profile.
|
||||
}
|
||||
|
||||
return {
|
||||
connection: {
|
||||
tenantId: document.getElementById('setTenantId').value.trim(),
|
||||
clientId: document.getElementById('setClientId').value.trim(),
|
||||
clientIdRo: document.getElementById('setClientIdRo').value.trim(),
|
||||
scopes: splitLines(document.getElementById('setScopes').value),
|
||||
scopesRo: splitLines(document.getElementById('setScopesRo').value),
|
||||
},
|
||||
connection,
|
||||
departments: {
|
||||
// Neuer Listen-Form: Praefixe aus Textarea. Alten Single-String 'prefix'
|
||||
// beim Save auf leer setzen, damit nicht beide Quellen divergieren —
|
||||
@@ -3987,9 +4196,6 @@ function readSettingsForm() {
|
||||
prefixes: splitLines(document.getElementById('setDeptPrefixes').value),
|
||||
prefix: '',
|
||||
},
|
||||
rpa: {
|
||||
groupNames: splitLines(document.getElementById('setRpaGroups').value),
|
||||
},
|
||||
userSearch: {
|
||||
fields,
|
||||
},
|
||||
@@ -4036,6 +4242,7 @@ async function saveSettings() {
|
||||
try {
|
||||
const res = await api('/api/settings', { method: 'PUT', body: payload });
|
||||
SettingsState.current = res.settings;
|
||||
refreshTenantSwitcher(); // Modus/Profil-Änderungen im Topbar-Umschalter spiegeln
|
||||
// Theme + Branding sofort live anwenden (kein Reload noetig)
|
||||
if (res.settings) {
|
||||
applyThemeColors(res.settings.theme && res.settings.theme.colors);
|
||||
@@ -4317,6 +4524,7 @@ document.getElementById('btnRestoreColors')?.addEventListener('click', () => {
|
||||
console.warn('Settings konnten beim Init nicht geladen werden:', e.message);
|
||||
}
|
||||
await refreshStatus();
|
||||
refreshTenantSwitcher();
|
||||
if (State.connected) autoLoadAfterConnect();
|
||||
// First-Run: wenn kritische Felder leer sind, Settings-Modal automatisch oeffnen.
|
||||
if (setupNeeded) {
|
||||
|
||||
Reference in New Issue
Block a user