diff --git a/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 b/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 new file mode 100644 index 0000000..0333d62 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/06-OrganizeDesktop.ps1 @@ -0,0 +1,418 @@ +# 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 diff --git a/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 b/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 new file mode 100644 index 0000000..b78d5aa --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/07-TaskbarLayout.ps1 @@ -0,0 +1,140 @@ +# 07-TaskbarLayout.ps1 - Minimal taskbar pinner. +# +# Reads the shortcuts that 06-OrganizeDesktop.ps1 created in +# C:\Users\Public\Desktop\Shopfloor Tools\ and writes a LayoutModification.xml +# that pins them to the taskbar along with Microsoft Edge. +# +# This script does NOT create, move, or copy any shortcuts - all shortcut +# management lives in 06. 07 is the last-mile taskbar config only. +# +# Pin order (left to right): Edge, WJ Shopfloor, UDC, eDNC, Defect_Tracker. +# NTLARS is intentionally not pinned - per the original spec it lives on +# the desktop root only. +# +# LayoutModification.xml is written to the Default User profile shell +# directory, which means the pins apply on FIRST LOGON of any new user +# profile. Existing profiles don't re-read the Default User template, so +# they won't pick up the pins without a manual re-pin or profile delete. +# This is a Windows design limitation - syspin.exe style hacks are the +# only workaround for existing profiles, and they're unsupported. + +$ErrorActionPreference = 'Continue' + +# ============================================================================ +# Admin check - writing to the Default User profile requires elevation. +# ============================================================================ +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + Write-Host "" + Write-Host "ERROR: 07-TaskbarLayout.ps1 must run as Administrator." -ForegroundColor Red + Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red + Write-Host "" + exit 1 +} + +$publicDesktop = 'C:\Users\Public\Desktop' +$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools' +$defaultUserShell = 'C:\Users\Default\AppData\Local\Microsoft\Windows\Shell' +$layoutXmlPath = Join-Path $defaultUserShell 'LayoutModification.xml' + +# ============================================================================ +# Pin list: exact ordered list of shortcut names to pin. Each entry points +# at a .lnk file via a Windows environment-variable-expanded path. Edge is +# special-cased to its default Start Menu location since Windows maintains +# it; all other entries reference C:\Users\Public\Desktop\Shopfloor Tools\ +# which 06-OrganizeDesktop.ps1 populates. +# ============================================================================ +$pinSpec = @( + @{ + Name = 'Microsoft Edge' + Path = '%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk' + # Resolved literal path used to check existence (can't Test-Path an + # unexpanded env var reliably on older PS versions) + Literal = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk' + } + @{ + Name = 'WJ Shopfloor' + Path = '%PUBLIC%\Desktop\Shopfloor Tools\WJ Shopfloor.lnk' + Literal = (Join-Path $shopfloorToolsDir 'WJ Shopfloor.lnk') + } + @{ + Name = 'UDC' + Path = '%PUBLIC%\Desktop\Shopfloor Tools\UDC.lnk' + Literal = (Join-Path $shopfloorToolsDir 'UDC.lnk') + } + @{ + Name = 'eDNC' + Path = '%PUBLIC%\Desktop\Shopfloor Tools\eDNC.lnk' + Literal = (Join-Path $shopfloorToolsDir 'eDNC.lnk') + } + @{ + Name = 'Defect_Tracker' + Path = '%PUBLIC%\Desktop\Shopfloor Tools\Defect_Tracker.lnk' + Literal = (Join-Path $shopfloorToolsDir 'Defect_Tracker.lnk') + } +) + +# ============================================================================ +# Build the pin list - skip any whose .lnk is missing +# ============================================================================ +Write-Host "" +Write-Host "Checking which Shopfloor Tools shortcuts exist..." + +$pinPaths = @() +foreach ($pin in $pinSpec) { + if (Test-Path -LiteralPath $pin.Literal) { + Write-Host " pin: $($pin.Name) -> $($pin.Literal)" + $pinPaths += $pin.Path + } else { + Write-Host " skip: $($pin.Name) - not found at $($pin.Literal)" -ForegroundColor DarkGray + } +} + +if ($pinPaths.Count -eq 0) { + Write-Warning "No pins to apply (nothing found in Shopfloor Tools\). Did 06-OrganizeDesktop.ps1 run first?" + exit 0 +} + +# ============================================================================ +# Write LayoutModification.xml +# ============================================================================ +$pinXml = ($pinPaths | ForEach-Object { + " " +}) -join "`r`n" + +$layoutXml = @" + + + + + +$pinXml + + + + +"@ + +try { + if (-not (Test-Path -LiteralPath $defaultUserShell)) { + New-Item -ItemType Directory -Path $defaultUserShell -Force -ErrorAction Stop | Out-Null + } + # -ErrorAction Stop so Access Denied becomes a catchable terminating error + # instead of silently falling through to the success message. + Set-Content -LiteralPath $layoutXmlPath -Value $layoutXml -Encoding UTF8 -Force -ErrorAction Stop + Write-Host "" + Write-Host "Wrote taskbar layout with $($pinPaths.Count) pin(s) to:" + Write-Host " $layoutXmlPath" + Write-Host "" + Write-Host "Pins will apply on first logon of any NEW user profile." +} catch { + Write-Warning "Failed to write $layoutXmlPath : $_" + exit 1 +} + +exit 0 diff --git a/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 b/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 new file mode 100644 index 0000000..f70c2e5 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/08-EdgeDefaultBrowser.ps1 @@ -0,0 +1,254 @@ +# 08-EdgeDefaultBrowser.ps1 - Set Microsoft Edge as the default browser +# and PDF handler for all new user profiles, and configure startup tabs +# so Edge opens with Plant Apps / Shop Floor Homepage / Shopfloor Dashboard +# when the end user first launches it. +# +# WHY THIS EXISTS: +# The West Jefferson shopfloor ppkg installs Google Chrome alongside Edge. +# On first URL click in a new Azure AD user profile, Windows pops a "Choose +# your default app" picker because multiple browsers are installed and +# nothing has been marked as default. End users hit this on every fresh PC +# and it derails the shopfloor workflow. +# +# HOW IT WORKS: +# Two layers, belt-and-suspenders: +# +# 1. DISM default-app-associations XML +# Writes an XML file mapping http/https/.htm/.html/.pdf/.svg/etc to +# Edge's ProgIds, then runs: +# dism /Online /Import-DefaultAppAssociations: +# This imports the XML into the Default User profile template. Any +# NEW user profile created after this point inherits the associations +# on first logon. Microsoft-supported, works across Win10/Win11. +# +# 2. Group Policy registry key +# HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration +# points at the same XML file. This is the "Set a default associations +# configuration file" GPO. With this set, Windows re-applies the XML +# on every logon, not just once at profile creation - so Windows +# update defaults-reset and similar edge cases don't un-do us. +# +# CAVEATS: +# - Applies to NEW profiles on first logon. An existing profile that's +# already been logged in (e.g. SupportUser) won't pick up the change +# without a manual Settings > Default apps visit. Acceptable because +# SupportUser isn't the end-user account. +# - Does not UNinstall Chrome. Chrome remains available, it's just not +# the default handler for URLs. +# - Microsoft blocks programmatic UserChoice hash tampering since Win10 +# 1703. The DISM + policy route is the only supported path, and it +# only fires for new profiles - that's by design. + +$ErrorActionPreference = 'Stop' + +# ---------------------------------------------------------------------------- +# Admin check +# ---------------------------------------------------------------------------- +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + Write-Host "" + Write-Host "ERROR: 08-EdgeDefaultBrowser.ps1 must run as Administrator." -ForegroundColor Red + Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red + Write-Host "" + exit 1 +} + +# ---------------------------------------------------------------------------- +# Sanity: Edge must be installed for this to mean anything +# ---------------------------------------------------------------------------- +$edgeCandidates = @( + 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', + 'C:\Program Files\Microsoft\Edge\Application\msedge.exe' +) +$edgePath = $edgeCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 +if (-not $edgePath) { + Write-Host "Microsoft Edge not found in either Program Files location - skipping default browser config." + exit 0 +} +Write-Host "Found Edge at: $edgePath" + +# ---------------------------------------------------------------------------- +# XML: Edge as default for web browsing + PDFs + common image formats +# ---------------------------------------------------------------------------- +$xml = @' + + + + + + + + + + + + + + +'@ + +# Write to a stable location that persists across reimaging (C:\Enrollment is +# the canonical staging dir in this repo, survives logoff/logon, referenced +# by the GP policy below so it must be a path every user-session can read). +$xmlDir = 'C:\Enrollment' +$xmlPath = Join-Path $xmlDir 'edge-default-associations.xml' + +if (-not (Test-Path -LiteralPath $xmlDir)) { + New-Item -ItemType Directory -Path $xmlDir -Force | Out-Null +} +Set-Content -LiteralPath $xmlPath -Value $xml -Encoding UTF8 -Force +Write-Host "Wrote associations XML to: $xmlPath" + +# ---------------------------------------------------------------------------- +# Layer 1: DISM import into the Default User profile template +# ---------------------------------------------------------------------------- +Write-Host "" +Write-Host "Layer 1: dism /Online /Import-DefaultAppAssociations..." +& dism.exe /Online /Import-DefaultAppAssociations:"$xmlPath" | Out-Null +$dismExit = $LASTEXITCODE +if ($dismExit -eq 0) { + Write-Host " OK - defaults imported into Default User template." +} else { + Write-Warning " dism returned exit code $dismExit - import may not have applied." +} + +# ---------------------------------------------------------------------------- +# Layer 2: Group Policy registry key - enforces XML on every logon +# ---------------------------------------------------------------------------- +Write-Host "" +Write-Host "Layer 2: Setting DefaultAssociationsConfiguration policy..." +$polKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' +if (-not (Test-Path $polKey)) { + New-Item -Path $polKey -Force | Out-Null +} +Set-ItemProperty -Path $polKey -Name 'DefaultAssociationsConfiguration' -Value $xmlPath -Type String -Force +Write-Host " OK - HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration = $xmlPath" + +Write-Host "" +Write-Host "================================================================" +Write-Host "Edge default browser config applied." +Write-Host "================================================================" + +# ---------------------------------------------------------------------------- +# Edge startup tabs + homepage +# +# End users should see Plant Apps, the Shop Floor Homepage, and the +# Shopfloor Dashboard open in three tabs the moment Edge launches. Rather +# than hardcoding URLs in this script, we read them out of the .url files +# that already live on the Public Desktop (copied there by ppkg). This +# way, if an admin renames or retargets a .url file later, the script +# picks up the new URL on the next imaging run without a code change. +# +# Fallback: if the .url file is missing, we fall back to a hardcoded URL +# (only for the ones we know for sure). Plant Apps has no fallback because +# I don't have its URL memorized. +# +# Machine policies (all under HKLM:\SOFTWARE\Policies\Microsoft\Edge): +# RestoreOnStartup = 4 (open a specific set of URLs) +# RestoreOnStartupURLs = subkey with "1","2","3"... = URLs in tab order +# HomepageLocation = first startup URL (home button opens same) +# HomepageIsNewTabPage = 0 (home button opens HomepageLocation, not NTP) +# ShowHomeButton = 1 (show the home button in the toolbar) +# ---------------------------------------------------------------------------- +Write-Host "" +Write-Host "Configuring Edge startup tabs..." + +function Get-UrlFromFile { + param([string]$Path) + try { + $content = Get-Content -LiteralPath $Path -ErrorAction Stop + $urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1 + if ($urlLine) { + return ($urlLine -replace '^URL=', '').Trim() + } + } catch {} + return $null +} + +function Resolve-StartupUrl { + param( + [string]$BaseName, # .url filename without extension + [string]$Fallback = '' + ) + # Look in Public Desktop root first, then Web Links subfolder (06's + # sweeper may have moved it there already). + $candidates = @( + "C:\Users\Public\Desktop\$BaseName.url", + "C:\Users\Public\Desktop\Web Links\$BaseName.url" + ) + foreach ($c in $candidates) { + if (Test-Path -LiteralPath $c) { + $url = Get-UrlFromFile $c + if ($url) { + Write-Host " resolved $BaseName -> $url (from $c)" + return $url + } + } + } + if ($Fallback) { + Write-Host " fallback $BaseName -> $Fallback (.url file not found)" -ForegroundColor DarkGray + return $Fallback + } + Write-Warning " $BaseName : no .url file found and no fallback - will be skipped" + return $null +} + +# Tab order as requested: Plant Apps, Shop Floor Homepage, Shopfloor Dashboard +$startupTabs = @() + +$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' +if ($plantApps) { $startupTabs += $plantApps } + +$shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'http://tsgwp00524.logon.ds.ge.com/' +if ($shopFloorHome) { $startupTabs += $shopFloorHome } + +$dashboard = Resolve-StartupUrl -BaseName 'Shopfloor Dashboard' -Fallback 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/' +if ($dashboard) { $startupTabs += $dashboard } + +if ($startupTabs.Count -eq 0) { + Write-Warning "No startup tab URLs resolved - skipping Edge startup config." +} else { + $edgePolKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' + if (-not (Test-Path $edgePolKey)) { + New-Item -Path $edgePolKey -Force | Out-Null + } + + # RestoreOnStartup = 4 means "open a specific set of URLs" (the list + # lives under the RestoreOnStartupURLs subkey below). + Set-ItemProperty -Path $edgePolKey -Name 'RestoreOnStartup' -Value 4 -Type DWord -Force + + # Build RestoreOnStartupURLs subkey: numbered string values "1","2","3" + $urlsKey = Join-Path $edgePolKey 'RestoreOnStartupURLs' + if (Test-Path $urlsKey) { + # Wipe any prior entries so we don't leak stale tabs if the list + # shrinks between runs + Remove-Item -Path $urlsKey -Recurse -Force + } + New-Item -Path $urlsKey -Force | Out-Null + for ($i = 0; $i -lt $startupTabs.Count; $i++) { + $name = [string]($i + 1) + Set-ItemProperty -Path $urlsKey -Name $name -Value $startupTabs[$i] -Type String -Force + } + + # Homepage = the first startup tab. Home button opens it, not NTP. + Set-ItemProperty -Path $edgePolKey -Name 'HomepageLocation' -Value $startupTabs[0] -Type String -Force + Set-ItemProperty -Path $edgePolKey -Name 'HomepageIsNewTabPage' -Value 0 -Type DWord -Force + Set-ItemProperty -Path $edgePolKey -Name 'ShowHomeButton' -Value 1 -Type DWord -Force + + Write-Host "" + Write-Host "Edge startup tabs set ($($startupTabs.Count) tab(s)):" + for ($i = 0; $i -lt $startupTabs.Count; $i++) { + Write-Host " $($i + 1). $($startupTabs[$i])" + } + Write-Host "Homepage set to: $($startupTabs[0])" +} + +Write-Host "" +Write-Host "================================================================" +Write-Host "08-EdgeDefaultBrowser.ps1 complete." +Write-Host "Effective for new user profiles on first logon (Azure AD users)." +Write-Host "Existing profiles will pick up the startup-tab policy on next" +Write-Host "Edge launch - it's a machine-wide GPO." +Write-Host "================================================================" + +exit 0