Three new baseline scripts that run during shopfloor imaging to clean up
the end-user Public Desktop. Before this, Azure AD users logged into a
shopfloor PC and saw 20+ loose shortcuts at the desktop root (Office
apps, OpenText sessions, WJ web portals, DNC utilities, Defect Tracker,
plus .url files for every intranet page) with no organization. End users
couldn't find anything.
06-OrganizeDesktop.ps1 - Single source of truth for Public Desktop layout
Phase 1: sweeps loose shortcuts at the desktop root into three category
folders - Office\, Shopfloor Tools\, Web Links\ - by filename regex,
extension, and .lnk target resolution. Allowlists eDNC.lnk and
NTLARS.lnk to stay at root since end users click them too often.
Unknown items are left at the root on purpose (never delete).
Phase 2: materializes specific app shortcuts into Shopfloor Tools\.
UDC / eDNC / NTLARS are built fresh from their .exe paths; WJ
Shopfloor and Defect_Tracker are MSI-advertised (empty TargetPath,
Darwin descriptor) so we copy the existing .lnk from wherever it
lives via a multi-location lookup. Each entry is conditional on its
source being present - script runs cleanly on PC types without DnC.
Phase 3: drops eDNC.lnk and NTLARS.lnk at desktop root from the
Shopfloor Tools\ copies, so end users have both a folder version
and a quick-access root version.
Phase 4: registers an "Organize Public Desktop" scheduled task that
re-runs phase 1 at every logon. Shortcuts added later by DSC /
Intune / msiexec get filed automatically without another imaging
pass. Admin check at the top, -ErrorAction Stop on Register-
ScheduledTask and directory creation so failures are caught
instead of printing false success.
07-TaskbarLayout.ps1 - Minimal taskbar pinner
Checks which .lnk files 06 created in Shopfloor Tools\, then writes
LayoutModification.xml to the Default User profile with taskbar pins
in order: Edge, WJ Shopfloor, UDC, eDNC, Defect_Tracker. No shortcut
creation in this script - all shortcut management lives in 06.
Missing .lnks are skipped (PC types without DnC just get fewer pins).
Applies on first logon of new user profiles (Azure AD users after
enrollment). Existing profiles don't re-read Default User - Windows
design limitation since 1703, no programmatic fix.
08-EdgeDefaultBrowser.ps1 - Edge as default browser + startup tabs
Motivated by the ppkg installing Chrome alongside Edge: new Azure AD
users hit a "Choose your default app" picker on first URL click
because nothing is marked default. Two layers:
1. dism /Online /Import-DefaultAppAssociations:<xml> writes an XML
with Edge ProgIds for http/https/.htm/.html/.pdf/.svg/.webp into
the Default User profile template. New profiles inherit.
2. HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\
DefaultAssociationsConfiguration registry value (the "Set a
default associations configuration file" GPO) points at the same
XML so Windows re-applies on every logon, catching Windows-update
defaults-reset cases.
Leaves Chrome installed, just not the default URL handler.
Also sets Edge startup tabs via machine-wide policies under
HKLM:\SOFTWARE\Policies\Microsoft\Edge:
RestoreOnStartup = 4 (open specific URLs)
RestoreOnStartupURLs = Plant Apps, WJ Shop Floor Homepage, Shopfloor
Dashboard (tab order per spec)
HomepageLocation = first tab (Plant Apps)
HomepageIsNewTabPage = 0
ShowHomeButton = 1
URLs are resolved dynamically from the .url files on the Public
Desktop (or Web Links\ after the sweep), so if WJDT changes a URL
later the script picks it up without a code change. Fallbacks are
hardcoded for the two portals we have URLs memorized for; Plant Apps
has no fallback and will be skipped if the .url file is missing.
Test workflow: admin-check in all three scripts fails fast on non-
elevated runs instead of spamming half-successful Access Denied output
like the first draft did.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Long debugging round on the shopfloor test PC with several overlapping
bugs. This commit folds all the fixes together.
sync_intune.bat
- Slim down to an elevation thunk that launches a NEW elevated PS
window via Start-Process -Verb RunAs (with -NoExit so the window
doesn't vanish on error). All UI now lives in the PS monitor, not
mixed into the cmd launcher.
- Goto-based control flow. Earlier version had nested if (...) blocks
with literal parens inside echo lines (e.g. "wrappers (Install-eDNC,
...etc)."); cmd parses if-blocks by counting parens character-by-
character, so the ")" in "etc)." closed the outer block early and
the leftover "." threw ". was unexpected at this time.", crashing
the elevated cmd /c window before pause ran.
- Multi-location Monitor-IntuneProgress.ps1 lookup so the user's
quick-test workflow (drop both files on the desktop) works without
manually editing the hardcoded path. Lookup order:
1. %~dp0lib\Monitor-IntuneProgress.ps1
2. %~dp0Monitor-IntuneProgress.ps1
3. C:\Users\SupportUser\Desktop\Monitor-IntuneProgress.ps1
4. C:\Enrollment\shopfloor-setup\Shopfloor\lib\Monitor-IntuneProgress.ps1
- Prints "Launching: <path>" as its first line so you can see which
copy it actually loaded. This caught a bug where a stale desktop
copy was shadowing the canonical file via fallback #2.
Set-MachineNumber.bat
- Same multi-location lookup pattern. Old version used
%~dp0Set-MachineNumber.ps1 and bombed when the bat was copied to
the desktop without its .ps1 sibling.
- Goto-based dispatch, no nested parens, for the same parser reason.
Monitor-IntuneProgress.ps1
- Start-Transcript at the top, writing to C:\Logs\SFLD\ (falls back
to %TEMP% if C:\Logs\SFLD isn't writable yet) with a startup banner
including a timestamp. Every run leaves a captured trace.
- Main polling loop wrapped in try/catch/finally. Unhandled exceptions
print a red report with type, message, position, and stack trace,
then block on Wait-ForAnyKey so the window can't auto-close on a
silent crash.
- Console window resize at startup via $Host.UI.RawUI.WindowSize /
BufferSize, wrapped in try/catch (Windows Terminal ignores it, but
classic conhost honors it).
- Clear-KeyBuffer / Read-SingleKey / Wait-ForAnyKey helpers. Drain any
buffered keystrokes from the polling loop before each prompt so an
accidental keypress can't satisfy a pause prematurely.
- Invoke-SetupComplete / Invoke-RebootPrompt final-state handlers.
The REBOOT REQUIRED branch now shows a yellow 3-line header, a
four-line explanation, and a cyan "Press Y to reboot now, or N to
cancel:" prompt via Read-SingleKey @('Y','N'). Y triggers
Restart-Computer -Force (with shutdown.exe fallback), N falls
through to Wait-ForAnyKey.
- Display order: status table FIRST, QR LAST. The cursor ends below
the QR so the viewport always follows it - keeps the QR on screen
regardless of window height. Works on both classic conhost and
Windows Terminal (neither reliably honors programmatic resize).
- Half-block QR renderer: walks QRCoder's ModuleMatrix directly and
emits U+2580 / U+2584 / U+2588 / space, one output line per two
matrix rows. Halves the rendered height vs AsciiQRCode full-block.
Quiet zone added manually via $pad=4 since QRCoder's ModuleMatrix
doesn't include one. Trade-off: may not be perfectly square on all
fonts, but the user accepted that for the smaller footprint after
multiple iterations comparing full-block vs half-block vs PNG popup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related fixes from a debugging round on the test PC:
1. PreInstall runner: detection-during-install kill is now opt-in via
"KillAfterDetection: true" on JSON entries that need it. Old behavior
killed any installer as soon as its detection passed - which broke
Oracle: Oracle creates its registry key partway through install,
the runner detected it at the 25s poll, killed msiexec mid-install,
and msiserver was still doing rollback when the next install (VC++
2008) started - so VC++ 2008 hit ERROR_INSTALL_ALREADY_RUNNING
(1618). Only UDC needs the detection-kill (its installer spawns a
hidden WPF window and never exits). Other installers exit cleanly
on their own and shouldn't be killed.
2. Track Setup-OpenText scripts in git. The bundled OpenText install
scripts (Setup-OpenText.ps1, Setup-OpenText.cmd, version.txt) live
at runtime in /home/camp/pxe-images/main/dependencies/opentext/
alongside the binary install files (~106 MB of MSI/CAB/MSP/MST plus
profile content). The binaries stay outside git but the script
logic and version stamp are mirrored into playbook/preinstall/
opentext/ here so git history captures changes to the install
logic and version bumps. README.md explains the workflow.
Latest Setup-OpenText.ps1 includes:
- $SourceDir default moved into script body (PowerShell evaluates
param([string]$X = $PSScriptRoot) defaults at parameter-binding
time, when $PSScriptRoot may not yet be populated, so the
default came out as empty string and Join-Path crashed)
- Logging set up FIRST so any startup error gets captured
- REBOOT=ReallySuppress dropped from both msiexec calls (base MSI
and SP1 patch) - OpenText installs shell extensions that hook
explorer.exe, and Restart Manager closes explorer to replace
the shell DLLs. With REBOOT=ReallySuppress, RM closed explorer
but interpreted the relaunch as a "reboot action" and refused
to do it, leaving the user with no desktop. /norestart on its
own prevents the actual Windows reboot but lets RM cleanly
close-and-relaunch explorer mid-install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the runner runs a Type:MSI install it injects /L*v <log> and tails
that log on failure to show what actually went wrong. Type:EXE installs
had no equivalent - if Setup-OpenText.cmd or any other EXE wrapper
failed, the installlog just showed "Exit code 1 - FAILED" with no clue
what happened inside.
Adds an optional LogFile field to JSON entries. When present on a
Type:EXE entry, the runner:
- Logs "Installer log: <path>" before launching the installer
- On failure, tails the last 30 lines of that file into the runner
log (same pattern as the MSI verbose log scan)
Wired up on the OpenText entry to point at C:\Logs\PreInstall\Setup-
OpenText.log (which Setup-OpenText.ps1 already writes itself). Other
EXE entries can opt in by adding their own LogFile field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run-ShopfloorSetup.ps1 line 46-47 does:
Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
foreach ($script in $scripts) { & $script.FullName }
This picks up EVERY *.ps1 in Shopfloor\ and runs it as a baseline
script. Last commit (66d13d8) put Monitor-IntuneProgress.ps1 in that
same directory, which means the dispatcher was running it as the LAST
baseline script (M sorts after 00/04/05). The monitor is an infinite
poll loop that never returns until the SFLD lifecycle is complete -
so the dispatcher hung there forever, and Standard\01-eDNC.ps1 and
Standard\Set-MachineNumber.ps1 never ran.
Symptoms in the test run:
- 00-PreInstall-MachineApps.ps1 ran (10 installed, 1 OpenText fail)
- 04-NetworkAndWinRM.ps1 ran silently
- 05-OfficeShortcuts.ps1 ran silently
- Monitor-IntuneProgress.ps1 started (Clear-Host + status table) and
hung in its main loop
- eDNC + Set-MachineNumber never ran
Fix: move Monitor-IntuneProgress.ps1 into Shopfloor\lib\ so the
dispatcher's non-recursive Get-ChildItem doesn't see it. Update
sync_intune.bat's MONITOR path to the new location, and add a
comment explaining WHY the monitor lives under lib\ to prevent this
mistake from being repeated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the 3-step pass/fail polling that lived in the .bat with a
PowerShell monitor that renders a full status table for the SFLD
enrollment lifecycle and handles the pre-reboot -> reboot -> post-reboot
transition explicitly.
Three structural problems with the old script:
1. Step 3 ("SFLD - Consume Credentials task exists") fired too early.
The task is created by SetupCredentials.log around 08:52 in the
pre-reboot phase, NOT post-reboot, so passing all 3 gates didn't
actually mean "fully done" - it just meant "credential setup ran".
2. No detection of the pre-reboot -> reboot -> post-reboot transition.
The script never read DSCDeployment.log, so it couldn't tell the
user "you need to reboot now to start the install phase". A device
stuck waiting for reboot was indistinguishable from one still
syncing.
3. No visibility into Phase 4 (per-script wrappers like Install-eDNC,
Install-UDC, Install-VCRedists, Install-OpenText). When something
hung you had to manually grep C:\Logs\SFLD\.
New layout:
sync_intune.bat - thin launcher (~50 lines): self-elevate, invoke
Monitor-IntuneProgress.ps1, branch on exit code
(0 = done / 2 = reboot needed / else = error).
Monitor-IntuneProgress.ps1 - the actual monitor (~340 lines):
- 5-phase status table (Identity / SFLD config / DSC deployment +
install / Custom scripts / Final) updated every 30s via Clear-
Host + redraw, with the QR code anchored at the top.
- Phase 4 auto-discovers custom scripts by parsing DSCInstall.log
for "Downloading script: <name>" lines AND scanning C:\Logs\SFLD\
Install-*.log files - so Display PCs running entirely different
scripts surface their own list automatically without hardcoding.
Statuses: pending / running / done / failed (mtime + tail-based).
- Boot-loop-safe reboot detection via Test-RebootState: only signals
'needed' if DSCDeployment.log was modified AFTER LastBootUpTime.
Once we've rebooted past it, just waits for DSCInstall.log.
- Caches monotonic Phase 1 indicators (AzureAdJoined, IntuneEnrolled,
EnterpriseMgmt task) so dsregcmd /status (slow ~1-2s) only runs
until the flag flips true, not on every poll.
- Triggers Intune sync at startup, re-triggers every 3 minutes (was
every 15 seconds in the old loop, which actively interrupted
in-flight CSP work).
Exit codes consumed by sync_intune.bat:
0 - DSCInstall.log shows "Installation completed successfully"
2 - DSCDeployment.log shows "Deployment completed successfully" AND
the deploy log is newer than LastBootUpTime (= reboot needed)
1 - error
Detection markers (decoded from a captured run at /home/camp/pxe-images/
Logs/ - see comment block at top of Monitor-IntuneProgress.ps1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Office Click-to-Run installs the binaries when an Office-bearing ppkg
is selected (e.g. GCCH_Prod_SFLD_StdOffice-x86_*) but doesn't create
desktop shortcuts - operators only see Office in the Start Menu's
Microsoft 365 folder. This baseline script fills that gap.
Self-detects Office by EXE existence at C:\\Program Files\\Microsoft
Office\\root\\Office16\\ or the (x86) equivalent. No Office found =
silent no-op, so it's safe to run on every PC type (Display kiosks,
Wax/Trace, Keyence, etc.) without needing a per-type filter.
Creates Excel.lnk / Word.lnk / PowerPoint.lnk in two places:
- C:\\Users\\Public\\Desktop\\ - visible to all users immediately
- C:\\Users\\Default\\AppData\\Roaming\\Microsoft\\Windows\\Start
Menu\\Programs\\ - inherited by every NEW user profile created
on the device (Azure AD operator logons after enrollment)
Numbered 05- so it runs after 00-PreInstall and 04-NetworkAndWinRM
in the Shopfloor baseline sequence. Idempotent - WScript.Shell's
CreateShortcut overwrites existing .lnks each run.
Outlook / OneNote / Access / Publisher intentionally not shortcutted
(scope decision; can be added by extending the $officeApps array).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related fixes for the desktop helper:
1. Stop hammering the Intune sync trigger every 15 seconds. The old
loop called :do_sync (Start-ScheduledTask on Schedule #3) on every
failed check, which started a fresh CSP pull before the previous
one had time to complete - the Intune engine treats a re-trigger
as "start over" and kills in-flight policy application work, so
nothing ever finished. New cadence: trigger sync once at the start
of each step, then poll every 30 s, only re-trigger every 6 polls
(~3 min). POLL_SECS and RETRIGGER_POLLS are top-of-script knobs.
2. Stop pushing the QR code off the top of the window. The old loop
echoed "Checking again in 15s..." on a new line every iteration,
so after a few minutes the QR code (which contains the device ID
the operator scans) had scrolled out of view. Replaced the per-
iteration echo with a single self-redrawing status line using a
captured CR character (copy /Z trick) and <nul set /p, padded to
clear leftover characters. Important transitions ("Re-triggering
sync...", "[DONE] ...") still print echo. lines so they survive in
the scrollback as permanent history.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three observability fixes that made the VC++ MSI failures actually
debuggable instead of showing "Exit code - FAILED" with an empty
value for every install:
1. Switch from Start-Process -PassThru (without -Wait) to
[System.Diagnostics.Process]::Start() with a ProcessStartInfo.
PowerShell 5.1 has a known bug where Start-Process disposes the
Process object's OS handle when control returns to the script,
so $proc.ExitCode reads as $null even after WaitForExit() - which
was causing every MSI install to be reported as failed regardless
of the actual result.
2. Pass /L*v <log> to msiexec on every MSI install so we get a full
verbose log per app at C:\Logs\PreInstall\msi-<safename>.log.
3. On install failure, scan the verbose log for *meaningful* lines
(Note: 1: <code>, "return value 3", custom action errors, "Failed
to", "Installation failed", common 2xxx error codes) instead of
tailing the last 25 lines, which is rollback/cleanup noise. This
surfaces the actual root-cause line directly in the runner log so
you don't have to dig through C:\Logs to diagnose.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace em-dash characters with plain hyphens across the 5 shopfloor
setup scripts (avoids cp1252 mojibake in .bat files and keeps the
PowerShell sources consistent). Also adds [Parameter(Position=1)] to
Write-PreInstallLog so the Level argument can be passed positionally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a local-install pipeline so Standard shopfloor PCs get Oracle, the
VC++ redists (2008-2022), and UDC installed during PXE imaging via Samba
instead of pulling ~215 MB per device from Azure blob over the corporate
WAN. Intune DSC then verifies (already-installed apps are skipped) and
the only Azure traffic on the happy path is ~11 KB of CustomScripts
wrapper polling.
New files:
- playbook/preinstall/preinstall.json — curated app list with PCTypes
filter and per-app detection rules. Install order puts VC++ 2008
LAST so its (formerly) reboot-triggering bootstrapper doesn't kill
the runner mid-loop. (2008 itself now uses extracted vc_red.msi with
REBOOT=ReallySuppress; the reorder is defense in depth.)
- playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 —
the runner. Numbered 00- so it runs first in the baseline sequence.
Reads preinstall.json, filters by PCTYPE, polls for completion via
detection check (handles UDC's hung WPF process by killing it once
detection passes), uses synchronous WriteThrough logging that
survives hard reboots, preserves log history across runs.
- playbook/shopfloor-setup/Standard/Set-MachineNumber.{ps1,bat} — desktop
helper for SupportUser. Reads current UDC + eDNC machine numbers,
prompts via VB InputBox, validates digits-only, kills running UDC,
edits both C:\ProgramData\UDC\udc_settings.json and HKLM\…\GE Aircraft
Engines\DNC\General\MachineNo, relaunches UDC. Lets a tech assign a
real machine number to a mass-produced PC without admin/LAPS.
- playbook/sync-preinstall.sh — workstation helper to push installer
binaries from /home/camp/pxe-images/main/ to the live PXE Samba.
Changes:
- playbook/startnet.cmd + startnet-template.cmd — add xcopy to stage
preinstall bundle from Y:\preinstall\ to W:\PreInstall\ during the
WinPE imaging phase, gated on PCTYPE being set.
- playbook/pxe_server_setup.yml — create /srv/samba/enrollment/preinstall
+ installers/ directories and deploy preinstall.json there.
- playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 — bump AutoLogonCount
to 99 at start (defense against any installer triggering an immediate
reboot mid-dispatcher; final line still resets to 2 on successful
completion). Copy Set-MachineNumber.{ps1,bat} to SupportUser desktop
on Standard PCs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete 02-OpenTextCSF.ps1 (CSF profile delivery moved to Intune YAML's
CopyFiles section in main/device-config.yaml — no longer needed at the
PXE/baseline layer)
- Strip MarkZebra install + post-config from 01-eDNC.ps1 (no longer
needed; only eDNC core install + Dnc x86→x64 mirror + Site reg + eMxInfo
deployment remain). Section numbering tightened.
- Add SITESELECTED="West Jefferson" to eDNC msiexec args so the MSI's
site-specific Components (NtLarsWjfRegComp — FTP/FMS/PPDCS hosts +
credentials) actually install. Without it, only the bare Site value was
being set and all the connection details were unconfigured.
- gitignore: blanket-block any **/eMxInfo*.txt from being committed —
the file contains obfuscated eDNC site credentials and must never go
in git. Canonical source lives at /home/camp/pxe-images/main/eMxInfo.txt
outside the repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bundles QRCoder.dll (184KB, .NET 4.0) to render the Azure AD device
GUID as a scannable QR code in the console when sync_intune.bat runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AutoLogonCount reduced from 2 to 1 in Run-ShopfloorSetup.ps1
- Remove default pinned Start Menu tiles and set blank layout for future users
- Add sync_intune.bat: triggers MDM sync and polls for SFLD group policies
- Update README.md and SETUP.md with current project state (boot chain, new
scripts, samba shares, webapp pages, commit history)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>