Phase 3+4 rename reorg: repo dir renames + startnet.cmd menu

Pairs with Phase 1+2 from earlier (alias maps in Install-FromManifest,
GE-Enforce, Get-PCProfile, verify-state). See project-shopfloor-rename-reorg
memory for the plan.

Phase 3 (repo + paths):
- git mv per-PC-type dirs to gea-shopfloor-* names:
    Standard      -> gea-shopfloor-collections
    CMM           -> gea-shopfloor-cmm
    Keyence       -> gea-shopfloor-keyence
    Genspect      -> gea-shopfloor-genspect
    WaxAndTrace   -> gea-shopfloor-waxtrace
    Display       -> gea-shopfloor-display
    Lab           -> gea-shopfloor-common (folded; Timeclock+Lab merge)
- New gea-shopfloor-nocollections/ (clone of collections sans UDC scripts).
- New gea-shopfloor-heattreat/ (placeholder, README only).
- Move Standard/ntlars-backups/ -> _ntlars-backups/ (per-MN, not per-type).
- Run-ShopfloorSetup.ps1: Resolve-PCTypeDir helper walks alias group when
  the on-disk dir for the current pcType is missing. Set-MachineNumber
  helper-copy gated on collections|nocollections|legacy Standard-Machine.
- Update-MachineNumber.ps1: pcProfiles lookups try gea-shopfloor-collections
  first, fall back to legacy Standard-Machine. PowerShell 5.1 compatible
  (no null-coalesce).

Phase 4 (startnet.cmd menu):
- Choice 3 "GEA Shopfloor" now drills into a 9-item sub-menu instead of
  going straight to enrollment. Sub-cats:
    1. Machine with Collections        -> gea-shopfloor-collections
    2. Machine without Collections     -> gea-shopfloor-nocollections
    3. Common (Timeclock, Lab)         -> gea-shopfloor-common
    4. Keyence                         -> gea-shopfloor-keyence
    5. CMM                             -> gea-shopfloor-cmm
    6. Genspect                        -> gea-shopfloor-genspect
    7. Heattreat                       -> gea-shopfloor-heattreat
    8. Wax and Trace                   -> gea-shopfloor-waxtrace
    9. Display                         -> gea-shopfloor-display
- Office menu (existing 6-option) follows for every sub-cat.
- Machine number prompt only for collections + nocollections.
- pc-subtype.txt + display-type.txt no longer written. PCTYPE is a
  single full string (gea-shopfloor-*); subtype-aware code paths fall
  back to empty and resolve via the alias map.
- CMM bootstrap stage gate switched from "%PCTYPE%"=="CMM" to
  "%PCTYPE%"=="gea-shopfloor-cmm".

Test harness:
- B-enforce/run.sh PCSUBTYPE default changed from "Machine" to "" so
  single-arg invocation matches the new single-string scheme. Two-arg
  legacy form ("Standard Machine") still works via aliasing.
- B-enforce/tamper.ps1 alias-aware Test-MatrixEntryMatches mirroring
  verify-state.ps1.

Smoke-tested on win11 VM as SYSTEM via qga: B-enforce harness 5-phase
cycle (stage / baseline / tamper / heal / idempotent) passes 10/10
with PCType=gea-shopfloor-collections AND with legacy "Standard Machine"
two-arg form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-04 08:09:16 -04:00
parent 48e20a7e73
commit 6dcf96e40a
262 changed files with 592 additions and 455 deletions

View File

