v0.1.24 — App Report, CSV Import, Read-Only Mode, Group Management

- App Report: Installationszähler pro App via Graph $batch (deviceStatuses)
- Geräte-Export: CSV mit Maschinennamen pro App (GET /api/apps/devicestatus)
- CSV-Import: Massenimport von Benutzern in Gruppen mit Header-Erkennung
- Read-Only-Modus: Schreib-Tabs/Buttons automatisch ausblenden
- Ergebnis-Banner nach CSV-Import (hinzugefügt/bereits Mitglied/Fehler/nicht gefunden)
- Dokumentation aktualisiert (README, help.md, CHANGELOG)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 07:12:49 +02:00
co-authored by Claude Sonnet 4.6
commit 34811ff583
24 changed files with 17052 additions and 0 deletions
+994
View File
@@ -0,0 +1,994 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Intune Manager</title>
<script>
// Theme vor erstem Render anwenden — verhindert Flash
(function () {
try {
var saved = localStorage.getItem('iam-theme');
var theme = saved || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
} catch (e) { /* localStorage-Block */ }
})();
</script>
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/vendor/flatpickr.min.css">
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%23111111'/%3E%3Ctext x='50' y='70' font-size='62' text-anchor='middle' fill='white' font-family='Inter,Arial' font-weight='600'%3EI%3C/text%3E%3C/svg%3E">
</head>
<body>
<!-- HEADER -->
<header class="topbar">
<div class="brand">
<div id="brandLogo" class="logo"><span class="logo-letter">I</span></div>
<div class="brand-text">
<div class="brand-name">Intune Manager</div>
</div>
</div>
<nav class="topbar-nav" id="topbarNav" aria-label="Hauptnavigation">
<button class="nav-tab active" data-view="apps" aria-selected="true">App Management</button>
<button class="nav-tab" data-view="groups" aria-selected="false">Group Management</button>
<button class="nav-tab" data-view="report" aria-selected="false">App Report</button>
</nav>
<div class="header-actions">
<button id="btnSettings" class="icon-btn icon-btn-header" title="Einstellungen" aria-label="Einstellungen öffnen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.86l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.86-.34 1.7 1.7 0 0 0-1.04 1.57V21a2 2 0 0 1-4 0v-.09a1.7 1.7 0 0 0-1.1-1.57 1.7 1.7 0 0 0-1.86.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.86 1.7 1.7 0 0 0-1.57-1.04H3a2 2 0 0 1 0-4h.09A1.7 1.7 0 0 0 4.66 9a1.7 1.7 0 0 0-.34-1.86l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.86.34H9a1.7 1.7 0 0 0 1.04-1.57V3a2 2 0 0 1 4 0v.09c0 .68.42 1.29 1.04 1.57.62.27 1.36.13 1.86-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.86V9a1.7 1.7 0 0 0 1.57 1.04H21a2 2 0 0 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>
</button>
<button id="btnHelp" class="icon-btn icon-btn-header" title="Hilfe" aria-label="Hilfe öffnen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</button>
<button id="btnThemeToggle" class="theme-toggle" role="switch" aria-checked="false" title="Dark/Light Mode umschalten" aria-label="Dark/Light Mode umschalten">
<span class="theme-track">
<svg class="theme-ico theme-ico-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg class="theme-ico theme-ico-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<span class="theme-knob"></span>
</span>
</button>
<div id="connStatus" class="conn-pill conn-off" title="Verbindungsstatus">
<span class="dot"></span>
<span class="conn-text">Offline</span>
</div>
<button id="btnDisconnect" class="icon-btn icon-btn-header hidden" title="Trennen" aria-label="Verbindung trennen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</div>
</header>
<!-- ONBOARDING (MSAL.js Browser-Popup) -->
<div id="onboarding" class="onboarding hidden">
<div class="onboarding-card">
<!-- View 1: idle -->
<div data-onboarding-view="idle">
<div class="onboarding-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M12 2 L4 6 v6 c0 5 3.5 9 8 10 c4.5-1 8-5 8-10 V6 z"/>
<path d="M9 12 l2 2 l4-4"/>
</svg>
</div>
<h1 class="onboarding-title">Mit Microsoft anmelden</h1>
<p class="onboarding-text">
Es öffnet sich ein Microsoft-Anmeldefenster. Wähle den gewünschten Account aus.
</p>
<div id="onboardingSetupHint" class="banner-warning hidden">
Konfiguration unvollständig — bitte zuerst <a href="#" id="onboardingOpenSettings">Einstellungen</a> öffnen und Tenant ID, Client ID und Abteilungs-Präfix setzen.
</div>
<button id="btnConnect" class="btn btn-primary btn-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M11.4 2.4H2.4v9h9v-9zm10.2 0h-9v9h9v-9zm-10.2 10.2H2.4v9h9v-9zm10.2 0h-9v9h9v-9z"/></svg>
Anmeldung starten
</button>
<div class="onboarding-meta">
Lese- und Zuweisungs-Rechte für Intune-Apps und Gruppenmitgliedschaften.
</div>
</div>
<!-- View 2: waiting -->
<div data-onboarding-view="awaiting" class="hidden">
<div class="dc-spinner-wrap">
<div class="spinner"></div>
</div>
<h2 class="onboarding-title">Anmeldung läuft</h2>
<p class="onboarding-text">
Bitte im Microsoft-Anmeldefenster fortfahren. Wir holen es automatisch in den Vordergrund —
falls es trotzdem im Hintergrund liegt, schau in der Taskleiste.
</p>
<button id="btnCancelConnect" class="btn btn-text">Abbrechen</button>
</div>
<!-- View 3: error -->
<div data-onboarding-view="error" class="hidden">
<div class="onboarding-icon onboarding-icon-error">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
</div>
<h2 class="onboarding-title">Anmeldung fehlgeschlagen</h2>
<p class="onboarding-text" id="dcError">Unbekannter Fehler.</p>
<button id="btnRetryConnect" class="btn btn-primary btn-lg">Erneut versuchen</button>
</div>
</div>
</div>
<!-- MAIN -->
<main class="layout" id="layout" data-view="apps">
<!-- LEFT: TARGETS -->
<section class="panel panel-targets">
<div class="panel-head">
<div class="panel-title">
<h2>Abteilungen &amp; Benutzer</h2>
<span id="targetCount" class="count-badge">0</span>
</div>
<button id="btnReloadTargets" class="icon-btn" title="Neu laden" aria-label="Neu laden">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
</button>
</div>
<div class="mode-segment" role="tablist">
<button class="seg active" data-mode="dept" role="tab">Abteilung</button>
<button class="seg" data-mode="rpa" role="tab">RPA</button>
<button class="seg" data-mode="user" role="tab">Benutzer</button>
</div>
<div class="search-wrap">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="targetSearch" placeholder="Filtern..." class="input input-search" autocomplete="off">
<span id="userSearchHint" class="search-hint hidden">min. 2 Zeichen</span>
</div>
<div class="select-toolbar">
<div class="select-actions">
<button id="btnTargetsSelectAll" class="link-btn">Alle</button>
<span class="dotsep">·</span>
<button id="btnTargetsClear" class="link-btn">Leeren</button>
</div>
</div>
<div id="pinnedTargets" class="pinned-targets hidden" aria-label="Aktuelle Auswahl">
<!-- populated by JS -->
</div>
<div id="targetList" class="list-area target-list">
<!-- populated by JS -->
</div>
</section>
<!-- CENTER: APPS -->
<section class="panel panel-apps">
<div class="panel-head">
<div class="panel-title">
<h2>Apps</h2>
<span id="appCount" class="count-badge">0</span>
</div>
<div class="panel-head-actions">
<button id="btnReloadApps" class="icon-btn" title="Neu laden (Cache umgehen)" aria-label="Neu laden">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
</button>
</div>
</div>
<div class="apps-toolbar">
<div class="search-wrap flex-1">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="appSearch" placeholder="Apps durchsuchen..." class="input input-search" autocomplete="off">
</div>
<select id="appFilter" class="select">
<option value="all">Alle Apps</option>
<option value="none">Ohne Zuweisungen</option>
<option value="available">Nur Available</option>
<option value="required">Nur Required</option>
<option value="multipleAvail">Mehrere Available</option>
<option value="multipleReq">Mehrere Required</option>
<option value="allUsers">All Users</option>
<option value="allDevices">All Devices</option>
<option value="srcIntune">Nur Intune-Apps</option>
<!-- Vendor-Optionen werden dynamisch von rebuildVendorFilterOptions() ergaenzt -->
</select>
<select id="appCategoryFilter" class="select" title="Nach App-Kategorie filtern">
<option value="">Alle Kategorien</option>
<!-- Kategorie-Optionen werden dynamisch von rebuildCategoryFilterOptions() befuellt -->
</select>
</div>
<div class="filter-chips">
<label class="check-chip">
<input type="checkbox" id="chkOnlyMember">
<span>Bereits Gruppenmitglied</span>
</label>
<label class="check-chip">
<input type="checkbox" id="chkHideNative">
<span>All Users/Devices ausblenden</span>
</label>
<div class="date-filter-chip" id="dateFilterChip">
<span class="date-filter-label" title="Nur Apps anzeigen die im angegebenen Zeitraum geändert wurden">Geändert</span>
<input type="date" id="dateModFrom" class="date-input" title="Von (inkl.)">
<span class="date-sep">–</span>
<input type="date" id="dateModTo" class="date-input" title="Bis (inkl.)">
<button type="button" id="dateModClear" class="date-clear" hidden title="Datum-Filter zurücksetzen">×</button>
</div>
<div class="legend-group">
<span class="legend-item" title="Alle ausgewählten Empfänger sind bereits Mitglied"><span class="row-status full">✓</span> bereits zugewiesen</span>
<span class="legend-item" title="Teilweise Mitglied"><span class="row-status partial">◐</span> teilweise</span>
</div>
</div>
<div id="appList" class="list-area apps-list">
<!-- populated by JS -->
</div>
</section>
<!-- RIGHT: SESSION -->
<aside class="panel panel-side">
<div class="panel-head">
<div class="panel-title">
<h2>Geplante Zuweisungen</h2>
<span id="sessionCount" class="count-badge accent">0</span>
</div>
</div>
<div class="side-pane" id="sidePaneSession">
<div id="sessionList" class="session-list">
<!-- populated by JS -->
</div>
<div class="session-summary">
<div class="summary-section">
<div class="summary-section-title">Ausgewählte Empfänger</div>
<div class="summary-row sub">
<span class="muted">Abteilungsgruppen</span>
<span id="sumGroupsSel">0</span>
</div>
<div class="summary-row sub">
<span class="muted">Einzelbenutzer</span>
<span id="sumUsersSel">0</span>
</div>
</div>
<div class="summary-section">
<div class="summary-section-title">Zuweisungen erhalten</div>
<div class="summary-row sub">
<span class="muted">Abteilungsgruppen</span>
<span id="sumGroupsOps">0</span>
</div>
<div class="summary-row sub">
<span class="muted">Einzelbenutzer</span>
<span id="sumUsersOps">0</span>
</div>
</div>
<div class="summary-row total">
<span>Gesamt-Vorgänge</span>
<span id="sumTotal">0</span>
</div>
</div>
<button id="btnApply" class="btn btn-apply" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>
<span id="btnApplyLabel">Zuweisungen ausführen</span>
</button>
<button id="btnSessionClear" class="link-btn link-btn-danger" hidden>Geplante Zuweisungen leeren</button>
</div>
<div class="side-pane hidden" id="sidePaneExisting">
<div class="side-pane-hint">Apps, in denen die ausgewählten Empfänger bereits Mitglied einer zugewiesenen Gruppe sind.</div>
<div id="existingList" class="existing-list">
<!-- populated by JS -->
</div>
</div>
</aside>
</main>
<!-- GROUP MANAGEMENT VIEW -->
<section class="group-mgmt-view hidden" id="groupMgmtView">
<div class="gex-body group-mgmt-body">
<!-- Tab-Leiste -->
<div class="gex-tab-bar group-mgmt-tabs">
<button class="gex-tab active" data-tab="export">Mitglieder &amp; Export</button>
<button class="gex-tab gex-add-pane" data-tab="add">Benutzer hinzufügen</button>
<button class="gex-tab gex-add-pane" data-tab="import">CSV-Import</button>
</div>
<div class="gex-columns group-mgmt-columns">
<!-- Linke Spalte: Gruppensuche -->
<div class="gex-left">
<div class="gex-section-title">Gruppe suchen</div>
<div class="search-wrap" style="margin-bottom:10px;">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="gexSearch" class="input input-search" placeholder="Gruppenname eingeben…" autocomplete="off">
</div>
<div id="gexSearchResults" class="gex-results">
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
</div>
</div>
<!-- Rechte Spalte: Export-Tab -->
<div class="gex-right" id="gexPaneExport">
<div class="gex-section-title" id="gexMembersTitle">Mitglieder</div>
<div class="search-wrap" style="margin-bottom:10px;">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="gexMemberFilter" class="input input-search" placeholder="Mitglieder filtern…" autocomplete="off">
</div>
<div id="gexMembersArea" class="gex-members-area">
<div class="gex-hint">Gruppe auswählen um Mitglieder zu laden.</div>
</div>
<div class="group-mgmt-foot">
<span id="gexStats" class="muted"></span>
<button class="btn btn-primary" id="gexExportBtn" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Als CSV exportieren
</button>
</div>
</div>
<!-- Rechte Spalte: CSV-Import -->
<div class="gex-right hidden" id="gexPaneImport">
<div class="gex-section-title">UPNs einfügen oder CSV hochladen</div>
<div class="csv-import-input-row">
<textarea id="csvImportText" class="csv-import-textarea" placeholder="Eine UPN/E-Mail pro Zeile, z.B.&#10;max.mustermann@firma.de&#10;erika.muster@firma.de&#10;&#10;Oder erste Spalte einer CSV (Header wird erkannt)."></textarea>
<label class="csv-upload-label" title="CSV-Datei auswählen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
CSV laden
<input type="file" id="csvImportFile" accept=".csv,.txt" style="display:none">
</label>
</div>
<div class="csv-import-actions">
<button class="btn btn-secondary btn-sm" id="csvResolveBtn">Benutzer auflösen</button>
<span id="csvResolveStats" class="muted" style="font-size:12px;"></span>
</div>
<!-- Vorschau-Tabelle -->
<div id="csvPreviewWrap" class="csv-preview-wrap hidden">
<div class="gex-section-title" style="margin-top:10px;">
Vorschau
<span id="csvFoundBadge" class="count-badge" style="background:var(--success,#4ade80);color:#000;margin-left:6px;">0 gefunden</span>
<span id="csvNotFoundBadge" class="count-badge" style="background:var(--error,#f87171);color:#fff;margin-left:4px;">0 nicht gefunden</span>
</div>
<div class="csv-preview-table-wrap">
<table class="csv-preview-table">
<thead><tr><th></th><th>UPN / E-Mail</th><th>Name</th><th>Status</th></tr></thead>
<tbody id="csvPreviewBody"></tbody>
</table>
</div>
</div>
<!-- Ergebnis-Banner (nach Import) -->
<div id="csvResultBanner" class="csv-result-banner hidden">
<div class="csv-result-stats">
<span id="csvResAdded" class="csv-res-chip csv-res-ok">0 hinzugefügt</span>
<span id="csvResExist" class="csv-res-chip csv-res-warn">0 bereits Mitglied</span>
<span id="csvResError" class="csv-res-chip csv-res-err">0 Fehler</span>
<span id="csvResSkipped" class="csv-res-chip csv-res-dim">0 nicht gefunden</span>
</div>
</div>
<div class="group-mgmt-foot">
<span class="muted" id="csvImportNote" style="font-size:11px;">Gruppe links auswählen, dann Benutzer auflösen.</span>
<button class="btn btn-primary" id="csvImportBtn" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
Gefundene importieren
</button>
</div>
</div>
<!-- Rechte Spalte: Benutzer hinzufügen -->
<div class="gex-right hidden" id="gexPaneAdd">
<div class="gex-section-title">Benutzer suchen</div>
<div class="search-wrap" style="margin-bottom:10px;">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="gexUserSearch" class="input input-search" placeholder="Name, UPN oder E-Mail…" autocomplete="off">
</div>
<div id="gexUserResults" class="gex-results gex-add-results">
<div class="gex-hint">Mindestens 2 Zeichen eingeben.</div>
</div>
<div class="gex-section-title" style="margin-top:12px;">
Ausgewählt <span id="gexSelectedCount" class="count-badge">0</span>
</div>
<div id="gexSelectedUsers" class="gex-selected-area">
<div class="gex-hint">Noch keine Benutzer ausgewählt.</div>
</div>
<div class="group-mgmt-foot">
<button class="btn btn-primary" id="gexAddBtn" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
Zu Gruppe hinzufügen
</button>
</div>
</div>
</div>
</div>
</section>
<!-- APP REPORT VIEW -->
<section class="app-report-view hidden" id="appReportView">
<div class="app-report-toolbar">
<div class="search-wrap">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="reportSearch" class="input input-search" placeholder="App suchen…" autocomplete="off">
</div>
<span id="reportCount" class="count-badge">0</span>
<button class="btn btn-secondary btn-sm" id="btnReportRefresh" title="Daten neu laden">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
Neu laden
</button>
<button class="btn btn-primary btn-sm" id="btnReportExport" disabled>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Als CSV exportieren
</button>
</div>
<div class="app-report-table-wrap">
<table class="app-report-table" id="reportTable">
<thead>
<tr>
<th class="sortable" data-col="AppName">App-Name <span class="sort-ico"></span></th>
<th class="sortable" data-col="Publisher">Publisher <span class="sort-ico"></span></th>
<th class="sortable" data-col="AppType">Typ <span class="sort-ico"></span></th>
<th class="sortable" data-col="Version">Version <span class="sort-ico"></span></th>
<th class="sortable num" data-col="InstalledDeviceCount">Installiert <span class="sort-ico"></span></th>
<th class="sortable num" data-col="PendingInstallDeviceCount">Ausstehend <span class="sort-ico"></span></th>
<th class="sortable num" data-col="FailedDeviceCount">Fehlgeschlagen <span class="sort-ico"></span></th>
<th class="sortable num" data-col="NotApplicableDeviceCount">Nicht anwendbar <span class="sort-ico"></span></th>
<th class="report-th-export" title="Gerätestatus-Export"></th>
</tr>
</thead>
<tbody id="reportBody">
<tr><td colspan="9" class="report-hint">Klicke auf „Neu laden" um Daten zu laden.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- TOAST CONTAINER -->
<div id="toasts" class="toasts"></div>
<!-- LOADING OVERLAY -->
<div id="loading" class="loading hidden">
<div class="loading-card">
<div class="spinner"></div>
<div id="loadingText" class="loading-text">Bitte warten...</div>
</div>
</div>
<!-- MODAL: Create Required Group -->
<div id="modalCreateGroup" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-lg">
<div class="modal-head">
<h3>Zuweisung hinzufügen</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="lbl" for="cgAppName">App</label>
<input type="text" id="cgAppName" class="input" readonly>
</div>
<div class="form-group">
<label class="lbl">Typ der Zuweisung</label>
<div class="cg-intent-grid">
<div class="cg-intent-col cg-intent-required">
<div class="cg-intent-head">
<span class="cg-intent-badge req">REQUIRED</span>
<span class="cg-intent-sub">App wird zwingend installiert</span>
</div>
<label class="cg-mode">
<input type="radio" name="cgMode" value="req-group" checked>
<span class="cg-mode-body">
<span class="cg-mode-title">Neue Gruppe</span>
<span class="cg-mode-desc">Neue Required-Gruppe nach Naming-Schema erstellen und zuweisen.</span>
</span>
</label>
<label class="cg-mode">
<input type="radio" name="cgMode" value="req-allusers">
<span class="cg-mode-body">
<span class="cg-mode-title">All Users</span>
<span class="cg-mode-desc">Native Required-Zuweisung an alle lizenzierten Benutzer.</span>
</span>
</label>
<label class="cg-mode">
<input type="radio" name="cgMode" value="req-alldevices">
<span class="cg-mode-body">
<span class="cg-mode-title">All Devices</span>
<span class="cg-mode-desc">Native Required-Zuweisung an alle Geräte des Tenants.</span>
</span>
</label>
</div>
<div class="cg-intent-col cg-intent-available">
<div class="cg-intent-head">
<span class="cg-intent-badge avail">AVAILABLE</span>
<span class="cg-intent-sub">Selbstinstallation via Firmenportal</span>
</div>
<label class="cg-mode">
<input type="radio" name="cgMode" value="avail-group">
<span class="cg-mode-body">
<span class="cg-mode-title">Neue Gruppe</span>
<span class="cg-mode-desc">Neue Available-Gruppe nach Naming-Schema erstellen und zuweisen.</span>
</span>
</label>
<label class="cg-mode">
<input type="radio" name="cgMode" value="avail-allusers">
<span class="cg-mode-body">
<span class="cg-mode-title">All Users</span>
<span class="cg-mode-desc">App erscheint im Firmenportal aller Benutzer.</span>
</span>
</label>
<label class="cg-mode">
<input type="radio" name="cgMode" value="avail-alldevices">
<span class="cg-mode-body">
<span class="cg-mode-title">All Devices</span>
<span class="cg-mode-desc">App ist auf allen Geräten verfügbar.</span>
</span>
</label>
</div>
</div>
</div>
<div class="form-group" id="cgGroupNameGroup">
<label class="lbl" for="cgGroupName">Gruppen-Name (Vorschlag — kann angepasst werden)</label>
<input type="text" id="cgGroupName" class="input">
<div id="cgStatus" class="form-hint"></div>
</div>
<label class="check-chip" id="cgAssignToAppWrap">
<input type="checkbox" id="cgAssignToApp" checked>
<span>Gruppe direkt der App zuweisen</span>
</label>
</div>
<div class="modal-foot">
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-primary" id="cgCreate">Erstellen</button>
</div>
</div>
</div>
<!-- MODAL: Apply Result -->
<div id="modalResult" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-lg">
<div class="modal-head">
<h3 id="resHeadline">Ergebnis</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div id="resBanner" class="res-banner"></div>
<div class="result-stats">
<div class="rstat ok"><div class="num" id="resOk">0</div><div class="lbl">Erfolgreich</div></div>
<div class="rstat warn"><div class="num" id="resSkip">0</div><div class="lbl">Übersprungen</div></div>
<div class="rstat err"><div class="num" id="resErr">0</div><div class="lbl">Fehler</div></div>
<div class="rstat tot"><div class="num" id="resTotal">0</div><div class="lbl">Gesamt</div></div>
</div>
<div id="resDetails" class="res-details"></div>
<div id="resReportLink" class="report-link"></div>
<div id="resDebug" class="res-debug"></div>
</div>
<div class="modal-foot">
<button class="btn btn-secondary" data-close>Schließen</button>
</div>
</div>
</div>
<!-- MODAL: Mitglieder einer Gruppe einsehen / einzelne User uebernehmen -->
<div id="modalMembers" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-lg">
<div class="modal-head">
<h3 id="mmTitle">Mitglieder</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div class="search-wrap" style="margin: 0 0 12px 0;">
<svg class="search-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="mmSearch" class="input input-search" placeholder="Mitglieder filtern...">
</div>
<div class="mm-toolbar">
<button class="link-btn" id="mmSelectAll">Alle</button>
<span class="dotsep">·</span>
<button class="link-btn" id="mmClear">Leeren</button>
</div>
<div id="mmList" class="member-list"></div>
</div>
<div class="modal-foot">
<span id="mmInfo" class="muted" style="margin-right: auto;">0 ausgewählt</span>
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-primary" id="mmApply">Als Benutzer-Auswahl übernehmen</button>
</div>
</div>
</div>
<!-- MODAL: Confirm Apply -->
<div id="modalConfirm" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box">
<div class="modal-head">
<h3>Zuweisungen ausführen?</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div id="confirmText" class="confirm-text"></div>
<div class="confirm-warn">Hinzufügen ist sofort wirksam — Mitgliedschaften können später manuell entfernt werden.</div>
</div>
<div class="modal-foot">
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-apply" id="btnConfirmApply">Ja, ausführen</button>
</div>
</div>
</div>
<!-- MODAL: Hilfe -->
<div id="modalHelp" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-xl">
<div class="modal-head">
<h3>Hilfe &amp; Funktionsverzeichnis</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body help-body">
<aside class="help-toc" id="helpToc">
<div class="help-toc-title">Inhalt</div>
<ul></ul>
</aside>
<div class="help-content md-content" id="helpContent">
<div class="help-loading">Hilfe wird geladen…</div>
</div>
</div>
<div class="modal-foot">
<span id="helpVersion" class="help-version-badge"></span>
<span class="modal-foot-spacer"></span>
<button class="btn btn-primary" data-close>Schließen</button>
</div>
</div>
</div>
<!-- MODAL: App umbenennen -->
<div id="modalRenameApp" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box">
<div class="modal-head">
<h3>App umbenennen</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="lbl">Aktueller Name</label>
<div id="raOldName" class="ra-old-name"></div>
</div>
<div class="form-group">
<label class="lbl" for="raNewName">Neuer Name</label>
<input type="text" id="raNewName" class="input" maxlength="256" autocomplete="off" spellcheck="false">
<div id="raError" class="form-hint form-hint-err"></div>
</div>
<div class="form-hint">Wirkt sofort in Intune. Vendor-verwaltete Apps (PatchMyPC, Robopack, …) können nicht hier umbenannt werden.</div>
</div>
<div class="modal-foot">
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-primary" id="raSubmit">Umbenennen</button>
</div>
</div>
</div>
<!-- MODAL: Setup-Datei aktualisieren -->
<div id="modalUpdateContent" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box">
<div class="modal-head">
<h3>Setup-Datei aktualisieren</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="lbl">App</label>
<div id="ucAppName" class="ra-old-name"></div>
</div>
<div class="banner-warning">
<strong>Achtung:</strong> Dies lädt eine neue Setup-Datei hoch und aktiviert sie als neue Version der App. Alle zugewiesenen Geräte erhalten die neue Version beim nächsten Intune-Sync. Vorgang ist nicht rückgängig zu machen.
</div>
<div class="form-group">
<label class="lbl" for="ucFilePath">Pfad zur neuen Datei (lokal auf dem Server-Rechner)</label>
<div class="uc-path-row">
<input type="text" id="ucFilePath" class="input mono" spellcheck="false" autocomplete="off" placeholder="C:\Pakete\app-2.0\app.intunewin">
<button type="button" class="btn btn-secondary" id="ucBrowse">Durchsuchen…</button>
</div>
<div class="form-hint" id="ucFileHint">Phase 1: nur <code>.intunewin</code> (win32LobApp). Der Server liest die Datei direkt von diesem Pfad. <em>Durchsuchen</em> öffnet den Datei-Dialog auf dem Rechner, auf dem das Tool läuft.</div>
</div>
<div class="form-group">
<label class="lbl" for="ucVersion">Neue Versionsnummer (optional)</label>
<input type="text" id="ucVersion" class="input mono" spellcheck="false" autocomplete="off" placeholder="z.B. 2.0.1">
</div>
<label class="check-chip"><input type="checkbox" id="ucConfirm"><span>Ich weiß, dass die ausgerollte Version ersetzt wird</span></label>
<div id="ucError" class="form-hint form-hint-err"></div>
<div id="ucProgress" class="content-update-progress hidden"></div>
</div>
<div class="modal-foot">
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-primary" id="ucSubmit" disabled>Hochladen &amp; aktivieren</button>
</div>
</div>
</div>
<!-- MODAL: Einstellungen -->
<div id="modalSettings" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-lg">
<div class="modal-head">
<h3>Einstellungen</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body">
<div id="settingsForm" class="settings-form">
<div id="setupBanner" class="banner-warning hidden">
<strong>Erste Konfiguration noetig.</strong>
Bitte mindestens Tenant ID, Client ID und Abteilungs-Praefix setzen. Danach <em>Verbindung + Lookups testen</em> ganz unten, dann speichern.
</div>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Microsoft Graph Verbindung</h4>
<div class="settings-sec-sub">Tenant- und App-Registrierung. Änderungen erzwingen einen Disconnect.</div>
</div>
<div class="form-group">
<label class="lbl" for="setTenantId">Tenant ID (GUID)</label>
<input type="text" id="setTenantId" class="input mono" placeholder="00000000-0000-0000-0000-000000000000" spellcheck="false" autocomplete="off">
<div class="form-hint">Findest du im Entra Admin Center → Übersicht → Tenant-Informationen.</div>
</div>
<div class="form-group">
<label class="lbl" for="setClientId">Client ID – Read/Write (GUID)</label>
<input type="text" id="setClientId" class="input mono" placeholder="00000000-0000-0000-0000-000000000000" spellcheck="false" autocomplete="off">
<div class="form-hint">App-Registrierung mit Schreibrechten. Erforderlich: <em>Allow public client flows</em> = Ja, Redirect URI <code>http://localhost</code>.</div>
</div>
<div class="form-group">
<label class="lbl" for="setClientIdRo">Client ID – Read Only (GUID, optional)</label>
<input type="text" id="setClientIdRo" class="input mono" placeholder="00000000-0000-0000-0000-000000000000" spellcheck="false" autocomplete="off">
<div class="form-hint">Separate App-Registrierung ohne Schreibrechte. Nutzer dieser App sehen alle Daten, können aber nichts ändern. Leer lassen wenn nicht benötigt.</div>
</div>
<div class="form-group">
<label class="lbl" for="setScopes">Scopes Read/Write (eine pro Zeile)</label>
<textarea id="setScopes" class="input mono" rows="4" spellcheck="false" placeholder="Group.ReadWrite.All&#10;GroupMember.ReadWrite.All&#10;User.Read.All&#10;DeviceManagementApps.ReadWrite.All"></textarea>
<div class="form-hint">Die Standard-Liste passt für den vollen Funktionsumfang.</div>
</div>
<div class="form-group">
<label class="lbl" for="setScopesRo">Scopes Read Only (eine pro Zeile)</label>
<textarea id="setScopesRo" class="input mono" rows="4" spellcheck="false" placeholder="Group.Read.All&#10;GroupMember.Read.All&#10;User.Read.All&#10;DeviceManagementApps.Read.All"></textarea>
<div class="form-hint">Scopes für die Read-Only-App-Registrierung.</div>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Abteilungs-Gruppen</h4>
<div class="settings-sec-sub">Ein oder mehrere Präfixe für den Abteilungs-Modus (linke Spalte). Alle Gruppen, deren <code>displayName</code> mit einem dieser Präfixe beginnt, werden geladen.</div>
</div>
<div class="form-group">
<label class="lbl" for="setDeptPrefixes">Präfixe (eine pro Zeile)</label>
<textarea id="setDeptPrefixes" class="input mono" rows="3" spellcheck="false" placeholder="abt-hm&#10;abt-extern"></textarea>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>RPA-Gruppen</h4>
<div class="settings-sec-sub">Genaue <code>displayName</code>-Werte der Gruppen, die im RPA-Tab angezeigt werden. Eine pro Zeile.</div>
</div>
<div class="form-group">
<label class="lbl" for="setRpaGroups">RPA-Gruppennamen</label>
<textarea id="setRpaGroups" class="input mono" rows="4" spellcheck="false" placeholder="intune-userrole-windowsclientuser-rpa&#10;intune-userrole-windowsclientuser-rpa-fbt&#10;intune-userrole-windowsclientuser-rpa-pro"></textarea>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Benutzer-Suche</h4>
<div class="settings-sec-sub">In welchen Feldern bei der Benutzersuche gesucht wird (alle markierten werden mit <code>OR</code> verknüpft).</div>
</div>
<div class="form-group form-check-grid">
<label class="check-chip"><input type="checkbox" id="setUsfDisplayName"><span>Display Name</span></label>
<label class="check-chip"><input type="checkbox" id="setUsfUpn"><span>UPN</span></label>
<label class="check-chip"><input type="checkbox" id="setUsfMail"><span>E-Mail</span></label>
<label class="check-chip"><input type="checkbox" id="setUsfDepartment"><span>Department</span></label>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Required-Gruppen-Naming</h4>
<div class="settings-sec-sub">Schema für neue Required-Gruppen aus dem App-Manager. Format: <code>{Präfix}{App-Slug}{Suffix}</code>.</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="lbl" for="setReqPrefix">Präfix</label>
<input type="text" id="setReqPrefix" class="input mono" placeholder="intune-win-app-">
</div>
<div class="form-group">
<label class="lbl" for="setReqSuffix">Suffix</label>
<input type="text" id="setReqSuffix" class="input mono" placeholder="-required">
</div>
</div>
<div class="form-hint">Vorschau: <code id="setReqPreview" class="set-preview"></code></div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Available-Gruppen-Naming</h4>
<div class="settings-sec-sub">Schema für neue Available-Gruppen aus dem App-Manager. Format: <code>{Präfix}{App-Slug}{Suffix}</code>.</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="lbl" for="setAvailPrefix">Präfix</label>
<input type="text" id="setAvailPrefix" class="input mono" placeholder="intune-win-app-">
</div>
<div class="form-group">
<label class="lbl" for="setAvailSuffix">Suffix</label>
<input type="text" id="setAvailSuffix" class="input mono" placeholder="-available">
</div>
</div>
<div class="form-hint">Vorschau: <code id="setAvailPreview" class="set-preview"></code></div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Branding — Logo</h4>
<div class="settings-sec-sub">Eigenes Logo links oben im Header. Wird im Projekt-Verzeichnis als <code>branding-logo.&lt;ext&gt;</code> gespeichert. Max. 2 MB.</div>
</div>
<div class="logo-row">
<div class="logo-preview" id="logoPreview">
<span class="logo-letter">I</span>
</div>
<div class="logo-actions">
<input type="file" id="logoFile" accept="image/png,image/jpeg,image/svg+xml,image/webp,image/gif" class="hidden">
<button type="button" class="btn btn-secondary btn-sm" id="btnLogoPick">Bild auswählen…</button>
<button type="button" class="btn btn-text link-btn-danger" id="btnLogoRemove" hidden>Entfernen</button>
<div class="form-hint" id="logoHint">PNG, JPG, SVG, WEBP, GIF</div>
</div>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head settings-sec-head-row">
<div>
<h4>Farben</h4>
<div class="settings-sec-sub">Akzentfarben für Highlights. Soft/Hover/Background-Varianten werden automatisch abgeleitet.</div>
</div>
<button type="button" class="btn btn-secondary btn-sm" id="btnRestoreColors" title="Alle Farben auf die Firmen-Standardwerte zurücksetzen">↺ Standardfarben wiederherstellen</button>
</div>
<div class="color-grid">
<div class="color-row" data-color-key="brand">
<span class="color-swatch" data-swatch="brand"></span>
<div class="color-meta">
<div class="color-name">Brand (Firmenfarbe)</div>
<div class="color-desc">Logo-Hintergrund, Stepper-Indikator</div>
</div>
<input type="color" id="setColBrand" class="color-picker">
<input type="text" id="setColBrandHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="brand" title="Auf Standard zurücksetzen">↺</button>
</div>
<div class="color-row" data-color-key="accent">
<span class="color-swatch" data-swatch="accent"></span>
<div class="color-meta">
<div class="color-name">Available</div>
<div class="color-desc">Available-Buttons, Links, Akzent-Hover</div>
</div>
<input type="color" id="setColAccent" class="color-picker">
<input type="text" id="setColAccentHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="accent" title="Auf Standard zurücksetzen">↺</button>
</div>
<div class="color-row" data-color-key="required">
<span class="color-swatch" data-swatch="required"></span>
<div class="color-meta">
<div class="color-name">Required</div>
<div class="color-desc">Required-Buttons und -Badges</div>
</div>
<input type="color" id="setColRequired" class="color-picker">
<input type="text" id="setColRequiredHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="required">↺</button>
</div>
<div class="color-row" data-color-key="success">
<span class="color-swatch" data-swatch="success"></span>
<div class="color-meta">
<div class="color-name">Erfolg / Bereits zugewiesen</div>
<div class="color-desc">Member-Indikatoren, Erfolgs-Toasts</div>
</div>
<input type="color" id="setColSuccess" class="color-picker">
<input type="text" id="setColSuccessHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="success">↺</button>
</div>
<div class="color-row" data-color-key="warning">
<span class="color-swatch" data-swatch="warning"></span>
<div class="color-meta">
<div class="color-name">Warnung / Teilweise</div>
<div class="color-desc">Partial-Member, Skip-Statuses</div>
</div>
<input type="color" id="setColWarning" class="color-picker">
<input type="text" id="setColWarningHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="warning">↺</button>
</div>
<div class="color-row" data-color-key="error">
<span class="color-swatch" data-swatch="error"></span>
<div class="color-meta">
<div class="color-name">Fehler</div>
<div class="color-desc">Fehler-Toasts, Delete-Hover, Validation</div>
</div>
<input type="color" id="setColError" class="color-picker">
<input type="text" id="setColErrorHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="error">↺</button>
</div>
<div class="color-row" data-color-key="rowSelected">
<span class="color-swatch" data-swatch="rowSelected"></span>
<div class="color-meta">
<div class="color-name">Markierte App-Zeile</div>
<div class="color-desc">Hintergrund der aufgeklappten App-Zeile + linker Akzentstreifen. Schrift/Icons passen sich automatisch dem Kontrast an.</div>
</div>
<input type="color" id="setColRowSelected" class="color-picker">
<input type="text" id="setColRowSelectedHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="rowSelected">↺</button>
</div>
<div class="color-row" data-color-key="detailPanel">
<span class="color-swatch" data-swatch="detailPanel"></span>
<div class="color-meta">
<div class="color-name">Details-Bereich</div>
<div class="color-desc">Hintergrund des aufgeklappten Detail-Blocks unter der App. Text-Farbe wird automatisch nach Lesbarkeit gewählt.</div>
</div>
<input type="color" id="setColDetailPanel" class="color-picker">
<input type="text" id="setColDetailPanelHex" class="input mono color-hex" maxlength="7" spellcheck="false">
<button type="button" class="link-btn" data-color-reset="detailPanel">↺</button>
</div>
</div>
</section>
<section class="settings-sec">
<div class="settings-sec-head">
<h4>Konfiguration pruefen</h4>
<div class="settings-sec-sub">Prueft die aktuell eingetragenen Werte gegen Microsoft Graph: Abteilungs-Gruppen-Lookup, RPA-Gruppen und Benutzer-Suche. Voraussetzung: Verbindung steht.</div>
</div>
<div class="form-row">
<button type="button" class="btn btn-secondary" id="btnSettingsTest">Verbindung + Lookups testen</button>
<div id="settingsTestResult" class="settings-test-result"></div>
</div>
</section>
<section class="settings-sec settings-sec-meta">
<div class="settings-sec-head">
<h4>Datei</h4>
</div>
<div class="form-hint">Gespeichert in: <code id="setFilePath" class="set-path">…</code></div>
</section>
</div>
</div>
<div class="modal-foot">
<button class="btn btn-text link-btn-danger" id="btnSettingsReset" type="button">Auf Standardwerte zurücksetzen</button>
<span class="modal-foot-spacer"></span>
<button class="btn btn-secondary" data-close>Abbrechen</button>
<button class="btn btn-primary" id="btnSettingsSave">Speichern</button>
</div>
</div>
</div>
<!-- MODAL: Gruppen-Export (leeres Stub — Inhalt jetzt in #groupMgmtView) -->
<div id="modalGroupExport" class="modal hidden">
<div class="modal-backdrop" data-close></div>
<div class="modal-box modal-xl">
<div class="modal-head">
<h3>Gruppen-Verwaltung</h3>
<button class="modal-close" data-close aria-label="Schließen">×</button>
</div>
<div class="modal-body"></div>
</div>
</div>
<!-- Markdown-Renderer (bundled, MIT, marked v15) — wird im App-Detail-Panel
fuer die Beschreibung benutzt. Vor app.js laden damit window.marked
beim ersten render() bereitsteht. -->
<script src="/vendor/marked.min.js"></script>
<!-- Date-Picker (bundled, MIT, flatpickr v4) — ersetzt den nativen
date-input Popup, vermeidet Browser-Quirks (schwarzes Initial-Render). -->
<script src="/vendor/flatpickr.min.js"></script>
<script src="/app.js"></script>
</body>
</html>