web app mit hinzugefügt
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,341 @@
|
||||
# Intune App-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** — beim ersten Start öffnet sich der Onboarding-Screen. *Anmeldung starten* klicken → Microsoft-Anmeldefenster bestätigen.
|
||||
2. **Empfänger wählen** — linke Spalte. Wechsle zwischen *Abteilung*, *RPA* oder *Benutzer* und setze die Haken.
|
||||
3. **Apps laden** — falls noch nicht geschehen, klicke das *Refresh*-Icon rechts oben in der Apps-Spalte.
|
||||
4. **Zuweisung sammeln** — bei einer App auf den **Available**- oder **Required**-Button klicken. Die ausgewählten Empfänger landen in der Session (rechte Spalte).
|
||||
5. **Ausführen** — rechts unten *Zuweisungen ausführen*. Ein HTML-Report öffnet sich automatisch.
|
||||
|
||||
---
|
||||
|
||||
## Apps zuweisen — der Hauptworkflow
|
||||
|
||||
Das ist der zentrale Anwendungsfall des Tools. Du nimmst eine Liste von **Empfängern** (Abteilungen, RPA-Gruppen oder Einzel-User), suchst eine oder mehrere **Apps** und fügst die Empfänger den **bereits an der App zugewiesenen Gruppen** als Mitglieder hinzu.
|
||||
|
||||
### Das mentale Modell
|
||||
|
||||
Das Tool ändert **nicht die App-Zuweisungen selbst** — die Liste der Available/Required-Gruppen einer App bleibt, was sie ist. Stattdessen ändert es die **Mitgliedschaften in genau diesen Gruppen**. Beispiel:
|
||||
|
||||
> Beispiel mit Platzhaltern. Die Präfixe `<dept-prefix>` und `intune-win-app-` sind in den Einstellungen konfigurierbar.
|
||||
|
||||
- App *Beispiel-App* hat die Required-Gruppe `intune-win-app-<app-slug>-required` zugewiesen.
|
||||
- Du wählst links die Abteilung `<dept-prefix>-team-controlling` als Empfänger.
|
||||
- Klick auf die *Required*-Pille der App → die Operation `Abteilung als Mitglied zur Required-Gruppe hinzufügen` wandert in die Session.
|
||||
- Beim Ausführen wird `<dept-prefix>-team-controlling` als Mitglied in `intune-win-app-<app-slug>-required` aufgenommen.
|
||||
- Intune ziehst dann beim nächsten Sync alle Mitglieder dieser Gruppe als Required-Targets der App.
|
||||
|
||||
### Schritt-für-Schritt
|
||||
|
||||
1. **Empfänger markieren (linke Spalte).** Du kannst Modi mischen — z. B. zwei Abteilungen *plus* drei Einzel-Benutzer gleichzeitig auswählen. Die *Pinned-Targets*-Leiste oben zeigt dir alles, was gerade aktiv ist.
|
||||
2. **Apps öffnen (mittlere Spalte).** Filter benutzen oder Suchfeld. Du musst die Apps nicht einzeln durchgehen — du kannst nacheinander bei mehreren Apps Pillen klicken, alle Operationen sammeln und am Ende einmal ausführen.
|
||||
3. **Available- oder Required-Button klicken.** Was passiert hängt vom Inhalt ab:
|
||||
- **App hat genau eine Available/Required-Gruppe**: Die ausgewählten Empfänger werden direkt zur Session als „diese Gruppe + dieser Intent" hinzugefügt.
|
||||
- **App hat mehrere Gruppen** (Count-Badge auf dem Button): Ein Picker öffnet sich und du wählst gezielt eine Gruppe aus der Liste.
|
||||
- **App hat keine Gruppe** dieses Typs: Button ist deaktiviert.
|
||||
4. **Status prüfen.** Die Buttons zeigen dir farblich an, wie der aktuelle Stand ist (siehe nächster Abschnitt).
|
||||
5. **Bei mehreren Apps wiederholen.** Die Session sammelt alles. Wenn du dieselbe Pille nochmal klickst, wird der Vorgang aus der Session **wieder entfernt** (Toggle-Verhalten).
|
||||
6. **Ausführen.** Rechts unten *Zuweisungen ausführen*. Das Tool durchläuft jede gesammelte Operation und schickt einen Member-Add an die Microsoft-Graph-API. Der HTML-Report listet Erfolge und Fehler je App.
|
||||
|
||||
### Bedeutung der Pillen-Farben
|
||||
|
||||
Die Buttons *Available* und *Required* an jeder App-Zeile signalisieren über Farbe und Indikator den Zustand **bezogen auf die gerade ausgewählten Empfänger**:
|
||||
|
||||
| Optik | Bedeutung |
|
||||
|---|---|
|
||||
| Heller Akzent-Rahmen, neutral | Standard-Zustand. Keine ausgewählten Empfänger oder noch nicht geprüft. |
|
||||
| **Grüner Rahmen + grünes Häkchen** in der Zeile | **Alle** ausgewählten Empfänger sind bereits Mitglied einer der zugewiesenen Gruppen dieses Typs. Klick erzeugt evtl. Dubletten — meist nicht nötig. |
|
||||
| **Gelber Rahmen + halbes Häkchen** | **Teilweise**: ein Teil der Empfänger ist schon Mitglied, der Rest nicht. Klick füllt die Lücken. |
|
||||
| **Schwarz gefüllt + Count-Badge** (z. B. „3") | **Aktiv in der Session**: Operation für diese App ist schon gesammelt. Anzahl = wie viele Operationen für diese App in der Session liegen. Erneuter Klick entfernt sie wieder. |
|
||||
| Italic Schrift, ausgegraut | **Native Zuweisung** (All Users / All Devices) ist die einzige Option — Mitgliedschaft kann nicht geändert werden, weil keine Gruppe existiert. |
|
||||
|
||||
### Picker bei mehreren Gruppen
|
||||
|
||||
Wenn eine App mehr als eine Available- oder Required-Gruppe hat, öffnet der Klick einen kleinen Picker:
|
||||
|
||||
- Liste aller Gruppen dieses Typs
|
||||
- Pro Gruppe wird der **Member-Status** (Mitglied / Partial mit n/m-Anzeige) angezeigt, sofern Empfänger ausgewählt sind
|
||||
- Klick auf eine Gruppe = zur Session hinzufügen oder entfernen (Toggle)
|
||||
- Pro Gruppe zusätzlich die *Aktions-Icons* (Intune-Portal-Link, Mitglieder, Zuweisung löschen)
|
||||
|
||||
### Die Session (rechte Spalte)
|
||||
|
||||
Die rechte Spalte ist deine **Vorschau aller noch nicht ausgeführten Vorgänge**. Pro Session-Eintrag siehst du:
|
||||
|
||||
- App-Name + Gruppen-Name
|
||||
- Empfänger-Tag (genau **ein** Empfänger pro Eintrag — bei mehreren Empfängern entstehen mehrere Einträge)
|
||||
- A/R-Badge (Available oder Required)
|
||||
- X-Button um den Eintrag wieder rauszunehmen
|
||||
|
||||
Darunter eine Zusammenfassung mit der **Gesamtzahl der Vorgänge** — der Apply-Button zeigt diese Zahl mit an.
|
||||
|
||||
### Ausführen und Report
|
||||
|
||||
Klick auf *Zuweisungen ausführen* → Bestätigungs-Dialog mit Counter → echte Ausführung:
|
||||
|
||||
- Für jeden Session-Eintrag wird ein Member-Add ausgeführt
|
||||
- Native Targets (`ALL_USERS`/`ALL_DEVICES`) werden übersprungen — sie haben keine Gruppen-Mitgliedschaft
|
||||
- Bereits-Mitglied-Fälle werden als „OK (bereits Mitglied)" geloggt — kein Fehler
|
||||
- Echte Fehler (403 Berechtigung, 404 Gruppe nicht gefunden, …) werden mit Details aufgeführt
|
||||
|
||||
Nach Abschluss öffnet sich automatisch ein **HTML-Report** mit:
|
||||
|
||||
- Erfolgs-/Fehler-/Skipped-Counter + Erfolgsquote
|
||||
- Empfänger-Chips (Abteilungen, Benutzer)
|
||||
- Zuweisungs-Chips (App + Gruppe)
|
||||
- Detail-Tabelle nach App gruppiert — Apps mit Fehlern sind aufgeklappt
|
||||
|
||||
Reports werden in `%TEMP%\IntuneAppManager-Reports\` abgelegt und können später erneut geöffnet werden.
|
||||
|
||||
### Wann sollte ich was nutzen?
|
||||
|
||||
| Du willst… | Workflow |
|
||||
|---|---|
|
||||
| 50 User aus einer neuen Abteilung in 10 Apps mitnehmen | Empfänger = Abteilung wählen, durch 10 Apps gehen, je 1 Required- oder Available-Pille klicken, ausführen. |
|
||||
| Eine neue App nur für 3 Test-Benutzer ausrollen | User-Modus, 3 User markieren. Bei der App entweder eine bestehende Available-Gruppe wählen oder *+ → Available: Neue Gruppe* erzeugen. |
|
||||
| Eine bestehende Gruppen-Zuweisung einer App entfernen | App aufklappen → *Zuweisungen*-Sektion → Mülleimer-Icon neben der Gruppe. |
|
||||
| Eine zugewiesene Gruppe in mehreren Apps prüfen | Bei einer App auf die Pille klicken → Picker → Gruppe aussehen → Mitglieder-Icon → Intune-Portal Mitglieder-Ansicht. Oder via Filter „Mehrere Available/Required" eingrenzen. |
|
||||
|
||||
---
|
||||
|
||||
## Header (oben)
|
||||
|
||||
| Element | Funktion |
|
||||
|---|---|
|
||||
| **Logo links** | Kann in den Einstellungen durch dein Firmen-Logo ersetzt werden. |
|
||||
| **Einstellungen** (Zahnrad-Icon) | Tenant/Client-ID, Naming-Schemata, Farben, Logo, Filter-Defaults. Wird unter `%APPDATA%\IntuneAppManager-Web\settings.json` persistiert. |
|
||||
| **Hilfe** (Fragezeichen-Icon) | Diese Seite. |
|
||||
| **Theme-Toggle** (Sonne/Mond-Slider) | Light/Dark-Mode umschalten. Wird im Browser-LocalStorage gemerkt. |
|
||||
| **Avatar** (Personen-Icon) | Zeigt den angemeldeten Account. Mouseover für vollen UPN. Grüner Punkt = verbunden. |
|
||||
| **Logout** (Tür-mit-Pfeil-Icon) | Trennt die Microsoft-Graph-Verbindung. |
|
||||
|
||||
---
|
||||
|
||||
## Linke Spalte — Empfänger
|
||||
|
||||
Drei Auswahl-Modi via Segment-Switch oben:
|
||||
|
||||
- **Abteilung** — lädt alle Gruppen, deren Name mit einem der in den Einstellungen konfigurierten Präfixe beginnt (mehrere möglich, z.B. `abt-hm` *und* `abt-extern` für interne plus externe Mitarbeiter — siehe *Einstellungen → Abteilungs-Gruppen*).
|
||||
- **RPA** — lädt die in den Einstellungen explizit aufgelisteten Gruppennamen.
|
||||
- **Benutzer** — Live-Suche über Microsoft Graph. Mindestens 2 Zeichen. Felder konfigurierbar (DisplayName, UPN, Mail, Department).
|
||||
|
||||
### Gruppen aufklappen
|
||||
|
||||
Der Pfeil `>` rechts an einer Gruppen-Zeile öffnet die **Mitglieder inline**. Mitglieder lassen sich einzeln per Checkbox markieren und mit *Benutzer übernehmen* in den User-Modus überführen — nützlich wenn nur ein Teil einer Gruppe zugewiesen werden soll.
|
||||
|
||||
### Icons pro Eintrag (immer sichtbar)
|
||||
|
||||
| Icon-Form | Wirkung |
|
||||
|---|---|
|
||||
| Externer-Link-Pfeil | Öffnet die Gruppe/User-Seite direkt im Intune-Portal in neuem Tab. |
|
||||
| Zwei-Personen-Icon | (nur bei Gruppen) Öffnet die *Mitglieder-Ansicht* der Gruppe im Intune-Portal in neuem Tab. |
|
||||
| Teams-T-im-Kasten | (nur bei Benutzern) Startet einen Teams-Chat mit der Person — öffnet die Teams-Desktop-App. |
|
||||
|
||||
### Mehrere Modi gleichzeitig
|
||||
|
||||
Die Auswahlen in den drei Modi sind voneinander unabhängig — du kannst etwa 5 Abteilungen + 2 RPA-Gruppen + 3 Einzel-Benutzer gleichzeitig anvisieren. Die *Pinned-Targets*-Leiste oben in der linken Spalte zeigt alles, was gerade markiert ist.
|
||||
|
||||
---
|
||||
|
||||
## Mittlere Spalte — Apps
|
||||
|
||||
### Filter
|
||||
|
||||
| Filter | Zweck |
|
||||
|---|---|
|
||||
| Suchfeld | Volltext-Suche im App-Namen. |
|
||||
| Dropdown rechts | Preset-Filter: *Alle*, *Ohne Zuweisungen*, *Nur Available*, *Nur Required*, *Mehrere Available/Required*, *All Users/Devices*, *Nur Intune* sowie ein Eintrag *pro konfiguriertem Vendor* (z.B. *Nur PatchMyPC-Apps*, *Nur Robopack-Apps*). |
|
||||
| `Bereits Gruppenmitglied` | Zeigt nur Apps, bei denen mindestens eine der ausgewählten Empfänger schon Gruppenmitglied einer zugewiesenen Gruppe ist. Funktioniert nur mit ausgewählten Empfängern. |
|
||||
| `All Users/Devices ausblenden` | Native Zuweisungen aus der Anzeige rausnehmen. |
|
||||
| `Geändert` (Datumsrange) | Filtert auf das *Last Modified*-Datum der App. |
|
||||
|
||||
### App-Zeile
|
||||
|
||||
Jede Zeile besteht von links nach rechts aus:
|
||||
|
||||
1. **Status-Indikator** (Häkchen wenn alle ausgewählten Empfänger schon Mitglied einer zugewiesenen Gruppe sind).
|
||||
2. **App-Name** + Hersteller. Klick öffnet das Detail-Panel.
|
||||
3. **Rename-Icon** (Stift) und **Intune-Link**. Bei Vendor-verwalteten Apps (PatchMyPC, Robopack, …) ist der Stift durch ein **Lock-Icon** ersetzt.
|
||||
4. **Source-Logo** — Intune, PatchMyPC, Robopack oder ein weiterer in den Einstellungen konfigurierter Vendor. Klick öffnet das jeweilige Portal in einem neuen Tab.
|
||||
5. **Typ-Chip** (WIN32, MSI, APPX, WINGET, …).
|
||||
6. **Version**.
|
||||
7. **Available**- und **Required**-Buttons mit Count (Details siehe *Apps zuweisen — der Hauptworkflow* oben).
|
||||
8. **Plus-Button** — Zuweisung hinzufügen (siehe nächster Abschnitt).
|
||||
9. **Delete-Icon** (Mülltonne) — App aus Intune entfernen. Bei Vendor-verwalteten Apps (PatchMyPC, Robopack, …) ersetzt durch ein Lock.
|
||||
|
||||
### Plus-Button — Zuweisung hinzufügen
|
||||
|
||||
Modal mit **sechs Optionen**, gruppiert in zwei Spalten:
|
||||
|
||||
**REQUIRED** (Pflichtinstallation)
|
||||
- *Neue Gruppe* — erstellt eine neue Azure-AD-Sicherheitsgruppe nach dem Naming-Schema aus den Einstellungen und weist sie der App zu.
|
||||
- *All Users* — native Zuweisung an alle lizenzierten Benutzer.
|
||||
- *All Devices* — native Zuweisung an alle Geräte.
|
||||
|
||||
**AVAILABLE** (Selbstinstallation via Firmenportal)
|
||||
- *Neue Gruppe* — analog, mit Available-Naming-Schema.
|
||||
- *All Users* — App erscheint im Firmenportal aller Benutzer.
|
||||
- *All Devices* — App ist auf allen Geräten verfügbar.
|
||||
|
||||
> **Wichtig**: Native Zuweisungen (*All Users*/*All Devices*) können je App und je Intent nur **einmal** existieren. Bereits aktive Kombinationen werden im Dialog **ausgegraut** — das verhindert 409-Fehler bei doppelten Anlagen.
|
||||
|
||||
> **Unterschied zum Hauptworkflow**: Der *Plus*-Button erstellt eine **neue Zuweisung an der App selbst** (z. B. eine neue Gruppe oder ein natives Target). Der Klick auf eine *Available*/*Required*-Pille fügt dagegen Empfänger zu einer **bereits zugewiesenen Gruppe** als Mitglieder hinzu.
|
||||
|
||||
---
|
||||
|
||||
## App-Detail (Klick auf App-Namen)
|
||||
|
||||
Klappt unter der App-Zeile auf. Inhalt:
|
||||
|
||||
| Sektion | Inhalt |
|
||||
|---|---|
|
||||
| **Zuweisungen** | Available + Required-Listen mit Klick-Aktionen (Intune-Link, Mitglieder, Löschen). |
|
||||
| **Allgemein** | Hersteller, Entwickler, Owner, Version, Typ, Min-OS, Featured-Flag. |
|
||||
| **Beschreibung** | Wird als **Markdown gerendert** — Tabellen, Listen, Code-Blöcke, fette Schrift, alles wie auf GitHub. |
|
||||
| **Installation** | Datei, Setup-Pfad, Install- und Uninstall-Befehl. Copy-Icons für die Commands. |
|
||||
| **Detection** | Detection-Regeln (Registry, Datei, MSI-ProductCode, PowerShell-Script). |
|
||||
| **Return-Codes** | Exit-Codes mit `success` / `softReboot` / `hardReboot` / `retry`. |
|
||||
| **Meta** | Erstellt + Geändert-Daten. Kombinierbar mit dem *Geändert*-Filter oben. |
|
||||
| **Installationen** | Statistik aus Intune: Erfolgs-/Fehler-/Pending-/N/A-Counts getrennt für *Geräte* und *Benutzer*. Erfolgsquote als farbige Pille. |
|
||||
| **Abhängigkeiten** | Visuelles Flow-Diagramm mit Klartext-Erklärung. Klick auf einen Dependency-Knoten springt zur Ziel-App (sofern in der aktuellen Liste). |
|
||||
| **Supersedence** | Was diese App ersetzt/aktualisiert — analog mit Erklärung. |
|
||||
|
||||
### Dependencies und Supersedence — was bedeutet was?
|
||||
|
||||
**Dependency** (Abhängigkeit)
|
||||
- Eine Dependency definiert, dass **diese App** eine andere App als **Voraussetzung** hat.
|
||||
- *autoInstall*: Intune installiert die Dependency automatisch vor dieser App, sobald die App zugewiesen wird.
|
||||
- *detect*: Die Dependency muss manuell vorhanden sein — Intune installiert sie nicht automatisch.
|
||||
|
||||
**Supersedence** (Ablösung)
|
||||
- Beschreibt, dass **diese App** eine andere App **ablöst**.
|
||||
- *update*: Vorhandene Installation der alten App wird aktualisiert, Daten/Konfiguration bleiben erhalten.
|
||||
- *replace*: Die alte App wird **deinstalliert**, dann wird die neue App installiert. Daten gehen verloren.
|
||||
|
||||
---
|
||||
|
||||
## Einstellungen
|
||||
|
||||
Persistiert in `%APPDATA%\IntuneAppManager-Web\settings.json`. Beim Speichern werden nur die *betroffenen* Caches geleert — bei reinen Farb-/Logo-Änderungen kein Reload nötig.
|
||||
|
||||
| Sektion | Inhalt |
|
||||
|---|---|
|
||||
| Microsoft Graph Verbindung | Tenant- und Client-ID + Scopes. Änderungen erzwingen einen Disconnect. |
|
||||
| Abteilungs-Gruppen | Ein oder mehrere Präfixe (eine pro Zeile) für den Abteilungs-Modus. |
|
||||
| RPA-Gruppen | Explizite Liste der RPA-Gruppennamen (eine pro Zeile). |
|
||||
| Benutzer-Suche | Welche Felder durchsucht werden (DisplayName / UPN / Mail / Department). |
|
||||
| Required-Gruppen-Naming | Präfix + Suffix für die Auto-Generierung von Required-Gruppen-Namen. |
|
||||
| Available-Gruppen-Naming | Analog für Available-Gruppen. |
|
||||
| Branding (Logo) | Eigenes Logo hochladen (PNG/JPG/SVG/WEBP/GIF, max. 2 MB). Wird im Projekt-Verzeichnis als `branding-logo.*` gespeichert. |
|
||||
| Farben | 8 Akzentfarben (Brand, Available, Required, Erfolg, Warnung, Fehler, Markierte Zeile, Detail-Bereich) + *Standardfarben wiederherstellen*-Button. |
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Einschränkungen
|
||||
|
||||
- **Vendor-verwaltete Apps** (PatchMyPC, Robopack und weitere in den Einstellungen konfigurierte Vendoren) können weder gelöscht noch umbenannt werden über das Tool — das funktioniert nur im jeweiligen Vendor-Portal selbst. Die UI zeigt für diese Apps ein Lock-Icon statt Trash/Pencil und das Backend blockiert mit HTTP 409 (`code = "<vendorId>Managed"`). Detection-Regeln pro Vendor (Field/Match/Pattern) liegen in `settings.json` und sind dort editierbar — Standardwerte: PMPC = `commandLine contains "PatchMyPC"`, Robopack = `developer equals "Robopack"`.
|
||||
- **Native Zuweisungen** (*All Users* / *All Devices*) sind je App und Intent **einzigartig**. Bereits aktive Kombinationen werden im Erstellen-Dialog ausgegraut.
|
||||
- **Required-Zuweisungen** werden bei der nächsten Geräte-Sync von Intune **erzwungen** — Vorsicht bei produktiven Apps. Bei Änderungen an produktiven Apps lieber erst Available zuweisen.
|
||||
- **App-Löschung** im Tool entfernt die App komplett aus Intune (inkl. aller Zuweisungen) — irreversibel. Gilt nicht für Vendor-verwaltete Apps (siehe oben).
|
||||
- **Cache-Verhalten**: Apps und Gruppen werden nach dem ersten Laden im PowerShell-Prozess gecached. Beim Refresh-Icon wird neu geladen. Wenn du Apps oder Gruppen außerhalb des Tools änderst, manuell refreshen.
|
||||
|
||||
---
|
||||
|
||||
## Vendor-Erkennung (PatchMyPC, Robopack, …)
|
||||
|
||||
Das Tool unterscheidet zwischen **Vendor-verwalteten Apps** (die ueber ein externes Portal wie PatchMyPC oder Robopack ausgerollt werden) und **nativen Intune-Apps** (manuell hochgeladen). Vendor-Apps bekommen ein eigenes Source-Logo + Lock-Icons statt Trash/Pencil, weil Aenderungen in Intune beim naechsten Vendor-Sync ueberschrieben wuerden.
|
||||
|
||||
### Wie wird erkannt?
|
||||
|
||||
Pro Vendor gibt es eine **Detection-Regel** in den Einstellungen (`settings.json` → `vendors[]`). Eine Regel besteht aus:
|
||||
|
||||
- **field** — welches Intune-App-Feld geprueft wird:
|
||||
- `commandLine` (default fuer PMPC): pruef install- und uninstall-Commandline
|
||||
- `developer` (default fuer Robopack): Entwickler-Feld der App
|
||||
- `publisher`: Publisher-Feld
|
||||
- `displayName`: App-Name
|
||||
- `notes`: Notes-Feld
|
||||
- `owner`: Owner-Feld
|
||||
- **match** — wie verglichen wird:
|
||||
- `contains`: Substring (case-insensitive)
|
||||
- `equals`: exakter Match
|
||||
- `startsWith`: Prefix-Match
|
||||
- `regex`: voller Regex
|
||||
- **pattern** — der zu suchende String / Regex
|
||||
|
||||
### Standard-Regeln
|
||||
|
||||
| Vendor | Feld | Match | Pattern | Erklärung |
|
||||
|---|---|---|---|---|
|
||||
| PatchMyPC | `commandLine` | `contains` | `PatchMyPC` | PMPC schreibt seinen Namen in den Install-/Uninstall-Aufruf (z. B. `PatchMyPC-ScriptRunner.ps1`) |
|
||||
| Robopack | `developer` | `equals` | `Robopack` | Robopack-Konvention setzt das Developer-Feld der App auf "Robopack" |
|
||||
|
||||
### Was, wenn meine Vendor-Apps nicht erkannt werden?
|
||||
|
||||
`settings.json` unter `%APPDATA%\IntuneAppManager-Web\` oeffnen und im `vendors[]`-Array die Detection-Regel anpassen. Beispiel: wenn deine Robopack-Apps statt im Developer-Feld ueber einen Prefix im Display-Namen erkennbar sind:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "robopack",
|
||||
"displayName": "Robopack",
|
||||
"portalUrl": "https://app.robopack.com/",
|
||||
"logoFile": "robopack.png",
|
||||
"detection": { "field": "displayName", "match": "startsWith", "pattern": "RP - " },
|
||||
"block": { "delete": true, "rename": true }
|
||||
}
|
||||
```
|
||||
|
||||
Nach dem Aendern: Tool neu starten. Im Einstellungen-Modal → **Verbindung + Lookups testen** klicken — dort wird pro Vendor die aktuelle Trefferzahl angezeigt. Steht da `0`, greift die Regel nicht.
|
||||
|
||||
### Reihenfolge im `vendors[]`-Array
|
||||
|
||||
Der **erste passende Vendor gewinnt**. Wenn du mehrere ueberlappende Regeln hast (z.B. PMPC erkennt commandLine, Robopack erkennt developer — und eine App passt zu beiden), wird der Vendor der weiter oben in der Liste steht zugewiesen. PMPC ist im Default oben.
|
||||
|
||||
### Eigenen Vendor hinzufuegen
|
||||
|
||||
Neuer Eintrag im `vendors[]`-Array — beliebige `id` (klein, ohne Sonderzeichen, wird Teil des HTTP-Status-Codes `<id>Managed`), eigener `displayName`, eigenes Logo unter `<id>.png` im Projekt-Root. Wenn das Logo fehlt, zeigt das Tool den ersten Buchstaben des Namens als Fallback-Badge.
|
||||
|
||||
---
|
||||
|
||||
## Setup-Datei aktualisieren
|
||||
|
||||
Native Intune-Apps koennen mit einer neuen Setup-Datei aktualisiert werden, ohne die App neu anzulegen — Zuweisungen, Detection-Rules und Einstellungen bleiben erhalten, nur der ausgerollte Datei-Inhalt (und optional die Versionsnummer) wird ersetzt.
|
||||
|
||||
- **Wo:** App-Detail aufklappen → Abschnitt **Setup-Datei** → *Setup-Datei aktualisieren…*. Der Button erscheint nur bei unterstuetzten, **nicht** vendor-verwalteten Apps (PatchMyPC/Robopack sind ausgeschlossen — die werden im jeweiligen Portal aktualisiert).
|
||||
- **Unterstuetzt (aktuell):** `.intunewin` (Win32-Apps). MSI und MSIX folgen.
|
||||
- **Datei-Quelle:** ein **lokaler Pfad auf dem Rechner, auf dem das Tool laeuft** (der Server liest die Datei direkt von der Platte — kein Browser-Upload). Beispiel: `C:\Pakete\app-2.0\app.intunewin`.
|
||||
- **Ablauf:** Pfad eingeben → optional neue Versionsnummer → Bestaetigung anhaken → *Hochladen & aktivieren*. Das Tool legt eine neue Content-Version an, laedt die (bereits durch IntuneWinAppUtil verschluesselte) Datei nach Azure, committet sie und stellt die App auf die neue Version um.
|
||||
|
||||
> **Achtung:** Das ersetzt die ausgerollte Version. Alle zugewiesenen Geraete bekommen beim naechsten Sync die neue Datei. Nicht rueckgaengig machbar — **immer zuerst an einer Test-App ausprobieren.**
|
||||
|
||||
---
|
||||
|
||||
## Tastatur / Tipps
|
||||
|
||||
- **Ctrl+F5** — Hard-Refresh (löscht den Browser-Cache vollständig, lädt das JS/CSS neu — Pflicht nach Tool-Updates).
|
||||
- **Esc** in jedem Modal — schließen.
|
||||
- **Enter** im *Umbenennen*-Modal — speichern.
|
||||
- **Klick auf eine App-Zeile** außerhalb von Buttons/Links — Detail-Panel öffnen/schließen.
|
||||
- **Beim Apply** wird der HTML-Report automatisch im Browser geöffnet — kann später in `%TEMP%\IntuneAppManager-Reports\` wieder aufgerufen werden.
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
| Symptom | Ursache / Lösung |
|
||||
|---|---|
|
||||
| „User canceled authentication" beim Verbinden | Das WAM-Anmeldefenster ist hinter anderen Fenstern verborgen. Das Tool versucht es automatisch nach vorne zu holen — wenn das nicht hilft, schaue in der Taskleiste oder klicke nochmal *Verbinden*. |
|
||||
| Apps laden ewig | Bei großen Tenants (> 1000 Apps) kann das initiale Laden 10–30 s dauern. Apps werden danach gecached. Neu laden über Refresh-Icon. |
|
||||
| Änderungen am Settings/Logo wirken nicht | Browser-Cache. **Ctrl+F5** drücken. |
|
||||
| „Gruppe existiert bereits" beim Erstellen | Wahrscheinlich gibt es schon eine Gruppe mit dem generierten Namen. Im Dialog den Vorschlag manuell anpassen. |
|
||||
| PowerShell-Konsole zeigt Errors beim Start | Module fehlen oder sind zu alt. `Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force` ausführen. |
|
||||
| Apply-Button bleibt grau / disabled | Es sind 0 Vorgänge in der Session, oder alle gesammelten Operationen sind native Targets (werden übersprungen). Mindestens einen Empfänger + Pille setzen. |
|
||||
|
||||
---
|
||||
|
||||
Stand: Mai 2026 · Tool-Version siehe Konsolen-Output beim Start und im Apply-Report-Footer. Hilfe-Datei: `www/help.md`. Vollständige Änderungshistorie: `CHANGELOG.md`.
|
||||
@@ -0,0 +1,807 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Intune App-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 App-Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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 (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 im Entra Admin Center. Erforderlich: <em>Allow public client flows</em> = Ja, Redirect URI <code>http://localhost</code>.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="lbl" for="setScopes">Scopes (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.Read.All"></textarea>
|
||||
<div class="form-hint">Die Standard-Liste passt fuer den vollen Funktionsumfang. Weniger Scopes = weniger Rechte.</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>
|
||||
|
||||
<!-- 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 App-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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user