diff --git a/StackMonkey.ps1 b/StackMonkey.ps1
new file mode 100644
index 0000000..0f4cc6e
--- /dev/null
+++ b/StackMonkey.ps1
@@ -0,0 +1,1453 @@
+# 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