Add scheduled maintenance, PC list export, and subnet scanner scripts

- Schedule-Maintenance.ps1: DPAPI credential storage + Task Scheduler integration
- Export-PCList.ps1: Pull PC lists from ShopDB API with type/BU filtering
- Find-ShopfloorPCs.ps1: Parallel subnet scanner with WinRM and DNS checks
- INSTRUCTIONS.txt: Schedule-Maintenance.ps1 documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-19 14:58:18 -05:00
parent 7d3519f613
commit 847ec402bd
4 changed files with 797 additions and 0 deletions

View File

@@ -0,0 +1,223 @@
<#
.SYNOPSIS
Queries ShopDB API and exports a computer list for Invoke-RemoteMaintenance.ps1.
.DESCRIPTION
Pulls shopfloor PC data from the ShopDB API and writes a text file
(one hostname per line) compatible with:
Invoke-RemoteMaintenance.ps1 -ComputerListFile "shopfloor-pcs.txt"
Can filter by PC type, business unit, or export all.
.PARAMETER PcType
Filter by PC type. Omit for all types.
.PARAMETER BusinessUnit
Filter by business unit. Omit for all BUs.
.PARAMETER OutputFile
Output file path (default: shopfloor-pcs.txt in script directory).
.PARAMETER ApiUrl
ShopDB API URL.
.PARAMETER IncludeDetails
Add IP, PC type, and business unit as comments in the output.
.EXAMPLE
.\Export-PCList.ps1
# Export all shopfloor PCs
.EXAMPLE
.\Export-PCList.ps1 -PcType Dashboard
# Export only Dashboard PCs
.EXAMPLE
.\Export-PCList.ps1 -BusinessUnit Blisk -OutputFile "blisk-pcs.txt"
.EXAMPLE
.\Export-PCList.ps1 -PcType Dashboard,Shopfloor -BusinessUnit Blisk,HPT
# Multiple filters (OR logic within each, AND between type/BU)
#>
[CmdletBinding()]
param(
[ValidateSet('Standard','Engineer','Shopfloor','CMM','Wax / Trace','Keyence',
'Genspect','Heat Treat','Inspection','Dashboard','Lobby Display','Uncategorized')]
[string[]]$PcType,
[ValidateSet('TBD','Blisk','HPT','Spools','Inspection','Venture','Turn/Burn','DT')]
[string[]]$BusinessUnit,
[string]$OutputFile,
[string]$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp",
[switch]$IncludeDetails
)
# ---------------------------------------------------------------------------
# Lookup tables (match IDs in ShopDB)
# ---------------------------------------------------------------------------
$PcTypeLookup = @{
'Standard' = 1; 'Engineer' = 2; 'Shopfloor' = 3; 'Uncategorized' = 4;
'CMM' = 5; 'Wax / Trace' = 6; 'Keyence' = 7; 'Genspect' = 8;
'Heat Treat' = 9; 'Inspection' = 10; 'Dashboard' = 11; 'Lobby Display' = 12
}
$PcTypeReverse = @{}
$PcTypeLookup.GetEnumerator() | ForEach-Object { $PcTypeReverse[$_.Value] = $_.Key }
$BusinessUnitLookup = @{
'TBD' = 1; 'Blisk' = 2; 'HPT' = 3; 'Spools' = 4;
'Inspection' = 5; 'Venture' = 6; 'Turn/Burn' = 7; 'DT' = 8
}
$BusinessUnitReverse = @{}
$BusinessUnitLookup.GetEnumerator() | ForEach-Object { $BusinessUnitReverse[$_.Value] = $_.Key }
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Write-Status {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "HH:mm:ss"
$color = switch ($Level) {
"OK" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
default { "Cyan" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
function Get-ShopfloorPCsFromApi {
param(
[string]$Url,
[int]$PcTypeId = 0,
[int]$BusinessUnitId = 0
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$queryParams = "action=getShopfloorPCs"
if ($PcTypeId -gt 0) {
$queryParams += "&pctypeid=$PcTypeId"
}
if ($BusinessUnitId -gt 0) {
$queryParams += "&businessunitid=$BusinessUnitId"
}
$fullUrl = "$Url`?$queryParams"
$webClient = New-Object System.Net.WebClient
$json = $webClient.DownloadString($fullUrl)
$response = $json | ConvertFrom-Json
if ($response.success -and $response.data) {
return $response.data
}
return @()
}
# ---------------------------------------------------------------------------
# Default output path
# ---------------------------------------------------------------------------
if (-not $OutputFile) {
$OutputFile = Join-Path $PSScriptRoot "shopfloor-pcs.txt"
}
# ---------------------------------------------------------------------------
# Query API
# ---------------------------------------------------------------------------
Write-Status "Querying ShopDB API..."
try {
$allPCs = Get-ShopfloorPCsFromApi -Url $ApiUrl
} catch {
Write-Status "Failed to query API: $_" -Level "ERROR"
exit 1
}
if ($allPCs.Count -eq 0) {
Write-Status "API returned no PCs." -Level "ERROR"
exit 1
}
Write-Status "API returned $($allPCs.Count) total PCs" -Level "OK"
# ---------------------------------------------------------------------------
# Filter
# ---------------------------------------------------------------------------
$filtered = $allPCs
if ($PcType) {
$typeIds = $PcType | ForEach-Object { $PcTypeLookup[$_] }
$filtered = $filtered | Where-Object { $_.pctypeid -in $typeIds }
Write-Status "Filtered to PC types: $($PcType -join ', ') -> $($filtered.Count) PCs"
}
if ($BusinessUnit) {
$buIds = $BusinessUnit | ForEach-Object { $BusinessUnitLookup[$_] }
$filtered = $filtered | Where-Object { $_.businessunitid -in $buIds }
Write-Status "Filtered to business units: $($BusinessUnit -join ', ') -> $($filtered.Count) PCs"
}
$filtered = @($filtered | Where-Object { $_.hostname })
if ($filtered.Count -eq 0) {
Write-Status "No PCs match the selected filters." -Level "WARN"
exit 0
}
# Sort by hostname
$filtered = $filtered | Sort-Object hostname
# ---------------------------------------------------------------------------
# Write output
# ---------------------------------------------------------------------------
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Shopfloor PC List - Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$lines.Add("# Source: ShopDB API")
if ($PcType) {
$lines.Add("# PC Type filter: $($PcType -join ', ')")
}
if ($BusinessUnit) {
$lines.Add("# Business Unit filter: $($BusinessUnit -join ', ')")
}
$lines.Add("# Total: $($filtered.Count) PCs")
$lines.Add("# Usage: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot")
$lines.Add("")
foreach ($pc in $filtered) {
if ($IncludeDetails) {
$typeName = $PcTypeReverse[[int]$pc.pctypeid]
$buName = $BusinessUnitReverse[[int]$pc.businessunitid]
$ip = if ($pc.ipaddress) { $pc.ipaddress } else { "no IP" }
$lines.Add("$($pc.hostname) # $ip | $typeName | $buName")
} else {
$lines.Add($pc.hostname)
}
}
$lines | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Status "Wrote $($filtered.Count) PCs to: $OutputFile" -Level "OK"
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
Write-Host ""
Write-Host "=== PC LIST ===" -ForegroundColor White
# Group by type for summary
$byType = $filtered | Group-Object pctypeid | Sort-Object Name
foreach ($group in $byType) {
$typeName = $PcTypeReverse[[int]$group.Name]
if (-not $typeName) { $typeName = "Unknown ($($group.Name))" }
Write-Host " $($typeName): $($group.Count)" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Output file: $OutputFile" -ForegroundColor Green
Write-Host "Next step: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot" -ForegroundColor Yellow

View File

@@ -0,0 +1,266 @@
<#
.SYNOPSIS
Scans a subnet for live hosts and tests WinRM connectivity to build a
usable computer list for Invoke-RemoteMaintenance.ps1.
.DESCRIPTION
1. Pings all IPs in the specified subnet to find responsive hosts.
2. Tests WinRM (TCP 5985) on each responsive host.
3. Attempts to resolve hostnames via reverse DNS.
4. Outputs a text file (one host per line) compatible with
Invoke-RemoteMaintenance.ps1 -ComputerListFile.
.PARAMETER Subnet
CIDR notation subnet to scan (default: 10.134.48.0/23).
.PARAMETER OutputFile
Path for the generated computer list (default: shopfloor-pcs.txt in script dir).
.PARAMETER PingTimeout
Ping timeout in milliseconds (default: 500).
.PARAMETER WinRMPort
WinRM port to test (default: 5985).
.PARAMETER WinRMTimeout
TCP connection timeout in milliseconds for WinRM test (default: 1000).
.PARAMETER ThrottleLimit
Max concurrent ping/test jobs (default: 50).
.PARAMETER SkipWinRM
Only do ping sweep, skip WinRM test.
.EXAMPLE
.\Find-ShopfloorPCs.ps1
# Scans 10.134.48.0/23, outputs shopfloor-pcs.txt
.EXAMPLE
.\Find-ShopfloorPCs.ps1 -Subnet "10.134.48.0/23" -OutputFile "C:\temp\pcs.txt"
.EXAMPLE
.\Find-ShopfloorPCs.ps1 -SkipWinRM
# Ping sweep only, no WinRM check
#>
[CmdletBinding()]
param(
[string]$Subnet = "10.134.48.0/23",
[string]$OutputFile,
[int]$PingTimeout = 500,
[int]$WinRMPort = 5985,
[int]$WinRMTimeout = 1000,
[int]$ThrottleLimit = 50,
[switch]$SkipWinRM
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Write-Status {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "HH:mm:ss"
$color = switch ($Level) {
"OK" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
"SKIP" { "DarkGray"}
default { "Cyan" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
function ConvertFrom-CIDR {
<# Returns an array of IP address strings for the given CIDR block.
Excludes network and broadcast addresses. #>
param([string]$CIDR)
$parts = $CIDR -split '/'
$networkIP = [System.Net.IPAddress]::Parse($parts[0])
$prefixLen = [int]$parts[1]
$ipBytes = $networkIP.GetAddressBytes()
# .NET gives bytes in network order (big-endian)
[Array]::Reverse($ipBytes)
$ipUint = [BitConverter]::ToUInt32($ipBytes, 0)
$hostBits = 32 - $prefixLen
$numAddresses = [math]::Pow(2, $hostBits)
$networkAddr = $ipUint -band ([uint32]::MaxValue -shl $hostBits)
$ips = [System.Collections.Generic.List[string]]::new()
# Skip .0 (network) and last (broadcast)
for ($i = 1; $i -lt ($numAddresses - 1); $i++) {
$addr = $networkAddr + $i
$bytes = [BitConverter]::GetBytes([uint32]$addr)
[Array]::Reverse($bytes)
$ips.Add(("{0}.{1}.{2}.{3}" -f $bytes[0], $bytes[1], $bytes[2], $bytes[3]))
}
return $ips
}
# ---------------------------------------------------------------------------
# Default output path
# ---------------------------------------------------------------------------
if (-not $OutputFile) {
$OutputFile = Join-Path $PSScriptRoot "shopfloor-pcs.txt"
}
# ---------------------------------------------------------------------------
# Phase 1: Generate IP list
# ---------------------------------------------------------------------------
Write-Status "Generating IP list for $Subnet"
$allIPs = ConvertFrom-CIDR -CIDR $Subnet
Write-Status "$($allIPs.Count) host addresses to scan"
# ---------------------------------------------------------------------------
# Phase 2: Ping sweep (parallel)
# ---------------------------------------------------------------------------
Write-Status "Starting ping sweep (timeout: ${PingTimeout}ms, threads: $ThrottleLimit)..."
$pingResults = $allIPs | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_
$ping = New-Object System.Net.NetworkInformation.Ping
try {
$reply = $ping.Send($ip, $using:PingTimeout)
if ($reply.Status -eq 'Success') {
[PSCustomObject]@{
IP = $ip
Alive = $true
RoundTrip = $reply.RoundtripTime
}
}
} catch {
# Silently skip unreachable hosts
} finally {
$ping.Dispose()
}
}
$aliveHosts = @($pingResults | Where-Object { $_.Alive })
Write-Status "$($aliveHosts.Count) hosts responded to ping" -Level "OK"
if ($aliveHosts.Count -eq 0) {
Write-Status "No hosts found. Check subnet and network connectivity." -Level "ERROR"
exit 1
}
# ---------------------------------------------------------------------------
# Phase 3: WinRM port test (parallel)
# ---------------------------------------------------------------------------
if ($SkipWinRM) {
Write-Status "Skipping WinRM test (-SkipWinRM)" -Level "SKIP"
$winrmHosts = $aliveHosts
} else {
Write-Status "Testing WinRM (port $WinRMPort) on $($aliveHosts.Count) hosts..."
$winrmResults = $aliveHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_.IP
$roundTrip = $_.RoundTrip
$port = $using:WinRMPort
$timeout = $using:WinRMTimeout
$tcp = New-Object System.Net.Sockets.TcpClient
try {
$task = $tcp.ConnectAsync($ip, $port)
$connected = $task.Wait($timeout)
if ($connected -and $tcp.Connected) {
[PSCustomObject]@{
IP = $ip
WinRM = $true
RoundTrip = $roundTrip
}
}
} catch {
# Port closed or filtered
} finally {
$tcp.Dispose()
}
}
$winrmHosts = @($winrmResults | Where-Object { $_.WinRM })
$noWinRM = $aliveHosts.Count - $winrmHosts.Count
Write-Status "$($winrmHosts.Count) hosts have WinRM open, $noWinRM alive but no WinRM" -Level "OK"
}
if ($winrmHosts.Count -eq 0) {
Write-Status "No hosts with WinRM found." -Level "ERROR"
exit 1
}
# ---------------------------------------------------------------------------
# Phase 4: Reverse DNS resolution
# ---------------------------------------------------------------------------
Write-Status "Resolving hostnames via reverse DNS..."
$resolvedHosts = $winrmHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_.IP
$roundTrip = $_.RoundTrip
$hostname = $null
try {
$entry = [System.Net.Dns]::GetHostEntry($ip)
if ($entry.HostName -and $entry.HostName -ne $ip) {
$hostname = $entry.HostName
}
} catch {
# No reverse DNS
}
[PSCustomObject]@{
IP = $ip
Hostname = $hostname
RoundTrip = $roundTrip
}
}
$withDNS = @($resolvedHosts | Where-Object { $_.Hostname })
$withoutDNS = @($resolvedHosts | Where-Object { -not $_.Hostname })
Write-Status "$($withDNS.Count) resolved via DNS, $($withoutDNS.Count) IP-only" -Level "OK"
# ---------------------------------------------------------------------------
# Phase 5: Write output file
# ---------------------------------------------------------------------------
$sortedHosts = $resolvedHosts | Sort-Object { [Version]($_.IP -replace '(\d+)\.(\d+)\.(\d+)\.(\d+)', '$1.$2.$3.$4') }
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Shopfloor PC List - Auto-generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$lines.Add("# Subnet: $Subnet")
$lines.Add("# Hosts scanned: $($allIPs.Count) | Alive: $($aliveHosts.Count) | WinRM: $($winrmHosts.Count)")
$lines.Add("# Usage: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot")
$lines.Add("#")
$lines.Add("# Format: hostname or IP (one per line). Lines starting with # are ignored.")
$lines.Add("")
foreach ($h in $sortedHosts) {
if ($h.Hostname) {
# Use short hostname (strip domain suffix) as the entry, comment with IP
$shortName = ($h.Hostname -split '\.')[0]
$lines.Add("$shortName # $($h.IP) ($($h.RoundTrip)ms)")
} else {
$lines.Add("$($h.IP) # no DNS ($($h.RoundTrip)ms)")
}
}
$lines | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Status "Wrote $($winrmHosts.Count) entries to: $OutputFile" -Level "OK"
# ---------------------------------------------------------------------------
# Summary table
# ---------------------------------------------------------------------------
Write-Host ""
Write-Host "=== SCAN RESULTS ===" -ForegroundColor White
Write-Host ("{0,-30} {1,-18} {2,8}" -f "HOSTNAME", "IP", "PING(ms)") -ForegroundColor DarkGray
Write-Host ("-" * 58) -ForegroundColor DarkGray
foreach ($h in $sortedHosts) {
$name = if ($h.Hostname) { ($h.Hostname -split '\.')[0] } else { "(no DNS)" }
Write-Host ("{0,-30} {1,-18} {2,8}" -f $name, $h.IP, $h.RoundTrip)
}
Write-Host ""
Write-Host "Output file: $OutputFile" -ForegroundColor Green
Write-Host "Next step: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot" -ForegroundColor Yellow

View File

@@ -0,0 +1,100 @@
============================================================
Schedule-Maintenance.ps1 - Documentation
============================================================
Wrapper script that stores credentials securely and
runs Invoke-RemoteMaintenance.ps1 unattended or on a
schedule via Windows Task Scheduler.
============================================================
REQUIREMENTS
============================================================
- PowerShell 5.1+
- Run as Administrator (required for scheduling only)
- Invoke-RemoteMaintenance.ps1 in the same folder
- A PC list text file (one hostname per line)
============================================================
PARAMETERS
============================================================
-SaveCredential Save credentials for unattended use
-Username Domain\username (use with -SaveCredential)
-Password Password (use with -SaveCredential)
-Task Maintenance task name (e.g. Reboot)
-ComputerListFile Path to text file with PC hostnames
-CreateScheduledTask Register a Windows Scheduled Task
-TaskFrequency Daily, Weekly, or Once (default: Weekly)
-TaskDay Day of week (default: Sunday)
-TaskTime Time in HH:mm format (default: 03:00)
-TaskDate Specific date for Once (e.g. 2026-02-22)
============================================================
USAGE
============================================================
1. SAVE CREDENTIALS (one time, does not require admin)
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
- Encrypted with Windows DPAPI
- Only your user account on this machine can decrypt
- Re-run if your password changes
2. RUN IMMEDIATELY (does not require admin)
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
3. SCHEDULE A ONE-TIME TASK (requires admin)
# Reboot one PC today at 3:00 PM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\test-reboot.txt" -Task Reboot -TaskFrequency Once -TaskTime "15:00" -TaskDate "2026-02-19"
# Reboot all PCs Sunday Feb 22 at 12:01 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
4. SCHEDULE A RECURRING TASK (requires admin)
# Every Sunday at 12:01 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
# Every day at 2:00 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task DiskCleanup -TaskFrequency Daily -TaskTime "02:00"
5. MANAGE SCHEDULED TASKS
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
============================================================
LOGS
============================================================
.\logs\maintenance-YYYY-MM-DD_HHMMSS-TaskName.log
============================================================
TROUBLESHOOTING
============================================================
"No saved credentials found"
-> Run -SaveCredential with -Username and -Password
"Access is denied" when scheduling
-> Right-click PowerShell -> Run as Administrator
"No credentials provided. Exiting."
-> GUI prompt failed. Use -Username and -Password flags
Password changed
-> Re-run -SaveCredential with new password

View File

@@ -0,0 +1,208 @@
<#
.SYNOPSIS
Wrapper for running Invoke-RemoteMaintenance.ps1 unattended via Task Scheduler.
.DESCRIPTION
Stores credentials securely using Windows DPAPI (encrypted to your user account)
and runs maintenance tasks without interactive prompts.
First run: use -SaveCredential to store your username/password.
Subsequent runs (or Task Scheduler): credentials load automatically.
.PARAMETER SaveCredential
Prompt to save credentials for future unattended use.
.PARAMETER Task
Maintenance task to run (passed to Invoke-RemoteMaintenance.ps1).
.PARAMETER ComputerListFile
Path to PC list file (passed to Invoke-RemoteMaintenance.ps1).
.PARAMETER CreateScheduledTask
Register a Windows Scheduled Task to run this script on a schedule.
.PARAMETER TaskTime
Time for the scheduled task (default: 03:00 AM).
.PARAMETER TaskFrequency
How often to run: Daily, Weekly, or Once (default: Weekly).
.PARAMETER TaskDay
Day of week for Weekly frequency (default: Sunday).
.EXAMPLE
# Step 1: Save your credentials (one time, interactive)
.\Schedule-Maintenance.ps1 -SaveCredential
.EXAMPLE
# Step 2: Test unattended run
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
.EXAMPLE
# Step 3: Create a scheduled task for weekly reboots
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "03:00"
#>
[CmdletBinding()]
param(
[switch]$SaveCredential,
[string]$Task,
[string]$ComputerListFile,
[switch]$CreateScheduledTask,
[string]$TaskTime = "03:00",
[ValidateSet('Daily','Weekly','Once')]
[string]$TaskFrequency = "Weekly",
[ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')]
[string]$TaskDay = "Sunday",
[string]$TaskDate,
[string]$Username,
[string]$Password
)
$credFile = Join-Path $PSScriptRoot ".maintenance-cred.xml"
$scriptDir = $PSScriptRoot
# ---------------------------------------------------------------------------
# Save credentials (DPAPI - only decryptable by this user on this machine)
# ---------------------------------------------------------------------------
if ($SaveCredential) {
if ($Username -and $Password) {
$secPass = ConvertTo-SecureString $Password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential($Username, $secPass)
} else {
Write-Host "Enter the credentials used to connect to remote shopfloor PCs:" -ForegroundColor Cyan
Write-Host "If no prompt appears, re-run with: -Username 'DOMAIN\user' -Password 'pass'" -ForegroundColor Yellow
$cred = Get-Credential -Message "Enter admin credentials for remote shopfloor PCs"
}
if (-not $cred) {
Write-Host "No credentials provided. Exiting." -ForegroundColor Red
exit 1
}
$cred | Export-Clixml -Path $credFile
Write-Host "Credentials saved to: $credFile" -ForegroundColor Green
Write-Host "Encrypted with DPAPI - only YOUR user account on THIS machine can decrypt them." -ForegroundColor Yellow
Write-Host ""
Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan
Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot"
exit 0
}
# ---------------------------------------------------------------------------
# Create Scheduled Task
# ---------------------------------------------------------------------------
if ($CreateScheduledTask) {
if (-not $Task) {
Write-Host "ERROR: -Task is required when creating a scheduled task." -ForegroundColor Red
exit 1
}
if (-not $ComputerListFile) {
Write-Host "ERROR: -ComputerListFile is required when creating a scheduled task." -ForegroundColor Red
exit 1
}
# Resolve to absolute paths
$absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path
$absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1"
if (-not (Test-Path $credFile)) {
Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red
exit 1
}
$taskName = "ShopfloorMaintenance-$Task"
$psArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$absScript`" -Task `"$Task`" -ComputerListFile `"$absListFile`""
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs -WorkingDirectory $scriptDir
$triggerTime = if ($TaskDate) {
[DateTime]::Parse("$TaskDate $TaskTime")
} else {
[DateTime]::Parse($TaskTime)
}
$trigger = switch ($TaskFrequency) {
'Daily' { New-ScheduledTaskTrigger -Daily -At $triggerTime }
'Weekly' { New-ScheduledTaskTrigger -Weekly -DaysOfWeek $TaskDay -At $triggerTime }
'Once' { New-ScheduledTaskTrigger -Once -At $triggerTime }
}
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable
# Register as current user (needed for DPAPI credential decryption)
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Write-Host "Creating scheduled task '$taskName' as $currentUser..." -ForegroundColor Cyan
Write-Host " Schedule: $TaskFrequency at $TaskTime$(if ($TaskFrequency -eq 'Weekly') { " ($TaskDay)" })" -ForegroundColor Gray
Write-Host " Task: $Task" -ForegroundColor Gray
Write-Host " PC List: $absListFile" -ForegroundColor Gray
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existingTask) {
Write-Host "Task '$taskName' already exists. Updating..." -ForegroundColor Yellow
Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings | Out-Null
} else {
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -User $currentUser -RunLevel Highest -Description "Shopfloor PC Maintenance - $Task (via Invoke-RemoteMaintenance.ps1)" | Out-Null
}
Write-Host "Scheduled task '$taskName' created successfully." -ForegroundColor Green
Write-Host ""
Write-Host "Manage in Task Scheduler or with:" -ForegroundColor Cyan
Write-Host " Get-ScheduledTask -TaskName '$taskName'"
Write-Host " Start-ScheduledTask -TaskName '$taskName' # run now"
Write-Host " Unregister-ScheduledTask -TaskName '$taskName' # delete"
exit 0
}
# ---------------------------------------------------------------------------
# Run maintenance (unattended)
# ---------------------------------------------------------------------------
if (-not $Task) {
Write-Host "ERROR: -Task is required. Example: -Task Reboot" -ForegroundColor Red
Write-Host " Or use -SaveCredential to store credentials first." -ForegroundColor Yellow
Write-Host " Or use -CreateScheduledTask to set up a scheduled run." -ForegroundColor Yellow
exit 1
}
if (-not $ComputerListFile) {
Write-Host "ERROR: -ComputerListFile is required." -ForegroundColor Red
exit 1
}
# Load saved credentials
if (-not (Test-Path $credFile)) {
Write-Host "ERROR: No saved credentials found at $credFile" -ForegroundColor Red
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
exit 1
}
try {
$cred = Import-Clixml -Path $credFile
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
} catch {
Write-Host "ERROR: Failed to load credentials: $_" -ForegroundColor Red
Write-Host "Re-run with -SaveCredential to save new credentials." -ForegroundColor Yellow
exit 1
}
# Log output
$logDir = Join-Path $scriptDir "logs"
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
$logFile = Join-Path $logDir "maintenance-$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-$Task.log"
Write-Host "Running: Invoke-RemoteMaintenance.ps1 -Task $Task -ComputerListFile $ComputerListFile" -ForegroundColor Cyan
Write-Host "Log: $logFile" -ForegroundColor Gray
$mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1"
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred 2>&1 | Tee-Object -FilePath $logFile
Write-Host ""
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green