Files
pxe-server/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1
cproudlock 900180cd12 Shopfloor: desktop folder org, taskbar pins, Edge defaults
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>
2026-04-09 14:38:38 -04:00

419 lines
16 KiB
PowerShell

# 06-OrganizeDesktop.ps1 - Single source of truth for the Public Desktop
# layout. Builds three category folders and fills them, so end users see
# a clean desktop instead of 20+ loose icons.
#
# Creates three subfolders under C:\Users\Public\Desktop:
# Office\ - Excel, Word, PowerPoint, Outlook, OneNote, etc.
# Shopfloor Tools\ - UDC, eDNC, HostExplorer, MarkZebra, PC-DMIS, Hexagon,
# WJ Shopfloor, Defect_Tracker, NTLARS, etc.
# Web Links\ - .url shortcuts (internal portals)
#
# Two phases:
# PHASE 1 (Invoke-DesktopSweep): walks everything at the desktop ROOT,
# classifies each .lnk / .url by filename / extension / .lnk target, and
# moves it into the right category folder. Unknown items are LEFT AT
# THE ROOT on purpose. eDNC.lnk and NTLARS.lnk are allowlisted to stay
# at the root - end users click them too often to bury in a folder.
#
# PHASE 2 (Add-ShopfloorToolsApps): materializes specific app shortcuts
# into Shopfloor Tools\ from known .exe paths (UDC, eDNC, NTLARS). For
# MSI-advertised shortcuts that don't have a resolvable target (WJ
# Shopfloor, Defect_Tracker), finds the existing .lnk wherever it lives
# and copies it in. This runs AFTER the sweep so it overwrites any
# stale copies the sweeper may have moved in.
#
# Also registers an "Organize Public Desktop" scheduled task that re-runs
# the sweep (phase 1) at every logon. Phase 2 doesn't re-run - the
# shortcuts it creates are stable and 06 on first run populates them.
#
# 07-TaskbarLayout.ps1 reads the result of this script and writes
# LayoutModification.xml to pin specific shortcuts to the taskbar.
$ErrorActionPreference = 'Continue'
# ============================================================================
# Admin check - writing to C:\Users\Public\Desktop\* and registering
# system-wide scheduled tasks both require elevation. Fail fast if not
# admin so we don't spam half-successful garbage across the output.
# ============================================================================
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host ""
Write-Host "ERROR: 06-OrganizeDesktop.ps1 must run as Administrator." -ForegroundColor Red
Write-Host " Re-run from an elevated PowerShell:" -ForegroundColor Red
Write-Host " Start -> type 'powershell' -> right-click -> Run as administrator" -ForegroundColor Red
Write-Host ""
exit 1
}
$publicDesktop = 'C:\Users\Public\Desktop'
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
$scriptPath = $MyInvocation.MyCommand.Path
# Filenames that always stay at the desktop root regardless of classification.
# End users click these many times a day and an extra folder click is real
# friction. Phase 2 also drops these here as a post-sweep safety net.
$keepAtRoot = @(
'eDNC.lnk',
'NTLARS.lnk'
)
# ============================================================================
# Phase 1: Sweep loose shortcuts at desktop root into category folders
# ============================================================================
function Invoke-DesktopSweep {
param([string]$DesktopPath)
if (-not (Test-Path -LiteralPath $DesktopPath)) {
Write-Host "Public desktop not found at $DesktopPath - skipping sweep."
return
}
# Category definitions. Order matters: first match wins. Each category
# can classify by file Extension, file Name (regex), or .lnk Target
# (regex, resolved via WScript.Shell).
$categories = [ordered]@{
'Office' = @{
Name = @(
'^(Excel|Word|PowerPoint|Outlook|OneNote|Access|Publisher|OneDrive)(\s|$)'
)
Target = @(
'\\(EXCEL|WINWORD|POWERPNT|OUTLOOK|ONENOTE|MSACCESS|MSPUB)\.EXE$',
'\\OneDrive\.exe$'
)
}
'Shopfloor Tools' = @{
Name = @(
'^UDC',
'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
'Host\s*Explorer', 'ShopFloor', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
'OpenText',
'Defect[_\s-]?Tracker',
'MarkZebra', 'Zebra',
'PC-?DMIS',
'Hexagon', 'CLM\s*Tools',
'Blancco',
'Keyence'
)
Target = @(
'\\UDC\.exe$',
'DncMain\.exe$', 'NTLARS\.exe$', 'DNCRemote',
'HostExplorer\.exe$', 'hostex32\.exe$', 'humicon15\.exe$',
'PC-?DMIS',
'Hexagon',
'Defect_Tracker\.application$'
)
}
'Web Links' = @{
Extension = @('.url')
}
}
# Ensure category folders exist
foreach ($cat in $categories.Keys) {
$dir = Join-Path $DesktopPath $cat
if (-not (Test-Path -LiteralPath $dir)) {
try {
New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null
} catch {
Write-Warning "Failed to create category folder '$cat' : $_"
}
}
}
# WScript.Shell for resolving .lnk targets
$shell = $null
try { $shell = New-Object -ComObject WScript.Shell } catch {}
# Only sweep files at the desktop root, never recurse into the category
# folders themselves (that would loop things back).
$items = @(Get-ChildItem -LiteralPath $DesktopPath -File -ErrorAction SilentlyContinue)
$moved = 0
$skipped = 0
foreach ($item in $items) {
# Allowlist check: never sweep items the user wants to keep visible
if ($keepAtRoot -contains $item.Name) {
Write-Host " keep: $($item.Name) (allowlisted)"
continue
}
$category = $null
# 1. Extension match (cheapest)
foreach ($cat in $categories.Keys) {
$exts = $categories[$cat].Extension
if ($exts -and ($exts -contains $item.Extension)) {
$category = $cat
break
}
}
# 2. File name regex match
if (-not $category) {
foreach ($cat in $categories.Keys) {
$pats = $categories[$cat].Name
if (-not $pats) { continue }
foreach ($p in $pats) {
if ($item.BaseName -match $p) {
$category = $cat
break
}
}
if ($category) { break }
}
}
# 3. .lnk target resolution
if (-not $category -and $item.Extension -ieq '.lnk' -and $shell) {
try {
$sc = $shell.CreateShortcut($item.FullName)
$target = $sc.TargetPath
if ($target) {
foreach ($cat in $categories.Keys) {
$pats = $categories[$cat].Target
if (-not $pats) { continue }
foreach ($p in $pats) {
if ($target -match $p) {
$category = $cat
break
}
}
if ($category) { break }
}
}
} catch {}
}
if ($category) {
$destDir = Join-Path $DesktopPath $category
$destPath = Join-Path $destDir $item.Name
try {
Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop
Write-Host " moved: $($item.Name) -> $category\"
$moved++
} catch {
Write-Warning "Failed to move $($item.Name) : $_"
}
} else {
$skipped++
}
}
if ($shell) {
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null } catch {}
}
Write-Host "Sweep complete: $moved moved, $skipped unclassified (left at root)."
}
# ============================================================================
# Phase 2: Materialize specific app shortcuts into Shopfloor Tools\
#
# Runs AFTER the sweep. Creates fresh .lnk files for apps we can target by
# .exe path (UDC, eDNC, NTLARS) and copies existing .lnk files for apps
# that are MSI-advertised and have no resolvable target (WJ Shopfloor,
# Defect_Tracker). Overwrites whatever the sweeper may have moved in -
# this is intentional because fresh shortcuts are more reliable than
# whatever install crud might be floating around.
#
# Each app is CONDITIONAL - if its source isn't present on this PC, the
# entry is silently skipped. Safe to run on all shopfloor PC types.
# ============================================================================
function New-ShopfloorLnk {
param(
[string]$Path,
[string]$Target,
[string]$Arguments = '',
[string]$WorkingDirectory = ''
)
$wsh = New-Object -ComObject WScript.Shell
try {
$sc = $wsh.CreateShortcut($Path)
$sc.TargetPath = $Target
if ($Arguments) { $sc.Arguments = $Arguments }
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
elseif ($Target) {
$parent = Split-Path -Parent $Target
if ($parent) { $sc.WorkingDirectory = $parent }
}
$sc.IconLocation = "$Target,0"
$sc.Save()
return $true
} catch {
Write-Warning "Failed to create $Path : $_"
return $false
} finally {
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
}
}
# Look up an existing .lnk file across the paths where installers / ppkgs
# typically drop them. Used for apps where we don't build the .lnk from
# an .exe (because the target is an MSI-advertised Darwin descriptor).
function Find-ExistingLnk {
param([string]$Filename)
$candidates = @(
(Join-Path $publicDesktop $Filename),
(Join-Path $shopfloorToolsDir $Filename),
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$Filename",
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Shopfloor Tools\$Filename"
)
foreach ($c in $candidates) {
if (Test-Path -LiteralPath $c) { return $c }
}
return $null
}
function Add-ShopfloorToolsApps {
if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
try {
New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
} catch {
Write-Warning "Failed to create $shopfloorToolsDir : $_"
return
}
}
# App registry - each entry describes how to materialize one shortcut
# into Shopfloor Tools\.
#
# Kind = 'exe' -> build a fresh .lnk from ExePath
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
$apps = @(
@{ Name = 'UDC'; Kind = 'exe'; ExePath = 'C:\Program Files\UDC\UDC.exe' }
@{ Name = 'eDNC'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\bin\DncMain.exe' }
@{ Name = 'NTLARS'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\Common\NTLARS.exe' }
@{ Name = 'WJ Shopfloor'; Kind = 'existing'; SourceName = 'WJ Shopfloor.lnk' }
@{ Name = 'Defect_Tracker'; Kind = 'existing'; SourceName = 'Defect_Tracker.lnk' }
)
foreach ($app in $apps) {
$dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"
switch ($app.Kind) {
'exe' {
if (Test-Path -LiteralPath $app.ExePath) {
if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) {
Write-Host " created: $($app.Name) -> $dest"
}
} else {
Write-Host " skip: $($app.Name) - target not installed ($($app.ExePath))" -ForegroundColor DarkGray
}
}
'existing' {
$src = Find-ExistingLnk $app.SourceName
if ($src) {
try {
Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop
Write-Host " copied: $($app.Name) from $src"
} catch {
Write-Warning "Failed to copy $src -> $dest : $_"
}
} else {
Write-Host " skip: $($app.Name) - no source .lnk found" -ForegroundColor DarkGray
}
}
}
}
}
# ============================================================================
# Phase 3: Pin eDNC and NTLARS shortcuts at the desktop root
#
# These are the two shortcuts end users touch most frequently. Allowlisted
# in the sweep, and also force-dropped here (copied from the Shopfloor
# Tools\ versions we just created) so a sweep-and-forget user still ends
# up with them loose at the root.
# ============================================================================
function Update-DesktopRootPins {
foreach ($name in @('eDNC.lnk', 'NTLARS.lnk')) {
$src = Join-Path $shopfloorToolsDir $name
$dst = Join-Path $publicDesktop $name
if (Test-Path -LiteralPath $src) {
try {
Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop
Write-Host " root: $name"
} catch {
Write-Warning "Failed to drop $dst : $_"
}
}
}
}
# ============================================================================
# Scheduled task registration (phase 1 re-run at every logon)
# ============================================================================
function Register-SweepScheduledTask {
param([string]$ScriptPath)
$taskName = 'Organize Public Desktop'
if (-not (Test-Path -LiteralPath $ScriptPath)) {
Write-Warning "Cannot register scheduled task - script not found at $ScriptPath"
return
}
try {
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
# Run as whichever user logs in, at Limited (standard) rights. Public
# desktop is writable by BUILTIN\Users so no elevation needed. Using
# the well-known Users group SID so this works on non-English Windows.
$principal = New-ScheduledTaskPrincipal `
-GroupId 'S-1-5-32-545' `
-RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
# -ErrorAction Stop is required because Register-ScheduledTask emits
# non-terminating errors on failure (Access Denied etc), which try/catch
# would otherwise miss and we'd falsely claim success.
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Trigger $trigger `
-Principal $principal `
-Settings $settings `
-Force `
-ErrorAction Stop | Out-Null
Write-Host "Scheduled task '$taskName' registered (runs at every logon)."
} catch {
Write-Warning "Failed to register scheduled task : $_"
}
}
# ============================================================================
# Main
# ============================================================================
Write-Host ""
Write-Host "=== Phase 1: sweep loose shortcuts into category folders ==="
Invoke-DesktopSweep -DesktopPath $publicDesktop
Write-Host ""
Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ==="
Add-ShopfloorToolsApps
Write-Host ""
Write-Host "=== Phase 3: drop eDNC / NTLARS at desktop root ==="
Update-DesktopRootPins
Write-Host ""
Write-Host "=== Phase 4: register logon sweeper scheduled task ==="
Register-SweepScheduledTask -ScriptPath $scriptPath
exit 0