@@ -0,0 +1,111 @@
# 01-eDNC.ps1 - Install eDNC and deploy custom eMxInfo.txt (Standard-Machine only)
# --- Transcript ---
$logDir = 'C:\Logs\SFLD'
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
try { Start-Transcript -Path (Join-Path $logDir '01-eDNC.log') -Append -Force | Out-Null } catch {}
# --- Skip on Timeclock sub-type ---
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
if (Test-Path $subtypeFile) {
$subtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
if ($subtype -eq 'Timeclock') {
Write-Host "=== eDNC Setup: skipped (Standard-Timeclock) ==="
try { Stop-Transcript | Out-Null } catch {}
return
}
}
Write-Host "=== eDNC Setup ==="
function Get-SiteConfig {
$configPath = 'C:\Enrollment\site-config.json'
if (-not (Test-Path -LiteralPath $configPath)) {
Write-Host "site-config.json not found - using defaults" -ForegroundColor DarkGray
return $null
}
try {
return (Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json)
} catch {
Write-Warning "Failed to parse site-config.json: $_"
return $null
}
}
$siteConfig = Get-SiteConfig
$siteName = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
$siteNameCompact = if ($siteConfig) { $siteConfig.siteNameCompact } else { 'WestJefferson' }
$edncDir = "C:\Enrollment\shopfloor-setup\Standard\eDNC"
if (-not (Test-Path $edncDir)) {
Write-Warning "eDNC folder not found at $edncDir - skipping."
try { Stop-Transcript | Out-Null } catch {}
exit 0
}
# --- Find installer ---
# Filter is eDNC*.msi (no dash) so we match both vendor naming styles:
# eDNC-6.4.3.msi (dash) and eDNC_6-4-5.msi (underscore). Imaging dir should
# only contain ONE version at a time; rollback to a prior version is handled
# post-imaging via the SFLD share's standard-machine/apps/ alternates.
$edncMsi = Get-ChildItem -Path $edncDir -Filter "eDNC*.msi" | Select-Object -First 1
$emxInfo = Join-Path $edncDir "eMxInfo.txt"
# --- 1. Install eDNC ---
if ($edncMsi) {
Write-Host "Installing eDNC: $($edncMsi.Name)..."
$p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$($edncMsi.FullName)`" /qn /norestart LAUNCHNTLARS=false SITESELECTED=`"$siteName`"" -Wait -PassThru
Write-Host " eDNC exit code: $($p.ExitCode)"
} else {
Write-Warning "eDNC installer not found in $edncDir (expected eDNC*.msi)"
}
# --- 2. Mirror x86 install to 64-bit Program Files (app uses hardcoded paths) ---
# mxTransactionDll.dll references \Dnc\Server Files\
$copies = @(
@{ Src = "C:\Program Files (x86)\Dnc"; Dst = "C:\Program Files\Dnc" }
)
foreach ($c in $copies) {
if (Test-Path $c.Src) {
if (-not (Test-Path $c.Dst)) {
New-Item -Path $c.Dst -ItemType Directory -Force | Out-Null
}
Copy-Item -Path "$($c.Src)\*" -Destination $c.Dst -Recurse -Force
Write-Host " Copied $($c.Src) -> $($c.Dst)"
}
}
# --- 3. Set DNC site + machine number ---
$regBase = "HKLM\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC"
reg add "$regBase\General" /v Site /t REG_SZ /d $siteNameCompact /f | Out-Null
Write-Host " DNC site set to $siteNameCompact."
# Set machine number if tech entered one during PXE menu (defaults to 9999)
$machineNumFile = 'C:\Enrollment\machine-number.txt'
$machineNum = '9999'
if (Test-Path -LiteralPath $machineNumFile) {
$num = (Get-Content -LiteralPath $machineNumFile -First 1 -ErrorAction SilentlyContinue).Trim()
if ($num -and $num -match '^\d+$') { $machineNum = $num }
}
reg add "$regBase\General" /v MachineNo /t REG_SZ /d $machineNum /f | Out-Null
Write-Host " DNC MachineNo set to $machineNum."
# --- 4. Deploy custom eMxInfo.txt to both Program Files paths ---
if (Test-Path $emxInfo) {
$dest86 = "C:\Program Files (x86)\DNC\Server Files"
$dest64 = "C:\Program Files\DNC\Server Files"
foreach ($dest in @($dest86, $dest64)) {
if (-not (Test-Path $dest)) {
New-Item -Path $dest -ItemType Directory -Force | Out-Null
}
Copy-Item -Path $emxInfo -Destination (Join-Path $dest "eMxInfo.txt") -Force
Write-Host " eMxInfo.txt -> $dest"
}
} else {
Write-Warning "eMxInfo.txt not found at $emxInfo"
}
Write-Host "=== eDNC Setup Complete ==="
try { Stop-Transcript | Out-Null } catch {}

View File

