Commit Graph

272 Commits

Author SHA1 Message Date
cproudlock
d359563a4c Export-FormtracepakInventory: drop Get-Service entirely (SCM hang)
WJF00052 / WJF00083 / WJF00084 / WJF00159 hung indefinitely on step [5/5]
during the 2026-05-24 capture pass and the operator killed the .bat,
leaving empty inventory CSVs. The earlier Start-Job + Wait-Job -Timeout 30
guard (commit fce6680) was insufficient: Stop-Job on a Get-Service that's
blocked on a degraded Service Control Manager can itself block in the SCM
call (Sentinel HASP driver service in particular has been observed to
wedge Get-Service for minutes), so the main script never unblocked even
after the 30s timeout fired.

Service state isn't load-bearing for identifying the install on a bay
(version + model + DeviceName come from disk + registry, not the SCM),
so the cleanest fix is to drop the Get-Service block entirely. Get-Process
is fast and stays. Step [5/5] label changed from "Checking running
processes and services" to "Checking running processes".

Smoke tested on win11 VM: full Export against a real v6.213 / AVANT
install completes in ~25 s (was 2.9 s with no install, 25 s now reflects
the legitimate ~95k registry walk that dominates the runtime - not the
service hang).
2026-05-24 11:03:20 -04:00
cproudlock
cb149ed8cd Backup-FormtracepakSettings: empirically-grounded version + model detection, HKEY_USERS sweep, manifest evidence stamp
Installed FormTracePak v6.213 on the win11 VM (picking FORMTRACER Avant
in the dialogs) and probed the resulting registry / disk layout to find
out what evidence a real FormTracePak install actually carries. Two
empirical findings:

1. ACTIVE MODEL lives at
   HKLM:\SOFTWARE\WOW6432Node\Mitutoyo\FORMPAK\Config\device map\DeviceName
   (string value). For the AVANT install the value is "FORMTRACER Avant";
   for the CV/SV/CS controllers the value contains the matching model id.
   The Surfpak\FormMes\MachineInfo\Machine\Machine* subtree lists EVERY
   supported machine and is NOT the active selection - the previous
   heuristic that scanned uninstall-entry DisplayName picked up bogus
   WinUSB driver-package entries from "Mitutoyo Corporation" instead.

2. INSTALLED BINARY VERSION is in Formtracepak.exe VersionInfo:
   FileVersion=6.2.0.51, ProductVersion=6.2.0.0. This does NOT match the
   Mitutoyo MSI release label ("6.213") that bay-config.csv uses. The
   uninstall entry's DisplayVersion is empty. So bay-config.csv stays
   canonical for the per-asset marketing version; exe FileVersion is a
   concrete cross-check.

Backup rewrites:
- Replace the previous one-shot version detection with evidence reading:
  bay-config.csv (asset->version+model), Formtracepak.exe VersionInfo,
  device-map\DeviceName. The over-broad uninstall-reg regex is gone.
- Normalize DeviceName ("FORMTRACER Avant" / "CV-4500" / "CV-3200" /
  "Contracer" / "Surftest") to bay-config notation (AVANT / CV-4500 / ...).
- Emit BayConfigMatch flag - true when bay-config-predicted model agrees
  with the device-map\DeviceName on disk. False = drift, tech rechecks
  before restoring to a new bay.
- manifest.json now stamps: AssetNumber, FormtracepakVersion, Model,
  BayConfigVersion, BayConfigModel, BayConfigSource, InstalledExeVersion,
  InstalledExePath, InstalledDeviceName, InstalledModelNormalized,
  BayConfigMatch.

HKEY_USERS sweep:
- Wax/Trace bays log in as a per-site user (lg782713sd at WJ today,
  ShopFloor post-SFLD-2.0, other accounts at other sites). The previous
  HKCU:\ scan only captured the script's running user. Sweep every
  loaded HKEY_USERS hive whose SID matches S-1-5-21-* (real user SIDs)
  for Software\Mitutoyo / FORMTRACEPAK / FORMPAK / SURFPAK subkeys,
  add them to $RegistryRoots. Username-agnostic - works at any site
  without changes.

reg.exe export now also accepts the Registry::HKEY_USERS\<sid>\... PSPath
form by stripping the "Registry::" prefix when building the reg.exe
argument (previously emitted "Invalid key name" errors on HKEY_USERS roots).

Smoke tested against a real v6.213 / FORMTRACER Avant install on win11
VM: bay-config lookup matches, exe FileVersion read, device-map
normalized to AVANT, all four .reg files (HKLM, HKLM-WOW6432Node, HKCU,
HKEY_USERS\<interactive-sid>) exported clean, 0 errors.

Restore-side SID translation (HKEY_USERS\<src-sid>\... -> target user's
SID or HKCU on the new bay) is a follow-up. HKLM tree carries the
critical device-map\DeviceName, controller config, and machine settings;
the HKEY_USERS hive captures per-user UI prefs only.
2026-05-24 09:58:24 -04:00
cproudlock
821e3179d1 Wax/Trace triad: switch to SHA256 hashes (FIPS-compliant) + separate hash-failure path from copy-failure path
Backup-FormtracepakSettings observed 17 Errors on a real shopfloor PC
(G5PRTW04ESF / WJF00159 capture) - all of the form:
  WARNING: Failed to copy ...App.ini: Exception calling ".ctor" with "0"
  argument(s): "This implementation is not part of the Windows Platform
  FIPS validated cryptographic algorithms."

