Truncated history

new root based on 0bd16c9
This commit is contained in:
2025-12-21 02:25:43 -05:00
commit fa307eaceb
19 changed files with 3973 additions and 0 deletions

BIN
Drivers/HP/UPD/package.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,167 @@
function New-SamyPrinterProfileJson {
<#
.SYNOPSIS
Generates a SAMY printer profile JSON template from existing printers
and optionally uploads it to a Git (Gitea) repository.
.DESCRIPTION
Enumerates local printers via Get-Printer, maps them into SAMY printer
profile objects, and writes them to a JSON file. The JSON is intended
as a starting point / template for building printers.json used by SAMY.
Each profile includes:
- ClientCode (from parameter)
- Location (from parameter)
- ProfileName (defaults to printer Name)
- DisplayName (printer Name)
- Type (TcpIp or Shared, best-effort guess)
- Address (for TCP/IP printers)
- PrintServer (for shared printers)
- ShareName (for shared printers)
- DriverName (printer DriverName)
- DriverInfPath, DriverPackagePath, DriverInfName (empty placeholders)
- IsDefault (true if this printer is default)
Optionally, the generated JSON can be uploaded to a Git repo using
a personal access token (PAT) passed as a SecureString.
.PARAMETER ClientCode
MSP/client code to stamp into each profile (for example "SVS").
.PARAMETER Location
Human-friendly location (for example "Embrun"). Used both as a field in
each profile and as part of the default JSON file name.
.PARAMETER OutputPath
Folder where the JSON file will be saved. Default is:
C:\ProgramData\SVS\Samy\Printers
.PARAMETER UploadToGit
When set, the function will attempt to upload the generated JSON file
to the specified Git (Gitea) repository and path.
.PARAMETER GitApiBase
Base URL for the Git API, for example:
https://git.svstools.ca/api/v1
.PARAMETER GitRepo
Repository identifier in the form "Owner/Repo", for example:
SVS_Public_Repo/SAMY
.PARAMETER GitBranch
Branch name to write to. Default is "beta".
.PARAMETER GitPath
Path inside the repo where the JSON should be written, for example:
Printers/SVS/Embrun/printers.json
.PARAMETER GitToken
Personal access token as a SecureString. Recommended source:
a secret environment variable (for example $env:GIT_PAT) converted via
ConvertTo-SecureString.
.EXAMPLE
New-SamyPrinterProfileJson -ClientCode "SVS" -Location "Embrun"
Generates a printers_SVS_Embrun.json in:
C:\ProgramData\SVS\Samy\Printers
.EXAMPLE
$secureToken = ConvertTo-SecureString $env:GIT_PAT -AsPlainText -Force
New-SamyPrinterProfileJson `
-ClientCode "SVS" `
-Location "Embrun" `
-UploadToGit `
-GitApiBase "https://git.svstools.ca/api/v1" `
-GitRepo "SVS_Public_Repo/SAMY" `
-GitBranch "beta" `
-GitPath "Printers/SVS/Embrun/printers.json" `
-GitToken $secureToken
Generates the JSON locally and uploads it to the specified path
in the Git repository.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ClientCode,
[Parameter(Mandatory = $true)]
[string]$Location,
[string]$OutputPath = "C:\ProgramData\SVS\Samy\Printers",
[switch]$UploadToGit,
[string]$GitApiBase,
[string]$GitRepo,
[string]$GitBranch = "beta",
[string]$GitPath,
[SecureString]$GitToken
)
try {
Write-Log "Starting New-SamyPrinterProfileJson for ClientCode='$ClientCode' Location='$Location'." "Info" "PrinterJson" -LogToEvent
# ------------------------------------------------------------------
# 1) Ensure output folder exists and build a safe file name
# ------------------------------------------------------------------
if (-not (Test-Path $OutputPath)) {
Write-Log "Creating output folder '$OutputPath'." "Info" "PrinterJson" -LogToEvent
New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
}
$safeLocation = $Location -replace '[^A-Za-z0-9_-]', '_'
$fileName = "printers_{0}_{1}.json" -f $ClientCode, $safeLocation
$filePath = Join-Path $OutputPath $fileName
# ------------------------------------------------------------------
# 2) Enumerate printers and build profile objects
# ------------------------------------------------------------------
$printers = Get-Printer -ErrorAction SilentlyContinue
if (-not $printers) {
Write-Log "No printers found on this system. JSON will be empty." "Warning" "PrinterJson" -LogToEvent
} else {
Write-Log ("Found {0} printer(s)." -f $printers.Count) "Info" "PrinterJson" -LogToEvent
}
$profiles = @()
foreach ($p in $printers) {
$profileName = $p.Name
$displayName = $p.Name
$driverName = $p.DriverName
$portName = $p.PortName
$isDefault = $p.Shared -eq $false -and $p.Default -eq $true
# Try to resolve port details
$port = $null
if ($portName) {
$port = Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue
}
$type = "TcpIp"
$address = $null
$printServer = $null
$shareName = $null
if ($port -and $port.PrinterHostAddress) {
# Standard TCP/IP port
$type = "TcpIp"
$address = $port.PrinterHostAddress
}
elseif ($p.Shared -and $p.ShareName) {
# Best guess at a shared printer
$type = "Shared"
$shareName = $p.ShareName
$printServer = $env:COMPUTERNAME
}
$profiles += [PSCustomObject]@{
ClientCode = $ClientCode
Location = $Location
ProfileName = $profileName
DisplayName = $displayName

BIN
SAMY.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
SAMY2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
SVS_Favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

23
SVS_logo.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 480 140">
<!-- Generator: Adobe Illustrator 29.3.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 91) -->
<defs>
<style>
.st0 {
fill: #750000;
}
.st1 {
fill: #267eb2;
}
</style>
</defs>
<path class="st1" d="M155.08,115.14c-5.99,0-11.27-.66-15.86-1.99s-8.6-3.2-12.05-5.63c-3.45-2.42-6.54-5.27-9.27-8.53l14.21-15.92c3.79,4.85,7.75,8.05,11.88,9.61,4.13,1.55,8.01,2.33,11.65,2.33,1.44,0,2.73-.13,3.87-.4,1.14-.26,2.01-.7,2.61-1.31.61-.61.91-1.44.91-2.5,0-.98-.32-1.82-.97-2.5s-1.5-1.27-2.56-1.76-2.22-.91-3.47-1.25-2.46-.62-3.64-.85c-1.18-.23-2.22-.46-3.13-.68-4.55-1.06-8.53-2.35-11.94-3.87-3.41-1.51-6.25-3.33-8.53-5.46-2.27-2.12-3.96-4.55-5.06-7.28s-1.65-5.8-1.65-9.21c0-3.87.89-7.39,2.67-10.57s4.17-5.91,7.16-8.19c2.99-2.27,6.4-4.02,10.23-5.23,3.83-1.21,7.79-1.82,11.88-1.82,5.99,0,10.99.55,15.01,1.65,4.02,1.1,7.39,2.67,10.12,4.72,2.73,2.05,5.08,4.43,7.05,7.16l-14.32,13.76c-1.67-1.59-3.41-2.9-5.23-3.92-1.82-1.02-3.69-1.78-5.63-2.27-1.93-.49-3.85-.74-5.74-.74-1.74,0-3.22.13-4.43.4s-2.16.68-2.84,1.25-1.02,1.35-1.02,2.33.44,1.8,1.31,2.44,1.97,1.19,3.3,1.65c1.32.46,2.65.82,3.98,1.08,1.33.27,2.44.47,3.35.62,4.17.76,8,1.8,11.48,3.13,3.49,1.33,6.54,3,9.15,5,2.61,2.01,4.62,4.51,6.03,7.5,1.4,3,2.1,6.54,2.1,10.63,0,5.84-1.46,10.73-4.38,14.67s-6.84,6.92-11.77,8.92c-4.93,2.01-10.42,3.01-16.48,3.01l.02.02Z"/>
<path class="st1" d="M219.31,114.01l-31.72-79.58h24.56l12.73,34.79c.91,2.35,1.65,4.36,2.22,6.03s1.08,3.24,1.53,4.72c.46,1.48.89,3.05,1.31,4.72.42,1.67.89,3.71,1.42,6.14h-3.98c.76-3.18,1.42-5.8,1.99-7.84.57-2.05,1.21-4.07,1.93-6.08.72-2.01,1.65-4.56,2.79-7.67l12.73-34.79h23.76l-31.95,79.58h-19.33v-.02Z"/>
<path class="st1" d="M302.98,115.14c-5.99,0-11.27-.66-15.86-1.99-4.59-1.33-8.6-3.2-12.05-5.63-3.45-2.42-6.54-5.27-9.27-8.53l14.21-15.92c3.79,4.85,7.75,8.05,11.88,9.61,4.13,1.55,8.02,2.33,11.65,2.33,1.44,0,2.73-.13,3.87-.4,1.14-.26,2.01-.7,2.62-1.31s.91-1.44.91-2.5c0-.98-.32-1.82-.97-2.5-.64-.68-1.5-1.27-2.56-1.76s-2.22-.91-3.47-1.25-2.46-.62-3.64-.85-2.22-.46-3.13-.68c-4.55-1.06-8.53-2.35-11.94-3.87-3.41-1.51-6.25-3.33-8.53-5.46-2.27-2.12-3.96-4.55-5.06-7.28s-1.65-5.8-1.65-9.21c0-3.87.89-7.39,2.67-10.57,1.78-3.18,4.17-5.91,7.16-8.19,2.99-2.27,6.4-4.02,10.23-5.23,3.83-1.21,7.79-1.82,11.88-1.82,5.99,0,10.99.55,15.01,1.65s7.39,2.67,10.12,4.72,5.08,4.43,7.05,7.16l-14.33,13.76c-1.67-1.59-3.41-2.9-5.23-3.92-1.82-1.02-3.69-1.78-5.63-2.27-1.93-.49-3.85-.74-5.74-.74-1.74,0-3.22.13-4.43.4s-2.16.68-2.84,1.25c-.68.57-1.02,1.35-1.02,2.33s.44,1.8,1.31,2.44,1.97,1.19,3.3,1.65,2.65.82,3.98,1.08c1.33.27,2.44.47,3.35.62,4.17.76,8,1.8,11.48,3.13,3.49,1.33,6.54,3,9.15,5,2.62,2.01,4.62,4.51,6.03,7.5,1.4,3,2.1,6.54,2.1,10.63,0,5.84-1.46,10.73-4.38,14.67s-6.84,6.92-11.77,8.92c-4.93,2.01-10.42,3.01-16.48,3.01l.02.02Z"/>
<path class="st1" d="M345.72,72.6v-39.35h10.45l14.33,23.33-8.49-.06,14.5-23.27h10.12v39.35h-11.69v-9.39c0-3.37.08-6.41.25-9.11.17-2.7.46-5.38.87-8.04l1.35,3.54-9.5,14.73h-3.71l-9.33-14.73,1.41-3.54c.41,2.51.7,5.09.87,7.73s.25,5.78.25,9.42v9.39h-11.69.01Z"/>
<path class="st1" d="M409.4,73.16c-2.96,0-5.57-.33-7.84-.98-2.27-.66-4.25-1.58-5.96-2.78s-3.23-2.6-4.58-4.22l7.03-7.87c1.87,2.4,3.83,3.98,5.87,4.75s3.96,1.15,5.76,1.15c.71,0,1.35-.07,1.91-.2s.99-.35,1.29-.65.45-.71.45-1.24c0-.49-.16-.9-.48-1.24s-.74-.63-1.26-.87c-.53-.24-1.1-.45-1.71-.62-.62-.17-1.22-.31-1.8-.42-.58-.11-1.1-.22-1.55-.34-2.25-.52-4.22-1.16-5.9-1.91-1.69-.75-3.09-1.65-4.22-2.7-1.12-1.05-1.96-2.25-2.5-3.6s-.81-2.87-.81-4.55c0-1.91.44-3.65,1.32-5.23.88-1.57,2.06-2.92,3.54-4.05s3.17-1.99,5.06-2.58,3.85-.9,5.87-.9c2.96,0,5.43.27,7.42.81s3.65,1.32,5,2.33c1.35,1.01,2.51,2.19,3.49,3.54l-7.08,6.8c-.83-.79-1.69-1.43-2.59-1.94s-1.83-.88-2.78-1.12c-.96-.24-1.9-.37-2.84-.37-.86,0-1.59.07-2.19.2s-1.07.34-1.41.62c-.34.28-.51.67-.51,1.15s.21.89.65,1.21c.43.32.97.59,1.63.82.66.22,1.31.4,1.97.53s1.21.23,1.66.31c2.06.38,3.95.89,5.68,1.55,1.72.66,3.23,1.48,4.53,2.47s2.29,2.23,2.98,3.71c.69,1.48,1.04,3.23,1.04,5.26,0,2.89-.72,5.3-2.16,7.25s-3.38,3.42-5.82,4.41-5.15,1.49-8.15,1.49v.02Z"/>
<path class="st1" d="M431.66,72.6v-39.35h17.71c2.7,0,5.1.58,7.2,1.74,2.1,1.16,3.75,2.75,4.95,4.78,1.2,2.02,1.8,4.35,1.8,6.97s-.6,5.17-1.8,7.31c-1.2,2.14-2.85,3.81-4.95,5.03-2.1,1.22-4.5,1.83-7.2,1.83h-5.56v11.69h-12.15ZM443.58,50.57h3.54c.71,0,1.35-.14,1.91-.42s1-.68,1.32-1.21c.32-.52.48-1.18.48-1.97s-.16-1.42-.48-1.91-.76-.85-1.32-1.1c-.56-.24-1.2-.37-1.91-.37h-3.54v6.97h0Z"/>
<polyline class="st1" points="17.09 25.67 109.17 25.67 63.13 108.6"/>
<polyline class="st0" points="73.19 103.73 113.01 32 125.59 32 79.54 114.93 73.19 103.73"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

70
docs/Datto.md Normal file
View File

@@ -0,0 +1,70 @@
<#
.SYNOPSIS
Installs/configures the Datto RMM agent, fetches site lists, and optionally saves the site list to disk.
.DESCRIPTION
Centralizes Datto RMM operations in one function:
- Fetch API credentials from a webhook (-UseWebhook)
- Acquire OAuth token
- Fetch site list (-FetchSites)
- Save site list to Desktop as JSON or CSV (-FetchSites + -SaveSitesList)
- Write site variables to registry (-PushSiteVars)
- Download & launch the RMM agent installer (-InstallRMM)
- Save a copy of the installer (-SaveCopy)
.PARAMETER UseWebhook
Fetches ApiUrl, ApiKey, and ApiSecretKey from the webhook when used with WebhookPassword.
.PARAMETER WebhookPassword
Password for authenticating to the credentials webhook.
.PARAMETER WebhookUrl
URL of the credentials webhook. Defaults to $Global:DattoWebhookUrl.
.PARAMETER ApiUrl
Direct Datto API endpoint URL (if not using webhook).
.PARAMETER ApiKey
Direct Datto API key (if not using webhook).
.PARAMETER ApiSecretKey
Direct Datto API secret (if not using webhook).
.PARAMETER FetchSites
Fetches the list of sites and skips all install steps.
.PARAMETER SaveSitesList
Saves the fetched site list to Desktop using OutputFile. Must be used with -FetchSites.
.PARAMETER OutputFile
Filename for saving the site list (.json or .csv). Defaults to 'datto_sites.csv'.
.PARAMETER PushSiteVars
Writes fetched site variables into HKLM:\Software\SVS\Deployment.
.PARAMETER InstallRMM
Downloads and runs the Datto RMM agent installer.
.PARAMETER SaveCopy
Saves a copy of the downloaded agent installer to C:\Temp.
.PARAMETER SiteUID
Unique identifier of the Datto site (required for install and registry push).
.PARAMETER SiteName
Friendly name of the Datto site (used for logging).
.EXAMPLE
# Fetch and save site list via webhook
Install-DattoRMM -UseWebhook -WebhookPassword 'Tndmeeisdwge!' -FetchSites -SaveSitesList -OutputFile 'sites.csv'
.EXAMPLE
# Headless install with site variables
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM
.EXAMPLE
# Download and save installer to C:\Temp without installing
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -SaveCopy
#>

120
docs/SAMY.help.md Normal file
View File

@@ -0,0 +1,120 @@
<#
.SYNOPSIS
Script Automation Monkey (SAMY) is a unified MSP assistant that automates onboarding, headless offboarding,
Datto RMM deployments, printer provisioning, and toolkit management through a local UI, HTTP endpoints,
or direct PowerShell switches.
.DESCRIPTION
SAMY can be used in three ways:
1) Local UI (default) - launches a local web UI and exposes endpoints for tasks.
2) Headless / RMM mode - run onboarding/offboarding/Datto flows using PowerShell switches.
3) Toolkit-only / cleanup - install or remove the SVSMSP module and related artifacts.
The Datto helper (Install-DattoRMM) centralizes:
- Credential retrieval (webhook or direct)
- OAuth token acquisition
- Site list retrieval and optional persistence
- Registry variable writes (HKLM:\Software\SVS\Deployment)
- Agent download/launch and optional installer archiving
Printer endpoints provide:
- /getprinters (fetch profiles from server; optionally updates local config)
- /installprinters (installs requested printers; supports -WhatIf when enabled)
Throughout, secrets are never written to logs or console, and all operations produce clear
success/failure messages via Write-LogHybrid.
.CHANGES
- Windows 11 25H2: execution policy relaunch logic updated to improve reliability.
- UI template: HTML has been moved to samy.html (pulled from repo at runtime).
- UI hint message: now controlled via the {{SamyHintText}} placeholder in samy.html.
Recommended: set $Script:SamyHintText in the script or via a config file (future enhancement).
- Printers: Invoke-InstallPrinters supports enabling -WhatIf on Invoke-SVSPrinterInstall
(see Invoke-InstallPrinters; the line is commented by default).
- Assets: branch/base configuration is centralized in the SAMY asset config section
(SamyBranch/SamyRepoBase). Update those values once to switch UI/CSS/JS/HTML assets.
.CONFIGURATION
SAMY asset config (branch + repo base):
$Script:SamyBranch = 'beta' # or 'main'
$Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch'
UI hint message:
- samy.html contains: {{SamyHintText}}
- PowerShell sets: $Script:SamyHintText = "Please use samy.svstools.ca"
.PARAMETER SilentInstall
Toolkit-only mode. Installs/updates the SVSMSP toolkit module and exits (no UI).
.PARAMETER Cleanup
Removes the SVSMSP module, unregisters repos, and clears related artifacts (including registry keys).
.PARAMETER Offboard
Runs every offboarding task sequentially (same behavior as checking "Select All" in the Off-Boarding tab),
without launching the web UI.
.PARAMETER UseWebhook
Fetches Datto API credentials from the webhook at WebhookUrl using WebhookPassword.
.PARAMETER WebhookPassword
Password used to authenticate to the credentials webhook. Mandatory when -UseWebhook is set
for headless Datto modes. May be blank when using an allowlisted IP flow (server-side).
.PARAMETER WebhookUrl
URL of the credentials webhook endpoint. Defaults to $Global:DattoWebhookUrl.
.PARAMETER FetchSites
Fetches the list of Datto RMM sites and skips all install/variable-push actions.
.PARAMETER SaveSitesList
Saves the fetched site list to the desktop as OutputFile. Must be used with -FetchSites.
.PARAMETER OutputFile
Name of the file to write the site list to (must end in ".csv" or ".json").
Defaults to 'datto_sites.csv'.
.PARAMETER SiteUID
The unique identifier of the Datto RMM site. Mandatory when performing install or variable-push.
.PARAMETER SiteName
The friendly name of the Datto RMM site (used in logging). Mandatory when performing install or variable-push.
.PARAMETER PushSiteVars
Fetches site-specific variables and writes them under HKLM:\Software\SVS\Deployment.
.PARAMETER InstallRMM
Downloads and launches the Datto RMM agent installer for the specified site.
.PARAMETER SaveCopy
Saves a copy of the downloaded Datto RMM installer into C:\Temp.
.NOTES
Default EventLog : SVSMSP Events
Default Source : SAMY
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'https://samy.svstools.com' -UseBasicParsing).Content)) -UseWebhook -WebhookPassword 'pwd' SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM
# Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry.
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'https://samy.svstools.com' -UseBasicParsing).Content)) -UseWebhook -WebhookPassword 'pwd' -FetchSites -SaveSitesList -OutputFile 'sites.json'
# Fetches the full site list via webhook and saves it as JSON to your Desktop.
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'samy.svstools.ca' -UseBasicParsing).Content)) -SilentInstall
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'samy.svstools.com' -UseBasicParsing).Content)) -Cleanup
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'samy.svstools.ca' -UseBasicParsing).Content)) -Offboard
# Runs the off-boarding tasks in sequence without launching the UI.
.EXAMPLE
# Printer provisioning (called from RMM / scripts)
$profiles = Get-SvsPrinterProfilesFromServer -Uri 'https://bananas.svstools.ca/getprinters' -Password $pw
Set-SvsPrinterLocalConfig -PrinterProfiles $profiles -SkipIfEmpty
#>

