manifest engine: reg-first machine number + per-app crash isolation; add start-layout DSC

Install-FromManifest.ps1:
- Get-CurrentMachineNumber reads the eDNC/DNC registry FIRST (reassignment-
  authoritative), falling back to C:\Enrollment\machine-number.txt. The txt is
  written once at imaging and is NOT updated on reassignment, so txt-first
  gated reassigned bays on a stale number.
- Per-entry try/catch in the app loop: a single entry that throws no longer
  aborts the whole scope (skipping every later entry + the status write). It is
  logged, counted failed, and the loop continues. This was silently killing the
  collections scope at the MTConnect Makino entry, which also stopped the
  ShopDB asset reporter (a later entry) from ever running.

Deploy-ShopfloorStartLayout.ps1 (new): local-DSC port of the Intune
desktop-weblinks + Start-menu pins (copies .url/.lnk to Public Desktop +
All-Users Start Menu, writes the ConfigureStartPins JSON policy, resets
start2.bin + restarts the shell). Verified on Win11: pins render after logon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-03 16:42:40 -04:00
parent a380b17112
commit a351160520
2 changed files with 157 additions and 14 deletions

View File

@@ -0,0 +1,127 @@
# Deploy-ShopfloorStartLayout.ps1
#
# Local-DSC port of the Intune SFLD desktop/Start-menu deployment. Creates the
# Public Desktop weblinks (.url) + app/folder shortcuts (.lnk) AND pins them to
# the Windows 11 Start menu - using the exact same mechanism Simple-Install.ps1
# uses: shortcuts in the All-Users Start Menu, a ConfigureStartPins JSON policy
# in the registry, and a StartMenuExperienceHost reset so it applies on next
# logon. Nothing here needs Intune/MDM - it is all file + registry-policy.
#
# Designed to run from the GE-Enforce manifest engine as a Type=PS1 entry
# (DetectionMethod=Always, or Hash on the pins.json). Idempotent.
#
# Usage:
# powershell -ExecutionPolicy Bypass -File Deploy-ShopfloorStartLayout.ps1
# -AssetsDir <globalassets dir on the share>
#
# AssetsDir holds the prebuilt .url/.lnk (the "globalassets" folder). The pin
# list + order below mirrors device-config.yaml StartMenuPins; entries with a
# Target are created on the fly (app/folder pins), the rest are copied from
# AssetsDir.
param(
[string]$AssetsDir = (Join-Path $PSScriptRoot 'globalassets'),
[string]$DesktopDir = 'C:\Users\Public\Desktop',
[switch]$NoShellRestart
)
$ErrorActionPreference = 'Continue'
$logDir = 'C:\Logs\Shopfloor'
New-Item -ItemType Directory -Path $logDir -Force -EA SilentlyContinue | Out-Null
$log = Join-Path $logDir ('start-layout-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
function Log($m){ "$([DateTime]::Now.ToString('s')) $m" | Tee-Object -FilePath $log -Append | Out-Null }
# Ordered pin set - mirrors device-config.yaml StartMenuPins. Name = the file
# in the All-Users Start Menu (and AssetsDir for prebuilt ones). Target set =>
# create the shortcut; Target empty => copy the prebuilt file from AssetsDir.
$Pins = @(
@{ Name = 'Shopfloor Dashboard.url' }
@{ Name = 'PN & SN Label Printing.url' }
@{ Name = 'WJ Shop Floor Homepage.url' }
@{ Name = 'WJ Web Reports.url' }
@{ Name = 'Blueprint PDF Viewer.url' }
@{ Name = 'Central CSF Web Reports.url' }
@{ Name = 'Plant Apps.url' }
@{ Name = 'Safety Good Catch Form.url' }
@{ Name = 'WJ IT Help Desk.url' }
@{ Name = 'OneIDM.url' }
@{ Name = 'M365 Webmail.url' }
@{ Name = 'HR Central.url' }
@{ Name = 'Defect_Tracker.lnk' }
@{ Name = 'Calculator.lnk' }
@{ Name = 'Notepad.lnk' }
@{ Name = 'eDNC.lnk'; Target = 'C:\Program Files\eDNC\eDNC.exe' }
@{ Name = 'NTLARS.lnk'; Target = 'C:\Program Files (x86)\NTLARS\NTLARS.exe' }
@{ Name = 'Shopfloor Tools.lnk'; Target = 'C:\Users\Public\Desktop\Shopfloor Tools' }
)
$startMenuDir = Join-Path $env:ALLUSERSPROFILE 'Microsoft\Windows\Start Menu\Programs'
function New-UrlShortcut([string]$Path,[string]$Url){
@('[InternetShortcut]', "URL=$Url") | Set-Content -LiteralPath $Path -Encoding ASCII
}
function New-LnkShortcut([string]$Path,[string]$Target,[string]$Args,[string]$Icon){
$sh = New-Object -ComObject WScript.Shell
$sc = $sh.CreateShortcut($Path)
$sc.TargetPath = $Target
if ($Args) { $sc.Arguments = $Args }
# working dir: parent of target for files, the folder itself for folder pins
$sc.WorkingDirectory = if (Test-Path -LiteralPath $Target -PathType Container) { $Target } else { Split-Path -Parent $Target }
if ($Icon) { $sc.IconLocation = $Icon }
$sc.Save()
}
Log "=== Deploy shopfloor start layout (assets: $AssetsDir) ==="
New-Item -ItemType Directory -Path $startMenuDir -Force -EA SilentlyContinue | Out-Null
New-Item -ItemType Directory -Path $DesktopDir -Force -EA SilentlyContinue | Out-Null
$pinnedList = @()
foreach ($pin in $Pins) {
$leaf = $pin.Name
$dst = Join-Path $startMenuDir $leaf
try {
if ($pin.Target) {
# create app/folder shortcut from Target
New-LnkShortcut -Path $dst -Target $pin.Target -Args $pin.Arguments -Icon $pin.IconLocation
Log "created (target) $leaf -> $($pin.Target)"
} else {
# copy prebuilt asset (.url/.lnk) from globalassets
$src = Join-Path $AssetsDir $leaf
if (-not (Test-Path -LiteralPath $src)) { Log "MISSING asset, skipping pin: $src"; continue }
Copy-Item -LiteralPath $src -Destination $dst -Force
# also drop on the Public Desktop
Copy-Item -LiteralPath $src -Destination (Join-Path $DesktopDir $leaf) -Force
Log "copied $leaf (start menu + desktop)"
}
$pinnedList += @{ desktopAppLink = "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\$leaf" }
} catch {
Log "ERROR pin ${leaf}: $($_.Exception.Message)"
}
}
# ConfigureStartPins JSON -> HKLM policy (same shape Simple-Install.ps1 writes)
$jsonDir = 'C:\ProgramData\SFLD\StartMenu'
New-Item -ItemType Directory -Path $jsonDir -Force -EA SilentlyContinue | Out-Null
$jsonPath = Join-Path $jsonDir 'pins.json'
([ordered]@{ applyOnce = $false; pinnedList = $pinnedList } | ConvertTo-Json -Depth 6) |
Set-Content -LiteralPath $jsonPath -Encoding UTF8
$reg = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
if (-not (Test-Path $reg)) { New-Item -Path $reg -Force | Out-Null }
New-ItemProperty -Path $reg -Name 'ConfigureStartPins' -PropertyType String `
-Value (Get-Content -LiteralPath $jsonPath -Raw -Encoding UTF8) -Force | Out-Null
Log "ConfigureStartPins policy written ($($pinnedList.Count) pins) -> $reg"
# Apply now: clear each real user's cached start layout + restart the shell.
if (-not $NoShellRestart) {
Get-ChildItem 'C:\Users' -Directory -EA SilentlyContinue |
Where-Object { $_.Name -notin @('Public','Default','Default User','All Users') } |
ForEach-Object {
$sb = Join-Path $_.FullName 'AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin'
if (Test-Path -LiteralPath $sb) { Remove-Item -LiteralPath $sb -Force -EA SilentlyContinue; Log "cleared start2.bin: $($_.Name)" }
}
Get-Process -Name 'StartMenuExperienceHost' -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
Log 'StartMenuExperienceHost restarted (pins apply on next shell load)'
}
Log '=== done ==='
exit 0