@@ -0,0 +1,81 @@
# 02-MachineNumberACLs.ps1 - Pre-grant write access on the UDC settings
# file and eDNC registry key so that STANDARD (non-admin) users can update
# the machine number via the Check-MachineNumber logon task without
# elevation or a UAC prompt.
#
# Runs during imaging as admin (type-specific Standard phase, after
# 01-eDNC.ps1 has installed DnC). Only touches Standard PCs.
#
# What gets opened up (narrow scope, not blanket admin):
# - C:\ProgramData\UDC\udc_settings.json -> BUILTIN\Users : Modify
# - HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General
# -> BUILTIN\Users : SetValue
# --- Transcript ---
$logDir = 'C:\Logs\SFLD'
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
try { Start-Transcript -Path (Join-Path $logDir '02-MachineNumberACLs.log') -Append -Force | Out-Null } catch {}
# --- Skip on Timeclock sub-type (no UDC/eDNC to grant ACLs for) ---
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
if (Test-Path $subtypeFile) {
$subtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
if ($subtype -eq 'Timeclock') {
Write-Host "02-MachineNumberACLs: skipped (Standard-Timeclock)"
try { Stop-Transcript | Out-Null } catch {}
return
}
}
Write-Host "02-MachineNumberACLs.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Host ""
Write-Host "Setting ACLs for standard-user machine number access..."
# --- UDC settings directory ---
# Set ACL on the DIRECTORY (not the file) with inheritance so that
# udc_settings.json inherits the permission whenever UDC.exe creates it.
# UDC_Setup.exe is killed by KillAfterDetection before UDC.exe writes the
# JSON, so the file doesn't exist at this point. Directory-level ACL with
# ContainerInherit + ObjectInherit covers any file created inside later.
$udcDir = 'C:\ProgramData\UDC'
if (Test-Path -LiteralPath $udcDir) {
try {
$acl = Get-Acl -LiteralPath $udcDir
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
'BUILTIN\Users', 'Modify',
([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
[System.Security.AccessControl.PropagationFlags]::None,
'Allow')
$acl.AddAccessRule($rule)
Set-Acl -LiteralPath $udcDir -AclObject $acl -ErrorAction Stop
Write-Host " UDC dir: BUILTIN\Users granted Modify (inherited) on $udcDir"
} catch {
Write-Warning " Failed to set ACL on $udcDir : $_"
}
} else {
Write-Host " UDC dir not found at $udcDir - skipping (UDC not installed?)" -ForegroundColor DarkGray
}
# --- eDNC registry key ---
$ednRegPathWin = 'SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General'
try {
$regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($ednRegPathWin, $true)
if ($regKey) {
$regSec = $regKey.GetAccessControl()
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
'BUILTIN\Users', 'SetValue', 'Allow')
$regSec.AddAccessRule($rule)
$regKey.SetAccessControl($regSec)
$regKey.Close()
Write-Host " eDNC reg: BUILTIN\Users granted SetValue on HKLM:\$ednRegPathWin"
} else {
Write-Host " eDNC registry key not found - skipping (eDNC not installed?)" -ForegroundColor DarkGray
}
} catch {
Write-Warning " Failed to set ACL on HKLM:\$ednRegPathWin : $_"
}
Write-Host "ACL setup complete."
try { Stop-Transcript | Out-Null } catch {}

View File