6
docs/json.txt Normal file
View File

@@ -0,0 +1,6 @@
# 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

383
samy.css Normal file
View File

@@ -0,0 +1,383 @@
:root {
/* Cool Palette */
--background-color: rgba(18, 18, 18, 1);
--border-color: rgba(255, 255, 255, 0.15);
/* Neutral Colors */
--white-color: rgba(255, 255, 255, 1);
--gray-color: rgba(102, 102, 102, 1);
--dark-gray-color: rgba(51, 51, 51, 1);
--light-gray-color: rgba(187, 187, 187, 1);
/* Sidebar Button Colors */
--btn-sidebar-light-gray: rgba(68, 68, 68, 1);
--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, 1);
--btn-success-disabled: rgba(108, 117, 125, 1);
--btn-danger: rgba(220, 53, 69, 1);
/* Monkey + status panel settings */
--monkey-size: 160px; /* size of SAMY */
--monkey-bottom: 135px; /* how high from bottom of sidebar */
--status-gap: 20px; /* space between status box and monkey */
--status-height: 140px; /* max height of status box */
}
/* Make sizing easier to reason about */
*, *::before, *::after {
box-sizing: border-box;
}
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 {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
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 */
.sidebar {
width: 200px;
background: var(--background-color);
padding: 10px;
position: relative;
padding-bottom: calc(var(--monkey-bottom) + var(--monkey-size) + var(--status-gap) + 10px);
}
/* Status panel above monkey */
#status-box {
position: absolute;
left: 10px;
bottom: calc(var(--monkey-bottom) + var(--monkey-size) + var(--status-gap));
width: calc(100% - 20px);
max-height: var(--status-height);
overflow-y: auto;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
font-family: "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol", system-ui, sans-serif;
font-size: 12px;
line-height: 1.35;
z-index: 1;
}
/* SAMY bottom-left */
.sidebar::after {
content: "";
position: absolute;
left: 10px;
bottom: var(--monkey-bottom);
width: var(--monkey-size);
height: var(--monkey-size);
background-image: url("SAMY.png");
background-repeat: no-repeat;
background-size: contain;
opacity: 0.95;
pointer-events: none;
}
.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;
padding-bottom: 200px;
overflow-y: auto;
max-height: calc(100vh - 50px);
}
/* Floating buttons (Exit / Run) */
.fixed-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px;
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);
}
/* Standard buttons (shared look across the app) */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol", system-ui, sans-serif;
font-size: 14px;
font-weight: 600;
padding: 10px 14px;
min-height: 36px;
color: var(--white-color);
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
user-select: none;
transition: transform 0.12s ease, background-color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
.btn:hover { transform: scale(1.02); }
.btn:active { transform: scale(0.99); }
.btn:focus-visible {
outline: none;
border-color: var(--border-color);
box-shadow: 0 0 0 3px rgba(255, 127, 0, 0.18);
}
/* Variants */
.btn-primary {
background-color: var(--btn-sidebar-blue);
}
.btn-primary:hover {
background-color: var(--btn-hover);
}
.btn-success {
background-color: var(--btn-success);
}
.btn-success:hover {
background-color: rgba(30, 140, 60, 1);
}
.btn-danger {
background-color: var(--btn-danger);
}
.btn-danger:hover {
background-color: rgba(190, 40, 55, 1);
}
.btn:disabled,
.btn[aria-disabled="true"] {
background-color: var(--btn-success-disabled);
opacity: 0.75;
cursor: not-allowed;
transform: none;
}
/* Tabs */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Columns & checkboxes */
.columns-container {
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: flex-start;
}
.column {
flex: 1;
max-width: 45%;
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;
}
/* Datto password + site dropdown (On-Boarding) */
#PasswordContainer,
#dattoRmmContainer {
margin: 16px 0;
padding: 10px 12px;
border-radius: 8px;
background-color: var(--dark-gray-color);
border: 1px solid var(--border-color);
/* Narrower so it fits under the stack column */
width: calc(25% - 10px);
max-width: 480px;
}
/* Printer panels on Devices tab */
#printerPasswordContainer,
#printerClientContainer,
#printerListContainer {
margin: 16px 0;
padding: 10px 12px;
border-radius: 8px;
background-color: var(--dark-gray-color);
border: 1px solid var(--border-color);
/* Full width in the Devices tab */
width: 100%;
max-width: 600px;
}
#PasswordContainer input,
#PasswordContainer button,
#dattoRmmContainer select,
#printerPasswordContainer input,
#printerPasswordContainer button,
#printerClientContainer select {
background-color: var(--dark-gray-color);
color: var(--white-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
/* Input + GO button inline rows */
#PasswordContainer > div,
#printerPasswordContainer > div {
display: flex;
gap: 6px;
}
#PasswordContainer input,
#printerPasswordContainer input {
flex: 1;
min-width: 0;
}
/* Dropdown fills panel width */
#dattoRmmContainer select,
#printerClientContainer select {
width: 100%;
}
/* GO button styling */
#PasswordContainer button,
.go-button {
background-color: var(--btn-sidebar-blue);
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
}
#PasswordContainer button:hover,
.go-button:hover {
background-color: var(--btn-hover);
transform: scale(1.03);
}
/* Tag line */
.tagline {
font-size: 1.2rem;
color: var(--light-gray-color);
font-weight: bold;
text-align: center;
}
/* Big red notice under tagline */
.samy-hint {
margin-top: 0.25rem;
font-size: 3rem;
color: #ff4d4d;
font-weight: 900;
text-transform: uppercase;
text-align: center;
grid-column: 1 / -1; /* span both grid columns */
}
/* Responsive */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.column {
max-width: 100%;
}
#PasswordContainer,
#dattoRmmContainer {
width: 100%;
max-width: 100%;
}
}

