diff --git a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 index 866e04e..7d18531 100644 --- a/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 +++ b/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 @@ -520,29 +520,35 @@ function Test-HostnameMatches { } # Machine-number filter. Stable identifier tied to the bay; survives PC -# replacement at the same machine. Source of truth = the value the tech -# entered at the PXE menu, persisted to C:\Enrollment\machine-number.txt -# by startnet.cmd. Falls back to the DNC registry if that file is missing -# (covers PCs that pre-date this filter being introduced). +# replacement at the same machine. +# +# Source of truth = the eDNC/DNC registry MachineNo. That is what the +# reassignment flow (Set-MachineNumber -> Update-MachineNumber) actually +# rewrites when a bay is re-numbered (e.g. 9999 placeholder -> 7501). The +# imaging-time C:\Enrollment\machine-number.txt is written ONCE by startnet.cmd +# at the PXE menu and is NOT updated on reassignment, so it goes stale. Read +# the registry FIRST so TargetMachineNumbers gating follows reassignment; fall +# back to the txt only when the registry has no value (covers non-DNC PCs or a +# bay where eDNC has not populated MachineNo yet). $script:_cachedMachineNumber = $null function Get-CurrentMachineNumber { if ($null -ne $script:_cachedMachineNumber) { return $script:_cachedMachineNumber } - $candidates = @( - 'C:\Enrollment\machine-number.txt' - ) - foreach ($p in $candidates) { - if (Test-Path -LiteralPath $p) { - $v = (Get-Content -LiteralPath $p -ErrorAction SilentlyContinue | Select-Object -First 1) - if ($v) { $script:_cachedMachineNumber = $v.Trim(); return $script:_cachedMachineNumber } - } - } foreach ($r in @( 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General', 'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General' )) { if (Test-Path $r) { $p = Get-ItemProperty -Path $r -ErrorAction SilentlyContinue - if ($p.MachineNo) { $script:_cachedMachineNumber = [string]$p.MachineNo; return $script:_cachedMachineNumber } + if ($p.MachineNo) { + $v = ([string]$p.MachineNo).Trim() + if ($v) { $script:_cachedMachineNumber = $v; return $script:_cachedMachineNumber } + } + } + } + foreach ($p in @('C:\Enrollment\machine-number.txt')) { + if (Test-Path -LiteralPath $p) { + $v = (Get-Content -LiteralPath $p -ErrorAction SilentlyContinue | Select-Object -First 1) + if ($v) { $script:_cachedMachineNumber = $v.Trim(); return $script:_cachedMachineNumber } } } $script:_cachedMachineNumber = '' @@ -575,6 +581,11 @@ foreach ($app in $config.Applications) { Write-InstallLog "==> $($app.Name)" + # Per-entry guard: a single entry that throws must NOT abort the whole + # scope (and silently skip every later entry + the status write). Catch, + # log, count as failed, move on. + try { + if (-not (Test-PCTypeMatches -App $app -Type $PCType -SubType $PCSubType)) { Write-InstallLog " PCTypes filter: entry targets $($app.PCTypes -join ',') but PC is $PCType$(if ($PCSubType) { "-$PCSubType" }) - skipping" $pcFiltered++ @@ -681,6 +692,11 @@ foreach ($app in $config.Applications) { $failed++ } + + } catch { + Write-InstallLog (" UNCAUGHT error processing {0}: {1} | at {2}" -f $app.Name, $_.Exception.Message, ($_.ScriptStackTrace -replace '\s+',' ')) 'ERROR' + $failed++ + } } Write-InstallLog '============================================' diff --git a/playbook/shopfloor-setup/common/scripts/Deploy-ShopfloorStartLayout.ps1 b/playbook/shopfloor-setup/common/scripts/Deploy-ShopfloorStartLayout.ps1 new file mode 100644 index 0000000..e7aeee0 --- /dev/null +++ b/playbook/shopfloor-setup/common/scripts/Deploy-ShopfloorStartLayout.ps1 @@ -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 +# +# 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