diff --git a/playbook/FlatUnattendW10-shopfloor.xml b/playbook/FlatUnattendW10-shopfloor.xml
index 52011fa..a810fc5 100644
--- a/playbook/FlatUnattendW10-shopfloor.xml
+++ b/playbook/FlatUnattendW10-shopfloor.xml
@@ -156,26 +156,31 @@
4
+ powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1"
+ Fetch bulk staging (shopfloor-setup tree + preinstall bundle) from the PXE share on a fresh mount, BEFORE the production-network switch takes the bay off the imaging LAN. Detailed log at C:\Logs\Fetch\.
+
+
+ 5
powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1"
Prompt to connect production network then wait for TCP 443 connectivity
- 5
+ 6
powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1"
Migrate from wired to WiFi if WiFi adapter present, else stay on wired
- 6
+ 7
msiexec.exe /i "C:\PreInstall\installers\powershell7\PowerShell-7.5.4-win-x64.msi" /qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1
Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix)
- 7
+ 8
powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"
Run GCCH Enrollment
- 8
+ 9
powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"
Run shopfloor PC type setup
diff --git a/playbook/shopfloor-setup/Fetch-StagingPayload.ps1 b/playbook/shopfloor-setup/Fetch-StagingPayload.ps1
new file mode 100644
index 0000000..8340a96
--- /dev/null
+++ b/playbook/shopfloor-setup/Fetch-StagingPayload.ps1
@@ -0,0 +1,172 @@
+# Fetch-StagingPayload.ps1 - post-boot bulk staging fetch (first-logon).
+#
+# WHY THIS EXISTS
+# WinPE used to stage the whole shopfloor-setup tree + preinstall bundle to
+# the target disk DURING the WinPE phase. But WinPE maps the enrollment share
+# (Y:) early, then idles for many minutes while the full Windows image applies.
+# Samba's `deadtime` drops idle sessions, so by the time WinPE reached the
+# copies the Y: mount was dead and most copies failed (symptom: a bay with
+# only site-config.json staged, then nothing). Doing the bulk copy here - at
+# first logon, in full Windows, on a FRESH share mount with no prior idle -
+# sidesteps that entirely.
+#
+# WHEN IT RUNS
+# The unattend FirstLogonCommands runs this BEFORE the PowerShell 7 MSI install
+# (which needs C:\PreInstall\installers\powershell7\) and before
+# Run-ShopfloorSetup.ps1 (which needs C:\Enrollment\shopfloor-setup\). So this
+# must populate both trees before those steps fire.
+#
+# WHAT IT FETCHES (generic bulk - Phase 1)
+# \\\enrollment\shopfloor-setup\Run-ShopfloorSetup.ps1 -> C:\Enrollment\
+# \\\enrollment\shopfloor-setup\{backup_lockdown.bat,Shopfloor,common,
+# _ntlars-backups,gea-shopfloor-} -> C:\Enrollment\shopfloor-setup\
+# \\\enrollment\pre-install\{preinstall.json,installers,udc-backups}
+# -> C:\PreInstall\
+# (Heavy per-type payloads - CMM/Keyence/WaxTrace - are still staged in WinPE
+# for now; Phase 2 moves those here too.)
+#
+# LOGGING
+# Verbose transcript + a per-item table to C:\Logs\Fetch\. Every robocopy logs
+# its exit code, file/dir counts, byte total, and elapsed time, so a failed
+# fetch is fully diagnosable (unlike the old opaque WinPE staging).
+#
+# Always exits 0 - a fetch failure must not abort the FirstLogonCommands chain;
+# the log carries the truth and Run-ShopfloorSetup surfaces missing pieces.
+
+$ErrorActionPreference = 'Continue'
+
+# --- Logging setup ---
+$logDir = 'C:\Logs\Fetch'
+if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
+$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
+$logFile = Join-Path $logDir "fetch-staging-$stamp.log"
+try { Start-Transcript -Path $logFile -Append -Force | Out-Null } catch {}
+
+function Log {
+ param([string]$Message, [string]$Level = 'INFO')
+ $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+ Write-Host "[$ts] [$Level] $Message"
+}
+
+Log "================================================================"
+Log "=== Fetch-StagingPayload start (PID $PID) ==="
+Log "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
+Log "Host: $env:COMPUTERNAME"
+Log "================================================================"
+
+# --- Resolve the share source + creds (written by startnet to fetch-source.txt;
+# falls back to the historical defaults if absent) ---
+$shareUnc = '\\172.16.9.1\enrollment'
+$shareUser = 'pxe-upload'
+$sharePass = 'pxe'
+$srcFile = 'C:\Enrollment\fetch-source.txt'
+if (Test-Path -LiteralPath $srcFile) {
+ # Format: line1=UNC, line2=user, line3=pass
+ $lines = @(Get-Content -LiteralPath $srcFile -ErrorAction SilentlyContinue)
+ if ($lines.Count -ge 1 -and $lines[0].Trim()) { $shareUnc = $lines[0].Trim() }
+ if ($lines.Count -ge 2 -and $lines[1].Trim()) { $shareUser = $lines[1].Trim() }
+ if ($lines.Count -ge 3 -and $lines[2].Trim()) { $sharePass = $lines[2].Trim() }
+ Log "fetch-source.txt: UNC=$shareUnc user=$shareUser"
+} else {
+ Log "fetch-source.txt absent - using defaults: UNC=$shareUnc user=$shareUser"
+}
+
+# --- pc-type (drives which gea-shopfloor- dir to fetch) ---
+$pcType = ''
+if (Test-Path -LiteralPath 'C:\Enrollment\pc-type.txt') {
+ $pcType = (Get-Content -LiteralPath 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim()
+}
+Log "PC type: $(if ($pcType) { $pcType } else { '(none)' })"
+
+# --- Status push (best-effort) ---
+$pxeStatusLib = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1'
+# (lib not fetched yet on first run; ignore if absent)
+if (Test-Path $pxeStatusLib) { try { . $pxeStatusLib; Send-PxeStatus -Stage 'Fetch-StagingPayload: starting' -StageIndex 1 -StageTotal 8 } catch {} }
+
+# --- Mount the share fresh (use Z:; retry to ride out a brief blip) ---
+$drive = 'Z:'
+function Mount-Share {
+ & net use $drive /delete /y 2>$null | Out-Null
+ $r = & net use $drive $shareUnc /user:$shareUser $sharePass /persistent:no 2>&1
+ return ($LASTEXITCODE -eq 0)
+}
+$mounted = $false
+for ($attempt = 1; $attempt -le 5; $attempt++) {
+ Log "Mounting $shareUnc as $drive (attempt $attempt/5)..."
+ if (Mount-Share) { $mounted = $true; Log "Mounted OK"; break }
+ Log "Mount failed (exit $LASTEXITCODE) - waiting 10s" 'WARN'
+ Start-Sleep -Seconds 10
+}
+if (-not $mounted) {
+ Log "Could not mount $shareUnc after 5 attempts - ABORTING fetch. Bay will be under-provisioned; re-run this script once the share is reachable." 'ERROR'
+ try { Stop-Transcript | Out-Null } catch {}
+ exit 0
+}
+
+# --- Fetch helper: robocopy one item, log exit + counts + timing ---
+$results = @()
+function Fetch-Item {
+ param(
+ [string]$Label,
+ [string]$SrcDir, # under $drive
+ [string]$DstDir,
+ [string[]]$Files, # named files for a flat copy; empty = whole-dir /E
+ [switch]$Recurse # /E whole directory
+ )
+ $src = Join-Path $drive $SrcDir
+ if (-not (Test-Path -LiteralPath $src)) {
+ Log "[SKIP] $Label - source not on share: $src" 'WARN'
+ $script:results += [pscustomobject]@{ Item=$Label; Exit='n/a'; Result='SOURCE-MISSING' }
+ return
+ }
+ if (-not (Test-Path -LiteralPath $DstDir)) { New-Item -ItemType Directory -Path $DstDir -Force | Out-Null }
+ $args = @($src, $DstDir)
+ if ($Recurse) { $args += '/E' } else { $args += $Files }
+ $args += @('/R:2','/W:3','/NFL','/NDL','/NP')
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
+ Log "[COPY] $Label : robocopy $src -> $DstDir $(if ($Recurse){'/E'}else{$Files -join ','})"
+ $out = & robocopy @args 2>&1
+ $rc = $LASTEXITCODE
+ $sw.Stop()
+ # robocopy 0-7 = success, 8+ = failure
+ $ok = ($rc -lt 8)
+ # pull the summary counts robocopy prints
+ $summary = ($out | Select-String -Pattern 'Files :|Dirs :|Bytes :' ) -join ' | '
+ Log "[$(if($ok){'OK'}else{'FAIL'})] $Label exit=$rc time=$([math]::Round($sw.Elapsed.TotalSeconds,1))s $summary"
+ $script:results += [pscustomobject]@{ Item=$Label; Exit=$rc; Result=$(if($ok){'OK'}else{'FAIL'}) }
+}
+
+# --- Generic bulk fetch ---
+$ENR = 'C:\Enrollment'
+$SFD = 'C:\Enrollment\shopfloor-setup'
+$PIN = 'C:\PreInstall'
+
+Fetch-Item -Label 'Run-ShopfloorSetup.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Run-ShopfloorSetup.ps1')
+Fetch-Item -Label 'backup_lockdown.bat' -SrcDir 'shopfloor-setup' -DstDir $SFD -Files @('backup_lockdown.bat')
+Fetch-Item -Label 'Shopfloor baseline' -SrcDir 'shopfloor-setup\Shopfloor' -DstDir (Join-Path $SFD 'Shopfloor') -Recurse
+Fetch-Item -Label 'common' -SrcDir 'shopfloor-setup\common' -DstDir (Join-Path $SFD 'common') -Recurse
+Fetch-Item -Label '_ntlars-backups' -SrcDir 'shopfloor-setup\_ntlars-backups' -DstDir (Join-Path $SFD '_ntlars-backups') -Recurse
+if ($pcType) {
+ Fetch-Item -Label "type:$pcType" -SrcDir "shopfloor-setup\$pcType" -DstDir (Join-Path $SFD $pcType) -Recurse
+}
+# preinstall bundle
+Fetch-Item -Label 'preinstall.json' -SrcDir 'pre-install' -DstDir $PIN -Files @('preinstall.json')
+Fetch-Item -Label 'preinstall installers' -SrcDir 'pre-install\installers' -DstDir (Join-Path $PIN 'installers') -Recurse
+Fetch-Item -Label 'udc-backups' -SrcDir 'pre-install\udc-backups' -DstDir (Join-Path $PIN 'udc-backups') -Recurse
+
+# --- Unmount ---
+& net use $drive /delete /y 2>$null | Out-Null
+
+# --- Summary table ---
+Log "================================================================"
+Log "FETCH SUMMARY:"
+foreach ($r in $results) { Log (" {0,-28} exit={1,-4} {2}" -f $r.Item, $r.Exit, $r.Result) }
+$failed = @($results | Where-Object { $_.Result -eq 'FAIL' })
+if ($failed.Count -gt 0) {
+ Log "$($failed.Count) item(s) FAILED: $(( $failed | ForEach-Object { $_.Item }) -join ', ')" 'ERROR'
+} else {
+ Log "All fetched items OK." 'INFO'
+}
+Log "=== Fetch-StagingPayload complete ==="
+try { Stop-Transcript | Out-Null } catch {}
+exit 0
diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd
index d98e051..3390d81 100644
--- a/playbook/startnet.cmd
+++ b/playbook/startnet.cmd
@@ -441,6 +441,17 @@ set /p CMMDODA= W:\Enrollment\pc-subtype.txt
:cmm_id_done
robocopy "Y:\shopfloor-setup" "W:\Enrollment" "Run-ShopfloorSetup.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
+REM Post-boot bulk re-fetch. WinPE staging below is best-effort (it can fail if
+REM the Y: mount went idle-dead during the WIM apply); Fetch-StagingPayload.ps1
+REM re-pulls the shopfloor-setup tree + preinstall bundle at first logon on a
+REM FRESH mount. The unattend FirstLogonCommands runs it before the PowerShell 7
+REM MSI + Run-ShopfloorSetup. Stage the script + its source coords here.
+robocopy "Y:\shopfloor-setup" "W:\Enrollment" "Fetch-StagingPayload.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
+> W:\Enrollment\fetch-source.txt (
+echo \\172.16.9.1\enrollment
+echo pxe-upload
+echo pxe
+)
REM --- Always copy Shopfloor baseline scripts ---
mkdir W:\Enrollment\shopfloor-setup 2>NUL
robocopy "Y:\shopfloor-setup" "W:\Enrollment\shopfloor-setup" "backup_lockdown.bat" /R:1 /W:1 /NFL /NDL /NJH /NJS