Files
Logo/testTaskGate.ps1
2025-05-22 21:13:54 -04:00

777 lines
27 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#region Config & Task Definitions
# Listening port for HTTP UI
$Port = 8081
# 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='Set-SVSPowerPlan'; Page='onboard'; Column='left' },
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Install-SVSMSPModule'; Page='onboard'; Column='left' },
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Install-CyberQP'; Page='onboard'; Column='left' },
@{ Id='installSplashtop'; Name='installSplashtop'; Label='Install Splashtop'; HandlerFn='Install-Splashtop'; Page='onboard'; Column='left' },
@{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Install-SVSHelpDesk'; Page='onboard'; Column='left' },
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Install-ThreatLocker'; Page='onboard'; Column='left' },
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Install-RocketCyber'; Page='onboard'; Column='left' },
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallRMM'; Page='onboard'; Column='left';
SubOptions= @(
@{ Value='inputVar'; Label='Copy Site Variables' },
@{ Value='rmm'; Label='Install DRMM Agent' },
@{ Value='exe'; Label='Download .exe' }
)
},
# 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';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
@{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' },
# 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="📝"; Warning="⚠️"; Error=""; Success=""; General="📦" }[$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 "⚠️ [EventLog] Failed to write: $($_.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 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
# POST /getpw → read JSON body, call helper, return JSON
function Handle-FetchSites {
param($Context)
Write-Host "[Debug] Handle-FetchSites invoked" # ← add this
try {
$body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$pw = (ConvertFrom-Json $body).password
$sites = Install-DattoRMM-Helper `
-ApiUrl $ApiUrl `
-ApiKey $ApiKey `
-ApiSecretKey $ApiSecretKey `
-FetchSitesOnly
if (-not $sites) { $Context.Response.StatusCode = 500; $sites = @() }
$json = $sites | ConvertTo-Json -Depth 2
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.ContentType = 'application/json'
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
} catch {
$Context.Response.StatusCode = 500
$Context.Response.ContentType = 'application/json'
$Context.Response.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('[]'),0,2)
} finally {
$Context.Response.OutputStream.Close()
}
}
# On-boarding handlers
function Set-SVSPowerPlan {
param($Context)
Write-LogHybrid "PowerPlan set" "Success" "OnBoard"
Respond-Text $Context "Powerplan applied"
}
function Install-SVSMSPModule {
param($Context)
Write-LogHybrid "SVSMSP Module installed" "Success" "OnBoard"
Respond-Text $Context "SVSMSP Module installed"
}
function Install-CyberQP {
param($Context)
Write-LogHybrid "CyberQP installed" "Success" "OnBoard"
Respond-Text $Context "CyberQP installed"
}
# Off-boarding handlers
function Uninstall-CyberQP {
param($Context)
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($Page, $Column)
(
$Global:Tasks |
Where-Object Page -EQ $Page |
Where-Object Column -EQ $Column |
ForEach-Object {
# 1) render the main checkbox
$html = "<label><input type='checkbox' id='$($_.Id)' name='$($_.Name)' data-column='$Column'> $($_.Label)</label>"
# 2) if this task has SubOptions, render them in a hidden container
if ($_.SubOptions) {
Write-Host "👉 Rendering SubOptions for task $($_.Id)"
$subHtml = (
$_.SubOptions | ForEach-Object {
"<label style='margin-left:20px; display:block;'>
<input type='checkbox' name='$($_.Value)' value='$($_.Value)'> $($_.Label)
</label>"
}
) -join "`n"
$html += @"
<div id='${($_.Id)}OptionsContainer' style='display:none; margin-top:4px;'>
$subHtml
</div>
"@
}
$html
}
) -join "`n"
}
function Get-UIHtml {
param([string]$Page = 'onboard')
#
# 1) Inline your full original CSS here
#
$style = @'
<style>
:root {
/* Cool Palette */
--background-color: rgba(18, 18, 18, 1);
--border-color: rgba(255,127,0,0.25);
/* Neutral Colors */
--white-color: rgba(255,255,255);
--gray-color: rgba(102,102,102);
--dark-gray-color: rgba(51,51,51);
--light-gray-color: rgba(187,187,187);
/* Sidebar Button Colors */
--btn-sidebar-light-gray: rgba(68,68,68);
--btn-sidebar-blue: rgba(30,144,255,1);
--btn-hover: rgba(0,86,179,1);
--btn-hover-scale: 1.05;
/* Button Colors */
--btn-success: rgba(40,167,69);
--btn-success-disabled: rgba(108,117,125);
--btn-danger: rgba(220,53,69);
}
body {
font-family: Arial, sans-serif;
margin: 0; padding: 0;
background-color: var(--background-color);
color: var(--white-color);
height: 100%; overflow: hidden;
}
.logo-container { text-align:left; padding:20px; }
.logo-container img { max-width:300px; height:auto; }
.subtitle { font-size: 1.2rem; color: var(--gray-color); margin-top: 0.5em; }
.container { display:flex; height:100vh; overflow:hidden; }
.sidebar { width:200px; background:var(--background-color); padding:10px; }
.sidebar button {
display:block; width:100%; margin-bottom:10px; padding:10px;
color:var(--white-color); background:var(--btn-sidebar-light-gray);
border:none; border-radius:5px; cursor:pointer; text-align:left;
transition:background-color 0.3s, transform 0.2s;
}
.sidebar button.active { background:var(--btn-sidebar-blue); }
.sidebar button:hover {
background:var(--btn-hover); transform:scale(var(--btn-hover-scale));
}
.content {
position: relative;
flex:1;
padding:20px;
overflow-y:auto;
max-height:calc(100vh - 50px);
}
.fixed-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px; /* space between Exit and Run */
z-index: 1000;
}
.exit-button,
.run-button {
border: none;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
color: var(--white-color);
}
.exit-button {
background-color: var(--btn-danger);
}
.run-button {
background-color: var(--btn-success);
}
.tab-content { display:none; }
.tab-content.active { display:block; }
.columns-container {
display:flex; gap:20px; flex-wrap:wrap; align-items:flex-start;
}
/* column styling, same as old script */
.column {
flex: 1; /* fill available space */
max-width: 45%; /* or whatever width you like */
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 10px;
background-color: var(--dark-gray-color);
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.checkbox-group label {
display:flex; align-items:center; margin-bottom:8px;
}
.button-group { text-align:right; margin-top:20px; }
.exit-button {
background:var(--btn-danger); color:var(--white-color);
padding:10px 20px; border:none; border-radius:5px; cursor:pointer;
}
@media (max-width:768px) {
.container { flex-direction:column; }
.sidebar { width:100%; }
}
</style>
'@
$script = @'
<script>
// =======================================================================
// Tab Navigation
// =======================================================================
const tabButtons = document.querySelectorAll(".tab-button");
const tabContents = document.querySelectorAll(".tab-content");
tabButtons.forEach(btn => {
btn.addEventListener("click", () => {
// clear active state
tabButtons.forEach(b => b.classList.remove("active"));
tabContents.forEach(c => c.classList.remove("active"));
// set new active
btn.classList.add("active");
document.getElementById(btn.dataset.tab).classList.add("active");
});
});
// initialize default tab on load
document.querySelector(".tab-button[data-tab='{{defaultPage}}Tab']").classList.add("active");
document.getElementById("{{defaultPage}}Tab").classList.add("active");
// =======================================================================
// Task Trigger
// =======================================================================
const tasks = [
{{tasksJsAll}}
];
// =======================================================================
// Column “Select All” toggling for On-Boarding
// =======================================================================
function toggleColumn(col) {
const master = document.getElementById(
`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`
);
document
.querySelectorAll(
`#onboardTab input[type=checkbox][data-column=${col}]`
)
.forEach(cb => (cb.checked = master.checked));
}
// =======================================================================
// Un-check “Select All” if any child is unchecked (& recheck if all are checked)
// =======================================================================
function updateSelectAll(col) {
const master = document.getElementById(
`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`
);
const children = document.querySelectorAll(
`#onboardTab input[type=checkbox][data-column=${col}]`
);
master.checked = Array.from(children).every(cb => cb.checked);
}
// Attach listeners on load
['left','right'].forEach(col => {
document
.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`)
.forEach(cb => cb.addEventListener('change', () => updateSelectAll(col)));
});
// =======================================================================
// DattoRMM Options
// =======================================================================
function toggleDattoRMMOptions() {
const master = document.getElementById('installDattoRMM');
const container = document.getElementById('installDattoRMMOptionsContainer');
if (!container) return;
container.style.display = master.checked ? 'block' : 'none';
container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = master.checked);
}
document.addEventListener('DOMContentLoaded', () => {
const master = document.getElementById('installDattoRMM');
if (master) master.addEventListener('change', toggleDattoRMMOptions);
});
// =======================================================================
// Fetch Sites Handler
// =======================================================================
async function fetchSites() {
const pwd = document.getElementById("Password").value;
if (!pwd) { alert("Please enter the password."); return; }
try {
const resp = await fetch("/getpw", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pwd })
});
if (!resp.ok) throw("HTTP " + resp.status);
const sites = await resp.json();
const dd = document.getElementById("dattoDropdown");
dd.innerHTML = ""; // clear old
sites.forEach(s => {
const opt = document.createElement("option");
opt.value = s.UID;
opt.text = s.Name;
dd.appendChild(opt);
});
document.getElementById("dattoRmmContainer").style.display = "block";
}
catch (e) {
console.error(e);
alert("Failed to fetch sites. Check password and try again.");
}
}
async function triggerInstall() {
for (const t of tasks) {
const cb = document.getElementById(t.id);
if (cb && cb.checked) {
try {
await fetch(t.handler, { method: "GET" });
} catch (e) {
console.error(`Error running ${t.label}:`, e);
}
}
}
}
// =======================================================================
// Shutdown Handler
// =======================================================================
function endSession() {
fetch("/quit", { method: "GET" })
.finally(() => window.close());
}
</script>
'@
#
# 3) The HTML skeleton with placeholders
#
$htmlTemplate = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>SVS TaskGate</title>
<link rel="icon" href="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_Favicon.ico">
$style
</head>
<body>
<div class="logo-container">
<img src="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_logo.svg" alt="SVS Logo">
</div>
<div class="container">
<div class="sidebar">
<button class="tab-button" data-tab="onboardTab">On-Boarding</button>
<button class="tab-button" data-tab="offboardTab">Off-Boarding</button>
<button class="tab-button" data-tab="tweaksTab">Tweaks</button>
<button class="tab-button" data-tab="SVSAppsTab">SVS APPs</button>
</div>
<div class="content">
<div id="onboardTab" class="tab-content">
<h2>On-Boarding</h2>
<h3 class="subtitle">This new deployment method ensures everything is successfully deployed with greater ease!</h3>
<!-- 1) Dynamic task checkboxes -->
<div class="columns-container">
<div class="checkbox-group column">
<h3>SVSMSP Stack</h3>
<label><input type="checkbox" id="selectAllLeftCheckbox" onclick="toggleColumn('left')"> Select All</label>
{{onboardLeftColumn}}
</div>
<div class="checkbox-group column">
<h3>Optional</h3>
<label><input type="checkbox" id="selectAllRightCheckbox" onclick="toggleColumn('right')"> Select All</label>
{{onboardRightColumn}}
</div>
</div>
<!-- 1) password & fetch button -->
<div id="PasswordContainer" style="margin-bottom:1em;">
<label for="Password">Password:</label>
<input type="password" id="Password" placeholder="Enter password" />
<button onclick="fetchSites()">Fetch Sites</button>
</div>
<!-- 2) Datto RMM dropdown (populated after fetch) -->
<div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
<label for="dattoDropdown">Select Datto Site:</label>
<select id="dattoDropdown"></select>
</div>
<div id="offboardTab" class="tab-content">
<h2>Off-Boarding</h2>
<div class="columns-container">
{{offboardCheckboxes}}
</div>
</div>
<div id="tweaksTab" class="tab-content">
<h2>Tweaks</h2>
<div class="columns-container">
{{tweaksCheckboxes}}
</div>
</div>
<div id="SVSAppsTab" class="tab-content">
<h2>SVS APPs</h2>
<div class="columns-container">
{{appsCheckboxes}}
</div>
</div>
</div>
</div>
$script
<!-- floating button group -->
<div class="fixed-buttons">
<button class="exit-button" onclick="endSession()">Exit</button>
<button class="run-button" onclick="triggerInstall()">Run Selected</button>
</div>
</body>
</html>
"@
#
# 4) Build the checkbox HTML and tasks JS from $Global:Tasks
#
# On-boarding now has two columns:
$onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
$onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
# Off-boarding, Tweaks, SVSApps stay one-column:
$offboard = Build-Checkboxes -Page 'offboard' -Column ''
$tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
$apps = Build-Checkboxes -Page 'SVSApps' -Column ''
# Tasks JS array (fixed)
$tasksJsAll = (
$Global:Tasks | ForEach-Object {
" { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
}
) -join ",`n"
#
# 5) Inject into template
#
$html = $htmlTemplate
$html = $html.Replace('{{onboardLeftColumn}}', $onboardLeft)
$html = $html.Replace('{{onboardRightColumn}}', $onboardRight)
$html = $html.Replace('{{offboardCheckboxes}}', $offboard)
$html = $html.Replace('{{tweaksCheckboxes}}', $tweaks)
$html = $html.Replace('{{appsCheckboxes}}', $apps)
$html = $html.Replace('{{tasksJsAll}}', $tasksJsAll)
$html = $html.Replace('{{defaultPage}}', $Page)
return $html
}
#endregion
#region HTTP Listener & Routing
# Handle shutdown command
if ($path -eq 'quit') {
Write-LogHybrid "Shutdown requested" "Info" "Server"
Respond-Text $Context "Server shutting down."
# This will break out of the while loop in Start-Server
$Global:Listener.Stop()
return
}
# Sends the HTML for a given page or invokes a task handler
function Dispatch-Request {
param($Context)
# figure out the path
$path = $Context.Request.Url.AbsolutePath.TrimStart('/')
# ---- Shutdown handler ----
if ($path -eq 'quit') {
Write-LogHybrid "Shutdown requested" "Info" "Server"
Respond-Text $Context "Server shutting down."
# stop the listener loop
$Global:Listener.Stop()
return
}
# ---- Fetch Sites endpoint ----
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
Handle-FetchSites $Context
return
}
# ---- Serve UI pages ----
if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) {
$page = if ($path -eq '') { 'onboard' } else { $path }
$html = Get-UIHtml -Page $page
Respond-HTML $Context $html
return
}
# ---- Task invocation ----
$task = $Global:Tasks | Where-Object Name -EQ $path
if ($task) {
& $task.HandlerFn $Context
return
}
# ---- 404 ----
$Context.Response.StatusCode = 404
Respond-Text $Context '404 - Not Found'
}
# Starts the HTTP listener loop
function Start-Server {
# make it accessible to Dispatch-Request
$Global:Listener = [System.Net.HttpListener]::new()
$Global:Listener.Prefixes.Add("http://localhost:$Port/")
$Global:Listener.Start()
Write-Host "Listening on http://localhost:$Port/ ..."
try {
while ($Global:Listener.IsListening) {
$ctx = $Global:Listener.GetContext()
try {
Dispatch-Request $ctx
} catch {
Write-LogHybrid "Dispatch error: $_" "Error" "Server"
}
}
} finally {
# once the loop exits, clean up
$Global:Listener.Close()
Write-LogHybrid "Listener closed." "Info" "Server"
}
}
#endregion
# Bootstrap: launch the server
Start-Server