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:
+3
-1
@@ -8,4 +8,6 @@ installers/*
|
||||
!installers/.gitkeep
|
||||
packages/winget/*
|
||||
!packages/winget/.gitkeep
|
||||
profiles/*.json
|
||||
blueprints/*.json
|
||||
!blueprints/Beispiel__*.json
|
||||
rdp/*.rdp
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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 } }
|
||||
|
||||
+264
-36
@@ -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
@@ -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 & Sicherheit</a>
|
||||
<a data-tab="power">Energie</a>
|
||||
<a data-tab="bitlocker">BitLocker</a>
|
||||
<a data-tab="updates">Updates & 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"/qn"">/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 & 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>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user