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)
130 lines
4.8 KiB
PowerShell
130 lines
4.8 KiB
PowerShell
# Shared helpers for USMT-Server.ps1, USMT-Worker.ps1 and Get-Software.ps1.
|
|
# Dot-source this file; it must stay compatible with Windows PowerShell 5.1 and PowerShell 7.
|
|
|
|
function Import-DashConfig {
|
|
param([string]$Path, [switch]$Demo)
|
|
|
|
if (-not $Path) { $Path = Join-Path (Split-Path $PSScriptRoot -Parent) "Config\usmt-dash.psd1" }
|
|
$Path = $Path -replace '[\\/]', [System.IO.Path]::DirectorySeparatorChar
|
|
if (-not (Test-Path $Path)) { throw "Config file not found: $Path" }
|
|
|
|
$cfg = Import-PowerShellDataFile -Path $Path
|
|
if ($Demo -or $env:USMT_MODE -eq 'Demo') { $cfg.Mode = 'Demo' }
|
|
$cfg.IsDemo = ($cfg.Mode -eq 'Demo')
|
|
return $cfg
|
|
}
|
|
|
|
function Join-DashPath {
|
|
# Join-Path that tolerates the backslash-separated relative paths used in config on Linux
|
|
param([string]$Base, [string]$Child)
|
|
return Join-Path $Base ($Child -replace '[\\/]', [System.IO.Path]::DirectorySeparatorChar)
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------------------
|
|
# Console log mirror
|
|
#
|
|
# Write-Host is proxied so every line printed to the console is also appended to a JSON
|
|
# lines file: {"i":<id>,"t":"<time>","s":[["text","Color"],...]}. The server's /get-logs
|
|
# route tails that file for the dashboard terminal panel. Call sites are unchanged.
|
|
# -----------------------------------------------------------------------------------------
|
|
$script:ConsoleLog = $null
|
|
|
|
function Initialize-ConsoleLog {
|
|
param([string]$Path, [int]$MaxLines = 3000, [string]$CommandLine)
|
|
|
|
$dir = Split-Path $Path -Parent
|
|
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
|
|
|
$script:ConsoleLog = @{
|
|
Path = $Path
|
|
MaxLines = $MaxLines
|
|
NextId = [long]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()) * 1000
|
|
Pending = [System.Collections.Generic.List[object]]::new()
|
|
Written = 0
|
|
}
|
|
if ($CommandLine) { Add-ConsoleLogLine -Segments @(, @($CommandLine, 'Prompt')) }
|
|
}
|
|
|
|
function ConvertTo-JsonString([string]$s) {
|
|
$sb = [System.Text.StringBuilder]::new('"')
|
|
foreach ($ch in $s.ToCharArray()) {
|
|
switch ($ch) {
|
|
'"' { [void]$sb.Append('\"') }
|
|
'\' { [void]$sb.Append('\\') }
|
|
"`r" { }
|
|
"`t" { [void]$sb.Append('\t') }
|
|
default {
|
|
if ([int]$ch -lt 32) { [void]$sb.AppendFormat('\u{0:x4}', [int]$ch) } else { [void]$sb.Append($ch) }
|
|
}
|
|
}
|
|
}
|
|
[void]$sb.Append('"')
|
|
return $sb.ToString()
|
|
}
|
|
|
|
function Add-ConsoleLogLine {
|
|
param([object[]]$Segments)
|
|
$log = $script:ConsoleLog
|
|
if (-not $log) { return }
|
|
|
|
$log.NextId++
|
|
$segJson = ($Segments | ForEach-Object { "[" + (ConvertTo-JsonString $_[0]) + "," + (ConvertTo-JsonString $_[1]) + "]" }) -join ','
|
|
$line = '{"i":' + $log.NextId + ',"t":"' + (Get-Date).ToString('HH:mm:ss') + '","s":[' + $segJson + ']}'
|
|
|
|
for ($try = 0; $try -lt 5; $try++) {
|
|
try { [System.IO.File]::AppendAllText($log.Path, $line + "`n"); break }
|
|
catch { Start-Sleep -Milliseconds 20 }
|
|
}
|
|
|
|
$log.Written++
|
|
if ($log.Written % 250 -eq 0) { Limit-ConsoleLog }
|
|
}
|
|
|
|
function Limit-ConsoleLog {
|
|
$log = $script:ConsoleLog
|
|
try {
|
|
$lines = [System.IO.File]::ReadAllLines($log.Path)
|
|
if ($lines.Count -gt $log.MaxLines) {
|
|
$keep = $lines[($lines.Count - $log.MaxLines)..($lines.Count - 1)]
|
|
$tmp = "$($log.Path).tmp"
|
|
[System.IO.File]::WriteAllLines($tmp, $keep)
|
|
Move-Item -Path $tmp -Destination $log.Path -Force
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
function Write-Host {
|
|
param(
|
|
[Parameter(Position = 0)][object]$Object = '',
|
|
[switch]$NoNewline,
|
|
[object]$Separator = ' ',
|
|
[System.ConsoleColor]$ForegroundColor,
|
|
[System.ConsoleColor]$BackgroundColor
|
|
)
|
|
Microsoft.PowerShell.Utility\Write-Host @PSBoundParameters
|
|
|
|
$log = $script:ConsoleLog
|
|
if (-not $log) { return }
|
|
|
|
$color = if ($PSBoundParameters.ContainsKey('ForegroundColor')) { "$ForegroundColor" } else { 'Default' }
|
|
$parts = "$Object" -split "`n"
|
|
for ($p = 0; $p -lt $parts.Count; $p++) {
|
|
if ($parts[$p] -ne '') { $log.Pending.Add(@($parts[$p], $color)) }
|
|
$isLast = ($p -eq $parts.Count - 1)
|
|
if (-not $isLast -or -not $NoNewline) {
|
|
Add-ConsoleLogLine -Segments $log.Pending.ToArray()
|
|
$log.Pending.Clear()
|
|
}
|
|
}
|
|
}
|
|
|
|
# Reads a file without locking it against the other process (worker writes while server reads)
|
|
function Read-SharedText {
|
|
param([string]$Path)
|
|
$fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
try {
|
|
$sr = New-Object System.IO.StreamReader($fs)
|
|
return $sr.ReadToEnd()
|
|
} finally { $fs.Close() }
|
|
}
|