Remove all environment-specific data and make the dashboard adoptable anywhere, then add a demo that runs the real PowerShell services. - Strip cached device data, internal knowledge-base articles and screenshots, server/site names and the old project notes; replace the knowledge base with generic sample articles and diagrams - Move every environment value into Config/usmt-dash.psd1 - Route all infrastructure access in the worker and Get-Software.ps1 through a provider (Providers/SCCM.ps1), with a config-driven asset inventory view instead of a hard-coded custom class - Add Providers/Demo.ps1: a deterministic simulated fleet whose migrations progress in real time, and Demo/New-DemoData.ps1 to seed it - Server: -Demo makes the API read-only; add get-config and get-logs; serve static files only from web/, Documentation/ and an allowlist of Data/ files with a path-traversal guard; gzip large responses - Mirror both services' console output to Data/Logs via a Write-Host proxy, and add a terminal panel to the dashboard that streams it - Demo sandbox (web/demo/demo.js): visitor changes to tags, articles and images are kept in the visitor's browser and echoed in the terminals; nothing a visitor does reaches the server - Vendor marked, highlight.js and html2pdf.js instead of loading CDNs - Dockerfile and compose file for hosting the demo (Coolify-ready)
141 lines
4.6 KiB
PowerShell
Executable File
141 lines
4.6 KiB
PowerShell
Executable File
param(
|
|
[string]$ComputerName,
|
|
[string]$ConfigPath,
|
|
[switch]$Demo
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ComputerName)) { return "[]" }
|
|
|
|
$scriptDir = $PSScriptRoot
|
|
$dataDir = Join-Path $scriptDir "Data"
|
|
|
|
if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null }
|
|
$cacheFile = Join-Path $dataDir "Software-Cache.json"
|
|
$blacklistFile = Join-Path $dataDir "Blacklisted-Apps.txt"
|
|
|
|
if (-not (Test-Path $cacheFile)) { Set-Content -Path $cacheFile -Value "{}" -Encoding UTF8 }
|
|
if (-not (Test-Path $blacklistFile)) { Set-Content -Path $blacklistFile -Value "" -Encoding UTF8 }
|
|
|
|
. (Join-Path $scriptDir "Lib/Common.ps1")
|
|
$config = Import-DashConfig -Path $ConfigPath -Demo:$Demo
|
|
$cacheHours = $config.Worker.SoftwareCacheHours
|
|
|
|
# --- 1. CHECK THE CACHE (24-Hour Timer Only) ---
|
|
$needsUpdate = $true
|
|
if (Test-Path $cacheFile) {
|
|
try {
|
|
$rawCache = Read-SharedText $cacheFile
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($rawCache)) {
|
|
$cacheData = $rawCache | ConvertFrom-Json
|
|
|
|
$propExists = $false
|
|
foreach ($p in $cacheData.psobject.properties) {
|
|
if ($p.Name -eq $ComputerName) { $propExists = $true; break }
|
|
}
|
|
|
|
if ($propExists) {
|
|
$deviceCache = $cacheData.$ComputerName
|
|
if ($null -ne $deviceCache.Timestamp -and $null -ne $deviceCache.Data) {
|
|
$cacheTime = [datetime]$deviceCache.Timestamp
|
|
|
|
# Logic Check: Is cache still fresh?
|
|
if ($cacheTime -gt (Get-Date).AddHours(-1 * $cacheHours)) {
|
|
$needsUpdate = $false
|
|
$outData = @($deviceCache.Data)
|
|
return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
# If cache is corrupt, proceed to the provider
|
|
}
|
|
}
|
|
|
|
if (-not $needsUpdate) { return }
|
|
|
|
# --- 2. LOAD BLACKLIST ---
|
|
$blacklist = @()
|
|
if (Test-Path $blacklistFile) {
|
|
$blacklist = Get-Content $blacklistFile | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }
|
|
}
|
|
|
|
# --- 3. PROVIDER QUERY (SCCM SQL in Live mode, simulated fleet in Demo mode) ---
|
|
$providerName = if ($config.IsDemo) { "Demo" } else { $config.Provider }
|
|
$v = $false
|
|
. (Join-Path $scriptDir "Providers/$providerName.ps1")
|
|
|
|
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
|
|
try {
|
|
foreach ($row in (Get-InstalledSoftware -ComputerName $ComputerName)) {
|
|
$appName = "$($row.Name)".Trim()
|
|
|
|
if ([string]::IsNullOrWhiteSpace($appName) -or $appName -eq "--") { continue }
|
|
|
|
$isSpecialty = $true
|
|
foreach ($badApp in $blacklist) {
|
|
if ($appName -like $badApp) {
|
|
$isSpecialty = $false
|
|
break
|
|
}
|
|
}
|
|
|
|
$results.Add([PSCustomObject]@{
|
|
Name = $appName
|
|
Version = $row.Version
|
|
Publisher = $row.Publisher
|
|
Source = $row.Source
|
|
InstallDate = $row.InstallDate
|
|
IsSpecialty = $isSpecialty
|
|
})
|
|
}
|
|
} catch {}
|
|
|
|
# --- 4. ATOMIC CACHE SAVE ---
|
|
try {
|
|
$cacheEntry = [PSCustomObject]@{
|
|
Timestamp = (Get-Date).ToString("o")
|
|
Data = @($results)
|
|
}
|
|
|
|
for ($i = 0; $i -lt 10; $i++) {
|
|
try {
|
|
$fs = [System.IO.File]::Open($cacheFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
|
$sr = New-Object System.IO.StreamReader($fs)
|
|
$rawCache = $sr.ReadToEnd()
|
|
|
|
$latestCache = [PSCustomObject]@{}
|
|
if (-not [string]::IsNullOrWhiteSpace($rawCache)) {
|
|
try { $latestCache = $rawCache | ConvertFrom-Json } catch {}
|
|
}
|
|
|
|
$propExists = $false
|
|
foreach ($p in $latestCache.psobject.properties) {
|
|
if ($p.Name -eq $ComputerName) { $propExists = $true; break }
|
|
}
|
|
|
|
if ($propExists) {
|
|
$latestCache.$ComputerName = $cacheEntry
|
|
} else {
|
|
$latestCache | Add-Member -MemberType NoteProperty -Name $ComputerName -Value $cacheEntry -Force
|
|
}
|
|
|
|
$fs.SetLength(0)
|
|
$sw = New-Object System.IO.StreamWriter($fs, [System.Text.Encoding]::UTF8)
|
|
$sw.Write(($latestCache | ConvertTo-Json -Depth 6 -Compress))
|
|
$sw.Flush()
|
|
$sw.Close()
|
|
$sr.Close()
|
|
$fs.Close()
|
|
break
|
|
} catch {
|
|
Start-Sleep -Milliseconds 300
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
$outData = @($results)
|
|
return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress)
|