Blaupausen je Kunde, Datei-Uploads, RDP-Verknuepfungen, Bloatware-Entfernung

- Blaupausen (blueprints\) mit Kunde/Beschreibung, gruppierte Ansicht,
  Duplizieren/Export, Schnellauswahl auf der Uebersicht, 2 Beispiele
- Upload von Setups (EXE/MSI/MSIX/PS1/CMD) per Drag & Drop inkl.
  gespeicherter Silent-Parameter; Upload von Icons und .rdp-Dateien
- RDP-Verknuepfungen fuer alle Benutzer: Generator (Gateway, RemoteApp,
  Umleitungen) oder hochgeladene .rdp-Dateien
- Bloatware-Tab: Store-Apps, Hersteller-Tools, Testversionen,
  vorinstalliertes Office/OneNote (C2R) und OneDrive entfernen;
  Katalog in config/bloatware.json, auch als Schritt in Blaupausen
- Fix: Top-Level-JSON-Arrays (Vorlagen) wurden falsch serialisiert

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 16:00:25 +02:00
co-authored by Claude Opus 5.5
parent f4c2a16612
commit ebfec7000e
11 changed files with 940 additions and 89 deletions
+3 -1
View File
@@ -8,4 +8,6 @@ installers/*
!installers/.gitkeep
packages/winget/*
!packages/winget/.gitkeep
profiles/*.json
blueprints/*.json
!blueprints/Beispiel__*.json
rdp/*.rdp
+43 -17
View File
@@ -157,13 +157,32 @@ function Invoke-Api {
'^GET /api/status$' { return Send-Json $Ctx (Get-SystemInfo) }
'^GET /api/catalog$' { return Send-Json $Ctx (Get-JsonFile 'config\apps.json') }
'^GET /api/presets$' {
return Send-Json $Ctx ([pscustomobject]@{ shortcuts = (Get-JsonFile 'config\shortcuts.json'); timezones = @(Get-TimeZone -ListAvailable | ForEach-Object { @{ id = $_.Id; name = $_.DisplayName } }) })
return Send-Json $Ctx ([pscustomobject]@{ shortcuts = @((Get-JsonFile 'config\shortcuts.json') | ForEach-Object { $_ }); timezones = @(Get-TimeZone -ListAvailable | ForEach-Object { @{ id = $_.Id; name = $_.DisplayName } }) })
}
'^GET /api/winget/search$' { return Send-Json $Ctx @(Search-Winget -Query $q['q'] -Source $(if ($q['source']) { $q['source'] } else { 'winget' })) }
'^GET /api/local-installers$' {
$dir = Join-Path $global:KdsRoot 'installers'
$files = @(Get-ChildItem $dir -Recurse -Include *.exe, *.msi -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName.Substring($global:KdsRoot.Length + 1) })
return Send-Json $Ctx $files
'^GET /api/local-installers$' { return Send-Json $Ctx @(Get-LocalInstallers) }
'^POST /api/local-installers$' { $b = Read-Body $Ctx; Set-InstallerMeta $b; return Send-Json $Ctx @{ ok = $true } }
# Datei-Upload: Body = Rohdaten, ?kind=installers|icons|rdp&name=Datei.ext
'^POST /api/upload$' {
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
New-Item $dir -ItemType Directory -Force | Out-Null
$target = Join-Path $dir $name
$fs = [IO.File]::Create($target)
try { $Ctx.Request.InputStream.CopyTo($fs, 1MB) } finally { $fs.Close() }
Write-Log "Upload: $($q['kind'])\$name ($([math]::Round((Get-Item $target).Length / 1MB, 1)) MB)"
return Send-Json $Ctx @{ path = $target.Substring($global:KdsRoot.Length + 1); name = $name }
}
'^GET /api/files$' {
$dir = (Resolve-UploadTarget $q['kind'] 'x.ico')[0]
return Send-Json $Ctx @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object Name -notlike '.*' | Where-Object Name -ne 'installers.json' |
ForEach-Object { [pscustomobject]@{ name = $_.Name; sizeMB = [math]::Round($_.Length / 1MB, 2) } })
}
'^DELETE /api/files$' {
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
Remove-Item (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
if ($q['kind'] -eq 'installers') { Set-InstallerMeta ([pscustomobject]@{ file = "installers\$name"; remove = $true }) }
return Send-Json $Ctx @{ ok = $true }
}
'^POST /api/apps/install$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Programme installieren ($(@($b.apps).Count))" 'Install-Apps' $b) } }
'^POST /api/apps/upgrade-all$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Alle Programme aktualisieren (winget)' 'Update-AllApps' @{}) } }
@@ -185,6 +204,10 @@ function Invoke-Api {
'^GET /api/icons$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'icons') -Include *.ico, *.exe, *.dll -Recurse -ErrorAction SilentlyContinue | ForEach-Object Name)
}
'^GET /api/rdpfiles$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'rdp') -Filter *.rdp -ErrorAction SilentlyContinue | ForEach-Object Name)
}
'^POST /api/rdp/preview$' { $b = Read-Body $Ctx; return Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes((New-RdpContent $b))) 'text/plain; charset=utf-8' }
'^POST /api/system$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Systemeinstellungen' 'Set-SystemConfig' $b) } }
'^POST /api/security$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Sicherheitseinstellungen' 'Set-SecurityConfig' $b) } }
@@ -193,6 +216,8 @@ function Invoke-Api {
$active = (powercfg /getactivescheme) -join ''
return Send-Json $Ctx @{ active = $active; hibernate = [bool](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled }
}
'^GET /api/bloatware$' { return Send-Json $Ctx @(Get-BloatwareInventory) }
'^POST /api/bloatware$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Bloatware entfernen' 'Remove-Bloatware' $b) } }
'^GET /api/bitlocker$' { return Send-Json $Ctx (Get-BitLockerInfo) }
'^POST /api/bitlocker$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "BitLocker: $($b.bitlocker.action)" 'Set-BitLockerConfig' $b) } }
@@ -201,22 +226,23 @@ function Invoke-Api {
'^POST /api/updates/vendor$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Hersteller-Treiber-Update' 'Invoke-VendorDriverUpdate' @{}) } }
'^POST /api/reboot$' { Restart-Computer -Force; return Send-Json $Ctx @{ ok = $true } }
'^GET /api/profiles$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'profiles') -Filter *.json -ErrorAction SilentlyContinue | ForEach-Object BaseName)
}
'^GET /api/profiles/(?<n>[\w\-\. ]+)$' {
$p = Join-Path $global:KdsRoot "profiles\$($Matches.n).json"
if (-not (Test-Path $p)) { return Send-Json $Ctx @{ error = 'nicht gefunden' } 404 }
# Blaupausen (Standard-Einrichtungen je Kunde) unter blueprints\<id>.json
'^GET /api/blueprints$' { return Send-Json $Ctx @(Get-Blueprints) }
'^GET /api/blueprints/(?<n>[^/\\]+)$' {
$p = Get-BlueprintPath $Matches.n
if (-not (Test-Path $p)) { return Send-Json $Ctx @{ error = 'Blaupause nicht gefunden' } 404 }
return Send-Response $Ctx ([IO.File]::ReadAllBytes($p))
}
'^POST /api/profiles/(?<n>[\w\-\. ]+)$' {
'^POST /api/blueprints/(?<n>[^/\\]+)$' {
$sr = New-Object IO.StreamReader($Ctx.Request.InputStream, [Text.Encoding]::UTF8); $txt = $sr.ReadToEnd(); $sr.Close()
$dir = Join-Path $global:KdsRoot 'profiles'; New-Item $dir -ItemType Directory -Force | Out-Null
[IO.File]::WriteAllText((Join-Path $dir "$($Matches.n).json"), $txt, (New-Object Text.UTF8Encoding($false)))
$null = $txt | ConvertFrom-Json # nur gueltiges JSON speichern
$p = Get-BlueprintPath $Matches.n
New-Item (Split-Path $p) -ItemType Directory -Force | Out-Null
[IO.File]::WriteAllText($p, $txt, (New-Object Text.UTF8Encoding($false)))
return Send-Json $Ctx @{ ok = $true }
}
'^DELETE /api/profiles/(?<n>[\w\-\. ]+)$' { Remove-Item (Join-Path $global:KdsRoot "profiles\$($Matches.n).json") -ErrorAction SilentlyContinue; return Send-Json $Ctx @{ ok = $true } }
'^POST /api/run-profile$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Profil ausfuehren: $($b.name)" 'Invoke-FullProfile' $b) } }
'^DELETE /api/blueprints/(?<n>[^/\\]+)$' { Remove-Item (Get-BlueprintPath $Matches.n) -ErrorAction SilentlyContinue; return Send-Json $Ctx @{ ok = $true } }
'^POST /api/run-profile$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Einrichtung: $(@($b.customer, $b.name) -ne '' -join ' / ')" 'Invoke-FullProfile' $b) } }
'^GET /api/jobs$' { Update-KdsJobs; return Send-Json $Ctx @($script:Jobs.Values | ForEach-Object { Get-KdsJobView $_ }) }
'^GET /api/jobs/(?<id>\d+)$' {
@@ -276,7 +302,7 @@ while ($script:Running -and $listener.IsListening) {
$async = $listener.BeginGetContext($null, $null)
while (-not $async.AsyncWaitHandle.WaitOne(1000)) { Update-KdsJobs }
$ctx = $listener.EndGetContext($async)
$path = $ctx.Request.Url.AbsolutePath
$path = [Uri]::UnescapeDataString($ctx.Request.Url.AbsolutePath)
try {
if ($path.StartsWith('/api/')) {
$t = $ctx.Request.Headers['X-Token']
+9 -6
View File
@@ -15,19 +15,20 @@ Die Web-API ist immer mit einem zufälligen Token geschützt, das bei jedem Star
## Funktionen
- **Programme**: Live-Suche im öffentlichen **winget**-Repository und im **Microsoft Store**, Schnellauswahl aus `config/apps.json`, eigene Installer aus `installers\`. Installation systemweit (`--scope machine`, Fallback auf Standard-Scope). „Alle aktualisieren“ über `winget upgrade --all`. Fehlt winget, wird es automatisch nachinstalliert (offline aus `packages\winget\`, sonst über Microsoft).
- **Programme**: Live-Suche im öffentlichen **winget**-Repository und im **Microsoft Store**, Schnellauswahl aus `config/apps.json`. Eigene Setups (EXE, MSI, MSIX, PS1, CMD) per Drag & Drop **hochladen**; Silent-Parameter werden je Datei gespeichert (`installers\installers.json`). Installation systemweit (`--scope machine`, Fallback auf Standard-Scope). „Alle aktualisieren“ über `winget upgrade --all`. Fehlt winget, wird es automatisch nachinstalliert (offline aus `packages\winget\`, sonst über Microsoft).
- **Office**: Konfigurator für das Office Deployment Tool (M365 Apps Enterprise/Business, LTSC 2024/2021, Home & Business 2024, Visio/Project, Sprachen, Apps abwählen, Kanal, Architektur, MSI-Entfernung, Shared Activation, Makro-Sperre …) mit Live-Vorschau der `configuration.xml`.
- *Online*: direkt vom Microsoft-CDN installieren
- *Build*: Installationsdateien nach `office\source\<Produkt>-<Arch>` auf den Stick laden
- *Offline*: vom Stick installieren
- `setup.exe` wird bei Bedarf automatisch geladen; generierte XMLs landen in `office\configs\`.
- **Desktop-Icons** für alle Benutzer (öffentlicher Desktop): Web-Links (`.url`) und Programmverknüpfungen (`.lnk`), eigene Icons aus `icons\` (werden nach `C:\ProgramData\KDS-Agent\Icons` kopiert). Vorlagen in `config/shortcuts.json`.
- **Desktop-Icons** für alle Benutzer (öffentlicher Desktop): Web-Links (`.url`), Programmverknüpfungen (`.lnk`) und **RDP-Verbindungen** (`.rdp`: im Formular erzeugt inkl. Gateway, RemoteApp, Umleitungen, oder vorhandene .rdp-Dateien hochladen), eigene Icons hochladbar nach `icons\` (werden nach `C:\ProgramData\KDS-Agent\Icons` kopiert). Vorlagen in `config/shortcuts.json`.
- **Bloatware**: Scan des Geräts und Entfernen per Klick: Windows-Store-Apps (für alle Benutzer + Vorlage für neue Benutzer), Hersteller-Tools (HP, Dell, Lenovo, Acer, ASUS), Testversionen (McAfee, Norton …), vorinstallierte Office/OneNote-Versionen (Click-to-Run, z. B. `Microsoft 365 - fr-fr`) und OneDrive. Empfohlene Einträge sind vorausgewählt; der Katalog ist in `config/bloatware.json` erweiterbar. Business-Office-Versionen (Volumen/Enterprise) werden angezeigt, aber nie automatisch empfohlen.
- **System**: Computername, Zeitzone, Dateiendungen anzeigen (auch für neue Benutzer), Consumer-Features/Werbe-Apps aus.
- **Sicherheit**: Defender (Echtzeit/Cloud/PUA), Firewall, SmartScreen, UAC, SMBv1 aus, Autorun aus, LSA-Schutz, Telemetrie, RDP an/aus, lokales Admin-Konto.
- **Energie**: Energieplan, Timeouts für Bildschirm/Standby/Ruhezustand (Netz & Akku), Deckel- und Netzschalteraktion, Ruhezustand, Schnellstart.
- **BitLocker**: Status, Aktivierung mit TPM oder TPM+PIN, XTS-AES 128/256, nur belegten Speicher, Sicherung in Entra ID / AD. Der Wiederherstellungsschlüssel wird **immer** unter `bitlocker-keys\` auf dem Stick gespeichert (Computername + Seriennummer).
- **Updates & Treiber**: Windows- und Treiberupdates über die Windows-Update-API (inkl. Microsoft Update) suchen, auswählen, installieren. Hersteller-Tools: Dell Command Update, Lenovo System Update, HP Image Assistant.
- **Profile**: Gesamte Konfiguration als JSON unter `profiles\` speichern/laden/importieren/exportieren.
- **Blaupausen**: Komplette Standard-Einrichtungen je Kunde unter `blueprints\` speichern, nach Kunde gruppiert anzeigen, suchen, laden, duplizieren, exportieren/importieren. Auf der Übersicht per Dropdown auswählen und direkt starten. Mitgeliefert: `Beispiel / Büro-Standard` und `Beispiel / Minimal`.
- **Einrichtung mit einem Klick**: Auf der Übersicht alle Schritte in sinnvoller Reihenfolge ausführen.
- **Aufgaben**: Alle Aktionen laufen als Hintergrund-Jobs in einer Warteschlange (nacheinander, damit sich Installer nicht blockieren). Live-Protokoll in der Oberfläche; Logs unter `logs\<Computername>\`.
@@ -40,11 +41,13 @@ lib\Functions.ps1 Alle Einrichtungsfunktionen
web\ Oberfläche (HTML/CSS/JS)
config\apps.json Programm-Schnellauswahl (anpassbar)
config\shortcuts.json Verknüpfungs-Vorlagen (anpassbar)
installers\ eigene Setups (.exe/.msi)
installers\ hochgeladene Setups + installers.json (Silent-Parameter)
icons\ eigene Icons (.ico)
rdp\ hochgeladene .rdp-Dateien
config\bloatware.json Bloatware-Katalog (anpassbar)
packages\winget\ optional: winget-Offlinepakete (*.msixbundle + Abhängigkeiten *.appx)
office\ setup.exe, configs\, source\ (Offline-Builds)
profiles\ gespeicherte Profile
blueprints\ Blaupausen (je Kunde)
logs\, bitlocker-keys\ werden zur Laufzeit angelegt
```
@@ -52,7 +55,7 @@ Ist der Stick schreibgeschützt, schreibt der Agent Logs und Schlüssel nach `C:
## Hinweise
- **Sicherheit**: Profile können Klartext-Kennwörter (lokaler Admin, BitLocker-PIN) enthalten, und `bitlocker-keys\` enthält Wiederherstellungsschlüssel. Den Stick entsprechend schützen. Beide Ordner sind per `.gitignore` von der Versionsverwaltung ausgeschlossen.
- **Sicherheit**: Blaupausen können Klartext-Kennwörter (lokaler Admin, BitLocker-PIN) enthalten, und `bitlocker-keys\` enthält Wiederherstellungsschlüssel. Den Stick entsprechend schützen. Blaupausen (außer den Beispielen), RDP-Dateien und Schlüssel sind per `.gitignore` von der Versionsverwaltung ausgeschlossen.
- BitLocker benötigt Windows 11 Pro/Enterprise/Education. Unter Home steht nur die Geräteverschlüsselung zur Verfügung.
- Die Store-Installation (`msstore`) braucht Internet und kann bei Konten ohne Store-Zugang fehlschlagen.
- Einige Einstellungen (Computername, LSA-Schutz, SMBv1, BitLocker mit Hardwaretest) werden erst nach einem Neustart wirksam.
+94
View File
@@ -0,0 +1,94 @@
{
"name": "Büro-Standard",
"customer": "Beispiel",
"description": "Typisches Büro-Notebook: M365 Business, Browser, PDF, Fernwartung, Bloatware raus, BitLocker an.",
"apps": [
{
"source": "winget",
"name": "Google Chrome",
"id": "Google.Chrome"
},
{
"source": "winget",
"name": "Adobe Acrobat Reader (64-Bit)",
"id": "Adobe.Acrobat.Reader.64-bit"
},
{
"source": "winget",
"name": "7-Zip",
"id": "7zip.7zip"
},
{
"source": "winget",
"name": "TeamViewer QuickSupport",
"id": "TeamViewer.TeamViewer.QuickSupport"
},
{
"source": "winget",
"name": "Visual C++ 2015-2022 x64",
"id": "Microsoft.VCRedist.2015+.x64"
}
],
"office": {
"languages": [
"de-de"
],
"arch": "64",
"channel": "MonthlyEnterprise",
"exclude": [
"Groove",
"Lync",
"Bing",
"Access",
"Publisher"
],
"product": "O365BusinessRetail"
},
"officeMode": "online",
"shortcuts": [
{
"name": "Microsoft 365",
"target": "https://www.office.com",
"type": "url"
},
{
"rdp": {
"printers": true,
"host": "ts01.firma.local",
"audio": "local",
"fullscreen": true,
"clipboard": true
},
"name": "Terminalserver",
"target": "ts01.firma.local",
"type": "rdp"
}
],
"bloatware": {
"ids": [
],
"recommended": true,
"keep": [
]
},
"bitlocker": {
"method": "XtsAes256",
"usedSpaceOnly": true,
"backupAad": true,
"skipHardwareTest": true
},
"steps": [
"system",
"bloatware",
"security",
"power",
"apps",
"office",
"shortcuts",
"drivers",
"updates",
"bitlocker"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"name": "Minimal",
"customer": "Beispiel",
"description": "Nur aufräumen, absichern und aktualisieren – keine Programme, kein Office.",
"apps": [
],
"shortcuts": [
],
"bloatware": {
"ids": [
],
"recommended": true,
"keep": [
]
},
"steps": [
"system",
"bloatware",
"security",
"power",
"updates"
]
}
+96
View File
@@ -0,0 +1,96 @@
{
"_info": "Bloatware-Katalog. appx: Paketname (Wildcards *). win32: Regex auf den Anzeigenamen in 'Programme und Features'. recommended = bei 'Empfohlene' vorausgewaehlt.",
"appx": [
{ "pattern": "Microsoft.BingNews", "name": "Microsoft News", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.BingWeather", "name": "Wetter", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.BingSearch", "name": "Bing-Suche", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.GetHelp", "name": "Hilfe anfordern", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Getstarted", "name": "Tipps", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.MicrosoftSolitaireCollection", "name": "Solitaire Collection", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.MicrosoftOfficeHub", "name": "Microsoft 365 (Office-Hub-App)", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Office.OneNote", "name": "OneNote für Windows 10 (App)", "category": "Office / OneNote vorinstalliert", "recommended": true },
{ "pattern": "Microsoft.People", "name": "Kontakte", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.SkypeApp", "name": "Skype", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.WindowsFeedbackHub", "name": "Feedback-Hub", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.WindowsMaps", "name": "Karten", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.ZuneMusic", "name": "Media Player / Groove", "category": "Windows-Apps", "recommended": false },
{ "pattern": "Microsoft.ZuneVideo", "name": "Filme & TV", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.MixedReality.Portal", "name": "Mixed Reality Portal", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Microsoft3DViewer", "name": "3D-Viewer", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.MSPaint", "name": "Paint 3D", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Wallet", "name": "Wallet", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Todos", "name": "Microsoft To Do", "category": "Windows-Apps", "recommended": false },
{ "pattern": "Microsoft.PowerAutomateDesktop", "name": "Power Automate", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.549981C3F5F10", "name": "Cortana", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.Windows.DevHome", "name": "Dev Home", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.MicrosoftJournal", "name": "Journal", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.OutlookForWindows", "name": "Outlook (neu, Consumer)", "category": "Windows-Apps", "recommended": false },
{ "pattern": "Microsoft.Copilot", "name": "Copilot", "category": "Windows-Apps", "recommended": false },
{ "pattern": "MicrosoftTeams", "name": "Teams (privat)", "category": "Windows-Apps", "recommended": true },
{ "pattern": "MicrosoftCorporationII.MicrosoftFamily", "name": "Family Safety", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Clipchamp.Clipchamp", "name": "Clipchamp", "category": "Windows-Apps", "recommended": true },
{ "pattern": "Microsoft.YourPhone", "name": "Smartphone-Link", "category": "Windows-Apps", "recommended": false },
{ "pattern": "Microsoft.GamingApp", "name": "Xbox", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.XboxApp", "name": "Xbox Console Companion", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.XboxGamingOverlay", "name": "Xbox Game Bar", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.XboxGameOverlay", "name": "Xbox Game Overlay", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.XboxSpeechToTextOverlay", "name": "Xbox Speech to Text", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.Xbox.TCUI", "name": "Xbox TCUI", "category": "Xbox", "recommended": true },
{ "pattern": "Microsoft.XboxIdentityProvider", "name": "Xbox Identity Provider", "category": "Xbox", "recommended": false },
{ "pattern": "E046963F.LenovoCompanion", "name": "Lenovo Vantage", "category": "Hersteller: Lenovo", "recommended": true },
{ "pattern": "E046963F.LenovoSettingsforEnterprise", "name": "Lenovo Commercial Vantage", "category": "Hersteller: Lenovo", "recommended": false },
{ "pattern": "E0469640.LenovoUtility", "name": "Lenovo Utility", "category": "Hersteller: Lenovo", "recommended": false },
{ "pattern": "E0469640.LenovoSmartCommunication", "name": "Lenovo Smart Communication", "category": "Hersteller: Lenovo", "recommended": true },
{ "pattern": "LenovoCorporation.LenovoID", "name": "Lenovo ID", "category": "Hersteller: Lenovo", "recommended": true },
{ "pattern": "LenovoCorporation.LenovoSettings", "name": "Lenovo Settings", "category": "Hersteller: Lenovo", "recommended": true },
{ "pattern": "AD2F1837.myHP", "name": "myHP", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPSupportAssistant", "name": "HP Support Assistant (App)", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPPrivacySettings", "name": "HP Privacy Settings", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPJumpStarts", "name": "HP JumpStarts", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPPCHardwareDiagnosticsWindows", "name": "HP PC Hardware Diagnostics", "category": "Hersteller: HP", "recommended": false },
{ "pattern": "AD2F1837.HPPowerManager", "name": "HP Power Manager", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPSureShieldAI", "name": "HP Sure Shield AI", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPSystemInformation", "name": "HP System Information", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPQuickDrop", "name": "HP QuickDrop", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPWorkWell", "name": "HP WorkWell", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPEasyClean", "name": "HP Easy Clean", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPDesktopSupportUtilities", "name": "HP Desktop Support Utilities", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "AD2F1837.HPAIExperienceCenter", "name": "HP AI Companion", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "DellInc.DellSupportAssistforPCs", "name": "Dell SupportAssist (App)", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "DellInc.DellDigitalDelivery", "name": "Dell Digital Delivery", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "DellInc.DellCustomerConnect", "name": "Dell Customer Connect", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "DellInc.PartnerPromo", "name": "Dell Partner Promo", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "DellInc.DellOptimizer", "name": "Dell Optimizer (App)", "category": "Hersteller: Dell", "recommended": false },
{ "pattern": "DellInc.MyDell", "name": "My Dell", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "AcerIncorporated.*", "name": "Acer-Apps", "category": "Hersteller: Acer", "recommended": true },
{ "pattern": "B9ECED6F.ASUSPCAssistant", "name": "MyASUS", "category": "Hersteller: ASUS", "recommended": true },
{ "pattern": "B9ECED6F.ASUSGIFTBOX", "name": "ASUS GiftBox", "category": "Hersteller: ASUS", "recommended": true },
{ "pattern": "5A894077.McAfeeSecurity", "name": "McAfee Security (App)", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "C27EB4BA.DropboxOEM", "name": "Dropbox (OEM-Angebot)", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*ExpressVPN*", "name": "ExpressVPN", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*Booking.com*", "name": "Booking.com", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*CandyCrush*", "name": "Candy Crush", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*Disney*", "name": "Disney+", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "SpotifyAB.SpotifyMusic", "name": "Spotify", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*Netflix*", "name": "Netflix", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "AmazonVideo.PrimeVideo", "name": "Prime Video", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*Facebook*", "name": "Facebook", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*Instagram*", "name": "Instagram", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*TikTok*", "name": "TikTok", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "7EE7776C.LinkedInforWindows", "name": "LinkedIn", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "*WildTangent*", "name": "WildTangent Games", "category": "Testversionen & Werbung", "recommended": true }
],
"win32": [
{ "pattern": "^HP (Wolf Security|Sure (Click|Sense|Run|Recover)|Security Update Service|Connection Optimizer|Documentation|Notifications|Support Assistant|System Default Settings|Insights|Smart Support|JumpStart|Client Security Manager|Privacy Settings|Audio Switch|Easy Clean|Collaboration Keyboard)", "category": "Hersteller: HP", "recommended": true },
{ "pattern": "^Dell (SupportAssist|Digital Delivery|Customer Connect|Update( for Windows)?$|Core Services|Trusted Device|Pair)", "category": "Hersteller: Dell", "recommended": true },
{ "pattern": "^Dell (Optimizer|Display Manager|Peripheral Manager)", "category": "Hersteller: Dell", "recommended": false },
{ "pattern": "^Lenovo (Vantage|Now|Welcome|Smart Appearance|Service Bridge|Voice|Pen Settings|Smart Meeting)", "category": "Hersteller: Lenovo", "recommended": true },
{ "pattern": "^Lenovo (Hotkeys|Utility|Commercial Vantage)", "category": "Hersteller: Lenovo", "recommended": false },
{ "pattern": "^Acer (Care Center|Jumpstart|Collection|Configuration Manager|Quick Access|Product Registration|Portal|User Experience|Purified Voice)", "category": "Hersteller: Acer", "recommended": true },
{ "pattern": "^(MyASUS|ASUS (GiftBox|Welcome|Live Update|Hello|Splendid|Armoury))", "category": "Hersteller: ASUS", "recommended": true },
{ "pattern": "^(McAfee|Norton|Avast|AVG |Avira)", "category": "Testversionen & Werbung", "recommended": true },
{ "pattern": "^(ExpressVPN|WildTangent|Booking\\.com|Dropbox (Promotion|25 GB))", "category": "Testversionen & Werbung", "recommended": true }
]
}
+294 -8
View File
@@ -190,12 +190,22 @@ function Install-Apps {
$file = $a.path
if (-not [IO.Path]::IsPathRooted($file)) { $file = Join-Path $global:KdsRoot $file }
if (-not (Test-Path $file)) { throw "Installer nicht gefunden: $file" }
if ($file -like '*.msi') {
$p = Start-Process msiexec.exe -ArgumentList "/i `"$file`" /qn /norestart $($a.args)" -Wait -PassThru
} else {
$p = Start-Process -FilePath $file -ArgumentList $a.args -Wait -PassThru
$ext = [IO.Path]::GetExtension($file).ToLower()
$sp = @{ Wait = $true; PassThru = $true; WorkingDirectory = (Split-Path $file) }
switch ($ext) {
'.msi' { $sp.FilePath = 'msiexec.exe'; $sp.ArgumentList = "/i `"$file`" /qn /norestart $($a.args)" }
'.ps1' { $sp.FilePath = 'powershell.exe'; $sp.ArgumentList = "-NoProfile -ExecutionPolicy Bypass -File `"$file`" $($a.args)" }
{ $_ -in '.cmd', '.bat' } { $sp.FilePath = 'cmd.exe'; $sp.ArgumentList = "/c `"`"$file`" $($a.args)`"" }
{ $_ -in '.msix', '.msixbundle', '.appx', '.appxbundle' } { $sp = $null }
default { $sp.FilePath = $file; if ($a.args) { $sp.ArgumentList = $a.args } }
}
if ($sp) {
$code = (Start-Process @sp).ExitCode
} else {
# Paket fuer alle (auch kuenftige) Benutzer bereitstellen
Add-AppxProvisionedPackage -Online -PackagePath $file -SkipLicense | Out-Null
$code = 0
}
$code = $p.ExitCode
} else {
$src = if ($a.source) { $a.source } else { 'winget' }
$base = @('install', '--id', $a.id, '--exact', '--source', $src, '--silent',
@@ -334,10 +344,14 @@ function Get-PublicDesktop { [Environment]::GetFolderPath('CommonDesktopDirector
function Get-PublicShortcuts {
$d = Get-PublicDesktop
$sh = New-Object -ComObject WScript.Shell
@(Get-ChildItem $d -File | Where-Object { $_.Extension -in '.lnk', '.url' } | ForEach-Object {
@(Get-ChildItem $d -File | Where-Object { $_.Extension -in '.lnk', '.url', '.rdp' } | ForEach-Object {
$target = ''
if ($_.Extension -eq '.lnk') { $target = $sh.CreateShortcut($_.FullName).TargetPath }
else { $m = Select-String -Path $_.FullName -Pattern '^URL=(.*)$' | Select-Object -First 1; if ($m) { $target = $m.Matches[0].Groups[1].Value } }
else {
$pat = if ($_.Extension -eq '.rdp') { '^full address:s:(.*)$' } else { '^URL=(.*)$' }
$m = Select-String -Path $_.FullName -Pattern $pat | Select-Object -First 1
if ($m) { $target = [string]$m.Matches[0].Groups[1].Value }
}
[pscustomobject]@{ file = $_.Name; name = $_.BaseName; type = $_.Extension.TrimStart('.'); target = $target }
})
}
@@ -361,7 +375,17 @@ function New-PublicShortcut {
$safe = ($S.name -replace '[\\/:*?"<>|]', '_').Trim()
if (-not $safe) { throw 'Name fehlt' }
$icon = Resolve-IconPath $S.icon
if ($S.type -eq 'url') {
if ($S.type -eq 'rdp') {
$file = Join-Path $d "$safe.rdp"
if ($S.rdpFile) {
# hochgeladene .rdp-Datei vom Stick verteilen
$src = Join-Path $global:KdsRoot "rdp\$(Split-Path $S.rdpFile -Leaf)"
if (-not (Test-Path $src)) { throw "RDP-Datei nicht gefunden: $src" }
Copy-Item $src $file -Force
} else {
Set-Content -Path $file -Value (New-RdpContent $S.rdp) -Encoding Unicode
}
} elseif ($S.type -eq 'url') {
$file = Join-Path $d "$safe.url"
$c = "[InternetShortcut]`r`nURL=$($S.target)`r`n"
if ($icon) { $c += "IconFile=$icon`r`nIconIndex=0`r`n" }
@@ -397,6 +421,123 @@ function Set-DesktopShortcuts {
}
}
function New-RdpContent {
# Erzeugt den Inhalt einer .rdp-Datei aus den Formularwerten
param($R)
if (-not $R.host) { throw 'RDP: Server/Host fehlt' }
$b = { param($v) if (Test-True $v) { 1 } else { 0 } }
$addr = $R.host
if ($R.port -and "$($R.port)" -ne '3389') { $addr += ":$($R.port)" }
$l = @(
"full address:s:$addr"
"screen mode id:i:$(if (Test-True $R.fullscreen) { 2 } else { 1 })"
"use multimon:i:$(& $b $R.multimon)"
'dynamic resolution:i:1'
'smart sizing:i:1'
"redirectclipboard:i:$(& $b $R.clipboard)"
"redirectprinters:i:$(& $b $R.printers)"
"redirectsmartcards:i:$(& $b $R.smartcards)"
"drivestoredirect:s:$(if (Test-True $R.drives) { '*' } else { '' })"
"audiomode:i:$(switch ($R.audio) { 'remote' { 1 } 'none' { 2 } default { 0 } })"
"audiocapturemode:i:$(& $b $R.microphone)"
"camerastoredirect:s:$(if (Test-True $R.camera) { '*' } else { '' })"
'authentication level:i:2'
'prompt for credentials:i:0'
'promptcredentialonce:i:1'
'enablecredsspsupport:i:1'
'autoreconnection enabled:i:1'
)
if ($R.user) { $l += "username:s:$($R.user)" }
if ($R.gateway) {
$l += "gatewayhostname:s:$($R.gateway)", 'gatewayusagemethod:i:1', 'gatewaycredentialssource:i:4', 'gatewayprofileusagemethod:i:1', 'gatewaybrokeringtype:i:0'
} else { $l += 'gatewayusagemethod:i:4' }
if ($R.remoteApp) {
$l += 'remoteapplicationmode:i:1', "remoteapplicationprogram:s:||$($R.remoteApp)", "alternate shell:s:||$($R.remoteApp)",
"remoteapplicationname:s:$(if ($R.remoteAppName) { $R.remoteAppName } else { $R.remoteApp })", 'disableremoteappcapscheck:i:1'
}
if (Test-True $R.admin) { $l += 'administrative session:i:1' }
if ($R.extra) { $l += @($R.extra -split "`r?`n" | Where-Object { $_ -match '^[^:]+:[isb]:' }) }
return ($l -join "`r`n") + "`r`n"
}
#endregion
#region Dateien / Uploads / Blaupausen --------------------------------------------
$script:UploadTypes = @{
installers = '.exe', '.msi', '.msix', '.msixbundle', '.appx', '.appxbundle', '.ps1', '.cmd', '.bat'
icons = '.ico', '.exe', '.dll'
rdp = '.rdp'
}
function Resolve-UploadTarget {
param([string]$Kind, [string]$Name)
if (-not $script:UploadTypes.ContainsKey("$Kind")) { throw "Unbekannter Upload-Typ: $Kind" }
$n = ([IO.Path]::GetFileName("$Name") -replace '[^\w\-\.\(\)\+ ]', '_').Trim()
$ext = [IO.Path]::GetExtension($n).ToLower()
if (-not $n -or $ext -notin $script:UploadTypes[$Kind]) { throw "Dateityp '$ext' ist fuer $Kind nicht erlaubt ($($script:UploadTypes[$Kind] -join ', '))" }
return @((Join-Path $global:KdsRoot $Kind), $n)
}
function Get-InstallerMetaFile { Join-Path $global:KdsRoot 'installers\installers.json' }
function Get-InstallerMeta {
$f = Get-InstallerMetaFile
if (Test-Path $f) { return @([IO.File]::ReadAllText($f) | ConvertFrom-Json | ForEach-Object { $_ }) }
return @()
}
function Set-InstallerMeta {
# Name + Silent-Parameter je Installer merken (installers\installers.json)
param($M)
$list = @(Get-InstallerMeta | Where-Object { $_.file -ne $M.file })
if (-not $M.remove) { $list += [pscustomobject]@{ file = $M.file; name = $M.name; args = $M.args } }
New-Item (Split-Path (Get-InstallerMetaFile)) -ItemType Directory -Force | Out-Null
[IO.File]::WriteAllText((Get-InstallerMetaFile), (ConvertTo-Json -InputObject @($list) -Depth 3), (New-Object Text.UTF8Encoding($false)))
}
function Get-LocalInstallers {
$dir = Join-Path $global:KdsRoot 'installers'
$meta = Get-InstallerMeta
@(Get-ChildItem $dir -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension.ToLower() -in $script:UploadTypes.installers } | ForEach-Object {
$rel = $_.FullName.Substring($global:KdsRoot.Length + 1)
$m = $meta | Where-Object file -eq $rel | Select-Object -First 1
[pscustomobject]@{
file = $rel
name = if ($m.name) { [string]$m.name } else { $_.BaseName }
args = if ($m) { [string]$m.args } else { '' }
sizeMB = [math]::Round($_.Length / 1MB, 1)
}
})
}
function Get-BlueprintPath {
param([string]$Id)
$safe = ($Id -replace '[^\w\-\. ]', '_').Trim(' .')
if (-not $safe) { throw 'Ungueltiger Name' }
return (Join-Path $global:KdsRoot "blueprints\$safe.json")
}
function Get-Blueprints {
@(Get-ChildItem (Join-Path $global:KdsRoot 'blueprints') -Filter *.json -ErrorAction SilentlyContinue | ForEach-Object {
$f = $_
try {
$j = [IO.File]::ReadAllText($f.FullName) | ConvertFrom-Json
[pscustomobject]@{
id = $f.BaseName
name = if ($j.name) { [string]$j.name } else { $f.BaseName }
customer = [string]$j.customer
description = [string]$j.description
apps = @($j.apps).Where({ $_ }).Count
shortcuts = @($j.shortcuts).Where({ $_ }).Count
office = [string]$j.office.product
steps = @($j.steps)
updated = $f.LastWriteTime.ToString('dd.MM.yyyy HH:mm')
}
} catch { Write-Log "Blaupause $($f.Name) fehlerhaft: $_" 'WARN' }
})
}
#endregion
#region System / Sicherheit -----------------------------------------------------
@@ -753,6 +894,150 @@ function Invoke-VendorDriverUpdate {
#endregion
#region Bloatware ---------------------------------------------------------------
function Get-UninstallEntries {
$keys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
Get-ItemProperty $keys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and -not $_.SystemComponent -and -not $_.ParentKeyName }
}
function Get-BloatwareInventory {
# Liefert alle auf diesem Geraet gefundenen Bloatware-Kandidaten
$cat = [IO.File]::ReadAllText((Join-Path $global:KdsRoot 'config\bloatware.json')) | ConvertFrom-Json
$items = New-Object System.Collections.ArrayList
# 1) Store-/Appx-Apps (installiert fuer irgendeinen Benutzer oder provisioniert fuer neue Benutzer)
$installed = @(Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue | Where-Object { -not $_.NonRemovable })
$provisioned = @(Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue)
foreach ($e in $cat.appx) {
$names = @($installed | Where-Object Name -like $e.pattern | ForEach-Object Name) + @($provisioned | Where-Object DisplayName -like $e.pattern | ForEach-Object DisplayName)
foreach ($n in ($names | Sort-Object -Unique)) {
if ($items | Where-Object id -eq "appx:$n") { continue }
[void]$items.Add([pscustomobject]@{
id = "appx:$n"; kind = 'appx'; name = $(if ($e.pattern -notmatch '\*') { $e.name } else { "$($e.name) ($n)" })
detail = $n; category = $e.category; recommended = [bool]$e.recommended
})
}
}
# 2) Klassische Programme + vorinstallierte Office-Versionen (Click-to-Run)
foreach ($u in Get-UninstallEntries) {
$dn = [string]$u.DisplayName
if ($u.UninstallString -match 'OfficeClickToRun\.exe') {
# Consumer-Vorinstallationen (Home/Personal/OneNote Free) empfehlen, Business-Versionen nur anzeigen
$consumer = $u.UninstallString -match 'productstoremove=(O365HomePrem|O365Personal|HomeStudent|HomeBusiness|Personal|OneNoteFree|Professional)Retail'
[void]$items.Add([pscustomobject]@{
id = "arp:$dn"; kind = 'win32'; name = $dn; detail = $(if ($u.UninstallString -match 'productstoremove=([^ ]+)') { $Matches[1] } else { 'Click-to-Run' })
category = 'Office / OneNote vorinstalliert'; recommended = $consumer
})
continue
}
if ($dn -match '^Microsoft OneDrive') { continue } # eigener Eintrag
$hit = $cat.win32 | Where-Object { $dn -match $_.pattern } | Select-Object -First 1
if ($hit) {
[void]$items.Add([pscustomobject]@{
id = "arp:$dn"; kind = 'win32'; name = $dn; detail = "$($u.Publisher) $($u.DisplayVersion)".Trim()
category = $hit.category; recommended = [bool]$hit.recommended
})
}
}
# 3) OneDrive (systemweit + Autostart fuer neue Benutzer)
if ((Get-OneDriveSetups) -or (Get-UninstallEntries | Where-Object DisplayName -match '^Microsoft OneDrive')) {
[void]$items.Add([pscustomobject]@{
id = 'onedrive'; kind = 'onedrive'; name = 'Microsoft OneDrive'; detail = 'Deinstallation + keine Einrichtung fuer neue Benutzer'
category = 'Office / OneNote vorinstalliert'; recommended = $false
})
}
return @($items)
}
function Get-OneDriveSetups {
@("$env:SystemRoot\System32\OneDriveSetup.exe", "$env:SystemRoot\SysWOW64\OneDriveSetup.exe") +
@(Get-ChildItem "$env:ProgramFiles\Microsoft OneDrive\*\OneDriveSetup.exe", "${env:ProgramFiles(x86)}\Microsoft OneDrive\*\OneDriveSetup.exe" -ErrorAction SilentlyContinue | ForEach-Object FullName) |
Where-Object { Test-Path $_ }
}
function Remove-OneDrive {
Get-Process OneDrive -ErrorAction SilentlyContinue | Stop-Process -Force
foreach ($s in Get-OneDriveSetups) {
$a = if ($s -like "$env:ProgramFiles*" -or $s -like "${env:ProgramFiles(x86)}*") { '/uninstall /allusers' } else { '/uninstall' }
Write-Log " $s $a"
Start-Process $s -ArgumentList $a -Wait -WindowStyle Hidden
}
# Automatische Einrichtung fuer neue Benutzerprofile verhindern
reg load HKU\KdsDefault "$env:SystemDrive\Users\Default\NTUSER.DAT" 2>$null | Out-Null
reg delete 'HKU\KdsDefault\Software\Microsoft\Windows\CurrentVersion\Run' /v OneDriveSetup /f 2>$null | Out-Null
[gc]::Collect(); reg unload HKU\KdsDefault 2>$null | Out-Null
Remove-Item "$env:PUBLIC\Desktop\OneDrive.lnk", "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk" -Force -ErrorAction SilentlyContinue
}
function Remove-Win32App {
param([string]$DisplayName)
$u = Get-UninstallEntries | Where-Object DisplayName -eq $DisplayName | Select-Object -First 1
if (-not $u) { Write-Log ' nicht (mehr) installiert'; return 0 }
$us = [string]$u.UninstallString
if ($us -match 'OfficeClickToRun\.exe') {
# ARP-String von Click-to-Run + DisplayLevel=False = stille Deinstallation
if ($us -match '^"([^"]+)"\s*(.*)$') { $exe = $Matches[1]; $rest = $Matches[2] }
else { $exe, $rest = $us -split ' ', 2 }
return (Start-Process $exe -ArgumentList "$rest DisplayLevel=False" -Wait -PassThru).ExitCode
}
if ($us -match 'msiexec' -and $u.PSChildName -match '^\{[0-9A-F\-]+\}$') {
return (Start-Process msiexec.exe -ArgumentList "/x $($u.PSChildName) /qn /norestart" -Wait -PassThru).ExitCode
}
if ($u.QuietUninstallString) {
return (Start-Process cmd.exe -ArgumentList "/c `"$($u.QuietUninstallString)`"" -Wait -PassThru -WindowStyle Hidden).ExitCode
}
# Fallback: winget kennt fuer viele ARP-Eintraege den passenden Silent-Schalter
if (Get-WingetPath) {
$arch = if ($u.PSPath -match 'WOW6432Node') { 'X86' } else { 'X64' }
$code = Invoke-Winget @('uninstall', '--id', "ARP\Machine\$arch\$($u.PSChildName)", '--exact', '--silent', '--force', '--accept-source-agreements', '--disable-interactivity')
if ($code -eq 0) { return 0 }
}
Write-Log ' Kein stiller Deinstaller bekannt - starte Standard-Deinstallation (evtl. mit Dialog)' 'WARN'
return (Start-Process cmd.exe -ArgumentList "/c `"$us`"" -Wait -PassThru).ExitCode
}
function Remove-Bloatware {
param($Data)
$cfg = $Data.bloatware
Write-Log 'Ermittle installierte Bloatware ...'
$inv = Get-BloatwareInventory
$ids = @($cfg.ids)
$keep = @($cfg.keep)
$sel = @($inv | Where-Object { ($_.id -in $ids) -or ((Test-True $cfg.recommended) -and $_.recommended -and $_.id -notin $keep) })
if (-not $sel.Count) { Write-Log 'Nichts zu entfernen.' 'OK'; return }
Write-Log "$($sel.Count) Eintraege werden entfernt"
$fail = 0
foreach ($it in $sel) {
Write-Log "- $($it.name) [$($it.category)]"
try {
switch ($it.kind) {
'appx' {
$n = $it.detail
Get-AppxPackage -AllUsers -Name $n -ErrorAction SilentlyContinue | ForEach-Object {
try { Remove-AppxPackage -Package $_.PackageFullName -AllUsers -ErrorAction Stop } catch { Write-Log " $($_.Exception.Message)" 'WARN' }
}
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -eq $n | ForEach-Object {
Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -AllUsers -ErrorAction SilentlyContinue | Out-Null
}
Write-Log ' entfernt' 'OK'
}
'win32' {
$code = Remove-Win32App ($it.id -replace '^arp:', '')
if ($code -in 0, 3010, 1605, 1641) { Write-Log " entfernt ($code)" 'OK' } else { throw "ExitCode $code" }
}
'onedrive' { Remove-OneDrive; Write-Log ' OneDrive entfernt' 'OK' }
}
} catch { $fail++; Write-Log " Fehler: $_" 'ERROR' }
}
Write-Log "Bloatware: $($sel.Count - $fail) entfernt, $fail Fehler" $(if ($fail) { 'WARN' } else { 'OK' })
if ($sel | Where-Object name -match 'McAfee') { Write-Log 'Tipp: McAfee-Reste ggf. mit dem McAfee Consumer Product Removal Tool (MCPR) entfernen.' 'WARN' }
}
#endregion
#region Gesamtprofil ------------------------------------------------------------
function Invoke-FullProfile {
@@ -765,6 +1050,7 @@ function Invoke-FullProfile {
try { & $block } catch { Write-Log "$title fehlgeschlagen: $_" 'ERROR' }
}
if ('system' -in $steps) { & $run 'System' { Set-SystemConfig $Data } }
if ('bloatware' -in $steps) { & $run 'Bloatware entfernen' { Remove-Bloatware $Data } }
if ('security' -in $steps) { & $run 'Sicherheit' { Set-SecurityConfig $Data } }
if ('power' -in $steps) { & $run 'Energie' { Set-PowerConfig $Data } }
if ('apps' -in $steps) { & $run 'Programme' { Install-Apps $Data } }
View File
+264 -36
View File
@@ -30,7 +30,7 @@ async function guard(fn) { try { return await fn(); } catch (e) { toast(e.messag
// ---------------------------------------------------------------- Zustand / Profil
const DEFAULTS = () => ({
name: '',
id: '', name: '', customer: '', description: '',
apps: [],
office: {
product: 'O365ProPlusRetail', channel: 'Current', arch: '64', languages: ['de-de'],
@@ -42,6 +42,7 @@ const DEFAULTS = () => ({
system: { computerName: '', timeZone: '', showExtensions: true, disableConsumer: true },
security: { defender: true, firewall: true, smartScreen: true, uac: false, disableSmb1: true, disableAutorun: true, lsaProtection: false, telemetryMin: false, rdp: 'keep', localAdminName: '', localAdminPassword: '' },
power: { plan: 'balanced', monitorAc: 15, monitorDc: 5, sleepAc: 0, sleepDc: 15, hibernateAc: 0, hibernateDc: 60, lidAc: 'nothing', lidDc: 'sleep', buttonAc: 'shutdown', buttonDc: 'shutdown', hibernate: '', fastStartup: 'off' },
bloatware: { recommended: true, ids: [], keep: [] },
bitlocker: { method: 'XtsAes256', usedSpaceOnly: true, skipHardwareTest: true, usePin: false, pin: '', backupAad: false, backupAd: false },
steps: ['system', 'security', 'power', 'apps', 'office', 'shortcuts', 'updates']
});
@@ -83,12 +84,13 @@ function fillForms() {
$$('input', box).forEach(i => i.checked = v.includes(i.value));
});
$$('#steps input').forEach(i => i.checked = arr(S.steps).includes(i.value));
$('#profileName').textContent = 'Profil: ' + (S.name || '(ungespeichert)');
$('#profileName').textContent = 'Blaupause: ' + ([S.customer, S.name].filter(Boolean).join(' / ') || '(ungespeichert)');
$$('.pinonly').forEach(e => e.classList.toggle('hidden', !S.bitlocker.usePin));
}
function onChange(path) {
if (path.startsWith('office')) { if (path === 'office.product') { renderOfficeChannels(); renderOfficeExtras(); } updateOfficeXml(); }
if (path === 'bitlocker.usePin') fillForms();
if (path === 'bloatware.recommended') renderBloat();
}
function loadState(obj) { S = deepMerge(DEFAULTS(), obj); persist(); fillForms(); renderAll(); }
@@ -96,7 +98,7 @@ function loadState(obj) { S = deepMerge(DEFAULTS(), obj); persist(); fillForms()
function showTab(t) {
$$('#nav a').forEach(a => a.classList.toggle('active', a.dataset.tab === t));
$$('.tab').forEach(s => s.classList.toggle('active', s.id === 'tab-' + t));
({ shortcuts: loadShortcuts, bitlocker: loadBitlocker, profiles: loadProfiles, jobs: pollJobs, power: loadPower, office: loadOfficeBuilds })[t]?.();
({ shortcuts: loadShortcuts, bitlocker: loadBitlocker, bloatware: () => { if (!bloat) scanBloat(); }, profiles: loadProfiles, jobs: pollJobs, power: loadPower, office: loadOfficeBuilds })[t]?.();
}
$('#nav').onclick = e => { const a = e.target.closest('a'); if (a) showTab(a.dataset.tab); };
@@ -114,7 +116,7 @@ function renderSummary() {
const o = S.office, prod = PRODUCTS.find(p => p.id === o.product);
$('#summary').innerHTML = [
`<b>${arr(S.apps).length}</b> Programme`, `Office: <b>${esc(prod?.name || o.product)}</b> (${o.arch}-Bit, ${esc(S.officeMode)})`,
`<b>${arr(S.shortcuts).length}</b> Desktop-Icons`, `Energieplan: <b>${esc(S.power.plan || 'unverändert')}</b>`,
`<b>${arr(S.shortcuts).length}</b> Desktop-Icons`, `Bloatware: <b>${S.bloatware.recommended ? 'Empfohlene' : ''}${S.bloatware.recommended && arr(S.bloatware.ids).length ? ' + ' : ''}${arr(S.bloatware.ids).length ? arr(S.bloatware.ids).length + ' gewählt' : (S.bloatware.recommended ? '' : 'keine')}</b>`, `Energieplan: <b>${esc(S.power.plan || 'unverändert')}</b>`,
S.system.computerName ? `Neuer Name: <b>${esc(S.system.computerName)}</b>` : ''
].filter(Boolean).join(' · ');
}
@@ -135,9 +137,76 @@ async function loadCatalog() {
$('#catalog').innerHTML = Object.entries(groups).map(([c, list]) => `<div class="cat"><h3>${esc(c)}</h3><div class="checks">${list.map(a =>
`<label><input type="checkbox" data-app="${esc(a.id)}"> ${esc(a.name)}</label>`).join('')}</div></div>`).join('');
syncCatalogChecks();
const local = arr(await api('GET', '/api/local-installers'));
$('#localFile').innerHTML = local.length ? local.map(f => `<option>${esc(f)}</option>`).join('') : '<option value="">(keine Dateien in installers\\)</option>';
await loadLocalInstallers();
}
// ---- Uploads (Installer, Icons, RDP) mit Fortschritt
function uploadFile(kind, file) {
return new Promise((resolve, reject) => {
const x = new XMLHttpRequest();
x.open('POST', `/api/upload?kind=${kind}&name=${encodeURIComponent(file.name)}`);
x.setRequestHeader('X-Token', TOKEN);
x.upload.onprogress = e => { if (e.lengthComputable) $('#uploadProgress').textContent = `${file.name}: ${Math.round(e.loaded / e.total * 100)} %`; };
x.onload = () => {
$('#uploadProgress').textContent = '';
let d = null; try { d = JSON.parse(x.responseText); } catch { }
x.status === 200 ? resolve(d) : reject(new Error(d?.error || `Upload fehlgeschlagen (${x.status})`));
};
x.onerror = () => reject(new Error('Upload fehlgeschlagen'));
x.send(file);
});
}
async function uploadMany(kind, files) {
const done = [];
for (const f of files) { try { done.push(await uploadFile(kind, f)); toast(`${f.name} hochgeladen`); } catch (e) { toast(`${f.name}: ${e.message}`, true); } }
return done;
}
let localInstallers = [];
async function loadLocalInstallers() {
localInstallers = arr(await api('GET', '/api/local-installers'));
$('#localList').innerHTML = localInstallers.length ? localInstallers.map((f, i) => `<div class="item">
<input type="checkbox" data-li="${i}" ${S.apps.some(a => a.id === 'local:' + f.file) ? 'checked' : ''} title="In Auswahl übernehmen">
<div class="grow"><div>${esc(f.name)}</div><div class="sub">${esc(f.file)} · ${f.sizeMB} MB</div></div>
<input class="args" data-la="${i}" value="${esc(f.args)}" placeholder="${/\.msi$/i.test(f.file) ? 'zusätzl. MSI-Eigenschaften' : 'Silent-Parameter'}">
<button class="small danger" data-ldel="${i}" title="Vom Stick löschen">✕</button></div>`).join('')
: '<p class="muted small">Noch keine eigenen Installer hochgeladen.</p>';
}
const localApp = f => ({ name: f.name, id: 'local:' + f.file, source: 'local', path: f.file, args: f.args });
$('#localList').onchange = e => guard(async () => {
const { li, la } = e.target.dataset;
if (li != null) { const f = localInstallers[li]; e.target.checked ? addApp(localApp(f)) : removeApp('local:' + f.file); }
if (la != null) {
const f = localInstallers[la]; f.args = e.target.value;
await api('POST', '/api/local-installers', { file: f.file, name: f.name, args: f.args });
const sel = S.apps.find(a => a.id === 'local:' + f.file); if (sel) { sel.args = f.args; persist(); }
toast('Parameter gespeichert');
}
});
$('#localList').onclick = e => {
const i = e.target.dataset.ldel; if (i == null) return;
const f = localInstallers[i];
if (!confirm(`"${f.file}" vom Stick löschen?`)) return;
guard(async () => { await api('DELETE', `/api/files?kind=installers&name=${encodeURIComponent(f.file.split('\\').pop())}`); removeApp('local:' + f.file); loadLocalInstallers(); });
};
let lastArgsInput = null;
$('#localList').addEventListener('focusin', e => { if (e.target.dataset.la != null) lastArgsInput = e.target; });
$$('.hint').forEach(h => h.onclick = () => {
if (!lastArgsInput) return toast('Erst in ein Parameter-Feld klicken', true);
lastArgsInput.value = h.dataset.hint; lastArgsInput.dispatchEvent(new Event('change', { bubbles: true }));
});
async function handleInstallerFiles(files) {
const up = await uploadMany('installers', files);
await loadLocalInstallers();
// frisch hochgeladene direkt auswählen
up.forEach(u => { const f = localInstallers.find(x => x.file === u.path); if (f) addApp(localApp(f)); });
loadLocalInstallers();
}
$('#instFile').onchange = e => { handleInstallerFiles([...e.target.files]); e.target.value = ''; };
const drop = $('#instDrop');
drop.ondragover = e => { e.preventDefault(); drop.classList.add('over'); };
drop.ondragleave = () => drop.classList.remove('over');
drop.ondrop = e => { e.preventDefault(); drop.classList.remove('over'); handleInstallerFiles([...e.dataTransfer.files]); };
$('#catalog').onchange = e => {
const id = e.target.dataset.app; if (!id) return;
const a = catalog.find(x => x.id === id);
@@ -153,6 +222,7 @@ function renderSelectedApps() {
<div class="sub">${esc(a.source)} · ${esc(a.source === 'local' ? a.path : a.id)}</div></div>
<button class="small ghost" data-rm="${esc(a.id)}">✕</button></div>`).join('') : '<p class="muted">Nichts ausgewählt.</p>';
syncCatalogChecks();
$$('#localList [data-li]').forEach(i => i.checked = S.apps.some(a => a.id === 'local:' + localInstallers[i.dataset.li]?.file));
}
$('#selectedApps').onclick = e => { const id = e.target.dataset.rm; if (id) removeApp(id); };
async function doSearch() {
@@ -167,10 +237,6 @@ async function doSearch() {
}
$('#btnSearch').onclick = doSearch;
$('#appSearch').onkeydown = e => { if (e.key === 'Enter') doSearch(); };
$('#btnAddLocal').onclick = () => {
const f = $('#localFile').value; if (!f) return;
addApp({ name: f.split('\\').pop(), id: 'local:' + f, source: 'local', path: f, args: $('#localArgs').value });
};
$('#btnInstallApps').onclick = () => guard(async () => {
if (!S.apps.length) return toast('Keine Programme ausgewählt', true);
jobStarted((await api('POST', '/api/apps/install', { apps: S.apps })).job);
@@ -229,24 +295,89 @@ async function loadOfficeBuilds() {
// ---------------------------------------------------------------- Desktop-Icons
let presets = [];
function scFromForm() {
return { type: $('#scType').value, name: $('#scName').value.trim(), target: $('#scTarget').value.trim(), args: $('#scArgs').value, icon: $('#scIcon').value.trim() };
const RDP_FIELDS = { host: 'rdpHost', port: 'rdpPort', user: 'rdpUser', gateway: 'rdpGateway', remoteApp: 'rdpRemoteApp', audio: 'rdpAudio', extra: 'rdpExtra' };
const RDP_CHECKS = { fullscreen: 'rdpFullscreen', multimon: 'rdpMultimon', clipboard: 'rdpClipboard', printers: 'rdpPrinters', drives: 'rdpDrives', smartcards: 'rdpSmartcards', microphone: 'rdpMicrophone', camera: 'rdpCamera', admin: 'rdpAdmin' };
function rdpFromForm() {
const r = {};
for (const [k, id] of Object.entries(RDP_FIELDS)) r[k] = $('#' + id).value.trim();
for (const [k, id] of Object.entries(RDP_CHECKS)) r[k] = $('#' + id).checked;
return r;
}
function scFromForm() {
const type = $('#scType').value;
const s = { type, name: $('#scName').value.trim() };
if (type === 'rdp') {
const file = $('#rdpSource').value;
if (file) { s.rdpFile = file; s.target = 'rdp\\' + file; }
else { s.rdp = rdpFromForm(); s.target = s.rdp.host + (s.rdp.remoteApp ? ` (RemoteApp ${s.rdp.remoteApp})` : ''); if (!s.rdp.host) s.target = ''; }
} else {
Object.assign(s, { target: $('#scTarget').value.trim(), args: type === 'lnk' ? $('#scArgs').value : '', icon: $('#scIcon').value.trim() });
}
return s;
}
function scValid(s) { if (!s.name || !s.target) { toast(s.type === 'rdp' ? 'Name und Server angeben' : 'Name und Ziel angeben', true); return false; } return true; }
function scTypeUi() {
const t = $('#scType').value;
$$('.t-url, .t-lnk, .t-rdp').forEach(e => e.classList.toggle('hidden', !e.classList.contains('t-' + t)));
$('#rdpForm').classList.toggle('hidden', !!$('#rdpSource').value);
if (t === 'rdp') updateRdpPreview();
}
$('#scType').onchange = scTypeUi;
$('#rdpSource').onchange = scTypeUi;
let rdpTimer;
function updateRdpPreview() {
clearTimeout(rdpTimer);
rdpTimer = setTimeout(async () => {
const r = rdpFromForm();
$('#rdpPreview').textContent = r.host ? await api('POST', '/api/rdp/preview', r, true) : '(Server angeben)';
}, 300);
}
$('#rdpForm').addEventListener('input', updateRdpPreview);
$('#rdpForm').addEventListener('change', updateRdpPreview);
$('#rdpUpload').onchange = async e => {
const up = await uploadMany('rdp', [...e.target.files]); e.target.value = '';
await loadRdpFiles();
if (up.length) { $('#rdpSource').value = up[0].name; if (!$('#scName').value) $('#scName').value = up[0].name.replace(/\.rdp$/i, ''); scTypeUi(); }
};
$('#iconUpload').onchange = async e => {
const up = await uploadMany('icons', [...e.target.files]); e.target.value = '';
if (up.length) $('#scIcon').value = up[0].name;
loadShortcuts();
};
async function loadRdpFiles() {
const files = arr(await api('GET', '/api/rdpfiles'));
const cur = $('#rdpSource').value;
$('#rdpSource').innerHTML = '<option value="">Neu konfigurieren</option>' + files.map(f => `<option value="${esc(f)}">Datei vom Stick: ${esc(f)}</option>`).join('');
$('#rdpSource').value = files.includes(cur) ? cur : '';
}
$('#scType').onchange = () => $$('.lnkonly').forEach(e => e.classList.toggle('hidden', $('#scType').value === 'url'));
$('#btnScCreate').onclick = () => guard(async () => {
const s = scFromForm(); if (!s.name || !s.target) return toast('Name und Ziel angeben', true);
const s = scFromForm(); if (!scValid(s)) return;
await api('POST', '/api/shortcuts', s); toast('Verknüpfung angelegt'); loadShortcuts();
});
$('#btnScPlan').onclick = () => {
const s = scFromForm(); if (!s.name || !s.target) return toast('Name und Ziel angeben', true);
const s = scFromForm(); if (!scValid(s)) return;
S.shortcuts = arr(S.shortcuts).filter(x => x.name !== s.name).concat(s); persist(); renderPlanned();
};
const SC_TYPES = { url: '🌐', lnk: '🔗', rdp: '🖥️' };
function renderPlanned() {
const l = arr(S.shortcuts);
$('#scPlanned').innerHTML = l.length ? l.map((s, i) => `<div class="item"><div class="grow"><div>${esc(s.name)}</div><div class="sub">${esc(s.target)}</div></div>
<button class="small ghost" data-i="${i}">✕</button></div>`).join('') : '<p class="muted">Keine geplant.</p>';
$('#scPlanned').innerHTML = l.length ? l.map((s, i) => `<div class="item"><span>${SC_TYPES[s.type] || ''}</span><div class="grow"><div>${esc(s.name)}</div><div class="sub">${esc(s.target)}</div></div>
<button class="small ghost" data-edit="${i}" title="Ins Formular laden">✎</button><button class="small ghost" data-i="${i}">✕</button></div>`).join('') : '<p class="muted">Keine geplant.</p>';
}
$('#scPlanned').onclick = e => { const i = e.target.dataset.i; if (i != null) { S.shortcuts.splice(i, 1); persist(); renderPlanned(); } };
$('#scPlanned').onclick = e => {
const { i, edit } = e.target.dataset;
if (i != null) { S.shortcuts.splice(i, 1); persist(); renderPlanned(); }
if (edit != null) {
const s = S.shortcuts[edit];
$('#scType').value = s.type || 'lnk'; $('#scName').value = s.name;
$('#scTarget').value = s.type === 'rdp' ? '' : s.target || ''; $('#scArgs').value = s.args || ''; $('#scIcon').value = s.icon || '';
$('#rdpSource').value = s.rdpFile || '';
const r = s.rdp || {};
for (const [k, id] of Object.entries(RDP_FIELDS)) $('#' + id).value = r[k] ?? (k === 'audio' ? 'local' : '');
for (const [k, id] of Object.entries(RDP_CHECKS)) if (k in r) $('#' + id).checked = !!r[k];
scTypeUi();
}
};
$('#btnScApplyAll').onclick = () => guard(async () => {
for (const s of arr(S.shortcuts)) { try { await api('POST', '/api/shortcuts', s); } catch (e) { toast(`${s.name}: ${e.message}`, true); } }
toast('Verknüpfungen angelegt'); loadShortcuts();
@@ -256,6 +387,7 @@ async function loadShortcuts() {
$('#scCurrent').innerHTML = arr(cur).length ? arr(cur).map(s => `<div class="item"><div class="grow"><div>${esc(s.name)}</div><div class="sub">${esc(s.target)}</div></div>
<button class="small danger" data-del="${esc(s.file)}">Löschen</button></div>`).join('') : '<p class="muted">Keine Verknüpfungen.</p>';
$('#iconList').innerHTML = arr(icons).map(i => `<option value="${esc(i)}">`).join('');
await loadRdpFiles();
}
$('#scCurrent').onclick = e => {
const f = e.target.dataset.del; if (!f || !confirm(`"${f}" vom Desktop aller Benutzer löschen?`)) return;
@@ -320,26 +452,121 @@ $('#btnVendor').onclick = () => guard(async () => jobStarted((await api('POST',
$('#btnWinget').onclick = () => guard(async () => jobStarted((await api('POST', '/api/winget/install', {})).job));
$('#btnReboot').onclick = () => { if (confirm('Computer jetzt neu starten?')) guard(() => api('POST', '/api/reboot', {})); };
// ---------------------------------------------------------------- Profile
async function loadProfiles() {
const l = arr(await api('GET', '/api/profiles'));
$('#profileList').innerHTML = l.length ? l.map(n => `<div class="item"><div class="grow">${esc(n)}</div>
<button class="small primary" data-load="${esc(n)}">Laden</button><button class="small danger" data-pdel="${esc(n)}">✕</button></div>`).join('') : '<p class="muted">Keine Profile gespeichert.</p>';
// ---------------------------------------------------------------- Bloatware
let bloat = null;
const bloatChecked = it => arr(S.bloatware.ids).includes(it.id) || (S.bloatware.recommended && it.recommended && !arr(S.bloatware.keep).includes(it.id));
async function scanBloat() {
$('#bloatList').innerHTML = '<div class="card muted">Scanne Gerät … (kann einige Sekunden dauern)</div>';
await guard(async () => { bloat = arr(await api('GET', '/api/bloatware')); renderBloat(); });
}
function renderBloat() {
if (!bloat) return;
const groups = {};
bloat.forEach((it, i) => (groups[it.category] ??= []).push([it, i]));
$('#bloatList').innerHTML = bloat.length ? Object.keys(groups).sort().map(c => `<div class="card"><h2>${esc(c)} <span class="badge">${groups[c].length}</span></h2>
<div class="list" style="max-height:none">${groups[c].map(([it, i]) => `<label class="item"><input type="checkbox" data-b="${i}" ${bloatChecked(it) ? 'checked' : ''}>
<div class="grow"><div>${esc(it.name)} ${it.recommended ? '<span class="state ok">empfohlen</span>' : ''}</div><div class="sub">${esc(it.detail)}</div></div></label>`).join('')}</div></div>`).join('')
: '<div class="card">🎉 Keine Bloatware gefunden.</div>';
}
$('#bloatList').onchange = e => {
const i = e.target.dataset.b; if (i == null) return;
const it = bloat[i], on = e.target.checked, b = S.bloatware;
b.ids = arr(b.ids).filter(x => x !== it.id); b.keep = arr(b.keep).filter(x => x !== it.id);
if (b.recommended && it.recommended) { if (!on) b.keep.push(it.id); } else if (on) b.ids.push(it.id);
persist();
};
$('#btnBloatScan').onclick = scanBloat;
$('#btnBloatRec').onclick = () => { S.bloatware.recommended = true; S.bloatware.keep = []; persist(); fillForms(); renderBloat(); };
$('#btnBloatNone').onclick = () => { S.bloatware.recommended = false; S.bloatware.ids = []; S.bloatware.keep = []; persist(); fillForms(); renderBloat(); };
$('#btnBloatRemove').onclick = () => guard(async () => {
if (!bloat) return toast('Bitte zuerst scannen', true);
const ids = bloat.filter(bloatChecked).map(it => it.id);
if (!ids.length) return toast('Nichts ausgewählt', true);
if (!confirm(`${ids.length} Einträge jetzt deinstallieren?`)) return;
jobStarted((await api('POST', '/api/bloatware', { bloatware: { ids, recommended: false } })).job);
});
// ---------------------------------------------------------------- Blaupausen
let blueprints = [];
const slug = s => s.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue').replace(/ß/g, 'ss')
.replace(/[^A-Za-z0-9\-_. ]/g, '_').trim();
const bpId = (customer, name) => slug(customer ? `${customer}__${name}` : name);
async function loadProfiles() {
blueprints = arr(await api('GET', '/api/blueprints'));
renderBlueprints(); renderQuickSelect();
$('#bpCustomers').innerHTML = [...new Set(blueprints.map(b => b.customer).filter(Boolean))].map(c => `<option value="${esc(c)}">`).join('');
$('#bpCustomer').value = S.customer || ''; $('#profileSaveName').value = S.name || ''; $('#bpDescription').value = S.description || '';
}
function renderBlueprints() {
const f = $('#bpFilter').value.trim().toLowerCase();
const list = blueprints.filter(b => !f || `${b.customer} ${b.name} ${b.description}`.toLowerCase().includes(f));
const groups = {};
list.forEach(b => (groups[b.customer || 'Ohne Kunde'] ??= []).push(b));
const keys = Object.keys(groups).sort((a, b) => a.localeCompare(b, 'de'));
$('#profileList').innerHTML = keys.length ? keys.map(c => `<div class="cust"><h3>${esc(c)}</h3><div class="bpgrid">${groups[c].map(b => `
<div class="bp ${b.id === S.id ? 'active' : ''}">
<div class="t">${esc(b.name)}</div>
<div class="d">${esc(b.description) || '<i>keine Beschreibung</i>'}</div>
<div class="meta">${b.apps} Programme · ${b.shortcuts} Icons${b.office ? ' · ' + esc(b.office) : ''} · ${esc(b.updated)}</div>
<div class="acts">
<button class="small primary" data-load="${esc(b.id)}">Laden</button>
<button class="small" data-run="${esc(b.id)}">▶ Einrichten</button>
<button class="small ghost" data-dup="${esc(b.id)}" title="Duplizieren">⧉</button>
<button class="small ghost" data-exp="${esc(b.id)}" title="Exportieren">⤓</button>
<button class="small danger" data-pdel="${esc(b.id)}" title="Löschen">✕</button>
</div></div>`).join('')}</div></div>`).join('')
: '<div class="card muted">Keine Blaupausen gefunden. Konfiguration in den Tabs zusammenstellen und rechts speichern.</div>';
}
function renderQuickSelect() {
const groups = {};
blueprints.forEach(b => (groups[b.customer || 'Ohne Kunde'] ??= []).push(b));
$('#bpQuick').innerHTML = '<option value="">– aktuelle Konfiguration –</option>' + Object.keys(groups).sort((a, b) => a.localeCompare(b, 'de')).map(c =>
`<optgroup label="${esc(c)}">${groups[c].map(b => `<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</optgroup>`).join('');
$('#bpQuick').value = blueprints.some(b => b.id === S.id) ? S.id : '';
$('#bpQuickDesc').textContent = S.description || '';
}
$('#bpFilter').oninput = renderBlueprints;
async function loadBlueprint(id) {
const data = await api('GET', '/api/blueprints/' + encodeURIComponent(id));
loadState({ ...data, id });
renderBlueprints(); renderQuickSelect();
$('#bpCustomer').value = S.customer || ''; $('#profileSaveName').value = S.name || ''; $('#bpDescription').value = S.description || '';
toast(`Blaupause "${[S.customer, S.name].filter(Boolean).join(' / ')}" geladen`);
}
$('#bpQuick').onchange = e => guard(async () => { if (e.target.value) await loadBlueprint(e.target.value); });
$('#profileList').onclick = e => guard(async () => {
const { load, pdel } = e.target.dataset;
if (load) { loadState(await api('GET', '/api/profiles/' + encodeURIComponent(load))); toast(`Profil "${load}" geladen`); }
if (pdel && confirm(`Profil "${pdel}" löschen?`)) { await api('DELETE', '/api/profiles/' + encodeURIComponent(pdel)); loadProfiles(); }
const { load, run, dup, exp, pdel } = e.target.dataset;
if (load) await loadBlueprint(load);
if (run) { await loadBlueprint(run); showTab('overview'); $('#btnRunAll').focus(); toast('Schritte prüfen und „Einrichtung starten“ klicken'); }
if (dup) {
const d = await api('GET', '/api/blueprints/' + encodeURIComponent(dup));
const name = prompt('Name der Kopie:', (d.name || dup) + ' (Kopie)'); if (!name) return;
const customer = prompt('Kunde:', d.customer || '') ?? d.customer;
d.name = name; d.customer = customer; d.id = bpId(customer, name);
await api('POST', '/api/blueprints/' + encodeURIComponent(d.id), JSON.stringify(d, null, 2)); loadProfiles();
}
if (exp) { const d = await api('GET', '/api/blueprints/' + encodeURIComponent(exp)); download(exp + '.json', JSON.stringify(d, null, 2), 'application/json'); }
if (pdel && confirm(`Blaupause "${pdel}" löschen?`)) { await api('DELETE', '/api/blueprints/' + encodeURIComponent(pdel)); if (S.id === pdel) S.id = ''; persist(); loadProfiles(); }
});
$('#btnProfileSave').onclick = () => guard(async () => {
const n = $('#profileSaveName').value.trim().replace(/[^\w\-. ]/g, '_');
if (!n) return toast('Namen angeben', true);
S.name = n; persist();
await api('POST', '/api/profiles/' + encodeURIComponent(n), JSON.stringify(S, null, 2));
toast(`Profil "${n}" gespeichert`); fillForms(); loadProfiles();
});
$('#btnExport').onclick = () => download((S.name || 'profil') + '.json', JSON.stringify(S, null, 2), 'application/json');
$('#importFile').onchange = async e => { const f = e.target.files[0]; if (f) guard(async () => { loadState(JSON.parse(await f.text())); toast('Profil importiert'); }); e.target.value = ''; };
async function saveBlueprint(asNew) {
const name = $('#profileSaveName').value.trim(), customer = $('#bpCustomer').value.trim();
if (!name) return toast('Namen angeben', true);
const id = bpId(customer, name);
if ((asNew || id !== S.id) && blueprints.some(b => b.id === id) && !confirm(`Blaupause "${name}" existiert bereits. Überschreiben?`)) return;
if (S.id && id !== S.id && !asNew && blueprints.some(b => b.id === S.id) && confirm('Alte Blaupause (vorheriger Name) entfernen?'))
await api('DELETE', '/api/blueprints/' + encodeURIComponent(S.id));
Object.assign(S, { id, name, customer, description: $('#bpDescription').value.trim() }); persist();
await api('POST', '/api/blueprints/' + encodeURIComponent(id), JSON.stringify(S, null, 2));
toast(`Blaupause "${name}" gespeichert`); fillForms(); loadProfiles();
}
$('#btnProfileSave').onclick = () => guard(() => saveBlueprint(false));
$('#btnProfileSaveAs').onclick = () => guard(() => saveBlueprint(true));
$('#btnExport').onclick = () => download((S.id || 'blaupause') + '.json', JSON.stringify(S, null, 2), 'application/json');
$('#importFile').onchange = async e => {
const f = e.target.files[0]; e.target.value = '';
if (f) guard(async () => { const d = JSON.parse(await f.text()); d.id = ''; loadState(d); loadProfiles(); toast('Importiert – zum Übernehmen „Speichern“ klicken'); });
};
$('#btnReset').onclick = () => { if (confirm('Alle Einstellungen auf Standard zurücksetzen?')) loadState(DEFAULTS()); };
function download(name, text, type) {
@@ -396,7 +623,7 @@ $('#btnShutdown').onclick = () => { if (confirm('Agent beenden? Laufende Aufgabe
function renderAll() { renderOffice(); renderSelectedApps(); renderPlanned(); renderSummary(); }
(async () => {
if (!TOKEN) { document.body.innerHTML = '<main><h1>Kein Zugriffstoken</h1><p>Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.</p></main>'; return; }
bindForms(); renderAll(); fillForms();
bindForms(); renderAll(); fillForms(); scTypeUi();
await guard(loadStatus);
await guard(loadCatalog);
await guard(async () => {
@@ -406,6 +633,7 @@ function renderAll() { renderOffice(); renderSelectedApps(); renderPlanned(); re
$('#tzSelect').insertAdjacentHTML('beforeend', arr(p.timezones).map(t => `<option value="${esc(t.id)}">${esc(t.name)}</option>`).join(''));
fillForms();
});
await guard(loadProfiles);
if (location.hash) showTab(location.hash.slice(1));
pollJobs();
})();
+94 -21
View File
@@ -15,15 +15,16 @@
<a data-tab="apps">Programme</a>
<a data-tab="office">Office</a>
<a data-tab="shortcuts">Desktop-Icons</a>
<a data-tab="bloatware">Bloatware</a>
<a data-tab="system">System &amp; Sicherheit</a>
<a data-tab="power">Energie</a>
<a data-tab="bitlocker">BitLocker</a>
<a data-tab="updates">Updates &amp; Treiber</a>
<a data-tab="profiles">Profile</a>
<a data-tab="profiles">Blaupausen</a>
<a data-tab="jobs">Aufgaben <b id="jobBadge" class="badge hidden">0</b></a>
</nav>
<div class="side-foot">
<div id="profileName" class="muted">Profil: (ungespeichert)</div>
<div id="profileName" class="muted">Blaupause: (ungespeichert)</div>
<button class="ghost" id="btnShutdown">Agent beenden</button>
</div>
</aside>
@@ -35,9 +36,14 @@
<div id="sysinfo" class="grid4"></div>
<div class="card">
<h2>Komplette Einrichtung mit einem Klick</h2>
<p class="muted">Führt die Einstellungen aus allen Tabs in sinnvoller Reihenfolge aus. Vorher in den Tabs konfigurieren oder ein Profil laden.</p>
<p class="muted">Blaupause wählen (oder in den Tabs selbst konfigurieren) und starten. Alle Einstellungen werden in sinnvoller Reihenfolge ausgeführt.</p>
<div class="row">
<label class="f">Blaupause / Kunde<select id="bpQuick"><option value="">– aktuelle Konfiguration –</option></select></label>
</div>
<p id="bpQuickDesc" class="muted small"></p>
<div class="checks" id="steps">
<label><input type="checkbox" value="system" checked> System</label>
<label><input type="checkbox" value="bloatware"> Bloatware</label>
<label><input type="checkbox" value="security" checked> Sicherheit</label>
<label><input type="checkbox" value="power" checked> Energie</label>
<label><input type="checkbox" value="apps" checked> Programme</label>
@@ -71,12 +77,20 @@
<div id="catalog"></div>
</div>
<div class="card">
<h2>Eigene Installer vom Stick <span class="muted small">(Ordner <code>installers\</code>)</span></h2>
<div class="row">
<select id="localFile"></select>
<input id="localArgs" placeholder="Silent-Parameter, z.B. /S oder /quiet">
<button id="btnAddLocal">Hinzufügen</button>
</div>
<h2>Eigene Installer <span class="muted small">(EXE, MSI, MSIX, PS1, CMD – Ordner <code>installers\</code>)</span></h2>
<label class="drop" id="instDrop">
<input type="file" id="instFile" multiple accept=".exe,.msi,.msix,.msixbundle,.appx,.appxbundle,.ps1,.cmd,.bat" hidden>
<b>Setup-Dateien hierher ziehen</b> oder klicken zum Auswählen
<span class="muted small">Die Dateien werden auf den Stick hochgeladen und stehen in allen Blaupausen zur Verfügung.</span>
</label>
<div id="uploadProgress" class="muted small"></div>
<div id="localList" class="list"></div>
<p class="muted small">Silent-Parameter (Vorschläge):
<code class="hint" data-hint="/S">/S</code> NSIS ·
<code class="hint" data-hint="/VERYSILENT /SUPPRESSMSGBOXES /NORESTART">/VERYSILENT …</code> Inno Setup ·
<code class="hint" data-hint="/quiet /norestart">/quiet /norestart</code> WiX/Burn ·
<code class="hint" data-hint="/s /v&quot;/qn&quot;">/s /v"/qn"</code> InstallShield ·
MSI: <code>/qn</code> wird automatisch gesetzt</p>
</div>
</div>
<div>
@@ -161,12 +175,48 @@
<div>
<div class="card">
<h2>Neue Verknüpfung</h2>
<label class="f">Typ<select id="scType"><option value="url">Webseite (URL)</option><option value="lnk">Programm / Datei / Ordner</option></select></label>
<label class="f">Name<input id="scName" placeholder="z.B. Intranet"></label>
<label class="f">Ziel<input id="scTarget" placeholder="https://… oder %ProgramFiles%\…\app.exe"></label>
<label class="f lnkonly">Argumente<input id="scArgs"></label>
<label class="f">Icon <span class="muted small">(Ordner <code>icons\</code> auf dem Stick oder Pfad)</span>
<input id="scIcon" list="iconList" placeholder="optional"></label>
<label class="f">Typ<select id="scType">
<option value="url">Webseite (URL)</option>
<option value="lnk">Programm / Datei / Ordner</option>
<option value="rdp">Remotedesktop (RDP)</option></select></label>
<label class="f">Name<input id="scName" placeholder="z.B. Intranet, Terminalserver"></label>
<label class="f t-url t-lnk">Ziel<input id="scTarget" placeholder="https://… oder %ProgramFiles%\…\app.exe"></label>
<label class="f t-lnk">Argumente<input id="scArgs"></label>
<div class="t-rdp">
<label class="f">Quelle<select id="rdpSource"><option value="">Neu konfigurieren</option></select></label>
<div id="rdpForm">
<div class="row">
<label class="f" style="flex:3">Server / Host<input id="rdpHost" placeholder="ts01.firma.local"></label>
<label class="f" style="flex:1">Port<input id="rdpPort" type="number" placeholder="3389"></label>
</div>
<div class="row">
<label class="f">Benutzer (optional)<input id="rdpUser" placeholder="FIRMA\benutzer"></label>
<label class="f">RD-Gateway (optional)<input id="rdpGateway" placeholder="gw.firma.de"></label>
</div>
<div class="row">
<label class="f">RemoteApp-Alias (optional)<input id="rdpRemoteApp" placeholder="z.B. DATEV"></label>
<label class="f">Audio<select id="rdpAudio"><option value="local">auf diesem PC</option><option value="remote">auf dem Server</option><option value="none">aus</option></select></label>
</div>
<div class="checks">
<label><input type="checkbox" id="rdpFullscreen" checked> Vollbild</label>
<label><input type="checkbox" id="rdpMultimon"> Alle Monitore</label>
<label><input type="checkbox" id="rdpClipboard" checked> Zwischenablage</label>
<label><input type="checkbox" id="rdpPrinters" checked> Drucker</label>
<label><input type="checkbox" id="rdpDrives"> Laufwerke</label>
<label><input type="checkbox" id="rdpSmartcards"> Smartcards</label>
<label><input type="checkbox" id="rdpMicrophone"> Mikrofon</label>
<label><input type="checkbox" id="rdpCamera"> Kamera</label>
<label><input type="checkbox" id="rdpAdmin"> Admin-Sitzung</label>
</div>
<label class="f" style="margin-top:10px">Weitere RDP-Zeilen (optional)<textarea id="rdpExtra" rows="2" placeholder="z.B. session bpp:i:32"></textarea></label>
<details><summary class="muted small">Vorschau der .rdp-Datei</summary><pre id="rdpPreview" class="code"></pre></details>
</div>
<p class="muted small">Vorhandene .rdp-Dateien hochladen:
<label class="button small">.rdp hochladen<input type="file" id="rdpUpload" accept=".rdp" multiple hidden></label></p>
</div>
<div class="f t-url t-lnk"><label class="f" for="scIcon" style="margin:0">Icon <span class="muted small">(aus <code>icons\</code> oder Pfad)</span></label>
<div class="row" style="margin-top:3px"><input id="scIcon" list="iconList" placeholder="optional">
<label class="button">.ico hochladen<input type="file" id="iconUpload" accept=".ico,.exe,.dll" hidden></label></div></div>
<datalist id="iconList"></datalist>
<div class="row">
<button class="primary" id="btnScCreate">Sofort anlegen</button>
@@ -192,6 +242,24 @@
</div>
</section>
<!-- Bloatware -->
<section id="tab-bloatware" class="tab">
<h1>Bloatware entfernen</h1>
<div class="card">
<div class="row wrap" style="margin-top:0">
<button id="btnBloatScan">↻ Gerät scannen</button>
<button id="btnBloatRec">Empfohlene auswählen</button>
<button id="btnBloatNone" class="ghost">Keine</button>
<span class="sep"></span>
<button class="primary" id="btnBloatRemove">Ausgewählte entfernen</button>
</div>
<label class="checks" style="margin-top:12px"><input type="checkbox" data-bind="bloatware.recommended">
<span>In der Blaupause: <b>alle empfohlenen</b> Einträge entfernen (plus die manuell angehakten, minus die abgewählten). So funktioniert die Blaupause auch auf anderen Geräten und bei anderen Herstellern.</span></label>
<p class="muted small">Gefunden werden Store-Apps (für alle Benutzer + Vorlage für neue Benutzer), Hersteller-Tools, Testversionen, vorinstallierte Office/OneNote-Versionen (Click-to-Run, z. B. „Microsoft 365 – fr-fr“) und OneDrive. Katalog erweiterbar in <code>config\bloatware.json</code>.</p>
</div>
<div id="bloatList"><div class="card muted">Noch nicht gescannt.</div></div>
</section>
<!-- System & Sicherheit -->
<section id="tab-system" class="tab">
<h1>System &amp; Sicherheit</h1>
@@ -311,15 +379,20 @@
<!-- Profile -->
<section id="tab-profiles" class="tab">
<h1>Profile</h1>
<h1>Blaupausen</h1>
<p class="muted">Eine Blaupause speichert die komplette Einrichtung (Programme, Office, Icons, RDP, Sicherheit, Energie, BitLocker, Schritte), zum Beispiel als Standard für einen Kunden. Ablage: <code>blueprints\</code> auf dem Stick.</p>
<div class="cols">
<div class="card">
<h2>Gespeicherte Profile <span class="muted small">(Ordner <code>profiles\</code>)</span></h2>
<div id="profileList" class="list"></div>
<div>
<div class="row" style="margin:0 0 12px"><input id="bpFilter" placeholder="Kunde oder Blaupause suchen …"></div>
<div id="profileList"></div>
</div>
<div class="card">
<div class="card sticky">
<h2>Aktuelle Konfiguration speichern</h2>
<div class="row"><input id="profileSaveName" placeholder="z.B. Buero-Standard"><button class="primary" id="btnProfileSave">Speichern</button></div>
<label class="f">Kunde<input id="bpCustomer" list="bpCustomers" placeholder="z.B. Müller GmbH"></label>
<datalist id="bpCustomers"></datalist>
<label class="f">Name der Blaupause<input id="profileSaveName" placeholder="z.B. Büro-Notebook, Außendienst"></label>
<label class="f">Beschreibung<textarea id="bpDescription" rows="3" placeholder="optional – Hinweise für Techniker"></textarea></label>
<div class="row"><button class="primary" id="btnProfileSave">Speichern</button><button id="btnProfileSaveAs">Als neue Blaupause</button></div>
<h3>Import / Export</h3>
<div class="row">
<button id="btnExport">Als JSON exportieren</button>
+16
View File
@@ -81,3 +81,19 @@ code { font-family: Consolas, monospace; font-size: 12px; }
nav a.active { border-bottom-color: var(--accent); } .side-foot { display: none; }
main { padding: 16px; } .cols, .cols.jobs { grid-template-columns: 1fr; }
}
textarea { font: inherit; color: var(--text); background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 7px 9px; width: 100%; resize: vertical; display: block; margin-top: 3px; }
.drop { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; border: 2px dashed var(--line); border-radius: 10px; padding: 18px; cursor: pointer; margin-bottom: 8px; }
.drop:hover, .drop.over { border-color: var(--accent); background: var(--accent-2); }
.hint { cursor: pointer; padding: 1px 4px; border-radius: 4px; background: var(--accent-2); }
.item input.args { flex: 1; max-width: 260px; padding: 4px 7px; font-size: 12px; }
.cust { margin-bottom: 18px; }
.cust > h3 { margin: 0 0 8px; }
.bpgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; }
.bp { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 14px; display: flex; flex-direction: column; gap: 6px; }
.bp.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.bp .t { font-weight: 600; } .bp .d { font-size: 12px; color: var(--muted); flex: 1; }
.bp .meta { font-size: 11px; color: var(--muted); }
.bp .acts { display: flex; gap: 4px; flex-wrap: wrap; }
details summary { cursor: pointer; margin: 6px 0; }
input[hidden] { display: none !important; }
div.f { margin-bottom: 10px; font-size: 12px; color: var(--muted); }