@@ -0,0 +1,86 @@
# 03-RestoreEDncConfig.ps1 - Restore per-machine eDNC config from .reg backup.
#
# Runs at shopfloor-setup time AFTER 01-eDNC.ps1 has installed eDNC. If the
# tech typed a real machine number at the PXE menu (not left blank -> 9999),
# look for a .reg backup matching that number on the PXE-local copy of the
# setup tree and import it. That restores everything eDNC-side the backup
# captured: eFocas IP/port, PPDCS serial (baud/parity/bits), Hssb KRelay1,
# etc. - instead of the reimaged PC coming up on factory defaults.
#
# After reg import, the tech-typed machine number is written to HKLM and to
# UDC's settings JSON to guarantee the current number wins over whatever the
# backup happened to contain (off-by-one backups exist in the historical
# dump; see ntlars-fixed/ work on the PXE host).
#
# Skipped when:
# - pc-subtype != Machine (Timeclock PCs do not use a machine number)
# - machine-number.txt missing / empty / 9999 (tech declined to set one)
# - ntlars-backups/ folder missing from the staged setup tree
# - no matching .reg file on disk for this machine number
$ErrorActionPreference = 'Continue'
$logDir = 'C:\Logs\SFLD'
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
try { Start-Transcript -Path (Join-Path $logDir '03-RestoreEDncConfig.log') -Append -Force | Out-Null } catch {}
Write-Host "=== Restore eDNC config from backup ==="
# ---- Sub-type gate ----
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
if (Test-Path $subtypeFile) {
$subtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
if ($subtype -eq 'Timeclock') {
Write-Host "Standard-Timeclock - skipping."
try { Stop-Transcript | Out-Null } catch {}
return
}
}
# ---- Read machine number captured at PXE time ----
$mnFile = 'C:\Enrollment\machine-number.txt'
if (-not (Test-Path $mnFile)) {
Write-Host "machine-number.txt not present - skipping (tech did not set one)."
try { Stop-Transcript | Out-Null } catch {}
return
}
$machineNum = (Get-Content $mnFile -First 1 -ErrorAction SilentlyContinue).Trim()
if (-not $machineNum -or $machineNum -eq '9999') {
Write-Host "Machine number is '$machineNum' (placeholder or empty) - skipping."
try { Stop-Transcript | Out-Null } catch {}
return
}
Write-Host "Machine number: $machineNum"
# ---- Locate local backup root (staged from PXE during imaging) ----
$backupRoot = 'C:\Enrollment\shopfloor-setup\Standard\ntlars-backups'
if (-not (Test-Path $backupRoot)) {
Write-Host "ntlars-backups folder not staged at $backupRoot - skipping."
try { Stop-Transcript | Out-Null } catch {}
return
}
. "$PSScriptRoot\..\Shopfloor\lib\Restore-EDncReg.ps1"
$imported = Import-EDncRegBackup -SourceRoot $backupRoot -MachineNumber $machineNum
if (-not $imported) {
Write-Host "No backup imported - leaving eDNC at installer defaults."
try { Stop-Transcript | Out-Null } catch {}
return
}
# ---- Tech-typed number wins: overwrite MachineNo in both eDNC and UDC. ----
# The imported .reg probably already has the right number (we rewrote the
# historical dump) but off-by-one backups exist, and this is cheap insurance.
. "$PSScriptRoot\..\Shopfloor\lib\Update-MachineNumber.ps1"
$current = Get-CurrentMachineNumber
Write-Host "Post-import state: UDC='$($current.Udc)' eDNC='$($current.Ednc)'"
$result = Update-MachineNumber -NewNumber $machineNum
if ($result.UdcUpdated) { Write-Host " UDC MachineNumber set to $machineNum" }
if ($result.EdncUpdated) { Write-Host " eDNC MachineNo set to $machineNum" }
foreach ($err in $result.Errors) { Write-Warning " $err" }
try { Stop-Transcript | Out-Null } catch {}

View File

@@ -0,0 +1,32 @@
@echo off
REM Install-eMxInfo.cmd - copy the site-specific eMxInfo.txt into both
REM DNC Program Files install paths. Run by Install-FromManifest.ps1
REM (Type=CMD) when Hash detection on the x86 path fails.
REM
REM Real install paths confirmed via 01-eDNC.ps1 (PXE preinstall) + a
REM real shopfloor install log capture:
REM C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt
REM C:\Program Files\DNC\Server Files\eMxInfo.txt
REM Both required because mxTransactionDll.dll references both.
REM
REM Earlier versions wrote to C:\Program Files\eDNC\eMxInfo.txt - that
REM path doesn't exist on disk; the eDNC product creates \DNC\ (no 'e')
REM with a "Server Files" subdir. Corrected 2026-04-28.
set "SRC=%~dp0eMxInfo.txt"
if not exist "%SRC%" (
echo Install-eMxInfo: source file not found at %SRC%
exit /b 1
)
set "DST64=C:\Program Files\DNC\Server Files"
set "DST86=C:\Program Files (x86)\DNC\Server Files"
if not exist "%DST64%\" mkdir "%DST64%" 2>nul
if not exist "%DST86%\" mkdir "%DST86%" 2>nul
copy /Y "%SRC%" "%DST64%\eMxInfo.txt" >nul || exit /b 2
copy /Y "%SRC%" "%DST86%\eMxInfo.txt" >nul || exit /b 3
echo Install-eMxInfo: deployed eMxInfo.txt to both DNC\Server Files paths
exit /b 0

View File