Cause: Windows FIPS policy is enabled on West Jefferson shopfloor PCs.
The per-file Get-FileHash -Algorithm MD5 call throws a hard .NET exception
that bypasses -ErrorAction SilentlyContinue (the throw is from the MD5
constructor, not the cmdlet's parameter binder). That exception was caught
by the broad try/catch around both Copy-Item + manifest add, producing a
misleading "Failed to copy" message even though Copy-Item already succeeded.
Net effect: files copied fine, but manifest rows were missing for those
files (Install would fall back to its bulk-copy path).

Two fixes:
- Switch the hash algorithm from MD5 to SHA256 in both Backup (manifest
  row capture) and Install (Restore-FileItem hash-skip compare). SHA256
  is Get-FileHash's default and is FIPS-compliant. Old MD5-hashed backups
  remain restorable because Install computes hashes fresh from disk at
  restore time and does not read the Hash column from file_manifest.csv.
- Split the broad try/catch in Backup's Copy-ToStaging into two
  try/catches: the first wraps only Copy-Item (real copy failure -> Errors
  counter + skip the file), the second wraps only Get-FileHash (hash
  failure -> log warning, manifest row gets a null Hash and is still
  recorded). A hash failure no longer pretends the copy failed.
- Install's hash compare is wrapped in try/catch too so a hash exception
  falls through to overwrite-mode rather than crashing the restore.

Smoke tested on win11 VM: SHA256 round-trip works (64-char hashes in
file_manifest.csv), Backup reports 0 Errors, Install hash-skip path
correctly skips Identical files on second-run idempotency check.
2026-05-24 09:23:53 -04:00
cproudlock
fce6680c6f Wax/Trace triad: relocate backup path + harden service enum against SCM hangs
Two operator-driven fixes.

1. Backup target moves from S:\2 WJ Scans Record Retention\backup\waxtrace
   to S:\DT\Shopfloor\backup\waxandtrace per the canonical SFLD layout.
   Backup creates the per-asset folder if missing; Install reads from the
   same path by default.

2. Export-FormtracepakInventory hung on step [5/5] when run on a shopfloor
   PC. The original `Get-Service | Where-Object { DisplayName -match ... }`
   pattern materializes every service via the Service Control Manager + post
   filters in PowerShell, which can block indefinitely when any single
   service (Sentinel HASP driver, GE-Enforce agent, etc.) is in a degraded
   state. Two-part fix:
   - Switch to Get-Service -DisplayName 'Mitutoyo*','*FORMTRACEPAK*',...
     so the SCM only materializes matching services (server-side wildcard
     filter, faster + lower blast radius).
   - Wrap the enumeration in Start-Job + Wait-Job -Timeout 30 so a
     degraded SCM aborts gracefully with a warning rather than wedging
     the whole inventory pass.

Smoke tested on win11 VM: full Export run with the new code completes in
2.9 s and emits the inventory CSV correctly.
2026-05-24 08:51:30 -04:00
cproudlock
ed12988591 Wax/Trace triad: harden against empty $PSScriptRoot
Tech ran Export-FormtracepakInventory.ps1 from S:\DT\shopfloor\scripts\
waxandtrace\ and the picker fired correctly but Export-Csv failed with
'Cannot bind argument to parameter Path because it is an empty string'.
Root cause: $OutputPath defaulted to $PSScriptRoot and $PSScriptRoot came
through empty in that invocation path (suspected ISE / IEX-style host or
remote wrapper). On a [string] param, $null/empty default coerces to ''
and Join-Path then errors.

Fix in all three triad scripts: resolve a local $scriptDir via a fallback
chain ($PSScriptRoot -> $PSCommandPath -> Get-Location), and use that
instead of $PSScriptRoot for sibling lookups (Select-WaxtraceAsset.ps1,
bay-config.csv).

Export additionally:
- Drops the $OutputPath = $PSScriptRoot param default in favor of the
  same fallback chain.
- Tests / creates $OutputPath BEFORE the 90k-item registry scan so a bad
  output dir surfaces immediately instead of after a long scan.

Smoke tested on win11 VM: explicit -OutputPath '' now resolves to a
writable directory and the CSV writes successfully.
2026-05-24 08:00:00 -04:00
cproudlock
b8bb00e2fe Wax/Trace triad: arrow-key bay picker + S: backup path
Two operator-UX improvements for the Backup / Export / Install triad.

1. Backup target moves from \\tsgwp00525\...\formtracepac to S:\2 WJ Scans
   Record Retention\backup\waxtrace\<asset>\. S: is mapped at shopfloor
   imaging time and stays mapped post-categorization, so the same default
   path works whether the operator runs the backup on an old bay (manual
   pre-image capture) or a freshly imaged one. The destination directory
   is created if missing.

2. New Select-WaxtraceAsset.ps1 - arrow-key bay picker patterned after
   the WinPE select-waxtrace-asset.ps1. Reads bay-config.csv (sibling
   file), shows asset_tag + ftpak_version + model + user_id per row, and
   returns the selected asset_tag via stdout. Falls back to a manual
   entry prompt if the CSV is missing or the operator picks "Other".

   Backup / Export / Install now invoke the picker when interactive AND
   bay-config.csv is alongside the script. Non-interactive paths
   (qga / SYSTEM / scheduled task) keep silently defaulting to
   COMPUTERNAME so unattended runs are unchanged.

   Export gained an -AssetNumber parameter and stamps it into the output
   CSV filename so multiple inventories from the same host stay
   distinguishable when the operator is auditing several bays in a row.

bay-config.csv is copied into the scripts\ dir so the picker has a
source of truth that ships next to the scripts (and into pxe-images
for tech distribution).

Smoke tested on win11 VM: all four PS1 parse-clean, non-interactive
backup path still produces a valid ZIP (silent COMPUTERNAME default),
picker handles missing-CSV gracefully (manual-entry fallback). The
arrow-key UX itself is operator-verifiable only on a real terminal.
2026-05-24 07:41:25 -04:00
cproudlock
a104cfdebb Wax/Trace triad: fix registry corruption + cover v6.213 vendor install path
Three fixes in Backup / Export / Install, validated end-to-end on the win11 VM
against a seeded HKCU\SOFTWARE\Mitutoyo\Formtracepak key carrying all five
registry value types (String, DWord, ExpandString, MultiString, Binary).

1. Registry corruption on REG_BINARY / REG_MULTI_SZ restore
   Backup wrote those values to registry_values.csv via [string]$val, which
   lossily coerces a byte[] to "System.Byte[]" and a string[] to a
   space-joined scalar. Install's CSV restore loop runs AFTER the .reg file
   import (which is lossless), so the CSV pass overwrites the good values
   with corrupted strings. Two-part fix:
   - Backup: skip Binary / MultiString / None / Unknown when writing the CSV.
     Only String, ExpandString, DWord, QWord roundtrip cleanly through
     New-ItemProperty -PropertyType, so capture only those. The .reg file
     remains authoritative for the rest.
   - Install: defensive filter on the CSV restore loop that skips any row
     whose Type is not in {String, ExpandString, DWord, QWord}. This catches
     legacy CSVs already on the share that were taken before this fix.

2. v6.213 vendor install path not scanned / not restored to
   The per-bay FormTracePak install (commit 54dddaa) lands under
   C:\Program Files (x86)\MitutoyoApp\Formtracepak, but the search-path
   lists in Backup + Export only covered C:\...\Mitutoyo (no MitutoyoApp).
   Result: a backup taken on a freshly imaged v6.213 bay produced Config
   Files = 0 because the script never looked at the actual install dir.
   Added MitutoyoApp (x86 + native ProgramFiles) ahead of the legacy
   paths in all three scripts.

3. Install $DefaultAppTargets fallback didn't include MitutoyoApp either,
   so a restore from an OLDER bay (source path C:\Mitutoyo\...) onto a
   freshly imaged v6.213 bay would fall back to ProgramFiles\Mitutoyo
   (does not exist), miss the MitutoyoApp\Formtracepak tree, and write
   the restored files into the first existing legacy path. Added the
   MitutoyoApp entries at the top of the ordered fallback table.

Smoke tested on win11 VM: backup of all 5 reg types, then corrupt every
value, then Install -RestoreAll restores all 5 byte-exact (incl. REG_BINARY
DE-AD-BE-EF-CA-FE-BA-BE-01-02-03-04 and REG_MULTI_SZ alpha.smp/beta.smp/
gamma.smp). Verified legacy poison-CSV path triggers the defensive filter
and the .reg-imported values survive untouched. -DryRun confirmed
non-mutating. Idempotency confirmed via hash-skip.
2026-05-24 07:29:27 -04:00
cproudlock
b57ba0fb6f webapp: add CSRF token to imaging Clear-all form
The dashboard Clear-all button posts to /imaging/delete-all but the form
was missing the hidden _csrf_token input that the rest of the webapp's
POST forms include, so the endpoint would reject the request when CSRF
enforcement is active.
2026-05-24 07:04:20 -04:00
cproudlock
54dddaa760 Wax/Trace: per-bay FormTracePak version via bay-config.csv
Bays span 7 FormTracePak versions (5.510 - 6.213) and 3 sub-versions
(AVANT / CV-4500 / CV-3200), each with a unique licensing USER ID. Previously
all bays got v6.213 with no model/USER hint to the tech.

- bay-config.csv: 15 rows mapping asset_tag to ftpak_version + model + user_id.
- resolve-bay-config.ps1: WinPE-runnable resolver. Looks up the asset and
  writes version.txt / model.txt / userid.txt / bay-info.txt under
  W:\Enrollment\waxtrace\.
- startnet.cmd: xcopy WaxTrace bundle minus formtracepak\, invoke the
  resolver with %MACHINENUM%, then cherry-pick only the matching
  FORMTRACEPAK-V<ver>.iso (~2 GB local vs ~12 GB if all were staged).
- 09-Setup-WaxAndTrace.ps1: read the per-bay files, mount the right ISO,
  drop <asset>-FTPak-install-info.txt on SupportUser's desktop, and print
  a banner with MODEL + USER ID so the tech has them top-of-mind when
  Setup.exe dialogs come up.
- sync-waxtrace.sh: loop over all FORMTRACEPAK-V*.iso instead of hard-coding
  v6.213; also push bay-config.csv + resolve-bay-config.ps1 to the share.
2026-05-24 07:04:15 -04:00
cproudlock
00d4105956 04-SetControllerNicIP: broaden adapter enum + store-mismatch fix + netsh fallback
- Drop -Physical + MediaType filter from Get-NetAdapter; some OEM driver
  stacks report HardwareInterface=False or localize MediaType, hiding the
  Realtek controller NIC from the previous query.
- Refine corp-vs-controller classification: skip only if a gateway is set
  AND it's not 192.168.1.x, OR if the IP looks corp (10.x / 172.16-31.x).
  Keep candidates that are unconfigured, link-local, or 192.168.x.
- Disable DHCP in both PersistentStore and ActiveStore before New-NetIPAddress
  to avoid "Inconsistent parameters PolicyStore PersistentStore and Dhcp
  enabled" failures.
- Fall back to netsh interface ip set address when the PS cmdlets still
  fight each other; netsh writes both stores cleanly.
2026-05-24 07:04:02 -04:00
cproudlock
86fbc132dd GE-Enforce: backfill Keyence pc-subtype.txt from installed ProductCode
Pre-2026-05 Keyence images didn't write pc-subtype.txt via startnet.cmd. Without
a subtype the share manifest's per-model PCTypes gate falls back to the default
(VR-6000), causing the wrong model to install on VR-3000 / VR-5000 boxes.
Detect the installed VR-3000/5000/6000 by its uninstall ProductCode and
persist the subtype so subsequent GE-Enforce cycles + the share manifest gate
route correctly.
2026-05-24 07:03:54 -04:00
cproudlock
4015adeb33 utilities/waxtrace-recovery: ship cal diagnostic + repair scripts
Pair of operational tools used when a wax/trace bay's cal apply fails
(218-378-13 series cal Setup.exe crash, or any future variant). Were
living in /home/camp/pxe-images/ on the workstation; promoting to the
repo so they ship with the codebase, get version-controlled, and can
be pushed onto each PXE server's enrollment share via the standard
sync flow.

debug-waxtrace-cal.ps1 (+ .bat launcher):
- 9-section forensic walkthrough that runs on the bay as admin.
- Autodetects FormTracePak install location, dumps data/ dir contents,
  finds + mounts the per-asset cal ISO, lists its contents, checks the
  on-disk 09-Setup-WaxAndTrace.ps1 for the direct-copy bypass marker,
  greps the imaging-time log for cal lines, pulls the last 24h of
  .NET Runtime / Application Error / WER events related to Setup.exe.
- Output: C:\Logs\WaxTrace\debug-waxtrace-cal.log

fix-waxtrace-cal.ps1 (+ .bat launcher):
- Idempotent recovery: mounts the bay's cal ISO, unconditionally copies
  data\* into FormTracePak's data dir, renames any filename containing
  ' _' (space-underscore) to drop the embedded space, clears read-only,
  dismounts. Works on both 218-378-13 (broken filenames) and 218-458A
  (clean filenames) since the rename is a no-op when no space is
  present. Bypasses the buggy vendor cal Setup.exe entirely.
- Output: C:\Logs\WaxTrace\fix-waxtrace-cal.log

Both already pushed to \\172.16.9.1\enrollment\tools\ on both PXE
servers earlier today; this commit lands them in the repo as the
source of truth so future PXE server builds + ad-hoc rsyncs pick
them up automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:10:23 -04:00
cproudlock
de3018512a 09-Setup-WaxAndTrace: fix broken-filename detection (Get-ChildItem -Filter)
The direct-copy bypass added earlier was never firing - Get-ChildItem -Filter
uses Win32 filename filtering and does NOT honor PowerShell wildcards or
character classes. The previous detection filter '*[0-9] _*.txt' matched
literal bracket-zero-through-nine text, which never appears in any
filename. $hasBrokenFilenames was therefore always False, and every
218-378-13 series cal apply fell through to the vendor setup.exe which
crashes with System.ArgumentException (exit -532462766).

Confirmed via debug-waxtrace-cal.ps1 log on WJF00159: section 6 reports
the on-disk script has the direct-copy fix, section 7 shows the actual
runtime log line 'running cal Setup.exe' followed by exit -532462766.
The "fix" was never executing because the gate was broken.

Replace -Filter with Get-ChildItem -File + Where-Object regex match on
' _\d+\.txt$' which catches the actual buggy filename pattern (space-
underscore-digits-.txt at end of name) regardless of probe series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:09:10 -04:00
cproudlock
7f93347f74 CMM: park DODA entry under _pending_doda_entry until binary arrives
Removed the placeholder DODA entry from Applications so a bay imaged
with the 'With DODA' submenu choice today does not log a 'Installer
not found: DODA-PLACEHOLDER.exe' error per cycle. Wiring is otherwise
unchanged: startnet.cmd still offers the With-DODA submenu, pc-subtype.txt
is still written as 'doda', and 09-Setup-CMM.ps1 still passes PCSubType
through to Install-FromManifest. When the DODA installer is sourced,
move the entry from _pending_doda_entry back into Applications (engine
ignores any top-level field other than Version + Applications).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:59:05 -04:00
cproudlock
548d85fed5 CMM: subtype gating + relax exact-version + conditional cleanup + DODA placeholder
Carries over the lessons learned from wax/trace + Keyence imaging today
and threads the same pattern through the CMM path.

09-Setup-CMM.ps1:
- Pass PCType + PCSubType to Install-FromManifest so the manifest's
  per-entry PCTypes filter is honored. Without this every entry runs
  regardless of bay variant - the same bug Keyence had before per-model
  gating was added.
- Move bootstrap cleanup to a conditional that only deletes
  C:\CMM-Install once every (filter-applicable) manifest entry detects
  as installed. If a Hexagon installer forces an unplanned reboot
  mid-install, the new Run-ShopfloorSetup self-resume RunOnce fires on
  the next auto-login; the staging dir needs to still be on disk for
  the re-run to recover. Logs "retained ... not all entries installed
  yet - will retry on next self-resumed run" when partial.

cmm-manifest.json:
- Drop exact DetectionValue from PC-DMIS 2016, PC-DMIS 2019 R2, CLM
  1.8.73, and goCMM. Detection is now uninstall-key presence only, so
  a Hexagon security patch that bumps the DisplayVersion does not
  trigger a re-install loop with exit 1638 every GE-Enforce cycle.
  Bumping the installer in apps/ is the upgrade path - manifest engine
  detection should not also be a version drift catcher for vendor MSIs
  whose backward-compat is established by the vendor.
- Specific to goCMM: the installer filename version (1.1.6718.31289)
  does not match what the installer registers under its uninstall key.
  Dropping DetectionValue silences the false-mismatch loop the prior
  version would have triggered.
- Add DODA placeholder entry gated to PCTypes=["cmm-doda"]. Real
  Installer filename, args, and DetectionPath still TODO once the
  DODA binary is sourced + dropped at installers-post/cmm/.

startnet.cmd:
- Add :cmm_submenu after the user picks gea-shopfloor-cmm from the
  main menu. Two options: Standard (default PC-DMIS + CLM + goCMM
  + Protect Viewer) or With DODA. Mirrors :keyence_submenu pattern.
- Write CMMVARIANT to W:\Enrollment\pc-subtype.txt so Install-FromManifest
  on the bay can apply the PCTypes filter against gea-shopfloor-cmm-doda.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:48:16 -04:00
cproudlock
45f39fd431 startnet.cmd: suppress 'System error 85' on duplicate Y: mount
The :prompt_waxtrace_asset picker block maps Y: to the enrollment share
so select-waxtrace-asset.ps1 can read INDEX.csv. The later :skip_machinenum
block also runs `net use Y: \\172.16.9.1\enrollment ...` to keep the share
mounted through the rest of imaging, but Y: is already mapped at that
point so the second net use throws "System error 85: The local device
name is already in use". Harmless (script proceeds and "the command
completed successfully" prints right after) but visible noise to the
operator during PXE imaging.

Gate the second net use behind `if exist Y:\` so we only map when Y: is
not already mounted. Same end state, no error message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:38:34 -04:00
cproudlock
b86b830568 Run-ShopfloorSetup: self-resume RunOnce + top up AutoLogonCount
Imaging chain stalled on WJF00159 after FormTracePak Setup.exe forced
a reboot: SupportUser auto-logged in fine, briefly flashed something
(an HKLM\Run logon hook), then idle - no resume of Run-ShopfloorSetup.
Confirmed via diag-dispatcher.ps1: bay had AutoLogon working, RunOnce
empty, no Stage-Dispatcher.ps1 on disk, no setup-stage.txt.

Root cause: Run-ShopfloorSetup launches once from unattend XML's
FirstLogonCommands and has no self-resume mechanism. If anything cuts
it off mid-flight (FormTracePak Setup, eDNC MSI, Oracle install with
forced reboot, etc) the chain dies and nothing brings it back.
Stage-Dispatcher.ps1 in the repo is academic infrastructure that was
never wired into the live flow - startnet.cmd does not stage it and
nothing creates setup-stage.txt.

Fix: have Run-ShopfloorSetup register ITSELF as RunOnce at the top of
the script. The script is idempotent throughout (detection checks
skip already-done work) so re-entry post-reboot picks up cleanly.
Normal completion path removes the RunOnce so it does not re-fire
after the planned end-of-script reboot.

Also top up AutoLogonCount to 10 at script start. The unattend XML's
LogonCount=7 budget gets consumed across typical imaging reboots
(Office, Oracle, FormTracePak, Run-ShopfloorSetup explicit, sync-intune)
and an unplanned FormTracePak forced reboot pushes the counter past 0,
clearing AutoAdminLogon and parking the bay at the login screen.
Restoring the budget every Run-ShopfloorSetup entry keeps SupportUser
auto-logging in across any number of forced reboots until normal
completion. Lockdown's Autologon.exe sets its own AutoAdminLogon for
the ShopFloor user when it runs, so the post-completion natural
decrement to 0 does not affect the final state.

Verified via diag-dispatcher.ps1 capture on WJF00159 today - that bay
had AutoLogonCount=4 and no Stage-Dispatcher.ps1 on disk, which both
match this root cause + fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:17:07 -04:00
cproudlock
a22d2f0313 Stage-Dispatcher: top up AutoLogonCount during shopfloor-setup stage
Unattend XML sets LogonCount=7 for SupportUser autologon. Each interactive
login decrements the counter; at 0 Windows clears AutoAdminLogon and the
bay parks at the login screen with no one to fire RunOnce -> dispatcher
never re-runs -> imaging chain stalls.

Typical imaging burns several logons: Windows OOBE first logon,
post-Office reboot, Oracle install reboot, FormTracePak Setup.exe forced
reboot, Run-ShopfloorSetup's own shutdown /r at end of script, stage
advances. The unplanned FormTracePak reboot pushes the count past 0 on
some bays - the exact failure mode the WJF00159 imaging today hit, where
Stage-Dispatcher.ps1 had the new defensive RunOnce-re-register fix
landed but the dispatcher still never re-fired because there was no
auto-logon to trigger it.

Top up AutoLogonCount to 10 every time the dispatcher hits the
'shopfloor-setup' stage. Restores the autologon budget across any
vendor-forced reboots that fire during the stage. When sync-intune
finishes the whole pipeline, AutoLogonCount is left to decrement
naturally; by then lockdown's Autologon.exe has set its own
AutoAdminLogon for the ShopFloor user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:59:22 -04:00
cproudlock
44554b95b0 gea-shopfloor-waxtrace: stage Mitutoyo Backup/Install/Export triad
User received three PowerShell scripts from a Mitutoyo source for
backing up + restoring FormTracePak settings per asset:

  Export-FormtracepakInventory.ps1 - audit: enumerate files + reg keys
  Backup-FormtracepakSettings.ps1  - capture: config + data + reg into
                                     timestamped ZIP, manifest-driven
  Install-FormtracepakSettings.ps1 - restore: replay ZIP to a new bay,
                                     hash-skip identicals, backup
                                     existing as .pre_restore_bak

Cleanup pass over the vendor-shipped versions:
- Strip Unicode box-drawing characters from banners (ASCII-only policy)
- Install: switch to [ordered]@{} for DefaultAppTargets/DefaultDataTargets
  so fallback priority is deterministic
- Install: add -AssetNumber gate that defaults to per-asset SFLD path
  \\tsgwp00525...\Shopfloor\backup\formtracepac\<AssetNumber>
- Install: timestamp the .pre_restore_bak filename so re-runs don't
  clobber the previous backup
- Install: handle BackupPath being a directory containing
  formtracepak_backup_*.zip files (picks newest)
- .bat launchers for each PS1 (bypass execution policy, double-click)

Not yet wired into 09-Setup-WaxAndTrace.ps1; pending reference-backup
capture from a known-good bay before promoting to imaging path. Today
the V6.213 vendor MSI install + per-asset cal ISO still handle the
imaging-time setup directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:39:24 -04:00
cproudlock
02499cf74b docs: post-deploy checklist + COM 2/4 PCIe serial-port flowchart update
New post-deploy-checklist.md (+ live.html + static.html siblings):
- Four-section terse coach checklist run after a freshly imaged PC
  reaches the login screen. Pairs with the existing post-deploy debug
  flowchart for failure paths.
- Sections: Common Shop Floor opens + connects, controller comms
  (ping 192.168.1.1 / NTLARS General+FMS), UDC COM port matches
  physical socket + no machine-comm error dialog + Tools > Retry
  Connection succeeds, printers ready (Genspect-specific note).

post-deploy-debug-flowchart.md:
- COM port mapping: PCIe add-in card now listed as 'COM 2 or COM 4'
  (Windows enumeration varies by hardware) instead of fixed COM 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:39:15 -04:00
cproudlock
27045d5e4a gea-shopfloor-collections: controller NIC auto-IP + credential break-glass
04-SetControllerNicIP.ps1 (imaging-time, runs once via Run-ShopfloorSetup):
- Finds the Realtek physical Ethernet adapter (controller NIC on every
  collections bay; corp LAN is Intel)
- Skips any candidate with a DHCP default gateway (that one is the corp
  LAN, not the controller)
- Skips any candidate already on 192.168.1.2
- Sets static 192.168.1.2/24, no gateway, clears DNS - matches the
  manual procedure documented in post-deploy-debug-flowchart.md section 2B
- Refuses to guess when multiple Realtek NICs remain ambiguous
- Imaging-time only, not enforced via GE-Enforce so the tech can override
  on a specific bay if needed without the drift-catcher reverting

Set-ControllerCredential.ps1 + manifest-entry-controller-credential.json:
- Break-glass cmdkey /add for the controller SMB share (\\192.168.1.1\md1
  used by DNC). Scoped to the 12 Okuma LOC650 machine numbers (3201-3212).
- Manifest entry is detection-less so it runs every enforce cycle if the
  script is armed (.ps1 extension); disarmed by default (.ps1.bak on the
  share) so a coach can rename when a bay loses its credential without
  the enforcer overwriting per-bay deviations between events.
- Smoke-tested end-to-end on win11 VM via QGA: SYSTEM context cmdkey /add
  succeeds, cmdkey /list shows the entry. DNC service runs as LocalSystem
  so SYSTEM vault is the right target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:38:59 -04:00
cproudlock
1f60c86ec8 Shopfloor: auto-register 9999-placeholder machine number prompt
If a bay is imaged with the 9999 placeholder (tech leaves the WinPE
prompt blank or types 9999), the lockdown+auto-login chain ends up at
the ShopFloor user with no real machine number. We had Check-MachineNumber.ps1
written - InputBox + Update-MachineNumber pulls per-machine NTLARS .reg
+ udc_settings_<N>.json from the SFLD share - but it only got registered
when a tech manually ran Configure-PC + toggled item 6. Fresh 9999 bays
never got the prompt, leaving the bay stuck on placeholder values until
someone noticed.

New Register-CheckMachineNumberTask.ps1 auto-registers the logon task
at imaging time. Gated on C:\Enrollment\machine-number.txt == 9999;
bays imaged with a real number never get the task (and any stale task
from a prior 9999-imaging on the same disk is cleaned up).

Wired into Run-ShopfloorSetup.ps1 right after the S: drive logon mapper
register. Skipped for self-contained types (display kiosks have no
machine number).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:38:48 -04:00
cproudlock
44d2f0afd5 Stage-Dispatcher: re-register RunOnce BEFORE calling Run-ShopfloorSetup
Setup chains we do not control (FormTracePak Setup.exe, eDNC MSI, any
vendor installer that forces an immediate reboot) cut & $script off
mid-flight. Without this, the dispatcher never returns from the call
and the post-call Register-NextRun never fires, leaving the next boot
with no RunOnce + a stalled imaging chain. Observed today on WJF00159
where the FormTracePak v6.213 Setup.exe rebooted the bay before the
dispatcher could advance the stage.

Register the dispatcher's own RunOnce defensively before invoking the
sub-script. If a reboot interrupts the call, the next boot re-fires the
same dispatcher, which re-reads the still-'shopfloor-setup' stage file,
re-runs Run-ShopfloorSetup (every step is idempotent + detects already-
installed state), and converges. The existing post-call Register-NextRun
+ stage advance still run on the happy path - cheap, idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:38:39 -04:00
cproudlock
5891a1966f Wax/Trace: heal 218-378-13 cal disc filename bug + VC++ 2017 + picker
09-Setup-WaxAndTrace.ps1 Step 3:
- Detect Mitutoyo's burn-time typo on 218-378-13 series cal discs
  (filenames carry a trailing space inside the probe ID component,
  e.g. "Linear_X_218-378-13 _100072210.txt"). Their own .NET Setup.exe
  calls FileSystemInfo.set_Attributes on the source path and throws
  System.ArgumentException because the path contains an embedded space
  component, crashing every cal apply on 218-378-13 bays (exit
  -532462766 = 0xE0434352, .NET unhandled exception). Confirmed via
  WER Event 1026 captured during today's WJF00159 imaging.
- When the buggy filenames are detected, bypass the broken vendor
  Setup.exe and direct-copy data\*.* into
  C:\Program Files (x86)\MitutoyoApp\Formtracepak\data\, renaming
  each file to strip ' _' (space-underscore) -> '_'. Clear read-only
  attr on each landed file. Older 218-458A discs have clean filenames
  and still use the vendor Setup.exe path.

waxtrace-manifest.json:
- Drop DetectionValue=v14.15.26706 from both VC++ 2017 redist entries.
  Windows Update routinely bumps the VS14 runtime to 14.16+ / 14.3x+,
  the older Mitutoyo redist refuses to install over the newer (exit
  1638 'Another version already installed') and the manifest engine
  marked it as failed even though the runtime was fine. Detection is
  now by registry-key+name presence, which any VC++ 2015-2022 redist
  satisfies (they are backward-compatible).

startnet.cmd:prompt_waxtrace_asset:
- Replace free-text input with select-waxtrace-asset.ps1 arrow-key
  picker driven from installers-post/waxtrace/calibrations/INDEX.csv.
- Map Y: enrollment share early so the picker can read INDEX.csv.
- Replace parens-in-parens block (echo of '(e.g. WJRP2335)' inside
  the if-paren caused 'to was unexpected at this time' parse error
  observed by tech mid-imaging) with goto-flow.
- Fall back to free-text prompt if picker unavailable or operator
  presses Esc.

select-waxtrace-asset.ps1:
- Sort bays descending by asset tag so WJRP* lands at top of menu.
- Also staged as gea-shopfloor-waxtrace/select-waxtrace-asset.ps1 so
  sync-waxtrace.sh ships it to installers-post/waxtrace/ on the share.

sync-waxtrace.sh:
- Push select-waxtrace-asset.ps1 next to INDEX.csv on the share.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:38:19 -04:00
cproudlock
e1ea6b7c62 Wax/Trace: switch baseline to FormTracePak v6.213 vendor install
Replace the V6.0 captured-binary replay (pf-x86-MitutoyoApp.zip +
c-MitutoyoApp.zip + hklm-wow-mitutoyo.reg.gz) with a real vendor install
from FORMTRACEPAK-V6.213.iso mounted via Mount-DiskImage.

09-Setup-WaxAndTrace.ps1:
- Step 2 rewritten: Mount-DiskImage on
  C:\WaxTrace-Install\formtracepak\FORMTRACEPAK-V6.213.iso, run the VB6
  Setup.exe wrapper from the assigned drive letter (DRIVE_CDROM check
  satisfied by virtual mount, no real CD needed), then Dismount.
- Header rewritten: drop captured/ description, note legacy fallback
  remains in the repo (captured-binary/ unchanged) for manual recovery
  if the v6.213 vendor install fails on a bay.

sync-waxtrace.sh:
- Push formtracepak/FORMTRACEPAK-V6.213.iso (2.0 GB) into the bundle
  instead of captured/ payload. Override path via $FTPAK_ISO env var if
  needed (e.g. testing a v6.213 patch ISO).
- Sanity check no longer demands pf-x86-MitutoyoApp.zip; only requires
  prereqs/ + the manifest + dispatcher PS1.

playbook/utilities/convert-cal-iso.sh:
- New helper. Rebuilds a Linux-dd of a multi-session UDF cal disc into
  a clean ISO9660+UDF hybrid that Windows Mount-DiskImage reads. mkisofs
  reads the file via loop-udf, repacks single-session with the asset tag
  as volume label. Run when `file CAL-*.iso` reports "data" (multi-session
  UDF dd produced a Linux-readable but Windows-unreadable container).
  Single-session 218-458A discs from dd are already ISO9660 and don't
  need this.

Verified on win11 VM via qga: V6.213 ISO mounts, Setup.exe locatable.
14 cal ISOs all converted to ISO9660 (md5s refreshed in INDEX.csv),
re-synced to /srv/samba/enrollment/installers-post/waxtrace/calibrations/.
PXE share bundle now 2.0 GB total (V6.213 ISO + 14 cal ISOs + prereqs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:44:42 -04:00
cproudlock
2a0b4885fe Keyence VR-3000 G2: imaging-time FIPS opt-out for .exe.configs
Under Intune-enforced LSA FIPS policy, Profilometer / VRAnalyzer /
VRInspection apps crash at device init when MD5CryptoServiceProvider's
ctor is called to verify the probe EEPROM calibration (see keyence3000.txt
+ .png in pxe-images for the dialog + stack).

Patch each .exe.config under C:\Program Files\KEYENCE\<model>\ with
<runtime><enforceFIPSPolicy enabled="false"/></runtime>. Scope is app-CLR
only; OS-wide Lsa FIPS policy stays enforced. CMMC posture: scoped
exception, non-CUI integrity hash, documented in SSP. Each affected bay's
hostname must be on InfoSec's FIPS-exception list before imaging.

09-Setup-Keyence.ps1 gates the patch behind model=vr3000 only. vr5000 /
vr6000 bays do not auto-apply. Verified on win11 VM via qga: 29 configs
across vr5000+vr6000 layouts (vr3000 install was incomplete on VM),
patched + idempotent on re-run, existing <runtime> children preserved.
Also verified on a real PC: 27 patched + 2 skipped (Keyence pre-shipped
the element in two configs), 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:35:29 -04:00
cproudlock
37357eee43 Shopfloor images: add Wax/Trace + Keyence per-model variants
Wax/Trace (gea-shopfloor-waxtrace):
- captured/ holds master FormTracePak v6.0 state (Program Files reg dump
  gzipped, ARP entries) taken from a win11 VM where the CD-ROM-bound VB6
  wrapper was driven to completion. xcopy + reg-import replays the install
  on real bays without running the wrapper itself.
- 09-Setup-WaxAndTrace.ps1 rewrites the stub: installs prereqs via manifest
  (VC++ 2008/2017 x86+x64, Sentinel HASP), expands the captured zips into
  C:\Program Files (x86)\MitutoyoApp + C:\MitutoyoApp, imports the reg
  hive, then mounts the bay's per-machine cal ISO (matched by asset tag
  in machine-number.txt) and runs its Setup.exe.
- waxtrace-manifest.json lists the 5 prereqs with InstallShield-style
  silent flags verified on the win11 VM.
- sync-waxtrace.sh ships captured-binary/ + prereqs + cal ISOs from
  /home/camp/pxe-images/iso/mitutoyo-cal/ to
  /srv/samba/enrollment/installers-post/waxtrace/ on the PXE box.
- select-waxtrace-asset.ps1 arrow-key bay picker for WinPE (parses
  INDEX.csv from the cal share, offers "Other (new bay)" fallback).
- startnet.cmd: prompt_waxtrace_asset prompt, skip_waxtrace_stage xcopy
  block (mirrors :skip_cmm_stage), machine-number.txt write covers bay
  asset tag (WJRP*).

Keyence (gea-shopfloor-keyence) - now multi-model:
- vr3000/manifest.json + vr5000/manifest.json + vr6000/manifest.json
  (current single-model VR-6000 moved into vr6000/ subdir). Each ships
  the model's MSI silent-install + DetectionPath via ProductCode.
  Big payloads (Data1.cab, Data11.cab) gitignored, staged via
  sync-keyence.sh from /home/camp/pxe-images/iso/keyence/.
- 09-Setup-Keyence.ps1 dispatches by C:\Enrollment\keyence-model.txt
  (written by startnet.cmd in :keyence_submenu) and points
  InstallerRoot at C:\KeyenceInstall\<model>. DXSETUP probe widened
  to all three Program Files paths (VR-3000 G2, VR-5000, VR-6000).
- startnet.cmd: :keyence_submenu picks vr3000/vr5000/vr6000,
  :skip_keyence_stage xcopy block selectively stages chosen model bundle,
  pc-subtype.txt also written = drops directly into existing GE-Enforce
  PCSubType wiring (looks for gea-shopfloor-keyence-<model>\manifest.json
  on the tsgwp00525 share for ongoing enforcement, no dispatcher change
  needed).
- sync-keyence.sh mirrors sync-waxtrace.sh pattern.

Verified silent MSI install for VR-3000 G2 v2.5.0 and VR-5000 v3.3.1 on
the win11 VM 2026-05-18 with /qn /norestart ALLUSERS=1 REBOOT=ReallySuppress
TRANSFORMS=1033.mst. boot.wim on 172.16.9.1 wimupdate'd with the new
startnet.cmd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:34:20 -04:00
cproudlock
3aabd47571 imaging dashboard: add Clear all button + endpoint
New /imaging/delete_all endpoint wipes every per-bay JSON in IMAGING_DIR
via imaging_status.delete_all_sessions(). Template adds "Clear all"
outline-danger button next to the count badge, gated on sessions list
non-empty, with confirm() prompt naming the count.

Deployed via scp + systemctl restart pxe-webapp on 172.16.9.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:58:31 -04:00
cproudlock
7f097013fc BIOS sub-stage: switch to flag-file signaling, move push after W: copies
check-bios.cmd: drop literal `^` from BIOS_STATUS (caret survives quoted
SET so substring search for `->` never matched). Write X:\bios-fired.flag
on flash_done + staged paths so startnet.cmd can detect via if-exist.

startnet.cmd: replace `call set` substring-replace with `if exist
X:\bios-fired.flag`. Move push to after W:\Enrollment xcopy completes
(before Y: cleanup) so dashboard reflects "BIOS firmware update" stage
once file staging is done, matching user mental model of imaging order.

Tested flag-file logic in win11 VM cmd.exe: missing -> SKIPS, present
-> FIRES, removed -> SKIPS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:58:25 -04:00
cproudlock
9108b495c9 startnet.cmd: fix BIOS sub-stage detection (substring-replace trick)
Original used 'echo %BIOS_STATUS% | findstr /C:"->"' to detect whether
check-bios.cmd actually applied a firmware update. The '>' inside the
value (from check-bios writing e.g. 'updated 1.5.0 -> 1.6.0') gets
parsed by cmd.exe as a redirect operator BEFORE the pipe is set up.
Result: echo wrote a file named '1.6.0' in cwd instead of piping to
findstr. findstr saw no input, returned errorlevel=1, block never
fired. Plus a stray file got created in X:\Windows\System32.

Confirmed empirically in the win11 VM with test bat:
 - echo|findstr approach: SKIPS even when '->' present  (bug)
 - substring-replace: FIRES iff '->' present  (correct)

Fix: replace echo/pipe/findstr with a substring-replace test:

  call set "BIOS_STATUS_STRIPPED=%%BIOS_STATUS:->=%%"
  if not "%BIOS_STATUS%"=="%BIOS_STATUS_STRIPPED%" ( ... )

The '>' inside %VAR:->=...% is parsed as part of the substring
substitution token, not as a redirect. Yields true diff only when
the arrow was actually in BIOS_STATUS.

xcopy region was never impacted by the bug because subsequent
if-exist blocks didn't depend on the previous errorlevel state.
But the BIOS sub-stage push to the dashboard was silently broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:50:12 -04:00
cproudlock
d8c64bef2b Add conditional BIOS-update sub-stage on idx=1
winpe-status-push.ps1 now accepts -CurrentStage / -StageIndex
params so callers can override the default "WinPE: PESetup / WIM
apply" string. Backwards compatible.

startnet.cmd: after the existing initial WinPE status push,
inspect $BIOS_STATUS for the "->" marker that check-bios.cmd
writes when an update was actually applied or staged. If present,
fire a second idx=1 push with stage="WinPE: BIOS firmware update -
<status>". No-op for clean "up to date" / "no update in catalog"
runs.

imaging.html: at stage_idx=1 with "bios" in current_stage, swap
friendly label to "Updating BIOS firmware" with a do-NOT-power-off
hint. Bays without firmware updates show the default "Booting from
PXE" label as before.

boot.wim startnet.cmd updated via wimupdate so live PXE clients
pick it up at next boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:26:31 -04:00
cproudlock
76a3ba513c Monitor: drop cert pre-gate + force Report IP after AESFMA connect
Two fixes for the AESFMA swap path:

1. Removed the X509Chain root-thumbprint pre-check. Bay user reported
   "claims connect not yet operational, but i was able to manually
   connect" - meaning the cert IS in LocalMachine\My but
   $chain.Build() returns a partial chain (probably missing an
   intermediate in the local trust store), so our root-thumbprint
   match returned false and Monitor never even tried the netsh
   connect. Letting netsh attempt directly - it's the source of
   truth on whether EAP-TLS auth succeeds. Rate-limited to 30s
   between attempts to avoid log spam when AESFMA truly isn't
   reachable.

2. Bumped post-connect verify sleep 8s -> 15s. WLAN auth + DHCP can
   take longer than 8s on first attempt.

3. New: once Test-AESFMAConnected returns true and INTERNETACCESS
   is deleted, force-run GE_ReportIP_3_v1.EXE /ForceUpdate=True /S
   so the webhook gets the corp-AESFMA IP immediately instead of
   waiting for the next DHCP-change trigger (which may never fire
   if AESFMA was the bay's first 10.x lease). $script:cache.
   ReportIpForced caches the one-shot fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:25:37 -04:00
cproudlock
3385bc87aa Monitor + imaging: per-phase sub-stages within idx=7
Monitor's Get-Snapshot already tracks Phase 1-5 (Intune Registration,
Device Configuration, Software Deployment, Credentials, Lockdown).
The webapp dashboard only saw a single idx=7 push for the entire
post-PPKG / pre-lockdown window, so the friendly label couldn't
reflect "where is this bay actually". Operator looking at the
dashboard had no idea whether to assign category or hit ARTS for
lockdown next.

Monitor now pushes additional idx=7 entries as it crosses Phase
boundaries:
 - On DeviceId capture: "Intune Device ID captured" (existing)
 - On Phase 2 done (SFLD policy delivered = category was assigned):
   "Phase 2 SFLD policy delivered (device configuration)"
 - On Phase 1-4 all complete: "Phases 1-4 complete - ready for
   lockdown (ARTS request)"
 - On lockdown done: idx=8 (existing)

imaging.html maps the stage_string substring to friendly labels:
 - default idx=7         -> "Registered - assign category"
 - 'sfld policy' / 'phase 2' -> "Phase 2 - device configuration"
 - 'credentials' / 'phase 4' -> "Phase 3 / 4 - DSC + credentials"
 - 'ready for lockdown' / 'request lockdown' -> "Ready - request
                              lockdown" (hint: click ARTS request)

Operator now knows exactly when to act vs when to wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:39:20 -04:00
cproudlock
220c5db5b9 webapp: LAPS clear actually removes the password from session JSON
Previous /imaging/<serial>/laps clear path used update_session() to
re-feed state minus laps_password. But update_session MERGES payload
into existing state - it cannot delete a key the existing state
already has. The laps_password persisted on disk across the "clear"
POST, then came back into the page on next reload.

Fix: bypass update_session for the clear case. Read the session JSON
directly, pop laps_password, write via atomic tempfile-rename. Same
write pattern update_session uses for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:33:07 -04:00
cproudlock
8debc4ddb3 imaging: LAPS input always visible, not gated on intune_device_id
Was hiding LAPS QR section until idx=7 pushed with a DeviceId.
Operator couldn't paste a password if Monitor hadn't gotten around
to capturing the DeviceId yet. The QR encoding doesn't depend on
DeviceId - it's just the password being encoded - so the section is
useful any time the bay is past the LAPS reboot.

Drop the {% if s.intune_device_id %} gate. LAPS section now appears
in every expanded tile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:30:48 -04:00
cproudlock
036090348c imaging: persist tile expanded state across page refresh
localStorage-backed set of serials at key 'imaging-expanded'. On
DOMContentLoaded, walk each .imaging-card; if its data-serial is in
the set, set card.open=true. On every <details> toggle, update the
set. Refresh no longer collapses the tile the operator was looking
at.

Per-browser state (localStorage), no server round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:29:45 -04:00
cproudlock
b9f66687ac imaging: search now also matches stage name, stage-N, and status
Extended the data-filter attribute to include:
 - friendly stage label (e.g. "awaiting intune lockdown")
 - "stage-N" token (e.g. type "stage-7" to find idx=7 bays)
 - status string (in_progress / succeeded / failed)

Use cases:
 - Find all bays waiting on lockdown: type "lockdown"
 - Find all bays at the same stage: "stage-7"
 - Find failed bays: "failed"
 - Find succeeded bays: "succeeded"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:28:08 -04:00
cproudlock
65eeead5a0 imaging: collapsible tile + ARTS link + reword stage 7
Tile is now <details>. Always-visible summary:
  - QR (96px)
  - serial / hostname / pctype / machine# / status badge
  - friendly stage label + N/M badge + pct
  - progress bar

Click to expand. Body shows:
  - friendly stage hint
  - Intune device id row with [copy] [set category] [ARTS request]
  - metadata one-liner (started / last / MAC / raw current_stage)
  - error banner (if any)
  - LAPS password QR generator
  - log tail
  - Clear button

ARTS button links to https://arts.dw.geaerospace.net/requests/type
for kicking off a new lockdown request (Intune-side step happens
externally; this is a deep-link for convenience).

Stage 7 wording: "Awaiting Intune lockdown" (was "awaiting category /
lockdown" - confusing when category was already assigned). Hint
explicitly mentions category check for cases where it isn't yet set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:27:35 -04:00
cproudlock
a9a7478d5a imaging: organize tile metadata into deterministic rows
Previously last_updated, MAC, started, Intune device id, and the
raw current_stage string were sprinkled around the card in
hard-to-track positions. Reorganized:

Row 1 (header): serial | hostname | pctype | machine# | status badge
Row 2 (stage):  friendly label + N/M badge | pct% (right)
Row 3:          full-width progress bar
Row 4:          friendly hint (optional)
Row 5:          Intune device id + copy + set-category (optional)
Row 6:          started ... last ... MAC ... raw current_stage

Each row consistent across all cards regardless of which fields
are populated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:16:34 -04:00
cproudlock
1c361e138b imaging: compact tile + search filter + stage 7 label tweak
Tile shrunk for fleet density:
 - QR: 160px -> 96px
 - Drop big h4 for serial, use fs-6 strong instead
 - DeviceId + buttons + MAC + started time consolidated into one
   small grey row instead of three separate sections
 - Progress bar 1.2rem -> 0.7rem
 - mb-4 -> mb-2 between cards
 - card-body py-2 for tighter vertical rhythm

Search:
 - Sticky search input above the card list
 - Filters live on serial, hostname, pctype, machinenumber,
   intune_device_id via lowercase substring match on a data-filter
   attribute
 - Visible-count badge updates as you type ("3/12")
 - Auto-refresh paused while query has text or while input is focused

Stage 7 label: was "assign category" only, now "awaiting category /
lockdown" to reflect that bays past category assignment are still
waiting on the Intune-driven LAPS-prompt reboot before lockdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:14:51 -04:00
cproudlock
ca647cb690 imaging: redesign tile + LAPS persist + 15s refresh
Tile redesign:
 - QR (or placeholder if not yet captured) on the left as a fixed 160px block
 - Right side: header (serial / hostname / pctype / machinenumber / status)
   then stage label as a big h4 with stage badge + % on the same row,
   then full-width progress bar, then friendly stage hint
 - Intune device id row with copy + set-category buttons consolidated
   under the progress section
 - Footer one-liner: started / last / MAC / raw current_stage (small grey)
 - LAPS QR + log tail still expandable below
 - shadow-sm for visual lift, no card-header line splitting

LAPS persist: POST password to /imaging/<serial>/laps so it survives
the dashboard refresh. Auto-renders QR on page load if the session
already has a stored password. Clear button POSTs empty string to
wipe server-side. No more 60s auto-clear - stays until cleared (or
daily server reset).

Refresh: 5s -> 15s. Reduces polling jitter + gives the eye time to
read before page flickers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:11:58 -04:00
cproudlock
5f322d1110 imaging: operator-friendly stage labels per bay card
Was showing the raw push label (e.g. "Run-ShopfloorSetup: handoff to
Monitor-IntuneProgress") which only makes sense if you know the
playbook internals. Added a stage_index -> (label, hint) lookup table:

  1  Booting from PXE                    WinPE loaded
  2  Configuring Windows                 First boot baseline scripts
  3  Installing apps                     09-Setup-<pctype>
  4  Apps installed                      preparing for enrollment
  5  Enrolling in Intune                 PPKG + AAD/Intune join
  6  Waiting on first Intune sync        post-PPKG settle (~120s)
  7  Registered - assign category        idx=7 with QR + set-category btn
  8  Imaging complete                    lockdown applied

Friendly label + one-line hint shown bold, raw stage string shown
underneath in small monospace for techs who want the playbook
breadcrumb. Stage index/total folded into a badge next to the
"Current stage" header so it doesn't need its own column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:08:05 -04:00
cproudlock
520d4aa791 Monitor: fix AESFMA-connected detection + stop retrying once connected
Two bugs causing "AESFMA cert detected, connecting AESFMA..." to log
over and over even after AESFMA is already up:

1. Regex 'SSID\s*:\s*AESFMA.*?State\s*:\s*connected' required SSID
   line BEFORE State line. Actual netsh wlan show interfaces order
   on Win11 is "Name / State / SSID" - State comes FIRST. The non-
   greedy match never succeeded. Always thought AESFMA wasn't
   connected. Refactor to a Test-AESFMAConnected helper that splits
   output into per-adapter blocks and checks SSID + State independently,
   tolerating either order.

2. Added a fast-path at top of the WiFi-swap block: if AESFMA is
   already connected (no help needed from us), just delete
   INTERNETACCESS if still present and flip the cache flag to stop
   running this block. Previously the block only set the flag after a
   successful connect-then-verify-then-delete cycle; if AESFMA was
   already up at first check, the cycle "succeeded" each tick but
   the flag never flipped, producing the log spam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:06:00 -04:00
cproudlock
894305e906 Monitor: drop AESFMA-connected from Phase 1 done; webapp: LAPS endpoint
1. Phase 1 done gate was requiring 'AESFMA WLAN connected' in addition
   to the data-side signals (AAD + Intune + EmTask + baseline). If the
   bay never reached AESFMA (cert never landed, RADIUS unreachable),
   Phase 1 stayed IN PROGRESS forever even though Intune registration
   was actually complete. Reverting to the data-side-only definition.

2. New webapp endpoint POST /imaging/<serial>/laps stores a LAPS
   password in the session JSON so it survives the 5s dashboard
   auto-refresh. Empty body clears the field. Daily reset of the
   server (cron/restart) is the lifetime cap on stored passwords.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:53:05 -04:00
cproudlock
1b7e1bfee4 imaging: pause page auto-refresh while a LAPS QR is showing
meta http-equiv=refresh fires every 5s and reloads the entire page,
wiping the LAPS QR state mid-scan. Replaced the meta tag with a
JS-driven setTimeout(location.reload, 5000) so renderLapsQR() can
clearTimeout it. Reload resumes when the QR is cleared (manual or
60s auto). Multi-bay safety: only resumes if no other bay still has
a QR rendered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:50:24 -04:00
cproudlock
d5398bdd74 imaging: LAPS-password-to-QR generator per bay card
Per-bay <details> section with:
 - Input field for LAPS password (paste from Intune portal manually,
   since deep-link to LAPS blade needs AAD objectId we can't obtain)
 - Make QR button generates a client-side QR from the input
 - QR displayed below at 280px with 4-cell quiet zone
 - Auto-clears input + QR after 60s with live countdown
 - Manual Clear button
 - Enter key on the input also triggers QR generation

Password never POSTs to server, never logged, never persists past the
60s window. Generated using the same qrcode-generator lib already
loaded for the device-id QR. Scan with a USB barcode scanner plugged
into the bay (HID keyboard mode) -> password types into bay login
field. Faster than reading off the Intune portal letter-by-letter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:48:43 -04:00
cproudlock
cdb6655e4a imaging: drop LAPS deep-link, keep only category
LAPS retrieval blade is keyed on AAD object id, not aadDeviceId /
mdmDeviceId. We capture aadDeviceId from dsregcmd; resolving to
objectId would require a Graph API call with Device.Read.All which
we don't have at WJ. Removed the LAPS button - operator goes to
Intune portal manually for LAPS as before.

set-category button stays - aadDeviceId works for that blade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:47:46 -04:00
cproudlock
74ba3d1339 imaging: deep-link buttons for Set Category + LAPS per bay
Two buttons next to the Intune device id on each bay card:
 - "set category" -> portal.azure.us Intune device blade properties
   via aadDeviceId/{deviceId}
 - "LAPS" -> intune.microsoft.us encryptionKeys blade via
   mdmDeviceId/{deviceId}

Both use the dsregcmd DeviceId we already capture - no Graph API
lookup or objectId resolution needed. One click from the dashboard
takes the tech to the right page for category assignment or LAPS
retrieval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:20:10 -04:00
cproudlock
4599c85509 Monitor: strip ANSI escape codes from dsregcmd output before regex
Smoking gun for "Monitor's on-screen QR works but no idx=7 push lands
on the PXE dashboard". Win11's dsregcmd emits ANSI VT100 escape codes
(e.g. \x1B[7mDeviceId\x1B[0m :) around field labels. Captured output
strings then have those codes between "DeviceId" and ":". The strict
regex 'DeviceId\s*:\s*<guid>' fails because \s* doesn't match ANSI
escape chars. $script:cache.DeviceId stays null, idx=7 push never
fires.

Build-QRCodeText was unaffected because it uses Select-String 'DeviceId'
(substring match, tolerates anything in between) then splits on ':'.

Fix: strip ANSI sequences via -replace '\x1B\[[0-9;]*[A-Za-z]', '' before
running the regex. Same pattern covers all CSI sequences dsregcmd uses.
Also force Out-String to get a single string back (was an array of lines
from 2>&1; -match on arrays returns matching elements but $matches
behavior across mixed objects is fragile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:17:05 -04:00
cproudlock
2d75935dfc PostPpkg settle 60s -> 120s
Empirical: a fresh-imaged bay often hasn't finished AAD-join + first
Intune sync by 60s, so the post-PPKG-reboot Monitor instance starts
without DeviceId visible to dsregcmd yet. Doubling the settle to 120s
gives MDM more time to land baseline policies before the reboot,
which means the post-reboot Monitor sees AAD-joined + DeviceId on
first tick and fires idx=7 immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:13:26 -04:00