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:
+5025
File diff suppressed because it is too large
Load Diff
+324
@@ -0,0 +1,324 @@
|
||||
# Intune Manager — Hilfe
|
||||
|
||||
Web-Frontend für Microsoft Intune. Lädt Apps und Gruppen aus deinem Tenant über Microsoft Graph und macht das Verwalten von Zuweisungen schneller als im Intune-Portal selbst.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart in 5 Schritten
|
||||
|
||||
1. **Verbinden** — oben rechts *Anmelden* klicken → Microsoft-Anmeldefenster bestätigen.
|
||||
2. **Tab wählen** — Topbar: *App Management* (Standard), *Group Management* oder *App Report*.
|
||||
3. **Empfänger wählen** (App Management) — linke Spalte: Abteilung, RPA oder Benutzer markieren.
|
||||
4. **Zuweisung sammeln** — bei einer App auf **Available** oder **Required** klicken.
|
||||
5. **Ausführen** — rechts unten *Zuweisungen ausführen*. HTML-Report öffnet sich automatisch.
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
Der Header enthält drei Tabs:
|
||||
|
||||
| Tab | Funktion |
|
||||
|---|---|
|
||||
| **App Management** | Haupt-Workflow: Apps laden, Gruppen zuweisen und entfernen |
|
||||
| **Group Management** | Mitglieder anzeigen, exportieren, hinzufügen, CSV-Import |
|
||||
| **App Report** | Installationszähler pro App, Geräte-Export als CSV |
|
||||
|
||||
> Im **Read-Only-Modus** (Fallback auf eingeschränkte App-ID) sind die Tabs *Benutzer hinzufügen* und *CSV-Import* ausgeblendet. Alle Schreib-Buttons verschwinden automatisch.
|
||||
|
||||
---
|
||||
|
||||
## App Management — der Hauptworkflow
|
||||
|
||||
### Das mentale Modell
|
||||
|
||||
Das Tool ändert **nicht die App-Zuweisungen selbst** — die Liste der Available/Required-Gruppen einer App bleibt unverändert. Stattdessen ändert es die **Mitgliedschaften in genau diesen Gruppen**. Beispiel:
|
||||
|
||||
- App *Beispiel-App* hat die Required-Gruppe `intune-win-app-<slug>-required` zugewiesen.
|
||||
- Du wählst links die Abteilung `<dept-prefix>-team-controlling`.
|
||||
- Klick auf die *Required*-Pille → Operation wandert in die Session.
|
||||
- Beim Ausführen wird `<dept-prefix>-team-controlling` als Mitglied in der Required-Gruppe aufgenommen.
|
||||
- Intune zieht beim nächsten Sync alle Mitglieder als Required-Targets.
|
||||
|
||||
### Schritt-für-Schritt
|
||||
|
||||
1. **Empfänger markieren** (linke Spalte) — Modi mischbar: Abteilungen + RPA + Benutzer gleichzeitig. Die *Pinned-Targets*-Leiste zeigt alles Aktive.
|
||||
2. **Apps durchgehen** (mittlere Spalte) — Filter oder Suchfeld nutzen.
|
||||
3. **Available- oder Required-Button klicken**:
|
||||
- *Eine Gruppe*: Empfänger direkt zur Session hinzufügen.
|
||||
- *Mehrere Gruppen* (Count-Badge): Picker öffnet sich.
|
||||
- *Keine Gruppe* dieses Typs: Button deaktiviert.
|
||||
4. **Session prüfen** (rechte Spalte) — alle gesammelten Vorgänge als Vorschau.
|
||||
5. **Ausführen** — rechts unten. Report öffnet automatisch.
|
||||
|
||||
### Pillen-Farben
|
||||
|
||||
| Optik | Bedeutung |
|
||||
|---|---|
|
||||
| Neutral | Standard — kein Empfänger gewählt oder noch nicht geprüft |
|
||||
| **Grüner Rahmen + Häkchen** | Alle Empfänger bereits Mitglied |
|
||||
| **Gelber Rahmen + halbes Häkchen** | Teilweise Mitglied |
|
||||
| **Schwarz gefüllt + Count-Badge** | Aktiv in der Session (Toggle: erneuter Klick entfernt) |
|
||||
| Kursiv, ausgegraut | Nur native Zuweisung (All Users/Devices) — keine Gruppe änderbar |
|
||||
|
||||
### Gruppen aus App entfernen (−R / −A)
|
||||
|
||||
Jede App-Zeile zeigt rechts **−R** (Required entfernen) und **−A** (Available entfernen).
|
||||
|
||||
- Klick auf −R / −A → Picker mit allen Gruppen dieses Typs → Gruppe auswählen → sofortiges Löschen der Zuweisung (nach Bestätigung).
|
||||
- Bei nativen Zuweisungen (All Users/All Devices) erscheint stattdessen der Mülleimer-Icon direkt im Detail-Panel.
|
||||
|
||||
### Plus-Button — Zuweisung hinzufügen
|
||||
|
||||
Modal mit 6 Optionen:
|
||||
|
||||
**REQUIRED** — Neue Gruppe / All Users / All Devices
|
||||
**AVAILABLE** — Neue Gruppe / All Users / All Devices
|
||||
|
||||
Bereits aktive native Kombinationen werden **ausgegraut** (verhindert Duplikate).
|
||||
|
||||
### Die Session (rechte Spalte)
|
||||
|
||||
Pro Eintrag: App-Name + Gruppen-Name + Empfänger-Tag + A/R-Badge + X-Entfernen.
|
||||
Darunter Gesamtzahl — der Apply-Button zeigt diese Zahl.
|
||||
|
||||
### Ausführen und Report
|
||||
|
||||
- Member-Add pro Session-Eintrag
|
||||
- Native Targets werden übersprungen
|
||||
- Bereits-Mitglied = „OK" — kein Fehler
|
||||
- HTML-Report in `%TEMP%\IntuneAppManager-Reports\`
|
||||
|
||||
---
|
||||
|
||||
## Group Management
|
||||
|
||||
### Mitglieder & Export
|
||||
|
||||
- Gruppe links suchen und auswählen
|
||||
- Mitgliederliste inkl. verschachtelter Gruppen
|
||||
- Filter zum Einschränken der Anzeige
|
||||
- **Als CSV exportieren** → `members-<Gruppe>-<Datum>.csv`
|
||||
|
||||
### Benutzer hinzufügen *(nur Read/Write)*
|
||||
|
||||
- Benutzer per Name, UPN oder E-Mail suchen
|
||||
- Mehrere auswählen und gemeinsam zur gewählten Gruppe hinzufügen
|
||||
|
||||
### CSV-Import *(nur Read/Write)*
|
||||
|
||||
Massenimport von Benutzern per CSV-Datei oder direkter Eingabe.
|
||||
|
||||
**Ablauf:**
|
||||
1. Gruppe links auswählen
|
||||
2. CSV hochladen **oder** UPNs/E-Mails einfügen (eine pro Zeile)
|
||||
3. **„Benutzer auflösen"** → Graph-Lookup → Vorschau mit ✓/✗
|
||||
4. **„Gefundene importieren"** → Benutzer werden zur Gruppe hinzugefügt
|
||||
|
||||
**CSV-Format:** Header-Erkennung für `UserPrincipalName`, `Mail`, `Email`, `UPN`.
|
||||
Ohne Header: erste Spalte mit `@` wird verwendet. Quoted fields und `,`/`;`/Tab als Trennzeichen werden unterstützt.
|
||||
|
||||
**Ergebnis-Banner nach Import:**
|
||||
|
||||
| Chip | Bedeutung |
|
||||
|---|---|
|
||||
| ✔ Grün | Erfolgreich hinzugefügt |
|
||||
| ≈ Gelb | Bereits Mitglied |
|
||||
| ✗ Rot | Fehler (Meldung in der Tabelle) |
|
||||
| Grau | UPN in Graph nicht gefunden |
|
||||
|
||||
---
|
||||
|
||||
## App Report
|
||||
|
||||
- **Neu laden** — lädt Installationszähler für alle Apps (kann 1–2 Min. dauern)
|
||||
- Tabelle sortierbar per Klick auf Spaltenheader (▲/▼)
|
||||
- Suchfeld filtert nach Name, Publisher, Typ
|
||||
- **Als CSV exportieren** — exportiert die gefilterte Tabelle
|
||||
- **Geräte-Export** (Button pro Zeile) — lädt alle Geräte mit Installationsstatus für diese App als CSV
|
||||
|
||||
> Zählwerte erfordern die Berechtigung `DeviceManagementManagedDevices.Read.All` in der Azure App-Registration. Fehlt sie, zeigen alle Spalten 0.
|
||||
|
||||
---
|
||||
|
||||
## Header
|
||||
|
||||
| Element | Funktion |
|
||||
|---|---|
|
||||
| **Logo** | Kann in den Einstellungen durch eigenes Firmenlogo ersetzt werden |
|
||||
| **App Management / Group Management / App Report** | Haupt-Navigation |
|
||||
| **Einstellungen** (Zahnrad) | Tenant/Client-ID, Naming, Farben, Logo |
|
||||
| **Hilfe** (Fragezeichen) | Diese Seite |
|
||||
| **Theme-Toggle** | Light/Dark-Mode — wird im LocalStorage gemerkt |
|
||||
| **Avatar** | Angemeldeter Account — Mouseover zeigt vollen UPN; grüner Punkt = verbunden |
|
||||
| **Logout** | Trennt die Graph-Verbindung |
|
||||
|
||||
---
|
||||
|
||||
## Read-Only-Modus
|
||||
|
||||
Ist nur eine Read-Only-App-ID konfiguriert (oder schlägt die RW-Anmeldung fehl), schaltet das Tool automatisch in den **Read-Only-Modus**:
|
||||
|
||||
- Toast-Meldung „(Read Only)" beim Verbinden
|
||||
- Alle Schreib-Buttons/Tabs werden ausgeblendet (`body.read-only`-CSS-Klasse)
|
||||
- Sichtbar bleibt: App-Liste, Mitglieder-Ansicht, Export-Funktionen, App Report
|
||||
|
||||
---
|
||||
|
||||
## Linke Spalte — Empfänger
|
||||
|
||||
Drei Modi via Segment-Switch:
|
||||
|
||||
- **Abteilung** — Gruppen mit konfigurierten Präfixen (mehrere möglich)
|
||||
- **RPA** — explizit in den Einstellungen gelistete Gruppen
|
||||
- **Benutzer** — Live-Suche über Graph (min. 2 Zeichen)
|
||||
|
||||
**Gruppen aufklappen** → Mitglieder inline → *Benutzer übernehmen* in den User-Modus.
|
||||
|
||||
**Icons pro Eintrag:**
|
||||
|
||||
| Icon | Wirkung |
|
||||
|---|---|
|
||||
| Externer Link | Gruppe/User im Intune-Portal öffnen |
|
||||
| Zwei Personen | Mitglieder-Ansicht im Intune-Portal |
|
||||
| Teams-T | Direkt-Chat starten (Teams-Desktop) |
|
||||
|
||||
---
|
||||
|
||||
## Mittlere Spalte — Apps
|
||||
|
||||
### Filter
|
||||
|
||||
| Filter | Zweck |
|
||||
|---|---|
|
||||
| Suchfeld | Volltext im App-Namen |
|
||||
| Dropdown | Preset: Alle / Ohne Zuweisungen / Nur Available / Nur Required / Mehrere / All Users / All Devices / Intune / pro Vendor |
|
||||
| Bereits Gruppenmitglied | Nur Apps wo Empfänger schon Mitglied ist |
|
||||
| All Users/Devices ausblenden | Native Zuweisungen rausnehmen |
|
||||
| Geändert (Datumsrange) | Nach Last-Modified-Datum filtern |
|
||||
|
||||
### App-Zeile
|
||||
|
||||
1. Status-Indikator (Häkchen)
|
||||
2. App-Name + Hersteller → Klick öffnet Detail-Panel
|
||||
3. Rename-Icon (Stift) / Lock bei Vendor-Apps
|
||||
4. Source-Logo (Intune, PatchMyPC, Robopack, …)
|
||||
5. Typ-Chip
|
||||
6. Version
|
||||
7. **Available**- und **Required**-Buttons mit Count
|
||||
8. **−R** (Required-Gruppe entfernen) und **−A** (Available-Gruppe entfernen)
|
||||
9. **Plus-Button** — Zuweisung hinzufügen
|
||||
10. **Delete-Icon** — App löschen (bei Vendor-Apps: Lock)
|
||||
|
||||
---
|
||||
|
||||
## App-Detail (Klick auf App-Namen)
|
||||
|
||||
| Sektion | Inhalt |
|
||||
|---|---|
|
||||
| Zuweisungen | Available + Required mit Aktionen (Link, Mitglieder, Löschen) |
|
||||
| Allgemein | Hersteller, Version, Typ, Min-OS |
|
||||
| Beschreibung | Markdown-gerendert |
|
||||
| Installation | Datei, Install-/Uninstall-Befehl (Copy-Icon) |
|
||||
| Detection | Registry, Datei, MSI-Code, PowerShell |
|
||||
| Return-Codes | Exit-Codes mit Typ |
|
||||
| Meta | Erstellt + Geändert |
|
||||
| Installationen | Geräte + Benutzer × Installiert/Fehler/Pending/N/A mit Erfolgsquote |
|
||||
| Abhängigkeiten | SVG-Flow-Diagramm + Klartext; Klick auf Knoten springt zur Ziel-App |
|
||||
| Supersedence | Was diese App ersetzt (update / replace) |
|
||||
|
||||
---
|
||||
|
||||
## Einstellungen
|
||||
|
||||
Persistiert in `%APPDATA%\IntuneAppManager-Web\settings.json`.
|
||||
|
||||
| Sektion | Inhalt |
|
||||
|---|---|
|
||||
| Verbindung | Tenant-ID, Client-ID (RW), Client-ID (RO), Scopes RW + RO |
|
||||
| Abteilungs-Gruppen | Präfixe (eine pro Zeile) |
|
||||
| RPA-Gruppen | Explizite Gruppennamen |
|
||||
| Benutzer-Suche | Felder (DisplayName / UPN / Mail / Department) |
|
||||
| Required-Naming | Präfix + Suffix |
|
||||
| Available-Naming | Präfix + Suffix |
|
||||
| Branding | Logo-Upload (PNG/JPG/SVG/WEBP/GIF, max. 2 MB) |
|
||||
| Farben | 8 Akzentfarben + Standardfarben wiederherstellen |
|
||||
|
||||
---
|
||||
|
||||
## Benötigte Graph-Berechtigungen
|
||||
|
||||
| Funktion | Berechtigung |
|
||||
|---|---|
|
||||
| Apps anzeigen | `DeviceManagementApps.Read.All` |
|
||||
| Gruppen / Mitglieder anzeigen | `GroupMember.Read.All` |
|
||||
| Benutzer suchen | `User.Read.All` |
|
||||
| Zuweisungen ändern | `DeviceManagementApps.ReadWrite.All` |
|
||||
| Gruppen-Mitglieder ändern | `GroupMember.ReadWrite.All` |
|
||||
| App Report / Geräte-Export | `DeviceManagementManagedDevices.Read.All` ⚠ |
|
||||
|
||||
> ⚠ Ohne `DeviceManagementManagedDevices.Read.All` zeigt der App Report 0 und der Geräte-Export liefert leere Dateien.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Einschränkungen
|
||||
|
||||
- **Vendor-Apps** (PatchMyPC, Robopack, …) können nicht gelöscht/umbenannt werden — Lock-Icon + HTTP 409.
|
||||
- **Native Zuweisungen** (All Users / All Devices) sind je App und Intent einzigartig — Duplikate werden ausgegraut.
|
||||
- **Required-Zuweisungen** werden beim nächsten Geräte-Sync erzwungen — bei Produktiv-Apps zuerst Available testen.
|
||||
- **App-Löschung** ist irreversibel und entfernt alle Zuweisungen.
|
||||
- **Cache**: Apps und Gruppen werden im PowerShell-Prozess gehalten — Refresh-Icon lädt neu.
|
||||
|
||||
---
|
||||
|
||||
## Vendor-Erkennung
|
||||
|
||||
Das Tool erkennt Vendor-Apps (PatchMyPC, Robopack, …) per konfigurierbaren Detection-Regeln in `settings.json → vendors[]`:
|
||||
|
||||
| Feld | Optionen |
|
||||
|---|---|
|
||||
| `field` | `commandLine`, `developer`, `publisher`, `displayName`, `notes`, `owner` |
|
||||
| `match` | `contains`, `equals`, `startsWith`, `regex` |
|
||||
| `pattern` | Suchstring oder Regex |
|
||||
|
||||
Erster Match gewinnt. Eigene Vendoren: neuen Eintrag im `vendors[]`-Array, Logo als `<id>.png` im Projekt-Root ablegen.
|
||||
|
||||
---
|
||||
|
||||
## Setup-Datei aktualisieren
|
||||
|
||||
Native Win32-Apps (`.intunewin`) können mit einer neuen Setup-Datei aktualisiert werden ohne die App neu anzulegen.
|
||||
|
||||
**Wo:** App-Detail → Abschnitt *Setup-Datei* → *Setup-Datei aktualisieren…*
|
||||
**Ablauf:** lokalen Pfad auf dem Server-Rechner eingeben → optionale neue Versionsnummer → Bestätigung → *Hochladen & aktivieren*
|
||||
|
||||
> **Achtung:** Ersetzt die ausgerollte Version für alle zugewiesenen Geräte. Nicht rückgängig machbar — zuerst an einer Test-App ausprobieren.
|
||||
|
||||
---
|
||||
|
||||
## Tastatur / Tipps
|
||||
|
||||
- **Ctrl+F5** — Hard-Refresh (Pflicht nach Tool-Updates)
|
||||
- **Esc** — Modal schließen
|
||||
- **Enter** im Umbenennen-Dialog — speichern
|
||||
- **Klick auf App-Zeile** außerhalb Buttons — Detail öffnen/schließen
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
| Symptom | Lösung |
|
||||
|---|---|
|
||||
| „User canceled authentication" | WAM-Fenster ist hinter anderen Fenstern — in Taskleiste suchen |
|
||||
| Apps laden sehr lange | Bei > 1000 Apps normal (10–30 s); danach gecached |
|
||||
| Änderungen wirken nicht | **Ctrl+F5** (Browser-Cache) |
|
||||
| App Report zeigt nur 0 | `DeviceManagementManagedDevices.Read.All` fehlt in der App-Registration |
|
||||
| Geräte-Export ist leer | Gleiche Ursache wie App Report |
|
||||
| Read-Only-Modus obwohl RW gewünscht | RW-Anmeldung fehlgeschlagen — Berechtigungen in Azure prüfen |
|
||||
| Port-Konflikt beim Start | `.\Start.ps1 -Port 8088` |
|
||||
| PowerShell-Errors beim Start | `Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force` |
|
||||
| Apply-Button bleibt grau | 0 Vorgänge in der Session oder nur native Targets |
|
||||
|
||||
---
|
||||
|
||||
Stand: Juli 2026 · Hilfe-Datei: `www/help.md` · Änderungshistorie: `CHANGELOG.md`
|
||||
+994
@@ -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 & 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 & 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. max.mustermann@firma.de erika.muster@firma.de 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 & 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 & 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 GroupMember.ReadWrite.All User.Read.All 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 GroupMember.Read.All User.Read.All 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 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 intune-userrole-windowsclientuser-rpa-fbt 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.<ext></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>
|
||||
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gruppen-Mitglieder · Intune Manager</title>
|
||||
<script>
|
||||
(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) {}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.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 class="members-page">
|
||||
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<div class="logo">I</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name">Gruppen-Mitglieder</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a id="entraLink" class="btn btn-secondary btn-sm" href="#" target="_blank" rel="noopener" title="In Entra oeffnen">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
In Entra
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="members-main">
|
||||
<section class="members-card">
|
||||
<header class="members-head">
|
||||
<h1 id="grpName" class="members-title">Mitglieder werden geladen...</h1>
|
||||
<div class="members-meta">
|
||||
<code id="grpId" class="meta-mono"></code>
|
||||
<span id="grpCount" class="count-badge accent">0</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="members-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="mFilter" placeholder="Mitglieder filtern (Name, UPN, E-Mail)..." class="input input-search" autocomplete="off">
|
||||
</div>
|
||||
<button id="btnReload" 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 id="mList" class="members-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toasts" class="toasts"></div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const groupId = params.get('id') || '';
|
||||
const groupName = params.get('name') || '';
|
||||
|
||||
const ENTRA_GROUP_URL = id => `https://intune.microsoft.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/Overview/groupId/${encodeURIComponent(id)}/menuId/`;
|
||||
const ENTRA_USER_URL = id => `https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/${encodeURIComponent(id)}`;
|
||||
const ENTRA_ICON_SVG = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
||||
}
|
||||
|
||||
function setTitle(name, count) {
|
||||
document.title = `${name || groupId} (${count}) · Mitglieder`;
|
||||
}
|
||||
|
||||
const state = { members: [], filter: '' };
|
||||
|
||||
function render() {
|
||||
const q = state.filter.trim().toLowerCase();
|
||||
const items = q
|
||||
? state.members.filter(m =>
|
||||
(m.DisplayName || '').toLowerCase().includes(q) ||
|
||||
(m.UserPrincipalName || '').toLowerCase().includes(q))
|
||||
: state.members;
|
||||
|
||||
const list = document.getElementById('mList');
|
||||
document.getElementById('grpCount').textContent = state.members.length;
|
||||
setTitle(document.getElementById('grpName').textContent, state.members.length);
|
||||
|
||||
if (state.members.length === 0) {
|
||||
list.innerHTML = `<div class="empty-state"><div class="empty-title">Keine Mitglieder</div><div class="empty-text">Diese Gruppe hat keine direkten Benutzer.</div></div>`;
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `<div class="empty-state small"><div class="empty-title">Keine Treffer</div><div class="empty-text">Filter zuruecksetzen.</div></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(m => {
|
||||
const upn = m.UserPrincipalName ? `<div class="m-upn">${esc(m.UserPrincipalName)}</div>` : '';
|
||||
const icon = `<a class="entra-link entra-link-static" href="${ENTRA_USER_URL(m.Id)}" target="_blank" rel="noopener" title="In Entra oeffnen">${ENTRA_ICON_SVG}</a>`;
|
||||
return `<div class="m-row">
|
||||
<div class="m-info">
|
||||
<div class="m-name">${esc(m.DisplayName || '?')}${icon}</div>
|
||||
${upn}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function load(force = false) {
|
||||
if (!groupId) {
|
||||
document.getElementById('mList').innerHTML = `<div class="empty-state"><div class="empty-title">Keine Gruppe</div><div class="empty-text">In der URL fehlt der Parameter <code>?id=...</code>.</div></div>`;
|
||||
return;
|
||||
}
|
||||
const titleEl = document.getElementById('grpName');
|
||||
titleEl.textContent = groupName || 'Mitglieder werden geladen...';
|
||||
document.getElementById('grpId').textContent = groupId;
|
||||
document.getElementById('entraLink').href = ENTRA_GROUP_URL(groupId);
|
||||
|
||||
document.getElementById('mList').innerHTML = `<div class="m-loading"><div class="spinner"></div><span>Lade Mitglieder...</span></div>`;
|
||||
try {
|
||||
const res = await fetch(`/api/groups/${encodeURIComponent(groupId)}/members${force ? '?refresh=1' : ''}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
state.members = (data.items || [])
|
||||
.sort((a, b) => (a.DisplayName || '').localeCompare(b.DisplayName || '', 'de', { sensitivity: 'base' }));
|
||||
titleEl.textContent = groupName || `Gruppe ${groupId.slice(0, 8)}`;
|
||||
render();
|
||||
} catch (e) {
|
||||
document.getElementById('mList').innerHTML = `<div class="empty-state"><div class="empty-title">Fehler</div><div class="empty-text">${esc(e.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('mFilter').addEventListener('input', e => {
|
||||
state.filter = e.target.value;
|
||||
render();
|
||||
});
|
||||
document.getElementById('btnReload').addEventListener('click', () => load(true));
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+5159
File diff suppressed because it is too large
Load Diff
Vendored
+13
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+69
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user