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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 16:00:25 +02:00
co-authored by Claude Opus 5.5
parent f4c2a16612
commit ebfec7000e
11 changed files with 940 additions and 89 deletions
+43 -17
View File
@@ -157,13 +157,32 @@ function Invoke-Api {
'^GET /api/status$' { return Send-Json $Ctx (Get-SystemInfo) }
'^GET /api/catalog$' { return Send-Json $Ctx (Get-JsonFile 'config\apps.json') }
'^GET /api/presets$' {
return Send-Json $Ctx ([pscustomobject]@{ shortcuts = (Get-JsonFile 'config\shortcuts.json'); timezones = @(Get-TimeZone -ListAvailable | ForEach-Object { @{ id = $_.Id; name = $_.DisplayName } }) })
return Send-Json $Ctx ([pscustomobject]@{ shortcuts = @((Get-JsonFile 'config\shortcuts.json') | ForEach-Object { $_ }); timezones = @(Get-TimeZone -ListAvailable | ForEach-Object { @{ id = $_.Id; name = $_.DisplayName } }) })
}
'^GET /api/winget/search$' { return Send-Json $Ctx @(Search-Winget -Query $q['q'] -Source $(if ($q['source']) { $q['source'] } else { 'winget' })) }
'^GET /api/local-installers$' {
$dir = Join-Path $global:KdsRoot 'installers'
$files = @(Get-ChildItem $dir -Recurse -Include *.exe, *.msi -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName.Substring($global:KdsRoot.Length + 1) })
return Send-Json $Ctx $files
'^GET /api/local-installers$' { return Send-Json $Ctx @(Get-LocalInstallers) }
'^POST /api/local-installers$' { $b = Read-Body $Ctx; Set-InstallerMeta $b; return Send-Json $Ctx @{ ok = $true } }
# Datei-Upload: Body = Rohdaten, ?kind=installers|icons|rdp&name=Datei.ext
'^POST /api/upload$' {
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
New-Item $dir -ItemType Directory -Force | Out-Null
$target = Join-Path $dir $name
$fs = [IO.File]::Create($target)
try { $Ctx.Request.InputStream.CopyTo($fs, 1MB) } finally { $fs.Close() }
Write-Log "Upload: $($q['kind'])\$name ($([math]::Round((Get-Item $target).Length / 1MB, 1)) MB)"
return Send-Json $Ctx @{ path = $target.Substring($global:KdsRoot.Length + 1); name = $name }
}
'^GET /api/files$' {
$dir = (Resolve-UploadTarget $q['kind'] 'x.ico')[0]
return Send-Json $Ctx @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object Name -notlike '.*' | Where-Object Name -ne 'installers.json' |
ForEach-Object { [pscustomobject]@{ name = $_.Name; sizeMB = [math]::Round($_.Length / 1MB, 2) } })
}
'^DELETE /api/files$' {
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
Remove-Item (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
if ($q['kind'] -eq 'installers') { Set-InstallerMeta ([pscustomobject]@{ file = "installers\$name"; remove = $true }) }
return Send-Json $Ctx @{ ok = $true }
}
'^POST /api/apps/install$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Programme installieren ($(@($b.apps).Count))" 'Install-Apps' $b) } }
'^POST /api/apps/upgrade-all$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Alle Programme aktualisieren (winget)' 'Update-AllApps' @{}) } }
@@ -185,6 +204,10 @@ function Invoke-Api {
'^GET /api/icons$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'icons') -Include *.ico, *.exe, *.dll -Recurse -ErrorAction SilentlyContinue | ForEach-Object Name)
}
'^GET /api/rdpfiles$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'rdp') -Filter *.rdp -ErrorAction SilentlyContinue | ForEach-Object Name)
}
'^POST /api/rdp/preview$' { $b = Read-Body $Ctx; return Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes((New-RdpContent $b))) 'text/plain; charset=utf-8' }
'^POST /api/system$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Systemeinstellungen' 'Set-SystemConfig' $b) } }
'^POST /api/security$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Sicherheitseinstellungen' 'Set-SecurityConfig' $b) } }
@@ -193,6 +216,8 @@ function Invoke-Api {
$active = (powercfg /getactivescheme) -join ''
return Send-Json $Ctx @{ active = $active; hibernate = [bool](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled }
}
'^GET /api/bloatware$' { return Send-Json $Ctx @(Get-BloatwareInventory) }
'^POST /api/bloatware$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Bloatware entfernen' 'Remove-Bloatware' $b) } }
'^GET /api/bitlocker$' { return Send-Json $Ctx (Get-BitLockerInfo) }
'^POST /api/bitlocker$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "BitLocker: $($b.bitlocker.action)" 'Set-BitLockerConfig' $b) } }
@@ -201,22 +226,23 @@ function Invoke-Api {
'^POST /api/updates/vendor$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Hersteller-Treiber-Update' 'Invoke-VendorDriverUpdate' @{}) } }
'^POST /api/reboot$' { Restart-Computer -Force; return Send-Json $Ctx @{ ok = $true } }
'^GET /api/profiles$' {
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'profiles') -Filter *.json -ErrorAction SilentlyContinue | ForEach-Object BaseName)
}
'^GET /api/profiles/(?<n>[\w\-\. ]+)$' {
$p = Join-Path $global:KdsRoot "profiles\$($Matches.n).json"
if (-not (Test-Path $p)) { return Send-Json $Ctx @{ error = 'nicht gefunden' } 404 }
# Blaupausen (Standard-Einrichtungen je Kunde) unter blueprints\<id>.json
'^GET /api/blueprints$' { return Send-Json $Ctx @(Get-Blueprints) }
'^GET /api/blueprints/(?<n>[^/\\]+)$' {
$p = Get-BlueprintPath $Matches.n
if (-not (Test-Path $p)) { return Send-Json $Ctx @{ error = 'Blaupause nicht gefunden' } 404 }
return Send-Response $Ctx ([IO.File]::ReadAllBytes($p))
}
'^POST /api/profiles/(?<n>[\w\-\. ]+)$' {
'^POST /api/blueprints/(?<n>[^/\\]+)$' {
$sr = New-Object IO.StreamReader($Ctx.Request.InputStream, [Text.Encoding]::UTF8); $txt = $sr.ReadToEnd(); $sr.Close()
$dir = Join-Path $global:KdsRoot 'profiles'; New-Item $dir -ItemType Directory -Force | Out-Null
[IO.File]::WriteAllText((Join-Path $dir "$($Matches.n).json"), $txt, (New-Object Text.UTF8Encoding($false)))
$null = $txt | ConvertFrom-Json # nur gueltiges JSON speichern
$p = Get-BlueprintPath $Matches.n
New-Item (Split-Path $p) -ItemType Directory -Force | Out-Null
[IO.File]::WriteAllText($p, $txt, (New-Object Text.UTF8Encoding($false)))
return Send-Json $Ctx @{ ok = $true }
}
'^DELETE /api/profiles/(?<n>[\w\-\. ]+)$' { Remove-Item (Join-Path $global:KdsRoot "profiles\$($Matches.n).json") -ErrorAction SilentlyContinue; return Send-Json $Ctx @{ ok = $true } }
'^POST /api/run-profile$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Profil ausfuehren: $($b.name)" 'Invoke-FullProfile' $b) } }
'^DELETE /api/blueprints/(?<n>[^/\\]+)$' { Remove-Item (Get-BlueprintPath $Matches.n) -ErrorAction SilentlyContinue; return Send-Json $Ctx @{ ok = $true } }
'^POST /api/run-profile$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Einrichtung: $(@($b.customer, $b.name) -ne '' -join ' / ')" 'Invoke-FullProfile' $b) } }
'^GET /api/jobs$' { Update-KdsJobs; return Send-Json $Ctx @($script:Jobs.Values | ForEach-Object { Get-KdsJobView $_ }) }
'^GET /api/jobs/(?<id>\d+)$' {
@@ -276,7 +302,7 @@ while ($script:Running -and $listener.IsListening) {
$async = $listener.BeginGetContext($null, $null)
while (-not $async.AsyncWaitHandle.WaitOne(1000)) { Update-KdsJobs }
$ctx = $listener.EndGetContext($async)
$path = $ctx.Request.Url.AbsolutePath
$path = [Uri]::UnescapeDataString($ctx.Request.Url.AbsolutePath)
try {
if ($path.StartsWith('/api/')) {
$t = $ctx.Request.Headers['X-Token']