webapp: imaging progress dashboard + serial column on reports list

Adds end-to-end progress tracking for PXE imaging sessions and surfaces
each Blancco report's BIOS serial in the report list.

webapp:
  * services/imaging_status.py - JSON-per-serial state store under
    IMAGING_DIR (default /var/log/pxe-imaging). Atomic write via
    tempfile + rename. log_tail capped at 50 lines. Merges partial
    updates so clients can post just the current_stage tick.
  * config.py - new IMAGING_DIR env-overridable path.
  * services/csrf.py - explicit exempt list for machine-to-machine
    endpoints; /imaging/status is the first entry. Air-gapped LAN;
    trust-by-network for client posts.
  * app.py - four new routes:
      GET  /imaging               dashboard (renders all sessions)
      POST /imaging/status        client status push (JSON body)
      GET  /imaging/<serial>.json raw session JSON for ad-hoc polling
      POST /imaging/delete/<s>    clear a session from the dashboard
    Also parses each Blancco XML in the /reports list to surface
    system.serial + system.model columns.
  * templates/imaging.html - Bootstrap dashboard with per-session
    cards (state badge, progress bar, stage idx/total, mac, elapsed,
    log tail). meta http-equiv refresh=5 for auto-tick.
  * templates/base.html - new "Imaging Progress" nav entry.
  * templates/reports.html - Serial + Model columns added.

playbook:
  * shopfloor-setup/Shopfloor/lib/Send-PxeStatus.ps1 - new helper.
    Dot-source this then call Send-PxeStatus -Stage X -StageIndex N
    -StageTotal M from any stage script. BIOS serial via CIM, MAC via
    Get-NetAdapter, pctype + machinenumber from C:\Enrollment.
    Failures are swallowed to a local log so a network blip doesn't
    block imaging.
  * shopfloor-setup/Run-ShopfloorSetup.ps1 - dot-sources helper +
    posts at three coarse milestones (start, PPKG enrollment,
    handoff to Monitor-IntuneProgress).
  * shopfloor-setup/gea-shopfloor-keyence/09-Setup-Keyence.ps1 -
    posts at session start + after Install-FromManifest with
    succeeded/failed status derived from $rc. Other 09-Setup-*.ps1
    scripts can follow the same pattern.

ID is BIOS serial (stable across WinPE -> Windows transition and
across reboots, unlike hostname which is random pre-PPKG). Operator
already knows the serial of the bay they imaged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-13 10:07:18 -04:00
parent 1d3f21f814
commit 9122b28c31
10 changed files with 478 additions and 10 deletions

View File

