# region changes to be done
#endregion
# STACK = Scripted Tooling for Automated Client Kickoff
# MONKEY = Module-based Onboarding & Next-step Kickoff Engine Yoke
# Conveys the idea of coupling tasks together and keeping them under control.
#region Config & Task Definitions
# Listening port for HTTP UI
$Port = 8082
# Configurable endpoints
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
# Define every task once here:
# Id → checkbox HTML `id`
# Name → URL path (`/Name`)
# Label → user-visible text
# HandlerFn → the PowerShell function to invoke
# Page → which tab/page it appears on
$Global:Tasks = @(
# On-Boarding, left column
@{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' },
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' },
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' },
@{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' },
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' },
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' },
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left';
SubOptions= @(
@{ Value='inputVar'; Label='Copy Site Variables' },
@{ Value='rmm'; Label='Install RMM Agent' },
@{ Value='exe'; Label='Download Executable' }
)
},
# On-Boarding, right column (optional bits)
@{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' },
# @{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' },
# Off-Boarding
@{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' },
@{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' },
# ─────────────────────────────────────────────────────────────────────────────
# Tweaks: one entry per PS1 file, with hidden
of radio‐buttons for params
# ─────────────────────────────────────────────────────────────────────────────
@{
Id = 'setEdgeDefaultSearch';
Name = 'setEdgeDefaultSearch';
Label = 'Set Edge Default Search';
Tooltip = 'Choose which search engine Edge should use (or Reset to defaults)';
HandlerFn = 'Handle-SetEdgeDefaultSearch';
Page = 'tweaks';
Column = '';
SubOptions = @(
@{
ParamName = 'SearchProviderName';
Type = 'radio';
Options = @('Google', 'Bing', 'DuckDuckGo');
Default = 'Google'
},
@{
ParamName = 'Reset';
Type = 'radio';
Options = @('No', 'Yes');
Default = 'No'
}
)
},
@{
Id = 'setTextEditor';
Name = 'setTextEditor';
Label = 'Set Default Text Editor';
Tooltip = 'Pick your preferred default text editor for .txt and .ps1 files.';
HandlerFn = 'Handle-SetTextEditor';
Page = 'tweaks';
Column = '';
SubOptions = @(
@{
ParamName = 'Editor';
Type = 'radio';
Options = @('Notepad', 'Notepad++', 'VSCode');
Default = 'Notepad'
}
)
},
@{
Id = 'setWindowsPerformance';
Name = 'setWindowsPerformance';
Label = 'Optimize Windows Performance';
Tooltip = 'Choose performance mode or restore defaults.';
HandlerFn = 'Handle-SetWindowsPerformance';
Page = 'tweaks';
Column = '';
SubOptions = @(
@{
ParamName = 'Mode';
Type = 'radio';
Options = @('Gaming', 'Balanced', 'PowerSaver');
Default = 'Balanced'
},
@{
ParamName = 'RestoreDefaults';
Type = 'radio';
Options = @('No', 'Yes');
Default = 'No'
}
)
},
@{
Id = 'stopUnnecessaryServices';
Name = 'stopUnnecessaryServices';
Label = 'Stop Unnecessary Services';
Tooltip = 'Log actions to file or just run silently.';
HandlerFn = 'Handle-StopUnnecessaryServices';
Page = 'tweaks';
Column = '';
SubOptions = @(
@{
ParamName = 'LogFilePathChoice';
Type = 'radio';
Options = @('NoLogging', 'DefaultPath', 'CustomPath');
Default = 'NoLogging'
}
# If you want to allow a “custom path” text‐box, you can detect when "CustomPath" is selected
# on the front‐end and show an
. (See note below.)
)
},
@{
Id = 'setBrowserPwdPolicy';
Name = 'setBrowserPwdPolicy';
Label = 'Enforce Browser Password Policy';
Tooltip = 'Enable or disable saving passwords and set expiration days.';
HandlerFn = 'Handle-SetBrowserPwdPolicy';
Page = 'tweaks';
Column = '';
SubOptions = @(
@{
ParamName = 'DisableSavePasswords';
Type = 'radio';
Options = @('No', 'Yes');
Default = 'No'
},
@{
ParamName = 'PasswordExpirationDays';
Type = 'radio';
Options = @('30', '60', '90', 'Never');
Default = '90'
}
)
},
# SVS Apps
@{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }
)
#endregion
#region Logging Helpers
# Initialize a global in-memory log cache
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
# Core Write-Log function (advanced with event-log support)
function Write-LogHelper {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Message,
[ValidateSet("Info","Warning","Error","Success","General")]
[string]$Level = "Info",
[string]$TaskCategory = "GeneralTask",
[switch]$LogToEvent,
[string]$EventSource = "SVSMSP_Module",
[string]$EventLog = "Application",
[int]$CustomEventID
)
$EventID = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }[$Level]
$Icon = @{Info=[System.Char]::ConvertFromUtf32(0x1F4CB);Warning=[char]0x26A0;Error=[char]0x274C;Success=[char]0x2705;General=[char]0x1F4E6}[$Level]
$logEntry = [PSCustomObject]@{
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Level = $Level
Message = "$Icon [$Level] [$TaskCategory] $Message (EventID:$EventID)"
}
[void]$Global:LogCache.Add($logEntry)
if ($LogToEvent) {
try {
if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) {
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue
}
Write-EventLog -LogName $EventLog -Source $EventSource `
-EntryType $Level -EventId $EventID `
-Message $Message
} catch {
Write-Host "$([System.Char]::ConvertFromUtf32(0x26A0))$([System.Char]::ConvertFromUtf32(0xFE0F)) [Warning] [EventLog] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
# Hybrid wrapper: uses your module's Write-Log if available, else falls back
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-Log @PSBoundParameters }
} else {
function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-LogHelper @PSBoundParameters }
}
#endregion
#region Handler Stubs
function Respond-Text {
param($Context, $Text)
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
$Context.Response.ContentType = 'text/plain'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
$Context.Response.OutputStream.Close()
}
function Respond-HTML {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][object] $Context,
[Parameter(Mandatory = $true)][string] $Html
)
$bytes = [Text.Encoding]::UTF8.GetBytes($Html)
$Context.Response.ContentType = 'text/html'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.OutputStream.Close()
}
# new helper to return JSON
function Respond-JSON {
param($Context, $Object)
$json = $Object | ConvertTo-Json -Depth 5
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.ContentType = 'application/json'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
$Context.Response.OutputStream.Close()
}
#region Get-DattoApiCreds
function Get-DattoApiCredentials {
param ([string]$Password)
$url = "https://automate.svstools.ca/webhook/svsmspkit"
$headers = @{ "SVSMSPKit" = $Password }
try {
$response = Invoke-RestMethod -Uri $url -Headers $headers -Method GET
return @{
ApiUrl = $response.ApiUrl
ApiKey = $response.ApiKey
ApiSecretKey = $response.ApiSecretKey
}
} catch {
Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" "Error" "DattoAuth"
return $null
}
}
#endregion
#region Install-DattoRMM-Helper
function Install-DattoRMM-Helper {
param (
[string]$ApiUrl,
[string]$ApiKey,
[string]$ApiSecretKey,
[switch]$FetchSitesOnly,
[string]$SiteName,
[string]$SiteUID
)
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
Write-LogHybrid -Message "Missing required parameters. Please provide ApiUrl, ApiKey, and ApiSecretKey." -Level "Error" -LogToEvent
return
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-LogHybrid -Message "Fetching OAuth token..." -Level "Info"
try {
$securePassword = ConvertTo-SecureString -String 'public' -AsPlainText -Force
$apiGenToken = Invoke-WebRequest -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('public-client', $securePassword)) `
-Uri ('{0}/auth/oauth/token' -f $ApiUrl) `
-Method 'POST' `
-ContentType 'application/x-www-form-urlencoded' `
-Body ('grant_type=password&username={0}&password={1}' -f $ApiKey, $ApiSecretKey) `
| ConvertFrom-Json
$requestToken = $apiGenToken.access_token
Write-LogHybrid -Message "OAuth token fetched successfully." -Level "Success" -LogToEvent
} catch {
Write-LogHybrid -Message "Failed to fetch OAuth token. Details: $($_.Exception.Message)" -Level "Error" -LogToEvent
return
}
$getHeaders = @{"Authorization" = "Bearer $requestToken"}
if ($FetchSitesOnly) {
Write-Host "Fetching list of sites from the Datto RMM API..." -ForegroundColor Cyan
try {
$getHeaders = @{"Authorization" = "Bearer $requestToken" }
$getSites = Invoke-WebRequest -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $getHeaders -ContentType "application/json"
$sitesJson = $getSites.Content | ConvertFrom-Json
$siteList = $sitesJson.sites | ForEach-Object {
[PSCustomObject]@{
Name = $_.name
UID = $_.uid
}
}
Write-Host "Successfully fetched list of sites." -ForegroundColor Green
return $siteList
}
catch {
Write-Host "Failed to fetch sites from the API. Details: $($_.Exception.Message)" -ForegroundColor Red
return
}
}
}
#endregion
#region SVS Module
function Install-SVSMSP {
param (
[switch] $Cleanup,
[switch] $InstallToolkit,
[Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }),
[Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }),
[Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP",
[Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo",
[Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/"
)
function Perform-Cleanup {
Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule"
# …your old cleanup logic here…
}
function Perform-ToolkitInstallation {
Perform-Cleanup
Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule"
if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
}
Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule"
Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force
Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule"
}
Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule"
if ($Cleanup) {
Perform-Cleanup; return
}
if ($InstallToolkit) {
Perform-ToolkitInstallation; return
}
# default if no switch passed:
Perform-ToolkitInstallation
}
#endregion
# POST /getpw → read JSON body, call helper, return JSON
function Handle-FetchSites {
param($Context)
# 1) Read incoming JSON (using block auto-disposes the reader)
<# powershell v7
using ($reader = [IO.StreamReader]::new($Context.Request.InputStream)) {
$raw = $reader.ReadToEnd()
}
try {
$pw = (ConvertFrom-Json $raw).password
if (-not $pw) { throw "Missing `password` field" }
} catch {
Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites"
returnRespondEmpty $Context 400
return
}
#>
$reader = [IO.StreamReader]::new($Context.Request.InputStream)
try {
$raw = $reader.ReadToEnd()
} finally {
$reader.Close()
}
try {
$pw = (ConvertFrom-Json $raw).password
} catch {
Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites"
returnRespondEmpty $Context
return
}
# 2) Fetch your Datto API creds from the webhook
Write-LogHybrid "Calling webhook for Datto credentials…" "Info" "FetchSites"
try {
$hdr = @{ "SVSMSPKit" = $pw }
$resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl -Headers $hdr -Method GET
# store for later RMM calls
$Global:ApiUrl = $resp.ApiUrl
$Global:ApiKey = $resp.ApiKey
$Global:ApiSecretKey = $resp.ApiSecretKey
Write-LogHybrid "Fetched and stored API credentials." "Success" "FetchSites"
} catch {
Write-LogHybrid "Webhook call failed: $($_.Exception.Message)" "Error" "FetchSites" -LogToEvent
returnRespondEmpty $Context 403
return
}
# 3) Exchange for a bearer token
Write-LogHybrid "Requesting OAuth token" "Info" "FetchSites"
try {
$securePublic = ConvertTo-SecureString 'public' -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential('public-client',$securePublic)
$tokenResp = Invoke-RestMethod `
-Uri "$Global:ApiUrl/auth/oauth/token" `
-Credential $creds `
-Method Post `
-ContentType 'application/x-www-form-urlencoded' `
-Body "grant_type=password&username=$Global:ApiKey&password=$Global:ApiSecretKey"
$token = $tokenResp.access_token
Write-LogHybrid "OAuth token acquired." "Success" "FetchSites"
} catch {
Write-LogHybrid "OAuth request failed: $($_.Exception.Message)" "Error" "FetchSites"
returnRespondEmpty $Context 500
return
}
# 4) Pull the site list
Write-LogHybrid "Fetching Datto RMM site list" "Info" "FetchSites"
try {
$hdr = @{ Authorization = "Bearer $token" }
$sitesResp = Invoke-RestMethod -Uri "$Global:ApiUrl/api/v2/account/sites" `
-Method Get `
-Headers $hdr `
-ContentType 'application/json'
$siteList = $sitesResp.sites | ForEach-Object {
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
}
Write-LogHybrid "Site list retrieved ($($siteList.Count) sites)." "Success" "FetchSites"
} catch {
Write-LogHybrid "Failed to fetch site list: $($_.Exception.Message)" "Error" "FetchSites"
returnRespondEmpty $Context 500
return
}
# 5) Return JSON array
$json = $siteList | ConvertTo-Json -Depth 2
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.ContentType = 'application/json'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.OutputStream.Close()
}
# Helper function to consistently return an empty JSON array
function returnRespondEmpty {
param(
[Parameter(Mandatory)][object]$Context,
[Parameter(Mandatory)][ValidateRange(100,599)][int]$StatusCode = 500
)
# Always return an empty JSON array body
$empty = [Text.Encoding]::UTF8.GetBytes("[]")
# Set the desired status code and headers
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = 'application/json'
$Context.Response.ContentLength64 = $empty.Length
# Write and close
$Context.Response.OutputStream.Write($empty, 0, $empty.Length)
$Context.Response.OutputStream.Close()
}
# On-boarding handlers
function Handle-SetSVSPowerPlan {
param($Context)
# 1) call into your module
Set-SVSPowerPlan
# 2) log & write back a simple text response
Write-LogHybrid "PowerPlan set" "Success" "OnBoard"
Respond-Text $Context "PowerPlan applied"
}
function Handle-InstallSVSMSP {
param($Context)
Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard"
try {
Install-SVSMSP -InstallToolkit
Respond-Text $Context "SVSMSP Module installed/updated."
} catch {
Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard"
Respond-Text $Context "ERROR: $_"
}
}
function Handle-InstallCyberQP {
param($Context)
# 1) call into your module
Install-CyberQP
# 2) log & write back a simple text response
Write-LogHybrid "CyberQP installed" "Success" "OnBoard"
Respond-Text $Context "CyberQP installed"
}
function Handle-InstallThreatLocker {
param($Context)
# 1) call into your module
Install-ThreatLocker
# 2) log & write back a simple text response
Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard"
Respond-Text $Context "ThreatLocker installed"
}
function Handle-InstallRocketCyber {
param($Context)
# 1) call into your module
Install-RocketCyber
# 2) log & write back a simple text response
Write-LogHybrid "RocketCyber installed" "Success" "OnBoard"
Respond-Text $Context "RocketCyber installed"
}
function Handle-InstallSVSHelpDesk {
param($Context)
# 1) call into your module
Install-SVSHelpDesk
# 2) log & write back a simple text response
Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard"
Respond-Text $Context "SVS HelpDesk installed"
}
function Handle-InstallDattoRMM {
param($Context)
$req = $Context.Request
$resp = $Context.Response
if ($req.HttpMethod -ne 'POST') {
$resp.StatusCode = 405; $resp.ContentType = 'text/plain'
$resp.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('Use POST'),0,7)
$resp.OutputStream.Close(); return
}
# parse JSON body
$body = (New-Object IO.StreamReader $req.InputStream).ReadToEnd()
$data = $body | ConvertFrom-Json
$checked = $data.checkedValues
$uid = $data.UID
$name = $data.Name
try {
Install-DattoRMM `
-ApiUrl $Global:ApiUrl `
-ApiKey $Global:ApiKey `
-ApiSecretKey $Global:ApiSecretKey `
-SiteUID $uid `
-SiteName $name `
-PushSiteVars:($checked -contains 'inputVar') `
-InstallRMM: ($checked -contains 'rmm') `
-SaveCopy: ($checked -contains 'exe')
Write-LogHybrid "RMM install triggered for $name" "Success" "DattoRMM"
$resp.StatusCode = 200
$responseString = "Triggered DattoRMM for $name"
}
catch {
Write-LogHybrid "Error in Install-DattoRMM: $_" "Error" "DattoRMM"
$resp.StatusCode = 500
$responseString = "ERROR: $($_.Exception.Message)"
}
$b = [Text.Encoding]::UTF8.GetBytes($responseString)
$resp.ContentType = 'text/plain'
$resp.ContentLength64 = $b.Length
$resp.OutputStream.Write($b,0,$b.Length)
$resp.OutputStream.Close()
}
#--------------------------------------------------------------------------
# Handle function for the Tweaks tab
#--------------------------------------------------------------------------
function Handle-SetEdgeDefaultSearch {
param($Context)
# Read which radio was chosen:
# You’ll receive a GET or POST? In the existing pattern, “tweaks” is GET‐only,
# so you can’t get the radio values over GET. You’ll probably want to change
# those “fetch(t.handler, { method: 'GET' })” calls in JS to send a POST
# with JSON containing all the checked‐plus‐radio values.
# Here’s a simple stub:
Write-LogHybrid "Handle-SetEdgeDefaultSearch called" "Info" "Tweaks"
# Response back:
Respond-Text $Context "Applied SetEdgeDefaultSearch with whatever was selected."
}
function Handle-SetTextEditor {
param($Context)
Write-LogHybrid "Handle-SetTextEditor called" "Info" "Tweaks"
Respond-Text $Context "Text editor preference applied."
}
function Handle-SetWindowsPerformance {
param($Context)
Write-LogHybrid "Handle-SetWindowsPerformance called" "Info" "Tweaks"
Respond-Text $Context "WindowsPerformance tweak applied."
}
function Handle-StopUnnecessaryServices {
param($Context)
Write-LogHybrid "Handle-StopUnnecessaryServices called" "Info" "Tweaks"
Respond-Text $Context "Stop-UnnecessaryServices tweak applied."
}
function Handle-SetBrowserPwdPolicy {
param($Context)
Write-LogHybrid "Handle-SetBrowserPwdPolicy called" "Info" "Tweaks"
Respond-Text $Context "Browser password policy tweak applied."
}
# Off-boarding handlers
function Handle-UninstallCyberQP {
param($Context)
# 1) call into your module
Uninstall-CyberQP
Write-LogHybrid "CyberQP uninstalled" "Success" "OffBoard"
Respond-Text $Context "CyberQP uninstalled"
}
function Cleanup-SVSMSP {
param($Context)
Write-LogHybrid "SVSMSP cleaned up" "Success" "OffBoard"
Respond-Text $Context "SVSMSP cleaned up"
}
# Tweaks handler
function Disable-Animations {
param($Context)
Write-LogHybrid "Animations disabled" "Success" "Tweaks"
Respond-Text $Context "Animations disabled"
}
# SVSApps handler
function Install-WingetLastPass {
param($Context)
Write-LogHybrid "Winget LastPass installed" "Success" "SVSApps"
Respond-Text $Context "Winget LastPass installed"
}
#endregion
#region UI Generation
function Build-Checkboxes {
param(
[string] $Page,
[string] $Column
)
# For each task in the given Page & Column, build a