<# .SYNOPSIS Reference orchestrator: source a GE-Enforce manifest from shopdb, run the UNCHANGED engine against it, and report the result back to shopdb. This is a thin wrapper around Install-FromManifest.ps1 (the engine). It is a reference a site adapts into its GE-Enforce.ps1 flow; it is not the live dispatcher. The engine, detection, self-heal, and SMB payload resolution are untouched - only the source of the manifest JSON moves from a share file to shopdb, plus a result report. .PARAMETER Scope The imaging pc-type / scope name (e.g. gea-shopfloor-cmm), same value the dispatcher already resolves from C:\Enrollment\pc-type.txt. .PARAMETER EnginePath Path to Install-FromManifest.ps1 (the engine lib, >= 2.6). .PARAMETER ShareManifestPath The current on-share manifest for this scope. In shadow mode the engine runs against THIS (unchanged behavior) and shopdb is only compared + reported. Once cut over, omit it and the engine runs against the shopdb-sourced manifest. .PARAMETER ShadowMode Fetch + compare + report, but install from the share (no behavior change). .PARAMETER IncludeCommon Also fetch the fleet-wide CommonScope and merge it into -Scope, so the PC enforces its own scope entries PLUS common's (on a Name conflict the -Scope entry wins). OFF by default: a scope is enforced ALONE (self-sufficient). Displays are self-sufficient and do NOT set this. Only a share-less non-display PC that genuinely needs the fleet-wide common entries over HTTPS turns it on. .PARAMETER CommonScope Name of the fleet-wide scope merged when -IncludeCommon is set (default 'common'). Ignored without -IncludeCommon. .NOTES Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC. Config comes from HKLM:\SOFTWARE\GE\ShopDB (BaseUrl, ApiToken) - see the psm1. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Scope, [Parameter(Mandatory)] [string]$EnginePath, [string]$ShareManifestPath, [switch]$ShadowMode, [switch]$IncludeCommon, [string]$CommonScope = 'common', [string]$BaseUrl, [string]$ApiToken, [string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log" ) function Write-Log { param([string]$Message, [string]$Level = 'INFO') $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message" try { $logDir = Split-Path -Parent $LogFile if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null } Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue } catch {} Write-Host $line } function Write-ShopdbEventLog { <# Write a Windows Application event-log entry under source 'ShopdbEnforce'. Used to make an otherwise silent fail-safe (no manifest and an empty cache) observable to whoever watches the display. Best-effort: registering the source needs admin, which the SYSTEM scheduled task has; any failure is swallowed so it can never break the fail-safe. #> param([string]$Message, [string]$EntryType = 'Warning', [int]$EventId = 1001) $source = 'ShopdbEnforce' try { if (-not [System.Diagnostics.EventLog]::SourceExists($source)) { New-EventLog -LogName Application -Source $source -ErrorAction Stop } Write-EventLog -LogName Application -Source $source -EntryType $EntryType ` -EventId $EventId -Message $Message -ErrorAction Stop } catch {} } try { Import-Module (Join-Path $PSScriptRoot 'ShopdbEnforceClient.psm1') -Force $config = Get-ShopdbConfig -BaseUrl $BaseUrl -ApiToken $ApiToken if (-not $config) { Write-Log 'No shopdb BaseUrl configured yet - retry next cycle.' 'WARN' exit 0 } $sync = Sync-ShopdbManifest -Scope $Scope -Config $config if (-not $sync.Path) { # Fail-safe stays (exit 0), but a fresh display with an empty cache would # otherwise enforce nothing SILENTLY. Surface it: a Windows event-log # entry plus a best-effort report ping so it shows in Enforcement Reports. $reason = if ($sync.Error) { $sync.Error } else { 'shopdb unreachable and no cached manifest' } Write-Log "No manifest available for $Scope ($reason)." 'WARN' Write-ShopdbEventLog -Message ("GE-Enforce could not fetch a manifest for scope '$Scope' and has no cached copy; nothing was enforced this cycle. Reason: $reason") -EntryType 'Error' -EventId 1001 try { $failReport = New-ShopdbReport -Scope $Scope -AppliedVersion 0 -Summary @{ Installed = 0; Skipped = 0; Failed = 1; Filtered = 0; EnforcerVersion = '2.6' Results = @(@{ Name = '(manifest-fetch)'; Action = 'failed'; Message = $reason }) } if (Send-ShopdbReport -Config $config -Report $failReport) { Write-Log 'Reported empty-cache fetch failure to shopdb.' } } catch {} exit 0 } Write-Log "Manifest for $Scope from $($sync.Source) (v$($sync.Version))." # Shadow mode: compare shopdb vs the share, but install from the share. if ($ShadowMode -and $ShareManifestPath -and (Test-Path $ShareManifestPath)) { $diff = Compare-ShopdbShadow -ShopdbManifestPath $sync.Path -ShareManifestPath $ShareManifestPath if ($diff.Same) { Write-Log 'Shadow: shopdb manifest matches the share.' } else { Write-Log ("Shadow DIFF: shopdb-only=[{0}] share-only=[{1}] orderDiff={2}" -f ` ($diff.ShopdbOnly -join ','), ($diff.ShareOnly -join ','), $diff.OrderDiff) 'WARN' } } # Which manifest the engine actually runs against. if ($ShadowMode -and $ShareManifestPath) { # Shadow: install from the share exactly as today (no payload resolve, # no common merge - the share already carries its own common scope). $manifestToRun = $ShareManifestPath } else { # Optional common-scope inheritance (OFF by default; displays are # self-sufficient). Only when -IncludeCommon is set do we fetch the # fleet-wide common scope (best-effort, same fail-safe cache) and merge # it in with the pctype winning on conflict. Skipped when this run IS # the common scope. $manifestToMerge = $sync.Path if ($IncludeCommon -and $CommonScope -and ($CommonScope -ine $Scope)) { $commonSync = Sync-ShopdbManifest -Scope $CommonScope -Config $config if ($commonSync.Path) { $manifestToMerge = Merge-ShopdbManifests -PrimaryManifestPath $sync.Path -CommonManifestPath $commonSync.Path if ($manifestToMerge -ne $sync.Path) { Write-Log "Merged common scope '$CommonScope' (from $($commonSync.Source), v$($commonSync.Version)) into $Scope." } } else { Write-Log "Common scope '$CommonScope' unavailable (no fetch, no cache) - enforcing $Scope alone." 'WARN' } } # Cutover: stage any http/inline payloads to local files and rewrite the # manifest to point at them, so the UNCHANGED engine installs from local # (no SMB needed for share-less PCs). $manifestToRun = Resolve-ShopdbPayloads -ManifestPath $manifestToMerge -Config $config if ($manifestToRun -ne $manifestToMerge) { Write-Log "Resolved http/inline payloads to local files: $manifestToRun" } } # --- INTEGRATION POINT --------------------------------------------------- # Run the engine. EXPECTED ENGINE CONTRACT: Install-FromManifest.ps1 returns # a summary object (hashtable or PSCustomObject) carrying integer counts # Installed / Skipped / Failed / Filtered # a string EnforcerVersion, and a Results list of per-entry outcomes # @{ Name; Action; SelfHealed; ExitCode; Message }. # The engine may not honor that yet: it might return $null, a bare return # code, or emit several objects. ConvertTo-ShopdbSummary adapts whatever it # returns into a well-formed summary hashtable so the report stage always # gets clean input (we do NOT assume the engine was fixed). # The engine requires -InstallerRoot (base for any relative Source/Installer # path) and -LogFile. Shadow runs off the share, so relative paths resolve # against the share scope dir. Cutover rewrites payloads to ABSOLUTE local # paths, so InstallerRoot is only a harmless fallback base (the payload cache). if ($ShadowMode -and $ShareManifestPath) { $installerRoot = Split-Path -Parent $ShareManifestPath } else { $installerRoot = Join-Path (Split-Path -Parent $manifestToRun) 'payloads' } if ($installerRoot -and -not (Test-Path $installerRoot)) { New-Item -ItemType Directory -Path $installerRoot -Force -ErrorAction SilentlyContinue | Out-Null } $engineLog = $LogFile -replace '\.log$', '-engine.log' Write-Log "Running engine against $manifestToRun" $engineResult = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope ` -InstallerRoot $installerRoot -LogFile $engineLog $summary = ConvertTo-ShopdbSummary -EngineResult $engineResult # Report the result (best-effort). $appliedVersion = 0 if ($sync.Version) { [int]::TryParse($sync.Version, [ref]$appliedVersion) | Out-Null } $report = New-ShopdbReport -Scope $Scope -AppliedVersion $appliedVersion -Summary $summary if (Send-ShopdbReport -Config $config -Report $report) { Write-Log "Reported: installed=$($summary.Installed) skipped=$($summary.Skipped) failed=$($summary.Failed)." } else { Write-Log 'Report POST failed (non-fatal).' 'WARN' } } catch { Write-Log "Unhandled error (non-fatal): $($_.Exception.Message)" 'ERROR' } exit 0