@@ -26,6 +26,21 @@ Write-Host " Transcript: $transcriptPath"
Write-Host "================================================================"
Write-Host ""
# Imaging-progress reporter. Posts coarse stage updates to the PXE webapp
# at http://10.9.100.1:9009/imaging/status so the operator can watch
# progress in a browser. Best-effort: failures never block imaging.
$pxeStatusLib = Join-Path $PSScriptRoot 'Shopfloor\lib\Send-PxeStatus.ps1'
if (Test-Path $pxeStatusLib) {
try { . $pxeStatusLib } catch { Write-Warning "Send-PxeStatus load failed: $_" }
}
function Report-Stage {
param([string]$Stage, [int]$Index, [int]$Total = 8, [string]$Status = 'in_progress', [string]$Error_ = '')
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
Send-PxeStatus -Stage $Stage -StageIndex $Index -StageTotal $Total -Status $Status -Error_ $Error_
}
}
Report-Stage -Stage 'Run-ShopfloorSetup: starting' -Index 1
# AutoLogonCount is NOT set here. Previously we bumped it to 99/4, but
# Windows decrements it per-logon and at 0 clears AutoAdminLogon -- which
# nukes the lockdown-configured ShopFloor autologon later in the chain.
@@ -452,6 +467,7 @@ if (-not $hasWifi -and -not $hasDefaultRoute) {
$enrollScript = Join-Path $enrollDir 'run-enrollment.ps1'
if (Test-Path -LiteralPath $enrollScript) {
Write-Host ""
Report-Stage -Stage 'Run-ShopfloorSetup: PPKG enrollment' -Index 4
Write-Host "=== Running enrollment (PPKG install) ==="
Write-Host "NOTE: PPKG schedules a near-immediate reboot. We will cancel"
Write-Host " it and hand off to Monitor-IntuneProgress -PostPpkg, which"
@@ -466,6 +482,7 @@ if (Test-Path -LiteralPath $enrollScript) {
# persistent @logon sync_intune task fires on the next boot to resume
# tracking through device-category-assignment + lockdown.
Write-Host ""
Report-Stage -Stage 'Run-ShopfloorSetup: handoff to Monitor-IntuneProgress' -Index 7
Write-Host "=== Handing off to Monitor-IntuneProgress -PostPpkg ==="
cmd /c "shutdown /a 2>nul" | Out-Null
$monitor = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'

View File

@@ -0,0 +1,77 @@
# Send-PxeStatus.ps1
# Posts a coarse-grained progress update to the PXE webapp's /imaging/status
# endpoint. Never blocks imaging on a failed push (try/catch with no rethrow).
# Air-gapped LAN; no auth - the webapp endpoint is CSRF-exempt for machine
# clients (see services/csrf.py).
function Send-PxeStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Stage,
[int]$StageIndex = 0,
[int]$StageTotal = 0,
[ValidateSet('in_progress','succeeded','failed')]
[string]$Status = 'in_progress',
[string]$Error_ = '',
[string[]]$LogLines = @(),
[string]$PxeServer = '10.9.100.1',
[int]$Port = 9009,
[int]$TimeoutSec = 5
)
# Get serial early; if WMI fails we still want to push under a best-effort id.
$serial = $null
try {
$serial = (Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber
} catch {
try { $serial = (Get-WmiObject -Class Win32_BIOS -ErrorAction Stop).SerialNumber } catch { }
}
if ([string]::IsNullOrWhiteSpace($serial)) { $serial = $env:COMPUTERNAME }
$serial = ($serial -as [string]).Trim()
# MAC of the first up wired adapter (best-effort; PXE servers see this MAC in DHCP).
$mac = $null
try {
$nic = Get-NetAdapter -Physical | Where-Object { $_.Status -eq 'Up' -and $_.MediaType -eq '802.3' } | Select-Object -First 1
if ($nic) { $mac = $nic.MacAddress -replace '-', ':' }
} catch { }
# Enrollment context files (present after startnet.cmd stages them).
$pctype = ''
$machno = ''
if (Test-Path 'C:\Enrollment\pc-type.txt') { $pctype = (Get-Content 'C:\Enrollment\pc-type.txt' -ErrorAction SilentlyContinue | Select-Object -First 1).Trim() }
if (Test-Path 'C:\Enrollment\machine-number.txt') { $machno = (Get-Content 'C:\Enrollment\machine-number.txt' -ErrorAction SilentlyContinue | Select-Object -First 1).Trim() }
$payload = @{
serial = $serial
mac = $mac
hostname_target = $env:COMPUTERNAME
pctype = $pctype
machinenumber = $machno
current_stage = $Stage
stage_index = $StageIndex
stage_total = $StageTotal
status = $Status
}
if ($Error_) { $payload.error = $Error_ }
if ($LogLines) { $payload.log_lines = $LogLines }
$body = $payload | ConvertTo-Json -Compress
$uri = "http://${PxeServer}:${Port}/imaging/status"
try {
Invoke-WebRequest -Uri $uri -Method POST `
-Body $body -ContentType 'application/json' `
-UseBasicParsing -TimeoutSec $TimeoutSec `
-ErrorAction Stop | Out-Null
} catch {
# Never block imaging on a failed status push. Write to local log only.
try {
$logDir = 'C:\Logs'
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
"$(Get-Date -Format s) Send-PxeStatus failed: $($_.Exception.Message)" |
Out-File -FilePath (Join-Path $logDir 'send-pxe-status.log') -Append -Encoding utf8
} catch { }
}
}

View File

@@ -44,6 +44,12 @@ Write-KeyenceLog "Running as: $([System.Security.Principal.WindowsIdentity]::Get
Write-KeyenceLog "Script root: $PSScriptRoot"
Write-KeyenceLog "================================================================"
# Status push to PXE webapp - best-effort, never blocks imaging.
$pxeStatusLib = Join-Path $PSScriptRoot '..\Shopfloor\lib\Send-PxeStatus.ps1'
if (Test-Path $pxeStatusLib) {
try { . $pxeStatusLib; Send-PxeStatus -Stage '09-Setup-Keyence: starting' -StageIndex 5 -StageTotal 8 } catch { }
}
# Diagnostic dump
foreach ($file in @('pc-type.txt','pc-subtype.txt','machine-number.txt')) {
$path = "C:\Enrollment\$file"
@@ -69,6 +75,23 @@ if (-not (Test-Path $manifestPath)) {
Write-KeyenceLog "Install-FromManifest returned $rc"
}
# ============================================================================
# Step 2: OpenText auto-start at login (HostExplorer "WJ Shopfloor" session)
# ============================================================================
$autoStartLib = Join-Path $PSScriptRoot '..\Shopfloor\lib\Set-OpenTextAutoStart.ps1'
if (Test-Path -LiteralPath $autoStartLib) {
Write-KeyenceLog "Calling $autoStartLib"
& $autoStartLib
} else {
Write-KeyenceLog "Set-OpenTextAutoStart.ps1 not found at $autoStartLib - OpenText auto-start NOT configured" 'WARN'
}
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
$finalStatus = if ($rc -eq 0) { 'in_progress' } else { 'failed' }
$finalErr = if ($rc -ne 0) { "Install-FromManifest exit $rc" } else { '' }
Send-PxeStatus -Stage '09-Setup-Keyence: complete' -StageIndex 6 -StageTotal 8 -Status $finalStatus -Error_ $finalErr
}
Write-KeyenceLog "================================================================"
Write-KeyenceLog "=== Keyence Setup session end ==="
Write-KeyenceLog "================================================================"