diff --git a/.gitignore b/.gitignore index bb27205..ed856c4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ installers/* !installers/.gitkeep packages/winget/* !packages/winget/.gitkeep -profiles/*.json +blueprints/*.json +!blueprints/Beispiel__*.json +rdp/*.rdp diff --git a/Agent.ps1 b/Agent.ps1 index 4186dc9..e725ddb 100644 --- a/Agent.ps1 +++ b/Agent.ps1 @@ -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/(?[\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\.json + '^GET /api/blueprints$' { return Send-Json $Ctx @(Get-Blueprints) } + '^GET /api/blueprints/(?[^/\\]+)$' { + $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/(?[\w\-\. ]+)$' { + '^POST /api/blueprints/(?[^/\\]+)$' { $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/(?[\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/(?[^/\\]+)$' { 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/(?\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'] diff --git a/README.md b/README.md index b5287c2..b696457 100644 --- a/README.md +++ b/README.md @@ -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\-` 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\\`. @@ -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. diff --git a/blueprints/Beispiel__Buero-Standard.json b/blueprints/Beispiel__Buero-Standard.json new file mode 100644 index 0000000..430b42c --- /dev/null +++ b/blueprints/Beispiel__Buero-Standard.json @@ -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" + ] +} \ No newline at end of file diff --git a/blueprints/Beispiel__Minimal.json b/blueprints/Beispiel__Minimal.json new file mode 100644 index 0000000..a16bb24 --- /dev/null +++ b/blueprints/Beispiel__Minimal.json @@ -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" + ] +} \ No newline at end of file diff --git a/config/bloatware.json b/config/bloatware.json new file mode 100644 index 0000000..d304fa0 --- /dev/null +++ b/config/bloatware.json @@ -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 } + ] +} diff --git a/lib/Functions.ps1 b/lib/Functions.ps1 index 3805503..1017476 100644 --- a/lib/Functions.ps1 +++ b/lib/Functions.ps1 @@ -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 } } diff --git a/profiles/.gitkeep b/rdp/.gitkeep similarity index 100% rename from profiles/.gitkeep rename to rdp/.gitkeep diff --git a/web/app.js b/web/app.js index bd8165d..e14e713 100644 --- a/web/app.js +++ b/web/app.js @@ -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 = [ `${arr(S.apps).length} Programme`, `Office: ${esc(prod?.name || o.product)} (${o.arch}-Bit, ${esc(S.officeMode)})`, - `${arr(S.shortcuts).length} Desktop-Icons`, `Energieplan: ${esc(S.power.plan || 'unverändert')}`, + `${arr(S.shortcuts).length} Desktop-Icons`, `Bloatware: ${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')}`, `Energieplan: ${esc(S.power.plan || 'unverändert')}`, S.system.computerName ? `Neuer Name: ${esc(S.system.computerName)}` : '' ].filter(Boolean).join(' · '); } @@ -135,9 +137,76 @@ async function loadCatalog() { $('#catalog').innerHTML = Object.entries(groups).map(([c, list]) => `

${esc(c)}

${list.map(a => ``).join('')}
`).join(''); syncCatalogChecks(); - const local = arr(await api('GET', '/api/local-installers')); - $('#localFile').innerHTML = local.length ? local.map(f => ``).join('') : ''; + 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) => `
+ a.id === 'local:' + f.file) ? 'checked' : ''} title="In Auswahl übernehmen"> +
${esc(f.name)}
${esc(f.file)} · ${f.sizeMB} MB
+ +
`).join('') + : '

Noch keine eigenen Installer hochgeladen.

'; +} +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() {
${esc(a.source)} · ${esc(a.source === 'local' ? a.path : a.id)}
`).join('') : '

Nichts ausgewählt.

'; 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 = '' + files.map(f => ``).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) => `
${esc(s.name)}
${esc(s.target)}
-
`).join('') : '

Keine geplant.

'; + $('#scPlanned').innerHTML = l.length ? l.map((s, i) => `
${SC_TYPES[s.type] || ''}
${esc(s.name)}
${esc(s.target)}
+
`).join('') : '

Keine geplant.

'; } -$('#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 => `
${esc(s.name)}
${esc(s.target)}
`).join('') : '

Keine Verknüpfungen.

'; $('#iconList').innerHTML = arr(icons).map(i => `' + Object.keys(groups).sort((a, b) => a.localeCompare(b, 'de')).map(c => + `${groups[c].map(b => ``).join('')}`).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 = '

Kein Zugriffstoken

Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.

'; 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 => ``).join('')); fillForms(); }); + await guard(loadProfiles); if (location.hash) showTab(location.hash.slice(1)); pollJobs(); })(); diff --git a/web/index.html b/web/index.html index 8ae0074..7c18b10 100644 --- a/web/index.html +++ b/web/index.html @@ -15,15 +15,16 @@ Programme Office Desktop-Icons + Bloatware System & Sicherheit Energie BitLocker Updates & Treiber - Profile + Blaupausen Aufgaben
-
Profil: (ungespeichert)
+
Blaupause: (ungespeichert)
@@ -35,9 +36,14 @@

Komplette Einrichtung mit einem Klick

-

Führt die Einstellungen aus allen Tabs in sinnvoller Reihenfolge aus. Vorher in den Tabs konfigurieren oder ein Profil laden.

+

Blaupause wählen (oder in den Tabs selbst konfigurieren) und starten. Alle Einstellungen werden in sinnvoller Reihenfolge ausgeführt.

+
+ +
+

+ @@ -71,12 +77,20 @@
-

Eigene Installer vom Stick (Ordner installers\)

-
- - - -
+

Eigene Installer (EXE, MSI, MSIX, PS1, CMD – Ordner installers\)

+ +
+
+

Silent-Parameter (Vorschläge): + /S NSIS · + /VERYSILENT … Inno Setup · + /quiet /norestart WiX/Burn · + /s /v"/qn" InstallShield · + MSI: /qn wird automatisch gesetzt

@@ -161,12 +175,48 @@

Neue Verknüpfung

- - - - - + + + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + +
+ +
Vorschau der .rdp-Datei
+
+

Vorhandene .rdp-Dateien hochladen: +

+
+
+
+
@@ -192,6 +242,24 @@
+ +
+

Bloatware entfernen

+
+
+ + + + + +
+ +

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 config\bloatware.json.

+
+
Noch nicht gescannt.
+
+

System & Sicherheit

@@ -311,15 +379,20 @@
-

Profile

+

Blaupausen

+

Eine Blaupause speichert die komplette Einrichtung (Programme, Office, Icons, RDP, Sicherheit, Energie, BitLocker, Schritte), zum Beispiel als Standard für einen Kunden. Ablage: blueprints\ auf dem Stick.

-
-

Gespeicherte Profile (Ordner profiles\)

-
+
+
+
-
+

Aktuelle Konfiguration speichern

-
+ + + + +

Import / Export

diff --git a/web/style.css b/web/style.css index 37326f8..31cd994 100644 --- a/web/style.css +++ b/web/style.css @@ -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); }