81
samy.functions.ps1 Normal file
View File

@@ -0,0 +1,81 @@
function Initialize-NuGetProvider {
[CmdletBinding()]
param()
#region — guarantee NuGet provider is present without prompting
# ─── Silent defaults ───
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference = 'None'
# ─── Pre-create folder if running as SYSTEM (avoids NuGet install bug) ───
$provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
if (-not (Test-Path $provPath)) {
try {
New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
}
}
# ─── Ensure PowerShellGet is available ───
if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
try {
Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
}
# ─── Ensure PackageManagement is up-to-date ───
$pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
if ($pkgMgmtVersion -lt [Version]"1.3.1") {
try {
Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
}
}
# ─── Import modules silently ───
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
# ─── Trust PSGallery if not already ───
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
try {
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
}
}
# ─── Ensure NuGet is installed silently ───
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
if (-not $nuget) {
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
} else {
Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent
}
# ─── Final import check ───
try {
Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
} catch {
Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
#endregion — guarantee NuGet provider is present without prompting
}

185
samy.html Normal file
View File

@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Script Automation Monkey</title>
<link rel="icon" href="{{SamyFaviconUrl}}">
<style>
{{CssContent}}
</style>
</head>
<body>
<div class="logo-container">
<!-- SVS Logo (left) -->
<div class="logo-left">
<img src="{{SamyTopLogoUrl}}" alt="SVS Logo">
{{moduleVersion}}
</div>
<!-- Centered rotating tagline -->
<div id="tagline" class="tagline">
Script Automation Monkey (Yeah!)
</div>
<!-- Big red line under tagline -->
<div id="samyHint" class="samy-hint">{{SamyHintText}}</div>
</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="devicesTab">Devices</button>
<div id="status-box" style="margin-top: 1em; font-family: monospace;"></div>
</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>
<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 id="renameComputerBlock" style="display:none; margin-left: 24px; margin-top: 6px;">
<label for="txtNewComputerName">New computer name:</label>
<input type="text" id="txtNewComputerName" placeholder="e.g. CORP-LAP-123" />
<small style="display:block; margin-top:4px;">
(Max 15 chars; letters, numbers, and hyphens only.)
</small>
</div>
</div>
</div> <!-- end columns-container -->
<div id="PasswordContainer" style="display:none; margin-bottom:1em;">
<label for="Password">Enter Password:</label>
<div style="display:flex; gap:5px;">
<input type="password" id="Password" placeholder="Enter Password" style="flex:1;" />
<button onclick="fetchSites()" class="go-button">GO!</button>
</div>
</div>
<div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
<label for="dattoDropdown">Select a Datto RMM site:</label>
<select id="dattoDropdown" style="width:100%;">
<option disabled selected>Fetching sites...</option>
</select>
</div>
</div> <!-- end onboardTab -->
<div id="offboardTab" class="tab-content">
<h2>Off-Boarding</h2>
<div class="columns-container">
<div class="checkbox-group column">
<h3>Remove Stack</h3>
<label>
<input type="checkbox" id="offboardSelectAll" onclick="toggleOffboardAll()">
Select All
</label>
{{offboardCheckboxes}}
</div>
</div>
</div>
<div id="tweaksTab" class="tab-content">
<h2>Tweaks</h2>
<div class="columns-container">
<div class="checkbox-group column">
<h3>Tweaks</h3>
{{tweaksCheckboxes}}
</div>
</div>
</div>
<div id="SVSAppsTab" class="tab-content">
<h2>SVS APPs</h2>
<div class="columns-container">
<div class="checkbox-group column">
<h3>Applications</h3>
{{appsCheckboxes}}
</div>
</div>
</div>
<!-- NEW: Devices tab for printers and future stuff -->
<div id="devicesTab" class="tab-content">
<h2>Devices</h2>
<h3 class="subtitle">Manage printers and other client devices.</h3>
<!-- Printer password + fetch -->
<div id="printerPasswordContainer" style="margin-bottom:1em;">
<label for="PrinterPassword">Enter Printer Password:</label>
<div style="display:flex; gap:5px;">
<input type="password"
id="PrinterPassword"
placeholder="Enter printer password"
style="flex:1;" />
<button onclick="fetchPrinters()" class="go-button">Get Printers</button>
</div>
</div>
<!-- Client code dropdown -->
<div id="printerClientContainer" style="display:none; margin-bottom:1em;">
<label for="printerClientDropdown">Select Client:</label>
<select id="printerClientDropdown" style="width:100%;">
<option disabled selected>Fetch printers first...</option>
</select>
</div>
<!-- Printer checkbox list -->
<div id="printerListContainer" style="display:none; margin-bottom:1em;">
<label>Printers for selected client:</label>
<small style="display:block; margin-bottom:4px; opacity:0.8;">
Check the printers to install, and mark one as "Make default" (optional).
</small>
<div id="printerCheckboxContainer"
style="max-height:200px; overflow-y:auto; border:1px solid #444; padding:6px; border-radius:4px;">
<!-- Populated by JS -->
</div>
<button id="installPrintersButton"
class="btn btn-success"
style="margin-top:8px;"
onclick="installSelectedPrinters()">
Install Selected Printers
</button>
</div>
</div>
</div>
</div>
<!-- Tiny inline bridge: pass dynamic data, then inline JS from Gitea -->
<script>
window.SAMY_TASKS = {{tasksJsAll}};
window.SAMY_DEFAULT_PAGE = "{{defaultPage}}";
</script>
<script>
{{JsContent}}
</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>