@@ -0,0 +1,38 @@
@echo off
REM Set-MachineNumber.bat - Wrapper for Set-MachineNumber.ps1
REM
REM Looks for the .ps1 in three places, in order:
REM 1. %~dp0Set-MachineNumber.ps1
REM - .bat and .ps1 side-by-side (normal desktop-copied case, repo layout)
REM 2. C:\Users\SupportUser\Desktop\Set-MachineNumber.ps1
REM - dispatcher-copied location, if this .bat lives somewhere else
REM 3. C:\Enrollment\shopfloor-setup\Standard\Set-MachineNumber.ps1
REM - canonical enrollment staging copy
REM
REM Goto-based dispatch - no nested if blocks, no literal parens in echo lines.
REM CMD parses "if (...)" blocks by counting parens and will silently eat any
REM "(" or ")" inside an echo, so keeping the flow flat avoids that class of
REM syntax bomb entirely.
setlocal
set "PS1=%~dp0Set-MachineNumber.ps1"
if exist "%PS1%" goto :run
set "PS1=C:\Users\SupportUser\Desktop\Set-MachineNumber.ps1"
if exist "%PS1%" goto :run
set "PS1=C:\Enrollment\shopfloor-setup\Standard\Set-MachineNumber.ps1"
if exist "%PS1%" goto :run
echo ERROR: Set-MachineNumber.ps1 not found in any of:
echo %~dp0Set-MachineNumber.ps1
echo C:\Users\SupportUser\Desktop\Set-MachineNumber.ps1
echo C:\Enrollment\shopfloor-setup\Standard\Set-MachineNumber.ps1
echo.
pause
exit /b 1
:run
echo Launching: %PS1%
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS1%"
exit /b %errorlevel%

View File

@@ -0,0 +1,87 @@
# Set-MachineNumber.ps1 - Update UDC + eDNC machine number on a Standard shopfloor PC
#
# Purpose:
# Both UDC and eDNC use the same per-machine identifier ("Workstation Number" /
# "Machine Number"). On Standard PCs imaged via PXE preinstall, both are installed
# with a placeholder. When the PC is brought to its physical machine and assigned
# a real number, this helper updates both apps in one step.
#
# Persistence locations updated:
# 1. UDC: C:\ProgramData\UDC\udc_settings.json (GeneralSettings.MachineNumber)
# 2. eDNC: HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General\MachineNo
#
# After updating, kills any running UDC.exe and relaunches it with the new args
# so the in-memory state matches the persisted value.
#
# Run as SupportUser (admin). Requires write access to ProgramData and HKLM.
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
. "$PSScriptRoot\..\Shopfloor\lib\Get-PCProfile.ps1"
. "$PSScriptRoot\..\Shopfloor\lib\Update-MachineNumber.ps1"
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
# --- Read current values for display ---
$currentMN = Get-CurrentMachineNumber
$currentUdc = $currentMN.Udc
$currentEdnc = $currentMN.Ednc
# --- Show prompt with current state ---
$promptLines = @()
$promptLines += "Current UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not set)' })"
$promptLines += "Current eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not set)' })"
$promptLines += ""
$promptLines += "Enter the new Machine Number for this PC:"
$prompt = $promptLines -join "`n"
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
if ([string]::IsNullOrWhiteSpace($new)) {
Write-Host "Cancelled."
exit 0
}
$new = $new.Trim()
# --- Validate: digits only (loosen if you need alphanumerics) ---
if ($new -notmatch '^\d+$') {
[System.Windows.Forms.MessageBox]::Show(
"Machine number must be digits only.`n`nYou entered: '$new'",
"Invalid Machine Number",
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Error
) | Out-Null
exit 1
}
$mnResult = Update-MachineNumber -NewNumber $new -Site $site
$results = @()
if ($mnResult.UdcUpdated) {
Write-Host "UDC: $currentUdc -> $new"
$results += "UDC updated to $new"
} elseif (-not (Test-Path 'C:\ProgramData\UDC\udc_settings.json')) {
$results += "UDC: settings file missing (run UDC.exe once first)"
}
if ($mnResult.EdncUpdated) {
Write-Host "eDNC: $currentEdnc -> $new"
$results += "eDNC updated to $new"
} elseif (-not (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General')) {
$results += "eDNC: registry key missing (eDNC not installed?)"
}
foreach ($err in $mnResult.Errors) {
Write-Warning $err
$results += $err
}
if ($mnResult.UdcUpdated) { Write-Host "UDC.exe relaunched." }
# --- Show summary ---
$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe."
[System.Windows.Forms.MessageBox]::Show(
$summary,
"Set Machine Number - Done",
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
) | Out-Null