OpenText / Host Explorer shortcut filenames vary by installed profile (e.g. 'WJ Shopfloor OpenText.lnk', 'WJ Shopfloor.lnk', 'HostExplorer ShopFloor.lnk'). The taskbar-pin path in site-config.json hardcodes 'Shopfloor Tools\WJ Shopfloor.lnk' - mismatches the actual filename so 07-TaskbarLayout silently skips pinning it. Drop OpenText/ShopFloor/HostExplorer pattern moves from 06's categorization regex. Shortcuts stay at the public-desktop top level where the OpenText installer placed them. Tech sees the icon on the desktop, no taskbar pin (the variable filename made the pin unreliable anyway). Other categories (UDC, eDNC, NTLARS, etc with stable filenames) still move into Shopfloor Tools and pin correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
635 lines
26 KiB
PowerShell
635 lines
26 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
|
|
}
|
|
|
|
# Transcript. 06 runs as a SYSTEM scheduled task at every logon via the
|
|
# 'Organize Public Desktop' task, so without a transcript its decisions
|
|
# (what got added, what got suppressed) are invisible. Append mode keeps
|
|
# a history across logons; session header + PID makes individual runs
|
|
# easy to find.
|
|
$transcriptDir = 'C:\Logs\SFLD'
|
|
if (-not (Test-Path $transcriptDir)) {
|
|
try { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } catch {}
|
|
}
|
|
$transcriptPath = Join-Path $transcriptDir '06-OrganizeDesktop.log'
|
|
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
|
Write-Host ""
|
|
Write-Host "================================================================"
|
|
Write-Host "=== 06-OrganizeDesktop session start (PID $PID, $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
|
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
|
Write-Host "================================================================"
|
|
|
|
# ============================================================================
|
|
# Cleanup: remove 'Shopfloor Intune Sync' task after sync completes
|
|
#
|
|
# Monitor-IntuneProgress.ps1 registers that task as BUILTIN\Users + RunLevel
|
|
# Limited (needs a user context to show the QR code / Configure-PC prompt),
|
|
# and a Limited task cannot Unregister-ScheduledTask itself. So when sync
|
|
# completes it just writes C:\Enrollment\sync-complete.txt as a marker and
|
|
# the task stays around firing on every logon as a no-op.
|
|
#
|
|
# 06 runs as SYSTEM via its own sweep task, which DOES have permission to
|
|
# delete any task. When we see the marker, drop the stale task so Task
|
|
# Scheduler doesn't keep the dead entry forever.
|
|
# ============================================================================
|
|
$syncCompleteMarker = 'C:\Enrollment\sync-complete.txt'
|
|
$syncTaskName = 'Shopfloor Intune Sync'
|
|
if (Test-Path -LiteralPath $syncCompleteMarker) {
|
|
$syncTask = Get-ScheduledTask -TaskName $syncTaskName -ErrorAction SilentlyContinue
|
|
if ($syncTask) {
|
|
try {
|
|
Unregister-ScheduledTask -TaskName $syncTaskName -Confirm:$false -ErrorAction Stop
|
|
Write-Host "Removed stale '$syncTaskName' task (sync complete, marker present)."
|
|
} catch {
|
|
Write-Warning "Failed to remove '$syncTaskName' task: $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Load site config + PC profile
|
|
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
|
|
|
$publicDesktop = 'C:\Users\Public\Desktop'
|
|
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
|
$scriptPath = $MyInvocation.MyCommand.Path
|
|
$scriptDir = Split-Path -Parent $scriptPath
|
|
|
|
# Filenames that always stay at the desktop root regardless of classification.
|
|
# Empty right now - every shortcut lives inside a category folder for a
|
|
# clean end-user desktop. Add entries here if a future shortcut needs to
|
|
# stay pinned at root.
|
|
$keepAtRoot = @()
|
|
|
|
# ============================================================================
|
|
# 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', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
|
# OpenText / 'WJ Shopfloor' / 'ShopFloor' shortcuts left on
|
|
# the desktop intentionally. The actual filename varies by
|
|
# OpenText profile (e.g. 'WJ Shopfloor OpenText.lnk') so the
|
|
# taskbar pin path mismatch silently skipped these. Leaving
|
|
# them at the public desktop top level instead.
|
|
'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')
|
|
}
|
|
}
|
|
|
|
# Category folders are created LAZILY - only when we're about to move
|
|
# something into them - so a Display / Wax-Trace PC without Office
|
|
# doesn't get an empty Office\ folder littering the desktop. See
|
|
# Remove-EmptyCategoryFolders at the end of the script for the
|
|
# post-sweep cleanup pass that finishes the job.
|
|
|
|
# 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
|
|
if (-not (Test-Path -LiteralPath $destDir)) {
|
|
try {
|
|
New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null
|
|
} catch {
|
|
Write-Warning "Failed to create category folder '$category' : $_"
|
|
continue
|
|
}
|
|
}
|
|
$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 {
|
|
# App registry - each entry describes how to materialize one shortcut
|
|
# into Shopfloor Tools\. Folder creation is deferred until we know we
|
|
# have at least one app to put in it (lazy creation, so PC types with
|
|
# nothing installed don't get an empty Shopfloor Tools folder).
|
|
#
|
|
# Kind = 'exe' -> build a fresh .lnk from ExePath
|
|
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
|
|
$cfgApps = Get-ProfileValue 'desktopApps'
|
|
|
|
if ($null -ne $cfgApps -and $cfgApps.Count -gt 0) {
|
|
$apps = @($cfgApps | ForEach-Object {
|
|
$entry = @{ Name = $_.name; Kind = $_.kind }
|
|
if ($_.kind -eq 'exe') { $entry.ExePath = $_.exePath }
|
|
if ($_.kind -eq 'existing') { $entry.SourceName = $_.sourceName }
|
|
$entry
|
|
})
|
|
} else {
|
|
$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 = 'exe'; ExePath = 'C:\Program Files (x86)\WJF_Defect_Tracker\Defect_Tracker.exe' }
|
|
)
|
|
}
|
|
|
|
# Lazy folder creation - only create Shopfloor Tools\ the first time
|
|
# we have an app that's actually going to land in it. PC types with
|
|
# nothing installed get no empty folder.
|
|
$ensureToolsDir = {
|
|
if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
|
|
try {
|
|
New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
|
|
return $true
|
|
} catch {
|
|
Write-Warning "Failed to create $shopfloorToolsDir : $_"
|
|
return $false
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
|
|
foreach ($app in $apps) {
|
|
$dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"
|
|
|
|
switch ($app.Kind) {
|
|
'exe' {
|
|
if (Test-Path -LiteralPath $app.ExePath) {
|
|
if (-not (& $ensureToolsDir)) { break }
|
|
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) {
|
|
# Skip if the sweep already moved the source into the
|
|
# destination folder (src == dest -> "cannot overwrite
|
|
# the item with itself").
|
|
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
|
|
(Join-Path $shopfloorToolsDir $app.SourceName)) {
|
|
Write-Host " exists: $($app.Name) (already in Shopfloor Tools)"
|
|
} else {
|
|
if (-not (& $ensureToolsDir)) { break }
|
|
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: Remove empty category folders
|
|
#
|
|
# If the sweep classified nothing into a given category (e.g. no Office
|
|
# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an
|
|
# empty folder cluttering the desktop. The scheduled-task sweep also runs
|
|
# this so categories that go empty between logons self-heal.
|
|
# ============================================================================
|
|
function Remove-EmptyCategoryFolders {
|
|
foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) {
|
|
$dir = Join-Path $publicDesktop $cat
|
|
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
|
$contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
|
|
if ($contents.Count -eq 0) {
|
|
try {
|
|
Remove-Item -LiteralPath $dir -Force -ErrorAction Stop
|
|
Write-Host " removed empty: $cat\"
|
|
} catch {
|
|
Write-Warning "Failed to remove empty $dir : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 4: Edge default browser + startup tabs (delegated to 08)
|
|
#
|
|
# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on
|
|
# the Public Desktop (or in Web Links\ after the sweep). The .url files
|
|
# are delivered by DSC AFTER this initial shopfloor setup, so the first
|
|
# run at imaging time won't find them (falls back to hardcoded URLs,
|
|
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
|
|
# scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
|
|
# the .url files and the next sweep files them into Web Links\, 08
|
|
# picks them up and updates the Edge policy. Self-healing.
|
|
# ============================================================================
|
|
function Invoke-EdgeDefaultBrowser {
|
|
$edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1'
|
|
if (-not (Test-Path -LiteralPath $edgeScript)) {
|
|
Write-Host " 08-EdgeDefaultBrowser.ps1 not found - skipping"
|
|
return
|
|
}
|
|
try {
|
|
& $edgeScript
|
|
} catch {
|
|
Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Scheduled task registration (re-runs 06 at every logon as SYSTEM)
|
|
# ============================================================================
|
|
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 SYSTEM so the script can (a) pass its admin check at the
|
|
# top, (b) write to Public Desktop / ProgramData Start Menu / the
|
|
# Default User profile without Access Denied, and (c) register the
|
|
# next iteration of itself idempotently. Earlier versions used
|
|
# -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon
|
|
# but the script exited immediately on the non-admin check.
|
|
$principal = New-ScheduledTaskPrincipal `
|
|
-UserId 'NT AUTHORITY\SYSTEM' `
|
|
-LogonType ServiceAccount `
|
|
-RunLevel Highest
|
|
|
|
$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 0: create Office shortcuts (if Office is installed) ==="
|
|
# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but
|
|
# doesn't finish streaming until AFTER the first reboot, so the first
|
|
# imaging-time run of 06 finds no Office and this no-ops. On the second
|
|
# logon (after reboot), Office is installed and 05 creates the shortcuts
|
|
# so Phase 1 below can sweep them into the Office\ folder.
|
|
$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1'
|
|
if (Test-Path -LiteralPath $officeScript) {
|
|
try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" }
|
|
} else {
|
|
Write-Host " 05-OfficeShortcuts.ps1 not found - skipping"
|
|
}
|
|
|
|
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: auto-apply startup items from PC profile ==="
|
|
# Creates .lnk files in AllUsers Startup folder based on the profile's
|
|
# startupItems. This runs as SYSTEM at every logon via the sweep scheduled
|
|
# task, so any item the tech removes via Configure-PC would bounce right
|
|
# back unless we remember the removal. startup-overrides.json tracks labels
|
|
# the tech has explicitly suppressed; items in that list are skipped here
|
|
# and re-enabling them via Configure-PC removes them from the list.
|
|
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
|
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
|
|
|
$suppressed = @()
|
|
if (Test-Path -LiteralPath $overridesPath) {
|
|
try {
|
|
$overrides = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
|
if ($overrides.suppressed) { $suppressed = @($overrides.suppressed) }
|
|
} catch {
|
|
Write-Warning " Failed to parse $overridesPath : $_"
|
|
}
|
|
}
|
|
|
|
$cfgStartup = Get-ProfileValue 'startupItems'
|
|
|
|
if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
|
|
if (-not (Test-Path $startupDir)) {
|
|
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
|
|
}
|
|
$edgePath = @(
|
|
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
|
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
|
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
|
|
|
foreach ($si in $cfgStartup) {
|
|
if ($suppressed -contains $si.label) {
|
|
Write-Host " suppressed: $($si.label) (tech disabled via Configure-PC)"
|
|
continue
|
|
}
|
|
$lnkPath = Join-Path $startupDir "$($si.label).lnk"
|
|
# Skip if already exists (idempotent - don't overwrite user changes)
|
|
if (Test-Path -LiteralPath $lnkPath) {
|
|
Write-Host " exists: $($si.label)"
|
|
continue
|
|
}
|
|
switch ($si.type) {
|
|
'exe' {
|
|
if (Test-Path -LiteralPath $si.target) {
|
|
if (New-ShopfloorLnk -Path $lnkPath -Target $si.target) {
|
|
Write-Host " added: $($si.label)"
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($si.label) - target not installed" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
'existing' {
|
|
$src = Find-ExistingLnk $si.sourceLnk
|
|
if ($src) {
|
|
try {
|
|
Copy-Item -LiteralPath $src -Destination $lnkPath -Force
|
|
Write-Host " added: $($si.label)"
|
|
} catch { Write-Warning " failed: $($si.label) - $_" }
|
|
} else {
|
|
Write-Host " skip: $($si.label) - source not found" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
'url' {
|
|
$url = $null
|
|
if ($si.urlKey -and $siteConfig -and $siteConfig.urls) {
|
|
$url = $siteConfig.urls.$($si.urlKey)
|
|
}
|
|
if ($url -and $edgePath) {
|
|
if (New-ShopfloorLnk -Path $lnkPath -Target $edgePath -Arguments "--new-window `"$url`"") {
|
|
Write-Host " added: $($si.label)"
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($si.label) - URL or Edge not available" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Write-Host " (no startup items in profile)"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 4: remove empty category folders ==="
|
|
Remove-EmptyCategoryFolders
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 5: Edge default browser + startup tabs ==="
|
|
Invoke-EdgeDefaultBrowser
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 6: register logon sweeper scheduled task ==="
|
|
Register-SweepScheduledTask -ScriptPath $scriptPath
|
|
|
|
Write-Host ""
|
|
Write-Host "=== 06-OrganizeDesktop session end ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
|
try { Stop-Transcript | Out-Null } catch {}
|
|
exit 0
|