691
samy.js Normal file
View File

@@ -0,0 +1,691 @@
// Use globals provided by the PowerShell-generated HTML bridge
const tasks = (window.SAMY_TASKS || []);
const defaultPage = (window.SAMY_DEFAULT_PAGE || "onboard");
let completedTasks = 0;
let totalTasks = 0;
// Progress / title handling
function setTotalTaskCount(count) {
totalTasks = count;
completedTasks = 0;
updateTitle();
}
function logProgress(label, isSuccess) {
const statusBox = document.getElementById("status-box");
completedTasks++;
updateTitle();
const msg = isSuccess
? ` ${completedTasks}/${totalTasks} done: ${label}`
: ` ${completedTasks}/${totalTasks} failed: ${label}`;
const div = document.createElement("div");
div.style.color = isSuccess ? "lime" : "red";
div.textContent = msg;
statusBox?.appendChild(div);
if (completedTasks === totalTasks) {
const finalMsg = document.createElement("div");
finalMsg.style.marginTop = "10px";
finalMsg.innerHTML = `<strong> All tasks completed (${completedTasks}/${totalTasks})</strong>`;
statusBox?.appendChild(finalMsg);
document.title = ` ScriptMonkey - Complete (${completedTasks}/${totalTasks})`;
const sound = new Audio(
"data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA="
);
sound.play().catch(() => {});
flashTitle(document.title);
}
}
function updateTitle() {
document.title = `ScriptMonkey - ${completedTasks}/${totalTasks} Done`;
}
function flashTitle(finalTitle) {
let flashes = 0;
const interval = setInterval(() => {
document.title = document.title === "" ? finalTitle : "";
flashes++;
if (flashes >= 10) {
clearInterval(interval);
document.title = finalTitle;
}
}, 800);
}
// =======================================================================
// Tab Navigation
// =======================================================================
document.addEventListener("DOMContentLoaded", () => {
const tabButtons = document.querySelectorAll(".tab-button");
const tabContents = document.querySelectorAll(".tab-content");
if (!tabButtons?.length || !tabContents?.length) {
console.error("ScriptMonkey: no tab buttons or tab contents found.");
return;
}
tabButtons.forEach((btn) => {
btn.addEventListener("click", () => {
tabButtons.forEach((b) => b.classList.remove("active"));
tabContents.forEach((c) => c.classList.remove("active"));
btn.classList.add("active");
const targetId = btn.dataset.tab;
const target = document.getElementById(targetId);
if (target) target.classList.add("active");
});
});
// Default tab from PS (onboard/offboard/tweaks/SVSApps)
const defaultTabId = `${defaultPage}Tab`;
const defaultBtn = document.querySelector(`.tab-button[data-tab='${defaultTabId}']`);
const defaultTab = document.getElementById(defaultTabId);
if (defaultBtn) defaultBtn.classList.add("active");
if (defaultTab) defaultTab.classList.add("active");
});
// =======================================================================
// Onboarding: Select-all left/right columns
// =======================================================================
function toggleColumn(col) {
const master = document.getElementById(
`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`
);
const children = document.querySelectorAll(
`#onboardTab input[type=checkbox][data-column="${col}"]`
);
children.forEach((cb) => {
cb.checked = master.checked;
});
setTimeout(() => {
children.forEach((cb) => {
cb.dispatchEvent(new Event("change"));
});
}, 0);
}
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);
}
document.addEventListener("DOMContentLoaded", () => {
["left", "right"].forEach((col) => {
document
.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`)
.forEach((cb) =>
cb.addEventListener("change", () => updateSelectAll(col))
);
});
});
// =======================================================================
// Off-boarding Select All
// =======================================================================
function toggleOffboardAll() {
const master = document.getElementById("offboardSelectAll");
const children = document.querySelectorAll(
"#offboardTab input[type=checkbox]:not(#offboardSelectAll)"
);
children.forEach((cb) => {
cb.checked = master.checked;
});
}
function updateOffboardSelectAll() {
const master = document.getElementById("offboardSelectAll");
if (!master) return;
const children = document.querySelectorAll(
"#offboardTab input[type=checkbox]:not(#offboardSelectAll)"
);
if (!children.length) {
master.checked = false;
return;
}
master.checked = Array.from(children).every((cb) => cb.checked);
}
document.addEventListener("DOMContentLoaded", () => {
const offChildren = document.querySelectorAll(
"#offboardTab input[type=checkbox]:not(#offboardSelectAll)"
);
if (!offChildren?.length) return;
offChildren.forEach((cb) =>
cb.addEventListener("change", updateOffboardSelectAll)
);
updateOffboardSelectAll();
});
// =======================================================================
// DattoRMM options + Enter key handling
// =======================================================================
function toggleDattoRMMOptions() {
const master = document.getElementById("installDattoRMM");
const container = document.getElementById("installDattoRMMOptionsContainer");
if (!container) return;
const checked = master?.checked;
container.style.display = checked ? "block" : "none";
container
.querySelectorAll('input[type="checkbox"]')
.forEach((cb) => (cb.checked = checked));
}
document.addEventListener("DOMContentLoaded", () => {
const master = document.getElementById("installDattoRMM");
if (master) {
master.addEventListener("change", toggleDattoRMMOptions);
}
const passwordField = document.getElementById("Password");
const goButton = document.querySelector("button[onclick='fetchSites()']");
if (passwordField && goButton) {
passwordField.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
goButton.click();
}
});
}
const siteDropdown = document.getElementById("dattoDropdown");
const runButton = document.querySelector(".run-button");
if (siteDropdown && runButton) {
siteDropdown.addEventListener("keydown", (e) => {
if (e.key === "Enter" && siteDropdown.value) {
runButton.click();
}
});
}
});
// =======================================================================
// Fetch Sites handler (calls /getpw)
// =======================================================================
async function fetchSites() {
const pwdInput = document.getElementById("Password");
const pwd = (pwdInput?.value ?? "").trim(); // allow blank, normalize whitespace
const dropdown = document.getElementById("dattoDropdown");
dropdown.innerHTML = '<option disabled selected>Loading sites...</option>';
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();
if (!Array.isArray(sites) || sites.length === 0) {
dropdown.innerHTML =
'<option disabled selected>No sites returned</option>';
alert("No Datto sites returned. Verify credentials/allowlist, or try again in a moment.");
return;
}
dropdown.innerHTML = "";
sites.forEach((site) => {
const option = document.createElement("option");
option.value = site.UID;
option.textContent = site.Name;
dropdown.appendChild(option);
});
document.getElementById("dattoRmmContainer").style.display = "block";
} catch (e) {
console.error(e);
dropdown.innerHTML =
'<option disabled selected>Error loading sites</option>';
alert("Failed to fetch sites. Check password or confirm your public IP is allowlisted.");
}
}
// =======================================================================
// Printer management (Devices tab)
// =======================================================================
let allPrinters = [];
// POST /getprinters with password from Devices tab
async function fetchPrinters() {
const pwdInput = document.getElementById("PrinterPassword");
const pwd = (pwdInput?.value ?? ""); // allow blank
const clientContainer = document.getElementById("printerClientContainer");
const listContainer = document.getElementById("printerListContainer");
const dropdown = document.getElementById("printerClientDropdown");
const checkboxContainer = document.getElementById("printerCheckboxContainer");
if (dropdown) {
dropdown.innerHTML = '<option disabled selected>Loading clients...</option>';
}
if (checkboxContainer) {
checkboxContainer.innerHTML = "";
}
if (clientContainer) clientContainer.style.display = "none";
if (listContainer) listContainer.style.display = "none";
try {
const resp = await fetch("/getprinters", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pwd }),
});
if (!resp.ok) throw new Error("HTTP " + resp.status);
const data = await resp.json();
allPrinters = Array.isArray(data) ? data : [];
if (!allPrinters.length) {
alert("No printers returned. Verify credentials/allowlist, or try again in a moment.");
return;
}
// Build unique sorted ClientCode list
const codes = [...new Set(allPrinters.map((p) => p.ClientCode))].sort();
dropdown.innerHTML = "";
const defaultOpt = new Option("Select a client...", "", true, true);
defaultOpt.disabled = true;
dropdown.appendChild(defaultOpt);
codes.forEach((code) => {
dropdown.appendChild(new Option(code, code));
});
if (clientContainer) clientContainer.style.display = "block";
} catch (e) {
console.error("fetchPrinters error:", e);
if (dropdown) {
dropdown.innerHTML =
'<option disabled selected>Error loading clients</option>';
}
alert("Failed to fetch printers. Check password or confirm your public IP is allowlisted.");
}
}
function renderPrintersForClient(clientCode) {
const container = document.getElementById("printerCheckboxContainer");
const listContainer = document.getElementById("printerListContainer");
if (!container) return;
container.innerHTML = "";
const printers = allPrinters.filter((p) => p.ClientCode === clientCode);
if (!printers.length) {
container.textContent = "No printers found for this client.";
if (listContainer) listContainer.style.display = "block";
return;
}
printers.forEach((p, idx) => {
const id = `printer_${clientCode}_${idx}`;
const label = document.createElement("label");
label.style.display = "block";
label.style.marginBottom = "4px";
//Install-Checkbox
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = id;
// stash all fields we might need later
cb.dataset.clientCode = p.ClientCode;
cb.dataset.profileName = p.ProfileName;
cb.dataset.displayName = p.DisplayName;
cb.dataset.location = p.Location;
cb.dataset.address = p.Address;
cb.dataset.printServer = p.PrintServer;
cb.dataset.shareName = p.ShareName;
cb.dataset.driverName = p.DriverName;
cb.dataset.driverInfPath = p.DriverInfPath;
const nameText = p.DisplayName || p.ProfileName || "Unnamed printer";
const locText = p.Location || "Unknown location";
// Line 1: install checkbox + printer label
label.appendChild(cb);
label.appendChild(document.createTextNode(" "));
label.appendChild(
document.createTextNode(`${nameText} (${locText})`)
);
// Line 2: radio for "Make default"
const defaultWrapper = document.createElement("div");
defaultWrapper.style.marginLeft = "24px";
defaultWrapper.style.fontSize = "0.85em";
defaultWrapper.style.opacity = "0.9";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "defaultPrinter";
radio.value = id; // associate default choice with this checkbox/printer
const radioLabel = document.createElement("span");
radioLabel.textContent = " Make default";
defaultWrapper.appendChild(radio);
defaultWrapper.appendChild(radioLabel);
label.appendChild(document.createElement("br"));
label.appendChild(defaultWrapper);
container.appendChild(label);
});
if (listContainer) listContainer.style.display = "block";
}
async function installSelectedPrinters() {
const container = document.getElementById("printerCheckboxContainer");
if (!container) return;
const checked = container.querySelectorAll("input[type=checkbox]:checked");
if (!checked.length) {
alert("Please select at least one printer.");
return;
}
// See which radio is checked for "Make default"
const defaultRadio = container.querySelector(
'input[type=radio][name="defaultPrinter"]:checked'
);
const defaultId = defaultRadio ? defaultRadio.value : null;
const selected = Array.from(checked).map((cb) => ({
ClientCode: cb.dataset.clientCode,
ProfileName: cb.dataset.profileName,
DisplayName: cb.dataset.displayName,
Location: cb.dataset.location,
Address: cb.dataset.address,
PrintServer: cb.dataset.printServer,
ShareName: cb.dataset.shareName,
DriverName: cb.dataset.driverName,
DriverInfPath: cb.dataset.driverInfPath,
// Only the printer whose checkbox id matches the selected radio gets SetAsDefault=true
SetAsDefault: defaultId !== null && cb.id === defaultId,
}));
try {
const resp = await fetch("/installprinters", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ printers: selected }),
});
if (!resp.ok) throw new Error("HTTP " + resp.status);
const result = await resp.json().catch(() => null);
console.log("Printer install result:", result);
} catch (e) {
console.error("installSelectedPrinters error:", e);
alert("Failed to trigger printer install.");
}
}
// =======================================================================
// Run Selected (main trigger)
// =======================================================================
async function triggerInstall() {
const runBtn = document.querySelector(".run-button");
if (!runBtn) return;
runBtn.disabled = true;
const statusBox = document.getElementById("status-box");
if (statusBox) statusBox.innerHTML = "";
try {
// Figure out which standard tasks are checked
const checkedTasks = tasks.filter((t) => {
if (["installDattoRMM", "installSVSMSPModule", "renameComputer"].includes(t.id)) return false;
const cb = document.getElementById(t.id);
return cb && cb.checked;
});
// Rename checkbox / textbox
const renameCB = document.getElementById("renameComputer");
const newNameInput = document.getElementById("txtNewComputerName");
// Count how many "extra" tasks (rename) we're doing
let extraTasks = 0;
if (renameCB && renameCB.checked) {
extraTasks = 1; // treat rename as one task in the progress counter
}
setTotalTaskCount(checkedTasks.length + extraTasks);
// 1. DattoRMM first
const dattoCB = document.getElementById("installDattoRMM");
if (dattoCB && dattoCB.checked) {
const sub = Array.from(
document.querySelectorAll(".sub-option-installDattoRMM:checked")
).map((x) => x.value);
const dropdown = document.getElementById("dattoDropdown");
const uid = dropdown?.value;
const name = dropdown?.selectedOptions?.[0]?.text || "Datto";
if (!uid) {
alert("Please select a Datto RMM site before running.");
logProgress("Install DattoRMM (no site selected)", false);
} else {
try {
await fetch("/installDattoRMM", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ checkedValues: sub, UID: uid, Name: name }),
});
logProgress("Install DattoRMM", true);
} catch (e) {
logProgress("Install DattoRMM", false);
console.error(e);
}
}
}
// 2. SVSMSP module second
const svsCB = document.getElementById("installSVSMSPModule");
if (svsCB && svsCB.checked) {
try {
await fetch("/installSVSMSPModule", { method: "GET" });
logProgress("Install SVSMSP Module", true);
} catch (e) {
logProgress("Install SVSMSP Module", false);
console.error(e);
}
}
// 3. Remaining tasks
for (const t of tasks) {
if (["installDattoRMM", "installSVSMSPModule", "renameComputer"].includes(t.id)) continue;
const cb = document.getElementById(t.id);
if (!cb || !cb.checked) continue;
try {
await fetch(t.handler, { method: "GET" });
logProgress(t.label || t.id, true);
} catch (e) {
logProgress(t.label || t.id, false);
console.error(`Error running ${t.id}:`, e);
}
}
// 4. Rename computer (LAST)
if (renameCB && renameCB.checked && newNameInput) {
const newName = newNameInput.value.trim();
// Same basic rules you'll enforce server-side
const nameIsValid =
newName.length > 0 &&
newName.length <= 15 &&
/^[A-Za-z0-9-]+$/.test(newName);
if (!nameIsValid) {
alert(
"Invalid computer name. Must be 1-15 characters and only letters, numbers, and hyphens."
);
// still mark it as a failed task so progress reaches 100%
logProgress("Rename computer", false);
} else {
try {
await fetch("/renameComputer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newName }),
});
logProgress("Rename computer", true);
} catch (e) {
console.error("Error calling /renameComputer:", e);
logProgress("Rename computer", false);
}
}
}
} catch (e) {
console.error("triggerInstall fatal error:", e);
} finally {
runBtn.disabled = false;
if (totalTasks > 0) {
console.info(
`[Info] All tasks completed (${completedTasks}/${totalTasks})`
);
}
// Best-effort notification to the server
try {
await fetch("/tasksCompleted", { method: "POST" });
} catch (err) {
console.warn("Could not notify server about completion:", err);
}
}
}
// =======================================================================
// Shutdown handler (Exit button & window close)
// =======================================================================
function endSession() {
fetch("/quit", { method: "GET" }).finally(() => window.close());
}
// Sub-options auto-toggle, tagline rotation, and beforeunload hook
document.addEventListener("DOMContentLoaded", () => {
// Sub-option containers
const tasksWithSubOptions = document.querySelectorAll(
'[id$="OptionsContainer"]'
);
tasksWithSubOptions.forEach((container) => {
const taskId = container.id.replace("OptionsContainer", "");
const masterCheckbox = document.getElementById(taskId);
if (!masterCheckbox) return;
function updateVisibility() {
const checked = masterCheckbox.checked;
container.style.display = checked ? "block" : "none";
container
.querySelectorAll('input[type="checkbox"]')
.forEach((cb) => (cb.checked = checked));
if (taskId === "installDattoRMM") {
const pwdBox = document.getElementById("PasswordContainer");
const rmmBox = document.getElementById("dattoRmmContainer");
if (pwdBox) pwdBox.style.display = checked ? "block" : "none";
if (rmmBox) rmmBox.style.display = checked ? "block" : "none";
}
}
masterCheckbox.addEventListener("change", updateVisibility);
updateVisibility();
});
// NEW: Rename computer checkbox -> show/hide text box
const renameCheckbox = document.getElementById("renameComputer");
const renameBlock = document.getElementById("renameComputerBlock");
if (renameCheckbox && renameBlock) {
function updateRenameVisibility() {
renameBlock.style.display = renameCheckbox.checked ? "block" : "none";
}
renameCheckbox.addEventListener("change", updateRenameVisibility);
updateRenameVisibility();
}
// Tagline rotation
const taglines = [
"Fast deployments, no monkey business.",
"Bananas for better builds.",
"Deploy without flinging code.",
"Tame your stack. Unleash the monkey.",
"Monkey see, monkey deploy.",
"Deploy smarter -- with a monkey on your team.",
"Don't pass the monkey -- let it deploy.",
"No more monkeying around. Stack handled.",
"Own your stack. But let the monkey do the work.",
"Why throw code when the monkey's got it?",
"Deployments so easy, a monkey could do it. Ours does.",
"Monkey in the stack, not on your back.",
];
const el = document.getElementById("tagline");
if (el) {
let idx = Math.floor(Math.random() * taglines.length);
el.textContent = taglines[idx];
setInterval(() => {
idx = (idx + 1) % taglines.length;
el.textContent = taglines[idx];
}, 10_000);
}
});
// printer dropdown
document.addEventListener("DOMContentLoaded", () => {
const clientDropdown = document.getElementById("printerClientDropdown");
if (clientDropdown) {
clientDropdown.addEventListener("change", (e) => {
const code = e.target.value;
if (code) renderPrintersForClient(code);
});
}
});
// notify server on window close
window.addEventListener("beforeunload", () => {
fetch("/quit", { method: "GET", keepalive: true });
});

2092
samy.ps1 Normal file

File diff suppressed because it is too large Load Diff

155
samy.tasks.json Normal file
View File

@@ -0,0 +1,155 @@
[
{
"Id": "setSVSPowerplan",
"Name": "setSVSPowerplan",
"Label": "Set SVS Powerplan",
"HandlerFn": "Invoke-SetSVSPowerPlan",
"Page": "onboard",
"Column": "left",
"Tooltip": "Applies the SVS power configuration"
},
{
"Id": "installSVSMSPModule",
"Name": "installSVSMSPModule",
"Label": "Install SVSMSP Module",
"HandlerFn": "Invoke-InstallSVSMSP",
"Page": "onboard",
"Column": "left",
"Tooltip": "Installs or updates the SVSMSP toolkit module"
},
{
"Id": "installCyberQP",
"Name": "installCyberQP",
"Label": "Install CyberQP",
"HandlerFn": "Invoke-InstallCyberQP",
"Page": "onboard",
"Column": "left"
},
{
"Id": "installHelpDesk",
"Name": "installHelpDesk",
"Label": "Install HelpDesk",
"HandlerFn": "Invoke-InstallHelpDesk",
"Page": "onboard",
"Column": "left"
},
{
"Id": "installThreatLocker",
"Name": "installThreatLocker",
"Label": "Install ThreatLocker",
"HandlerFn": "Invoke-InstallThreatLocker",
"Page": "onboard",
"Column": "left"
},
{
"Id": "installRocketCyber",
"Name": "installRocketCyber",
"Label": "Install RocketCyber",
"HandlerFn": "Invoke-InstallRocketCyber",
"Page": "onboard",
"Column": "left"
},
{
"Id": "installDattoRMM",
"Name": "installDattoRMM",
"Label": "Install DattoRMM",
"HandlerFn": "Invoke-InstallDattoRMM",
"Page": "onboard",
"Column": "left",
"Tooltip": "Fetches sites then installs/configures Datto RMM",
"SubOptions": [
{ "Value": "inputVar", "Label": "Copy Site Variables" },
{ "Value": "rmm", "Label": "Install RMM Agent" },
{ "Value": "exe", "Label": "Download Executable" }
]
},
{
"Id": "enableBitLocker",
"Name": "EnableBitLocker",
"Label": "Enable BitLocker",
"HandlerFn": "Set-SVSBitLocker",
"Page": "onboard",
"Column": "right",
"Tooltip": "Enables BitLocker drive encryption"
},
{
"Id": "setEdgeDefaultSearch",
"Name": "setedgedefaultsearch",
"Label": "Set Edge Default Search",
"HandlerFn": "Invoke-SetEdgeDefaultSearchEngine",
"Page": "onboard",
"Column": "right",
"Tooltip": "Will configure Edge to use Google as default search provider"
},
{
"Id": "renameComputer",
"Name": "renameComputer",
"Label": "Rename Computer",
"HandlerFn": "Invoke-RenameComputer",
"Page": "onboard",
"Column": "right",
"Tooltip": "Renames the device (reboot required)"
},
{
"Id": "disableAnimations",
"Name": "disableAnimations",
"Label": "Disable Animations",
"HandlerFn": "Disable-Animations",
"Page": "onboard",
"Column": "right",
"Tooltip": "Disables Windows UI animations for performance"
},
{
"Id": "wingetChrome",
"Name": "wingetChrome",
"Label": "Google Chrome",
"HandlerFn": "Invoke-InstallChrome",
"Page": "onboard",
"Column": "right"
},
{
"Id": "wingetAcrobat",
"Name": "wingetAcrobat",
"Label": "Adobe Acrobat Reader (64-bit)",
"HandlerFn": "Invoke-InstallAcrobat",
"Page": "onboard",
"Column": "right"
},
{
"Id": "offUninstallCyberQP",
"Name": "offUninstallCyberQP",
"Label": "Uninstall CyberQP",
"HandlerFn": "Invoke-UninstallCyberQP",
"Page": "offboard"
},
{
"Id": "offUninstallHelpDesk",
"Name": "offUninstallHelpDesk",
"Label": "Uninstall HelpDesk",
"HandlerFn": "Invoke-UninstallHelpDesk",
"Page": "offboard"
},
{
"Id": "offUninstallThreatLocker",
"Name": "offUninstallThreatLocker",
"Label": "Uninstall ThreatLocker",
"HandlerFn": "Invoke-UninstallThreatLocker",
"Page": "offboard"
},
{
"Id": "offUninstallRocketCyber",
"Name": "offUninstallRocketCyber",
"Label": "Uninstall RocketCyber",
"HandlerFn": "Invoke-UninstallRocketCyber",
"Page": "offboard"
},
{
"Id": "offCleanupSVSMSPModule",
"Name": "offCleanupSVSMSPModule",
"Label": "Cleanup SVSMSP Toolkit",
"HandlerFn": "Invoke-CleanupSVSMSP",
"Page": "offboard"
}
]