Add config GUI, USB launcher, flash folder; fix bugs
- config-editor.hta: lightweight WYSIWYG HTA editor for config.json - Step on/off toggles with info tooltips - Editable software list (winget packages) - Settings: timezone, admin account, desktopInfo, PDF default - Run.cmd: USB launcher with UAC auto-elevation and deployment menu - flash/: minimal USB-ready subset (Deploy, scripts, config, GUI, launcher) - config.json: add steps section for per-step enable/disable - Deploy-Windows.ps1: read steps from config, CLI switches override - 03-system-registry.ps1: add SearchOnTaskbarMode HKLM policy (Win11 search fix) - 04-default-profile.ps1: fix systray - clear TrayNotify cache + proper Explorer restart - 06-scheduled-tasks.ps1: fix Register-Task trigger array, ShowAllTrayIcons Win11 fix, PDF-DefaultApp runs as SYSTEM via HKCR (bypasses UserChoice Hash validation) - 02-software.ps1: remove unreliable UserChoice ProgId write without Hash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79fcfea8df
commit
80a542252d
20 changed files with 3693 additions and 63 deletions
|
|
@ -96,83 +96,117 @@ Invoke-Step -Name "Load config.json" -Action {
|
|||
Write-Log "Config loaded from $ConfigFile" -Level INFO
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Build step enable/disable map from config + CLI overrides
|
||||
# -----------------------------------------------------------------------
|
||||
$stepsEnabled = @{
|
||||
adminAccount = $true
|
||||
bloatware = $true
|
||||
software = $true
|
||||
systemRegistry = $true
|
||||
defaultProfile = $true
|
||||
personalization = $true
|
||||
scheduledTasks = $true
|
||||
desktopInfo = $true
|
||||
activation = $true
|
||||
}
|
||||
if ($Config -and $Config.steps) {
|
||||
foreach ($key in @($stepsEnabled.Keys)) {
|
||||
$val = $Config.steps.$key
|
||||
if ($null -ne $val) { $stepsEnabled[$key] = [bool]$val }
|
||||
}
|
||||
}
|
||||
# CLI switches override config.steps
|
||||
if ($SkipBloatware) { $stepsEnabled['bloatware'] = $false }
|
||||
if ($SkipSoftware) { $stepsEnabled['software'] = $false }
|
||||
if ($SkipDefaultProfile) { $stepsEnabled['defaultProfile'] = $false }
|
||||
|
||||
function Skip-Step {
|
||||
param([string]$Name)
|
||||
Write-Log "$Name - SKIPPED (disabled in config)" -Level WARN
|
||||
$StepResults.Add(@{ Name = $Name; Status = "SKIPPED" })
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 0a - Admin account
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['adminAccount']) {
|
||||
Invoke-Step -Name "Step 0a - Admin account" -Action {
|
||||
& "$ScriptRoot\scripts\00-admin-account.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 0a - Admin account" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 0b - Windows activation
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['activation']) {
|
||||
Invoke-Step -Name "Step 0b - Windows activation" -Action {
|
||||
& "$ScriptRoot\scripts\08-activation.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 0b - Windows activation" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 1 - Bloatware removal
|
||||
# -----------------------------------------------------------------------
|
||||
if ($SkipBloatware) {
|
||||
Write-Log "Step 1 - Bloatware removal: SKIPPED (-SkipBloatware)" -Level WARN
|
||||
$StepResults.Add(@{ Name = "Step 1 - Bloatware removal"; Status = "SKIPPED" })
|
||||
} else {
|
||||
if ($stepsEnabled['bloatware']) {
|
||||
Invoke-Step -Name "Step 1 - Bloatware removal" -Action {
|
||||
& "$ScriptRoot\scripts\01-bloatware.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
}
|
||||
} else { Skip-Step "Step 1 - Bloatware removal" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 2 - Software installation
|
||||
# -----------------------------------------------------------------------
|
||||
if ($SkipSoftware) {
|
||||
Write-Log "Step 2 - Software installation: SKIPPED (-SkipSoftware)" -Level WARN
|
||||
$StepResults.Add(@{ Name = "Step 2 - Software installation"; Status = "SKIPPED" })
|
||||
} else {
|
||||
if ($stepsEnabled['software']) {
|
||||
Invoke-Step -Name "Step 2 - Software installation" -Action {
|
||||
& "$ScriptRoot\scripts\02-software.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
}
|
||||
} else { Skip-Step "Step 2 - Software installation" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 3 - System registry (HKLM)
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['systemRegistry']) {
|
||||
Invoke-Step -Name "Step 3 - System registry" -Action {
|
||||
& "$ScriptRoot\scripts\03-system-registry.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 3 - System registry" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 4 - Default profile (NTUSER.DAT)
|
||||
# -----------------------------------------------------------------------
|
||||
if ($SkipDefaultProfile) {
|
||||
Write-Log "Step 4 - Default profile: SKIPPED (-SkipDefaultProfile)" -Level WARN
|
||||
$StepResults.Add(@{ Name = "Step 4 - Default profile"; Status = "SKIPPED" })
|
||||
} else {
|
||||
if ($stepsEnabled['defaultProfile']) {
|
||||
Invoke-Step -Name "Step 4 - Default profile" -Action {
|
||||
& "$ScriptRoot\scripts\04-default-profile.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
}
|
||||
} else { Skip-Step "Step 4 - Default profile" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 5 - Personalization
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['personalization']) {
|
||||
Invoke-Step -Name "Step 5 - Personalization" -Action {
|
||||
& "$ScriptRoot\scripts\05-personalization.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 5 - Personalization" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 6 - Scheduled tasks
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['scheduledTasks']) {
|
||||
Invoke-Step -Name "Step 6 - Scheduled tasks" -Action {
|
||||
& "$ScriptRoot\scripts\06-scheduled-tasks.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 6 - Scheduled tasks" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 7 - DesktopInfo
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['desktopInfo']) {
|
||||
Invoke-Step -Name "Step 7 - DesktopInfo" -Action {
|
||||
& "$ScriptRoot\scripts\07-desktop-info.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 7 - DesktopInfo" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
|
|
|
|||
125
Run.cmd
Normal file
125
Run.cmd
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Auto-elevate to Administrator if not already elevated
|
||||
:: -----------------------------------------------------------------------
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Requesting administrator privileges...
|
||||
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Paths
|
||||
:: -----------------------------------------------------------------------
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "DEPLOY_PS1=%SCRIPT_DIR%Deploy-Windows.ps1"
|
||||
set "CONFIG_JSON=%SCRIPT_DIR%config\config.json"
|
||||
set "CONFIG_EDITOR=%SCRIPT_DIR%config-editor.hta"
|
||||
set "LOG_FILE=C:\Windows\Setup\Scripts\Deploy.log"
|
||||
|
||||
:MENU
|
||||
cls
|
||||
echo.
|
||||
echo ================================================
|
||||
echo X9 - Windows Deployment
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Config : %CONFIG_JSON%
|
||||
echo Log : %LOG_FILE%
|
||||
echo.
|
||||
echo [1] Full deployment (uses config.json)
|
||||
echo [2] Dry run (no changes, log only)
|
||||
echo [3] Skip bloatware removal
|
||||
echo [4] Skip software install
|
||||
echo [5] Open config editor (config-editor.hta)
|
||||
echo [0] Exit
|
||||
echo.
|
||||
set /p CHOICE=" Select [0-5]: "
|
||||
|
||||
if "%CHOICE%"=="0" goto EXIT
|
||||
if "%CHOICE%"=="1" goto FULL
|
||||
if "%CHOICE%"=="2" goto DRYRUN
|
||||
if "%CHOICE%"=="3" goto SKIP_BLOATWARE
|
||||
if "%CHOICE%"=="4" goto SKIP_SOFTWARE
|
||||
if "%CHOICE%"=="5" goto OPEN_EDITOR
|
||||
|
||||
echo Invalid choice. Try again.
|
||||
timeout /t 2 >nul
|
||||
goto MENU
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [1] Full deployment
|
||||
:: -----------------------------------------------------------------------
|
||||
:FULL
|
||||
cls
|
||||
echo.
|
||||
echo Starting full deployment...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%"
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [2] Dry run
|
||||
:: -----------------------------------------------------------------------
|
||||
:DRYRUN
|
||||
cls
|
||||
echo.
|
||||
echo Starting dry run (no changes will be made)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -DryRun
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [3] Skip bloatware
|
||||
:: -----------------------------------------------------------------------
|
||||
:SKIP_BLOATWARE
|
||||
cls
|
||||
echo.
|
||||
echo Starting deployment (bloatware removal skipped)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -SkipBloatware
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [4] Skip software
|
||||
:: -----------------------------------------------------------------------
|
||||
:SKIP_SOFTWARE
|
||||
cls
|
||||
echo.
|
||||
echo Starting deployment (software install skipped)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -SkipSoftware
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [5] Config editor
|
||||
:: -----------------------------------------------------------------------
|
||||
:OPEN_EDITOR
|
||||
if not exist "%CONFIG_EDITOR%" (
|
||||
echo ERROR: config-editor.hta not found: %CONFIG_EDITOR%
|
||||
pause
|
||||
goto MENU
|
||||
)
|
||||
start "" mshta.exe "%CONFIG_EDITOR%"
|
||||
goto MENU
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Done
|
||||
:: -----------------------------------------------------------------------
|
||||
:DONE
|
||||
echo.
|
||||
echo ================================================
|
||||
echo Deployment finished.
|
||||
echo Log: %LOG_FILE%
|
||||
echo ================================================
|
||||
echo.
|
||||
pause
|
||||
goto MENU
|
||||
|
||||
:EXIT
|
||||
endlocal
|
||||
exit /b 0
|
||||
632
config-editor.hta
Normal file
632
config-editor.hta
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>X9 - Deployment Config Editor</title>
|
||||
<HTA:APPLICATION
|
||||
ID="ConfigEditor"
|
||||
APPLICATIONNAME="X9 Config Editor"
|
||||
SCROLL="no"
|
||||
SINGLEINSTANCE="yes"
|
||||
WINDOWSTATE="normal"
|
||||
INNERBORDER="no"
|
||||
SELECTION="no"
|
||||
CONTEXTMENU="no"
|
||||
/>
|
||||
<meta http-equiv="x-ua-compatible" content="ie=11">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: Segoe UI, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
background: #1a2a33;
|
||||
color: #d0dde3;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#header {
|
||||
background: #223B47;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 2px solid #2e5568;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e8f4f8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
#header .config-path {
|
||||
font-size: 11px;
|
||||
color: #7aabbd;
|
||||
margin-top: 3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
#tabs {
|
||||
display: flex;
|
||||
background: #1a2a33;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
color: #7aabbd;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab:hover { color: #b0d4e0; }
|
||||
.tab.active { color: #e8f4f8; border-bottom: 2px solid #4a9aba; background: #1e3340; }
|
||||
#content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-panel { display: none; padding: 16px 18px; }
|
||||
.tab-panel.active { display: block; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th {
|
||||
background: #223B47;
|
||||
color: #9ac8d8;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 7px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #1e3340;
|
||||
vertical-align: middle;
|
||||
}
|
||||
tr:hover td { background: #1e3340; }
|
||||
.step-num {
|
||||
color: #4a9aba;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
width: 36px;
|
||||
}
|
||||
.step-name { color: #d0dde3; }
|
||||
.info-btn {
|
||||
background: #2e5568;
|
||||
border: none;
|
||||
color: #7aabbd;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.info-btn:hover { background: #3a6f8a; color: #e8f4f8; }
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: #4a9aba;
|
||||
}
|
||||
input[type="text"], select {
|
||||
background: #1a2a33;
|
||||
border: 1px solid #2e5568;
|
||||
color: #d0dde3;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
input[type="text"]:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: #4a9aba;
|
||||
}
|
||||
.label-cell {
|
||||
width: 160px;
|
||||
color: #9ac8d8;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-top: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.section-title {
|
||||
color: #4a9aba;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 16px 0 8px 0;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
}
|
||||
.section-title:first-child { margin-top: 0; }
|
||||
.btn {
|
||||
background: #223B47;
|
||||
border: 1px solid #2e5568;
|
||||
color: #d0dde3;
|
||||
padding: 5px 12px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn:hover { background: #2e5568; color: #e8f4f8; }
|
||||
.btn-danger {
|
||||
background: #3d1f1f;
|
||||
border-color: #6b2c2c;
|
||||
color: #d08080;
|
||||
}
|
||||
.btn-danger:hover { background: #5a2020; color: #f0a0a0; }
|
||||
.btn-add {
|
||||
background: #1a3a2a;
|
||||
border-color: #2e6a4a;
|
||||
color: #80c8a0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn-add:hover { background: #235a38; color: #a0e0b8; }
|
||||
#footer {
|
||||
background: #223B47;
|
||||
border-top: 2px solid #2e5568;
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#btn-save {
|
||||
background: #1a5c3a;
|
||||
border: 1px solid #2a8a58;
|
||||
color: #80e0b0;
|
||||
padding: 7px 22px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
#btn-save:hover { background: #206e46; color: #a0f0c8; }
|
||||
#status {
|
||||
font-size: 12px;
|
||||
color: #7aabbd;
|
||||
flex: 1;
|
||||
}
|
||||
#tooltip {
|
||||
position: fixed;
|
||||
background: #0f1e26;
|
||||
border: 1px solid #3a7a9a;
|
||||
color: #c0dde8;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
max-width: 340px;
|
||||
line-height: 1.5;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||
}
|
||||
.settings-grid { display: grid; grid-template-columns: 160px 1fr; gap: 8px 12px; align-items: center; }
|
||||
.settings-grid .span2 { grid-column: 1 / -1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="header">
|
||||
<h1>X9 - Deployment Config</h1>
|
||||
<div class="config-path" id="config-path-display">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="tabs">
|
||||
<div class="tab active" onclick="showTab('steps')">Steps</div>
|
||||
<div class="tab" onclick="showTab('software')">Software</div>
|
||||
<div class="tab" onclick="showTab('settings')">Settings</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<!-- STEPS TAB -->
|
||||
<div class="tab-panel active" id="tab-steps">
|
||||
<table id="steps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">On</th>
|
||||
<th style="width:36px;">Step</th>
|
||||
<th>Name</th>
|
||||
<th style="width:32px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="steps-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SOFTWARE TAB -->
|
||||
<div class="tab-panel" id="tab-software">
|
||||
<table id="software-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package Name</th>
|
||||
<th>Winget ID</th>
|
||||
<th style="width:70px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="software-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn btn-add" onclick="addSoftwareRow()">+ Add package</button>
|
||||
</div>
|
||||
|
||||
<!-- SETTINGS TAB -->
|
||||
<div class="tab-panel" id="tab-settings">
|
||||
<div class="section-title">Deployment</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Timezone</div>
|
||||
<div><input type="text" id="s-timezone" onchange="updateSetting('deployment','timezone',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Locale</div>
|
||||
<div><input type="text" id="s-locale" onchange="updateSetting('deployment','locale',this.value)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Admin Account</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Username</div>
|
||||
<div><input type="text" id="s-admin-user" onchange="updateSetting('adminAccount','username',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Password</div>
|
||||
<div><input type="text" id="s-admin-pass" onchange="updateSetting('adminAccount','password',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Description</div>
|
||||
<div><input type="text" id="s-admin-desc" onchange="updateSetting('adminAccount','description',this.value)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">PDF Default</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Force Adobe Reader</div>
|
||||
<div><input type="checkbox" id="s-force-adobe" onchange="updateSettingBool('pdfDefault','forceAdobeReader',this.checked)"></div>
|
||||
|
||||
<div class="label-cell">Scheduled Task</div>
|
||||
<div><input type="checkbox" id="s-pdf-task" onchange="updateSettingBool('pdfDefault','scheduledTaskEnabled',this.checked)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Desktop Info</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Enabled</div>
|
||||
<div><input type="checkbox" id="s-di-enabled" onchange="updateSettingBool('desktopInfo','enabled',this.checked)"></div>
|
||||
|
||||
<div class="label-cell">Position</div>
|
||||
<div>
|
||||
<select id="s-di-position" onchange="updateSetting('desktopInfo','position',this.value)">
|
||||
<option value="bottomRight">Bottom Right</option>
|
||||
<option value="bottomLeft">Bottom Left</option>
|
||||
<option value="topRight">Top Right</option>
|
||||
<option value="topLeft">Top Left</option>
|
||||
<option value="center">Center</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="label-cell">Font Size</div>
|
||||
<div><input type="text" id="s-di-fontsize" onchange="updateSettingInt('desktopInfo','fontSize',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Font Color</div>
|
||||
<div><input type="text" id="s-di-fontcolor" onchange="updateSetting('desktopInfo','fontColor',this.value)" placeholder="#FFFFFF"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Activation</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Product Key</div>
|
||||
<div><input type="text" id="s-act-key" onchange="updateSetting('activation','productKey',this.value)" placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"></div>
|
||||
|
||||
<div class="label-cell">KMS Server</div>
|
||||
<div><input type="text" id="s-act-kms" onchange="updateSetting('activation','kmsServer',this.value)" placeholder="kms.example.com"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<button id="btn-save" onclick="saveConfig()">Save config.json</button>
|
||||
<div id="status">Ready.</div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip"></div>
|
||||
|
||||
<script language="JScript">
|
||||
|
||||
var STEP_DEFS = [
|
||||
{ key: "adminAccount", num: "00", label: "Admin account", info: "Creates local admin account for MSP remote access. Account name and generated password are saved to Deploy.log." },
|
||||
{ key: "bloatware", num: "01", label: "Bloatware removal", info: "Removes 44 pre-installed UWP apps (Teams, Xbox, Cortana, Solitaire, Office Hub, Skype...) and unused Windows Capabilities and Features. Calculator is kept." },
|
||||
{ key: "software", num: "02", label: "Software install", info: "Installs packages from the list below via winget. Also sets Adobe Reader as default PDF viewer via HKCR." },
|
||||
{ key: "systemRegistry", num: "03", label: "System registry", info: "HKLM-wide tweaks: disables Widgets, Teams auto-install, Edge first run, OneDrive, Outlook auto-install, GameDVR, Recall. Sets timezone. Hides taskbar search box via policy (Win11)." },
|
||||
{ key: "defaultProfile", num: "04", label: "Default profile", info: "Modifies C:\\Users\\Default\\NTUSER.DAT so ALL future users inherit: left-aligned taskbar, hidden search/Copilot/TaskView/Widgets buttons, file extensions visible, Explorer opens to This PC, Num Lock on." },
|
||||
{ key: "personalization", num: "05", label: "Personalization", info: "Dark system theme, light app theme, accent color #223B47, transparency off, accent on title bars. Applied to Default profile and current user." },
|
||||
{ key: "scheduledTasks", num: "06", label: "Scheduled tasks", info: "Registers 4 tasks: ShowAllTrayIcons (logon - clears systray cache + restarts Explorer), UnlockStartLayout (once), PDF-DefaultApp (logon as SYSTEM - restores HKCR association), DesktopInfo (logon - renders wallpaper)." },
|
||||
{ key: "desktopInfo", num: "07", label: "Desktop info", info: "Custom desktop wallpaper showing: computer name, IP, OS version, username, deployment date. Rendered as BMP on every logon via scheduled task. Replaces BackInfo.exe." },
|
||||
{ key: "activation", num: "08", label: "Windows activation", info: "Checks and applies Windows activation." }
|
||||
];
|
||||
|
||||
var configPath = "";
|
||||
var config = null;
|
||||
var fso = null;
|
||||
|
||||
function init() {
|
||||
try {
|
||||
fso = new ActiveXObject("Scripting.FileSystemObject");
|
||||
|
||||
// Derive config path from HTA location
|
||||
var htaPath = location.pathname.replace(/\//g, "\\");
|
||||
// Remove leading backslash from pathname
|
||||
if (htaPath.charAt(0) === "\\") {
|
||||
htaPath = htaPath.substring(1);
|
||||
}
|
||||
var dir = fso.GetParentFolderName(htaPath);
|
||||
configPath = dir + "\\config\\config.json";
|
||||
|
||||
document.getElementById("config-path-display").innerText = configPath;
|
||||
|
||||
loadConfig();
|
||||
buildStepsTable();
|
||||
resizeWindow();
|
||||
} catch(e) {
|
||||
setStatus("Init error: " + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeWindow() {
|
||||
window.resizeTo(800, 720);
|
||||
window.moveTo(
|
||||
(screen.width - 800) / 2,
|
||||
(screen.height - 720) / 2
|
||||
);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
if (!fso.FileExists(configPath)) {
|
||||
setStatus("config.json not found: " + configPath, true);
|
||||
config = {};
|
||||
return;
|
||||
}
|
||||
var f = fso.OpenTextFile(configPath, 1, false, -1);
|
||||
var raw = f.ReadAll();
|
||||
f.Close();
|
||||
config = JSON.parse(raw);
|
||||
setStatus("Loaded: " + configPath);
|
||||
populateSettings();
|
||||
buildSoftwareTable();
|
||||
} catch(e) {
|
||||
setStatus("Load error: " + e.message, true);
|
||||
config = {};
|
||||
}
|
||||
}
|
||||
|
||||
function buildStepsTable() {
|
||||
var tbody = document.getElementById("steps-tbody");
|
||||
var html = "";
|
||||
for (var i = 0; i < STEP_DEFS.length; i++) {
|
||||
var s = STEP_DEFS[i];
|
||||
var checked = "";
|
||||
// Check config.steps if loaded, default true
|
||||
if (config && config.steps && config.steps[s.key] === false) {
|
||||
checked = "";
|
||||
} else {
|
||||
checked = "checked";
|
||||
}
|
||||
html += "<tr>";
|
||||
html += "<td><input type='checkbox' " + checked + " onclick=\"toggleStep('" + s.key + "', this.checked)\"></td>";
|
||||
html += "<td class='step-num'>" + s.num + "</td>";
|
||||
html += "<td class='step-name'>" + s.label + "</td>";
|
||||
html += "<td><button class='info-btn' onmouseover=\"showTooltip(event, '" + escapeQ(s.info) + "')\" onmouseout='hideTooltip()'>i</button></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function buildSoftwareTable() {
|
||||
var tbody = document.getElementById("software-tbody");
|
||||
var html = "";
|
||||
if (config && config.software && config.software.install) {
|
||||
var list = config.software.install;
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
html += makeSoftwareRow(i, list[i].name, list[i].wingetId);
|
||||
}
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function makeSoftwareRow(idx, name, wingetId) {
|
||||
return "<tr id='sw-row-" + idx + "'>" +
|
||||
"<td><input type='text' value='" + escapeQ(name) + "' onchange=\"updateSoftwareName(" + idx + ", this.value)\"></td>" +
|
||||
"<td><input type='text' value='" + escapeQ(wingetId) + "' onchange=\"updateSoftwareId(" + idx + ", this.value)\"></td>" +
|
||||
"<td><button class='btn btn-danger' onclick='removeSoftwareRow(" + idx + ")'>Remove</button></td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
function addSoftwareRow() {
|
||||
if (!config.software) { config.software = {}; }
|
||||
if (!config.software.install) { config.software.install = []; }
|
||||
config.software.install.push({ name: "", wingetId: "" });
|
||||
buildSoftwareTable();
|
||||
setStatus("Package added. Fill in name and Winget ID, then save.");
|
||||
}
|
||||
|
||||
function removeSoftwareRow(idx) {
|
||||
if (!config || !config.software || !config.software.install) { return; }
|
||||
config.software.install.splice(idx, 1);
|
||||
buildSoftwareTable();
|
||||
setStatus("Package removed. Click Save to persist.");
|
||||
}
|
||||
|
||||
function updateSoftwareName(idx, val) {
|
||||
if (config && config.software && config.software.install && config.software.install[idx] !== undefined) {
|
||||
config.software.install[idx].name = val;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSoftwareId(idx, val) {
|
||||
if (config && config.software && config.software.install && config.software.install[idx] !== undefined) {
|
||||
config.software.install[idx].wingetId = val;
|
||||
}
|
||||
}
|
||||
|
||||
function populateSettings() {
|
||||
if (!config) { return; }
|
||||
|
||||
if (config.deployment) {
|
||||
setVal("s-timezone", config.deployment.timezone || "");
|
||||
setVal("s-locale", config.deployment.locale || "");
|
||||
}
|
||||
if (config.adminAccount) {
|
||||
setVal("s-admin-user", config.adminAccount.username || "");
|
||||
setVal("s-admin-pass", config.adminAccount.password || "");
|
||||
setVal("s-admin-desc", config.adminAccount.description || "");
|
||||
}
|
||||
if (config.pdfDefault) {
|
||||
setChk("s-force-adobe", config.pdfDefault.forceAdobeReader !== false);
|
||||
setChk("s-pdf-task", config.pdfDefault.scheduledTaskEnabled !== false);
|
||||
}
|
||||
if (config.desktopInfo) {
|
||||
setChk("s-di-enabled", config.desktopInfo.enabled !== false);
|
||||
setSelVal("s-di-position", config.desktopInfo.position || "bottomRight");
|
||||
setVal("s-di-fontsize", config.desktopInfo.fontSize !== undefined ? String(config.desktopInfo.fontSize) : "12");
|
||||
setVal("s-di-fontcolor", config.desktopInfo.fontColor || "#FFFFFF");
|
||||
}
|
||||
if (config.activation) {
|
||||
setVal("s-act-key", config.activation.productKey || "");
|
||||
setVal("s-act-kms", config.activation.kmsServer || "");
|
||||
}
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) { el.value = val; }
|
||||
}
|
||||
|
||||
function setChk(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) { el.checked = !!val; }
|
||||
}
|
||||
|
||||
function setSelVal(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) { return; }
|
||||
for (var i = 0; i < el.options.length; i++) {
|
||||
if (el.options[i].value === val) {
|
||||
el.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStep(key, enabled) {
|
||||
if (!config) { return; }
|
||||
if (!config.steps) { config.steps = {}; }
|
||||
config.steps[key] = enabled;
|
||||
}
|
||||
|
||||
function updateSetting(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
config[section][key] = val;
|
||||
}
|
||||
|
||||
function updateSettingBool(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
config[section][key] = !!val;
|
||||
}
|
||||
|
||||
function updateSettingInt(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
var n = parseInt(val, 10);
|
||||
config[section][key] = isNaN(n) ? val : n;
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
if (!config) { setStatus("No config loaded.", true); return; }
|
||||
try {
|
||||
// Sync steps checkboxes before save (in case table was rebuilt)
|
||||
syncStepsFromTable();
|
||||
|
||||
var json = JSON.stringify(config, null, 2);
|
||||
var f = fso.CreateTextFile(configPath, true, true);
|
||||
f.Write(json);
|
||||
f.Close();
|
||||
setStatus("Saved: " + configPath + " [" + now() + "]");
|
||||
} catch(e) {
|
||||
setStatus("Save error: " + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function syncStepsFromTable() {
|
||||
if (!config.steps) { config.steps = {}; }
|
||||
var rows = document.getElementById("steps-tbody").getElementsByTagName("tr");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var chk = rows[i].getElementsByTagName("input")[0];
|
||||
if (chk && STEP_DEFS[i]) {
|
||||
config.steps[STEP_DEFS[i].key] = chk.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showTab(name) {
|
||||
var panels = document.getElementsByClassName("tab-panel");
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
panels[i].className = "tab-panel";
|
||||
}
|
||||
var tabs = document.getElementsByClassName("tab");
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
tabs[i].className = "tab";
|
||||
}
|
||||
document.getElementById("tab-" + name).className = "tab-panel active";
|
||||
var allTabs = document.getElementById("tabs").getElementsByClassName("tab");
|
||||
var nameMap = { steps: 0, software: 1, settings: 2 };
|
||||
if (nameMap[name] !== undefined) {
|
||||
allTabs[nameMap[name]].className = "tab active";
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip(evt, text) {
|
||||
var t = document.getElementById("tooltip");
|
||||
t.innerText = text;
|
||||
t.style.display = "block";
|
||||
positionTooltip(evt);
|
||||
}
|
||||
|
||||
function positionTooltip(evt) {
|
||||
var t = document.getElementById("tooltip");
|
||||
var x = evt.clientX + 12;
|
||||
var y = evt.clientY + 12;
|
||||
if (x + 350 > document.body.clientWidth) { x = evt.clientX - 350; }
|
||||
if (y + 80 > document.body.clientHeight) { y = evt.clientY - 80; }
|
||||
t.style.left = x + "px";
|
||||
t.style.top = y + "px";
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
document.getElementById("tooltip").style.display = "none";
|
||||
}
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
var el = document.getElementById("status");
|
||||
el.innerText = msg;
|
||||
el.style.color = isError ? "#d08080" : "#7aabbd";
|
||||
}
|
||||
|
||||
function now() {
|
||||
var d = new Date();
|
||||
return d.getHours() + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
||||
}
|
||||
|
||||
function pad(n) { return n < 10 ? "0" + n : String(n); }
|
||||
|
||||
function escapeQ(s) {
|
||||
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, """);
|
||||
}
|
||||
|
||||
window.onload = function() { init(); };
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +1,15 @@
|
|||
{
|
||||
"steps": {
|
||||
"adminAccount": true,
|
||||
"bloatware": true,
|
||||
"software": true,
|
||||
"systemRegistry": true,
|
||||
"defaultProfile": true,
|
||||
"personalization": true,
|
||||
"scheduledTasks": true,
|
||||
"desktopInfo": true,
|
||||
"activation": true
|
||||
},
|
||||
"deployment": {
|
||||
"timezone": "Central Europe Standard Time",
|
||||
"locale": "cs-CZ"
|
||||
|
|
|
|||
244
flash/Deploy-Windows.ps1
Normal file
244
flash/Deploy-Windows.ps1
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#Requires -RunAsAdministrator
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipBloatware,
|
||||
[switch]$SkipSoftware,
|
||||
[switch]$SkipDefaultProfile,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Paths
|
||||
# -----------------------------------------------------------------------
|
||||
$ScriptRoot = $PSScriptRoot
|
||||
$LogDir = "C:\Windows\Setup\Scripts"
|
||||
$LogFile = "$LogDir\Deploy.log"
|
||||
$ConfigFile = "$ScriptRoot\config\config.json"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ValidateSet("INFO","OK","ERROR","WARN","STEP")]
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
$timestamp = Get-Date -Format "HH:mm:ss"
|
||||
$line = "[$timestamp] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
switch ($Level) {
|
||||
"OK" { Write-Host $line -ForegroundColor Green }
|
||||
"ERROR" { Write-Host $line -ForegroundColor Red }
|
||||
"WARN" { Write-Host $line -ForegroundColor Yellow }
|
||||
"STEP" { Write-Host $line -ForegroundColor Cyan }
|
||||
default { Write-Host $line }
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step runner - catches errors, logs, always continues
|
||||
# -----------------------------------------------------------------------
|
||||
$StepResults = [System.Collections.Generic.List[hashtable]]::new()
|
||||
|
||||
function Invoke-Step {
|
||||
param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
Write-Log "---- $Name ----" -Level STEP
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Log "DryRun - skipping execution" -Level WARN
|
||||
$StepResults.Add(@{ Name = $Name; Status = "DRYRUN" })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
& $Action
|
||||
Write-Log "$Name - OK" -Level OK
|
||||
$StepResults.Add(@{ Name = $Name; Status = "OK" })
|
||||
}
|
||||
catch {
|
||||
Write-Log "$Name - ERROR: $_" -Level ERROR
|
||||
$StepResults.Add(@{ Name = $Name; Status = "ERROR" })
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Init
|
||||
# -----------------------------------------------------------------------
|
||||
if (-not (Test-Path $LogDir)) {
|
||||
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Log "========================================" -Level INFO
|
||||
Write-Log "Deploy-Windows.ps1 started" -Level INFO
|
||||
Write-Log "Computer: $env:COMPUTERNAME" -Level INFO
|
||||
Write-Log "User: $env:USERNAME" -Level INFO
|
||||
Write-Log "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Level INFO
|
||||
if ($DryRun) { Write-Log "Mode: DRY RUN" -Level WARN }
|
||||
Write-Log "========================================" -Level INFO
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Load config
|
||||
# -----------------------------------------------------------------------
|
||||
$Config = $null
|
||||
Invoke-Step -Name "Load config.json" -Action {
|
||||
if (-not (Test-Path $ConfigFile)) {
|
||||
throw "config.json not found: $ConfigFile"
|
||||
}
|
||||
$script:Config = Get-Content $ConfigFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Write-Log "Config loaded from $ConfigFile" -Level INFO
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Build step enable/disable map from config + CLI overrides
|
||||
# -----------------------------------------------------------------------
|
||||
$stepsEnabled = @{
|
||||
adminAccount = $true
|
||||
bloatware = $true
|
||||
software = $true
|
||||
systemRegistry = $true
|
||||
defaultProfile = $true
|
||||
personalization = $true
|
||||
scheduledTasks = $true
|
||||
desktopInfo = $true
|
||||
activation = $true
|
||||
}
|
||||
if ($Config -and $Config.steps) {
|
||||
foreach ($key in @($stepsEnabled.Keys)) {
|
||||
$val = $Config.steps.$key
|
||||
if ($null -ne $val) { $stepsEnabled[$key] = [bool]$val }
|
||||
}
|
||||
}
|
||||
# CLI switches override config.steps
|
||||
if ($SkipBloatware) { $stepsEnabled['bloatware'] = $false }
|
||||
if ($SkipSoftware) { $stepsEnabled['software'] = $false }
|
||||
if ($SkipDefaultProfile) { $stepsEnabled['defaultProfile'] = $false }
|
||||
|
||||
function Skip-Step {
|
||||
param([string]$Name)
|
||||
Write-Log "$Name - SKIPPED (disabled in config)" -Level WARN
|
||||
$StepResults.Add(@{ Name = $Name; Status = "SKIPPED" })
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 0a - Admin account
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['adminAccount']) {
|
||||
Invoke-Step -Name "Step 0a - Admin account" -Action {
|
||||
& "$ScriptRoot\scripts\00-admin-account.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 0a - Admin account" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 0b - Windows activation
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['activation']) {
|
||||
Invoke-Step -Name "Step 0b - Windows activation" -Action {
|
||||
& "$ScriptRoot\scripts\08-activation.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 0b - Windows activation" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 1 - Bloatware removal
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['bloatware']) {
|
||||
Invoke-Step -Name "Step 1 - Bloatware removal" -Action {
|
||||
& "$ScriptRoot\scripts\01-bloatware.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 1 - Bloatware removal" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 2 - Software installation
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['software']) {
|
||||
Invoke-Step -Name "Step 2 - Software installation" -Action {
|
||||
& "$ScriptRoot\scripts\02-software.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 2 - Software installation" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 3 - System registry (HKLM)
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['systemRegistry']) {
|
||||
Invoke-Step -Name "Step 3 - System registry" -Action {
|
||||
& "$ScriptRoot\scripts\03-system-registry.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 3 - System registry" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 4 - Default profile (NTUSER.DAT)
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['defaultProfile']) {
|
||||
Invoke-Step -Name "Step 4 - Default profile" -Action {
|
||||
& "$ScriptRoot\scripts\04-default-profile.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 4 - Default profile" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 5 - Personalization
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['personalization']) {
|
||||
Invoke-Step -Name "Step 5 - Personalization" -Action {
|
||||
& "$ScriptRoot\scripts\05-personalization.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 5 - Personalization" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 6 - Scheduled tasks
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['scheduledTasks']) {
|
||||
Invoke-Step -Name "Step 6 - Scheduled tasks" -Action {
|
||||
& "$ScriptRoot\scripts\06-scheduled-tasks.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 6 - Scheduled tasks" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Step 7 - DesktopInfo
|
||||
# -----------------------------------------------------------------------
|
||||
if ($stepsEnabled['desktopInfo']) {
|
||||
Invoke-Step -Name "Step 7 - DesktopInfo" -Action {
|
||||
& "$ScriptRoot\scripts\07-desktop-info.ps1" -Config $Config -LogFile $LogFile
|
||||
}
|
||||
} else { Skip-Step "Step 7 - DesktopInfo" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "========================================" -Level INFO
|
||||
Write-Log "SUMMARY" -Level INFO
|
||||
Write-Log "========================================" -Level INFO
|
||||
|
||||
$countOK = ($StepResults | Where-Object { $_.Status -eq "OK" }).Count
|
||||
$countError = ($StepResults | Where-Object { $_.Status -eq "ERROR" }).Count
|
||||
$countSkipped = ($StepResults | Where-Object { $_.Status -eq "SKIPPED" }).Count
|
||||
$countDryRun = ($StepResults | Where-Object { $_.Status -eq "DRYRUN" }).Count
|
||||
|
||||
foreach ($r in $StepResults) {
|
||||
$lvl = switch ($r.Status) {
|
||||
"OK" { "OK" }
|
||||
"ERROR" { "ERROR" }
|
||||
"SKIPPED" { "WARN" }
|
||||
"DRYRUN" { "WARN" }
|
||||
}
|
||||
Write-Log "$($r.Status.PadRight(8)) $($r.Name)" -Level $lvl
|
||||
}
|
||||
|
||||
Write-Log "----------------------------------------" -Level INFO
|
||||
Write-Log "OK: $countOK ERROR: $countError SKIPPED: $countSkipped DRYRUN: $countDryRun" -Level INFO
|
||||
Write-Log "Log saved to: $LogFile" -Level INFO
|
||||
Write-Log "========================================" -Level INFO
|
||||
|
||||
if ($countError -gt 0) {
|
||||
Write-Log "Deployment finished with errors. Review log: $LogFile" -Level ERROR
|
||||
exit 1
|
||||
} else {
|
||||
Write-Log "Deployment finished successfully." -Level OK
|
||||
exit 0
|
||||
}
|
||||
125
flash/Run.cmd
Normal file
125
flash/Run.cmd
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Auto-elevate to Administrator if not already elevated
|
||||
:: -----------------------------------------------------------------------
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo Requesting administrator privileges...
|
||||
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Paths
|
||||
:: -----------------------------------------------------------------------
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "DEPLOY_PS1=%SCRIPT_DIR%Deploy-Windows.ps1"
|
||||
set "CONFIG_JSON=%SCRIPT_DIR%config\config.json"
|
||||
set "CONFIG_EDITOR=%SCRIPT_DIR%config-editor.hta"
|
||||
set "LOG_FILE=C:\Windows\Setup\Scripts\Deploy.log"
|
||||
|
||||
:MENU
|
||||
cls
|
||||
echo.
|
||||
echo ================================================
|
||||
echo X9 - Windows Deployment
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Config : %CONFIG_JSON%
|
||||
echo Log : %LOG_FILE%
|
||||
echo.
|
||||
echo [1] Full deployment (uses config.json)
|
||||
echo [2] Dry run (no changes, log only)
|
||||
echo [3] Skip bloatware removal
|
||||
echo [4] Skip software install
|
||||
echo [5] Open config editor (config-editor.hta)
|
||||
echo [0] Exit
|
||||
echo.
|
||||
set /p CHOICE=" Select [0-5]: "
|
||||
|
||||
if "%CHOICE%"=="0" goto EXIT
|
||||
if "%CHOICE%"=="1" goto FULL
|
||||
if "%CHOICE%"=="2" goto DRYRUN
|
||||
if "%CHOICE%"=="3" goto SKIP_BLOATWARE
|
||||
if "%CHOICE%"=="4" goto SKIP_SOFTWARE
|
||||
if "%CHOICE%"=="5" goto OPEN_EDITOR
|
||||
|
||||
echo Invalid choice. Try again.
|
||||
timeout /t 2 >nul
|
||||
goto MENU
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [1] Full deployment
|
||||
:: -----------------------------------------------------------------------
|
||||
:FULL
|
||||
cls
|
||||
echo.
|
||||
echo Starting full deployment...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%"
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [2] Dry run
|
||||
:: -----------------------------------------------------------------------
|
||||
:DRYRUN
|
||||
cls
|
||||
echo.
|
||||
echo Starting dry run (no changes will be made)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -DryRun
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [3] Skip bloatware
|
||||
:: -----------------------------------------------------------------------
|
||||
:SKIP_BLOATWARE
|
||||
cls
|
||||
echo.
|
||||
echo Starting deployment (bloatware removal skipped)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -SkipBloatware
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [4] Skip software
|
||||
:: -----------------------------------------------------------------------
|
||||
:SKIP_SOFTWARE
|
||||
cls
|
||||
echo.
|
||||
echo Starting deployment (software install skipped)...
|
||||
echo.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%DEPLOY_PS1%" -SkipSoftware
|
||||
goto DONE
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: [5] Config editor
|
||||
:: -----------------------------------------------------------------------
|
||||
:OPEN_EDITOR
|
||||
if not exist "%CONFIG_EDITOR%" (
|
||||
echo ERROR: config-editor.hta not found: %CONFIG_EDITOR%
|
||||
pause
|
||||
goto MENU
|
||||
)
|
||||
start "" mshta.exe "%CONFIG_EDITOR%"
|
||||
goto MENU
|
||||
|
||||
:: -----------------------------------------------------------------------
|
||||
:: Done
|
||||
:: -----------------------------------------------------------------------
|
||||
:DONE
|
||||
echo.
|
||||
echo ================================================
|
||||
echo Deployment finished.
|
||||
echo Log: %LOG_FILE%
|
||||
echo ================================================
|
||||
echo.
|
||||
pause
|
||||
goto MENU
|
||||
|
||||
:EXIT
|
||||
endlocal
|
||||
exit /b 0
|
||||
632
flash/config-editor.hta
Normal file
632
flash/config-editor.hta
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>X9 - Deployment Config Editor</title>
|
||||
<HTA:APPLICATION
|
||||
ID="ConfigEditor"
|
||||
APPLICATIONNAME="X9 Config Editor"
|
||||
SCROLL="no"
|
||||
SINGLEINSTANCE="yes"
|
||||
WINDOWSTATE="normal"
|
||||
INNERBORDER="no"
|
||||
SELECTION="no"
|
||||
CONTEXTMENU="no"
|
||||
/>
|
||||
<meta http-equiv="x-ua-compatible" content="ie=11">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: Segoe UI, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
background: #1a2a33;
|
||||
color: #d0dde3;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#header {
|
||||
background: #223B47;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 2px solid #2e5568;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e8f4f8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
#header .config-path {
|
||||
font-size: 11px;
|
||||
color: #7aabbd;
|
||||
margin-top: 3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
#tabs {
|
||||
display: flex;
|
||||
background: #1a2a33;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
color: #7aabbd;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab:hover { color: #b0d4e0; }
|
||||
.tab.active { color: #e8f4f8; border-bottom: 2px solid #4a9aba; background: #1e3340; }
|
||||
#content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
.tab-panel { display: none; padding: 16px 18px; }
|
||||
.tab-panel.active { display: block; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th {
|
||||
background: #223B47;
|
||||
color: #9ac8d8;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 7px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #1e3340;
|
||||
vertical-align: middle;
|
||||
}
|
||||
tr:hover td { background: #1e3340; }
|
||||
.step-num {
|
||||
color: #4a9aba;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
width: 36px;
|
||||
}
|
||||
.step-name { color: #d0dde3; }
|
||||
.info-btn {
|
||||
background: #2e5568;
|
||||
border: none;
|
||||
color: #7aabbd;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.info-btn:hover { background: #3a6f8a; color: #e8f4f8; }
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: #4a9aba;
|
||||
}
|
||||
input[type="text"], select {
|
||||
background: #1a2a33;
|
||||
border: 1px solid #2e5568;
|
||||
color: #d0dde3;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
input[type="text"]:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: #4a9aba;
|
||||
}
|
||||
.label-cell {
|
||||
width: 160px;
|
||||
color: #9ac8d8;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-top: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.section-title {
|
||||
color: #4a9aba;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 16px 0 8px 0;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #2e5568;
|
||||
}
|
||||
.section-title:first-child { margin-top: 0; }
|
||||
.btn {
|
||||
background: #223B47;
|
||||
border: 1px solid #2e5568;
|
||||
color: #d0dde3;
|
||||
padding: 5px 12px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn:hover { background: #2e5568; color: #e8f4f8; }
|
||||
.btn-danger {
|
||||
background: #3d1f1f;
|
||||
border-color: #6b2c2c;
|
||||
color: #d08080;
|
||||
}
|
||||
.btn-danger:hover { background: #5a2020; color: #f0a0a0; }
|
||||
.btn-add {
|
||||
background: #1a3a2a;
|
||||
border-color: #2e6a4a;
|
||||
color: #80c8a0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn-add:hover { background: #235a38; color: #a0e0b8; }
|
||||
#footer {
|
||||
background: #223B47;
|
||||
border-top: 2px solid #2e5568;
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#btn-save {
|
||||
background: #1a5c3a;
|
||||
border: 1px solid #2a8a58;
|
||||
color: #80e0b0;
|
||||
padding: 7px 22px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
#btn-save:hover { background: #206e46; color: #a0f0c8; }
|
||||
#status {
|
||||
font-size: 12px;
|
||||
color: #7aabbd;
|
||||
flex: 1;
|
||||
}
|
||||
#tooltip {
|
||||
position: fixed;
|
||||
background: #0f1e26;
|
||||
border: 1px solid #3a7a9a;
|
||||
color: #c0dde8;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
max-width: 340px;
|
||||
line-height: 1.5;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||
}
|
||||
.settings-grid { display: grid; grid-template-columns: 160px 1fr; gap: 8px 12px; align-items: center; }
|
||||
.settings-grid .span2 { grid-column: 1 / -1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="header">
|
||||
<h1>X9 - Deployment Config</h1>
|
||||
<div class="config-path" id="config-path-display">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="tabs">
|
||||
<div class="tab active" onclick="showTab('steps')">Steps</div>
|
||||
<div class="tab" onclick="showTab('software')">Software</div>
|
||||
<div class="tab" onclick="showTab('settings')">Settings</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<!-- STEPS TAB -->
|
||||
<div class="tab-panel active" id="tab-steps">
|
||||
<table id="steps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">On</th>
|
||||
<th style="width:36px;">Step</th>
|
||||
<th>Name</th>
|
||||
<th style="width:32px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="steps-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SOFTWARE TAB -->
|
||||
<div class="tab-panel" id="tab-software">
|
||||
<table id="software-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package Name</th>
|
||||
<th>Winget ID</th>
|
||||
<th style="width:70px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="software-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn btn-add" onclick="addSoftwareRow()">+ Add package</button>
|
||||
</div>
|
||||
|
||||
<!-- SETTINGS TAB -->
|
||||
<div class="tab-panel" id="tab-settings">
|
||||
<div class="section-title">Deployment</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Timezone</div>
|
||||
<div><input type="text" id="s-timezone" onchange="updateSetting('deployment','timezone',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Locale</div>
|
||||
<div><input type="text" id="s-locale" onchange="updateSetting('deployment','locale',this.value)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Admin Account</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Username</div>
|
||||
<div><input type="text" id="s-admin-user" onchange="updateSetting('adminAccount','username',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Password</div>
|
||||
<div><input type="text" id="s-admin-pass" onchange="updateSetting('adminAccount','password',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Description</div>
|
||||
<div><input type="text" id="s-admin-desc" onchange="updateSetting('adminAccount','description',this.value)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">PDF Default</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Force Adobe Reader</div>
|
||||
<div><input type="checkbox" id="s-force-adobe" onchange="updateSettingBool('pdfDefault','forceAdobeReader',this.checked)"></div>
|
||||
|
||||
<div class="label-cell">Scheduled Task</div>
|
||||
<div><input type="checkbox" id="s-pdf-task" onchange="updateSettingBool('pdfDefault','scheduledTaskEnabled',this.checked)"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Desktop Info</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Enabled</div>
|
||||
<div><input type="checkbox" id="s-di-enabled" onchange="updateSettingBool('desktopInfo','enabled',this.checked)"></div>
|
||||
|
||||
<div class="label-cell">Position</div>
|
||||
<div>
|
||||
<select id="s-di-position" onchange="updateSetting('desktopInfo','position',this.value)">
|
||||
<option value="bottomRight">Bottom Right</option>
|
||||
<option value="bottomLeft">Bottom Left</option>
|
||||
<option value="topRight">Top Right</option>
|
||||
<option value="topLeft">Top Left</option>
|
||||
<option value="center">Center</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="label-cell">Font Size</div>
|
||||
<div><input type="text" id="s-di-fontsize" onchange="updateSettingInt('desktopInfo','fontSize',this.value)"></div>
|
||||
|
||||
<div class="label-cell">Font Color</div>
|
||||
<div><input type="text" id="s-di-fontcolor" onchange="updateSetting('desktopInfo','fontColor',this.value)" placeholder="#FFFFFF"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Activation</div>
|
||||
<div class="settings-grid">
|
||||
<div class="label-cell">Product Key</div>
|
||||
<div><input type="text" id="s-act-key" onchange="updateSetting('activation','productKey',this.value)" placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"></div>
|
||||
|
||||
<div class="label-cell">KMS Server</div>
|
||||
<div><input type="text" id="s-act-kms" onchange="updateSetting('activation','kmsServer',this.value)" placeholder="kms.example.com"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<button id="btn-save" onclick="saveConfig()">Save config.json</button>
|
||||
<div id="status">Ready.</div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip"></div>
|
||||
|
||||
<script language="JScript">
|
||||
|
||||
var STEP_DEFS = [
|
||||
{ key: "adminAccount", num: "00", label: "Admin account", info: "Creates local admin account for MSP remote access. Account name and generated password are saved to Deploy.log." },
|
||||
{ key: "bloatware", num: "01", label: "Bloatware removal", info: "Removes 44 pre-installed UWP apps (Teams, Xbox, Cortana, Solitaire, Office Hub, Skype...) and unused Windows Capabilities and Features. Calculator is kept." },
|
||||
{ key: "software", num: "02", label: "Software install", info: "Installs packages from the list below via winget. Also sets Adobe Reader as default PDF viewer via HKCR." },
|
||||
{ key: "systemRegistry", num: "03", label: "System registry", info: "HKLM-wide tweaks: disables Widgets, Teams auto-install, Edge first run, OneDrive, Outlook auto-install, GameDVR, Recall. Sets timezone. Hides taskbar search box via policy (Win11)." },
|
||||
{ key: "defaultProfile", num: "04", label: "Default profile", info: "Modifies C:\\Users\\Default\\NTUSER.DAT so ALL future users inherit: left-aligned taskbar, hidden search/Copilot/TaskView/Widgets buttons, file extensions visible, Explorer opens to This PC, Num Lock on." },
|
||||
{ key: "personalization", num: "05", label: "Personalization", info: "Dark system theme, light app theme, accent color #223B47, transparency off, accent on title bars. Applied to Default profile and current user." },
|
||||
{ key: "scheduledTasks", num: "06", label: "Scheduled tasks", info: "Registers 4 tasks: ShowAllTrayIcons (logon - clears systray cache + restarts Explorer), UnlockStartLayout (once), PDF-DefaultApp (logon as SYSTEM - restores HKCR association), DesktopInfo (logon - renders wallpaper)." },
|
||||
{ key: "desktopInfo", num: "07", label: "Desktop info", info: "Custom desktop wallpaper showing: computer name, IP, OS version, username, deployment date. Rendered as BMP on every logon via scheduled task. Replaces BackInfo.exe." },
|
||||
{ key: "activation", num: "08", label: "Windows activation", info: "Checks and applies Windows activation." }
|
||||
];
|
||||
|
||||
var configPath = "";
|
||||
var config = null;
|
||||
var fso = null;
|
||||
|
||||
function init() {
|
||||
try {
|
||||
fso = new ActiveXObject("Scripting.FileSystemObject");
|
||||
|
||||
// Derive config path from HTA location
|
||||
var htaPath = location.pathname.replace(/\//g, "\\");
|
||||
// Remove leading backslash from pathname
|
||||
if (htaPath.charAt(0) === "\\") {
|
||||
htaPath = htaPath.substring(1);
|
||||
}
|
||||
var dir = fso.GetParentFolderName(htaPath);
|
||||
configPath = dir + "\\config\\config.json";
|
||||
|
||||
document.getElementById("config-path-display").innerText = configPath;
|
||||
|
||||
loadConfig();
|
||||
buildStepsTable();
|
||||
resizeWindow();
|
||||
} catch(e) {
|
||||
setStatus("Init error: " + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeWindow() {
|
||||
window.resizeTo(800, 720);
|
||||
window.moveTo(
|
||||
(screen.width - 800) / 2,
|
||||
(screen.height - 720) / 2
|
||||
);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
if (!fso.FileExists(configPath)) {
|
||||
setStatus("config.json not found: " + configPath, true);
|
||||
config = {};
|
||||
return;
|
||||
}
|
||||
var f = fso.OpenTextFile(configPath, 1, false, -1);
|
||||
var raw = f.ReadAll();
|
||||
f.Close();
|
||||
config = JSON.parse(raw);
|
||||
setStatus("Loaded: " + configPath);
|
||||
populateSettings();
|
||||
buildSoftwareTable();
|
||||
} catch(e) {
|
||||
setStatus("Load error: " + e.message, true);
|
||||
config = {};
|
||||
}
|
||||
}
|
||||
|
||||
function buildStepsTable() {
|
||||
var tbody = document.getElementById("steps-tbody");
|
||||
var html = "";
|
||||
for (var i = 0; i < STEP_DEFS.length; i++) {
|
||||
var s = STEP_DEFS[i];
|
||||
var checked = "";
|
||||
// Check config.steps if loaded, default true
|
||||
if (config && config.steps && config.steps[s.key] === false) {
|
||||
checked = "";
|
||||
} else {
|
||||
checked = "checked";
|
||||
}
|
||||
html += "<tr>";
|
||||
html += "<td><input type='checkbox' " + checked + " onclick=\"toggleStep('" + s.key + "', this.checked)\"></td>";
|
||||
html += "<td class='step-num'>" + s.num + "</td>";
|
||||
html += "<td class='step-name'>" + s.label + "</td>";
|
||||
html += "<td><button class='info-btn' onmouseover=\"showTooltip(event, '" + escapeQ(s.info) + "')\" onmouseout='hideTooltip()'>i</button></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function buildSoftwareTable() {
|
||||
var tbody = document.getElementById("software-tbody");
|
||||
var html = "";
|
||||
if (config && config.software && config.software.install) {
|
||||
var list = config.software.install;
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
html += makeSoftwareRow(i, list[i].name, list[i].wingetId);
|
||||
}
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function makeSoftwareRow(idx, name, wingetId) {
|
||||
return "<tr id='sw-row-" + idx + "'>" +
|
||||
"<td><input type='text' value='" + escapeQ(name) + "' onchange=\"updateSoftwareName(" + idx + ", this.value)\"></td>" +
|
||||
"<td><input type='text' value='" + escapeQ(wingetId) + "' onchange=\"updateSoftwareId(" + idx + ", this.value)\"></td>" +
|
||||
"<td><button class='btn btn-danger' onclick='removeSoftwareRow(" + idx + ")'>Remove</button></td>" +
|
||||
"</tr>";
|
||||
}
|
||||
|
||||
function addSoftwareRow() {
|
||||
if (!config.software) { config.software = {}; }
|
||||
if (!config.software.install) { config.software.install = []; }
|
||||
config.software.install.push({ name: "", wingetId: "" });
|
||||
buildSoftwareTable();
|
||||
setStatus("Package added. Fill in name and Winget ID, then save.");
|
||||
}
|
||||
|
||||
function removeSoftwareRow(idx) {
|
||||
if (!config || !config.software || !config.software.install) { return; }
|
||||
config.software.install.splice(idx, 1);
|
||||
buildSoftwareTable();
|
||||
setStatus("Package removed. Click Save to persist.");
|
||||
}
|
||||
|
||||
function updateSoftwareName(idx, val) {
|
||||
if (config && config.software && config.software.install && config.software.install[idx] !== undefined) {
|
||||
config.software.install[idx].name = val;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSoftwareId(idx, val) {
|
||||
if (config && config.software && config.software.install && config.software.install[idx] !== undefined) {
|
||||
config.software.install[idx].wingetId = val;
|
||||
}
|
||||
}
|
||||
|
||||
function populateSettings() {
|
||||
if (!config) { return; }
|
||||
|
||||
if (config.deployment) {
|
||||
setVal("s-timezone", config.deployment.timezone || "");
|
||||
setVal("s-locale", config.deployment.locale || "");
|
||||
}
|
||||
if (config.adminAccount) {
|
||||
setVal("s-admin-user", config.adminAccount.username || "");
|
||||
setVal("s-admin-pass", config.adminAccount.password || "");
|
||||
setVal("s-admin-desc", config.adminAccount.description || "");
|
||||
}
|
||||
if (config.pdfDefault) {
|
||||
setChk("s-force-adobe", config.pdfDefault.forceAdobeReader !== false);
|
||||
setChk("s-pdf-task", config.pdfDefault.scheduledTaskEnabled !== false);
|
||||
}
|
||||
if (config.desktopInfo) {
|
||||
setChk("s-di-enabled", config.desktopInfo.enabled !== false);
|
||||
setSelVal("s-di-position", config.desktopInfo.position || "bottomRight");
|
||||
setVal("s-di-fontsize", config.desktopInfo.fontSize !== undefined ? String(config.desktopInfo.fontSize) : "12");
|
||||
setVal("s-di-fontcolor", config.desktopInfo.fontColor || "#FFFFFF");
|
||||
}
|
||||
if (config.activation) {
|
||||
setVal("s-act-key", config.activation.productKey || "");
|
||||
setVal("s-act-kms", config.activation.kmsServer || "");
|
||||
}
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) { el.value = val; }
|
||||
}
|
||||
|
||||
function setChk(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) { el.checked = !!val; }
|
||||
}
|
||||
|
||||
function setSelVal(id, val) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) { return; }
|
||||
for (var i = 0; i < el.options.length; i++) {
|
||||
if (el.options[i].value === val) {
|
||||
el.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStep(key, enabled) {
|
||||
if (!config) { return; }
|
||||
if (!config.steps) { config.steps = {}; }
|
||||
config.steps[key] = enabled;
|
||||
}
|
||||
|
||||
function updateSetting(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
config[section][key] = val;
|
||||
}
|
||||
|
||||
function updateSettingBool(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
config[section][key] = !!val;
|
||||
}
|
||||
|
||||
function updateSettingInt(section, key, val) {
|
||||
if (!config) { return; }
|
||||
if (!config[section]) { config[section] = {}; }
|
||||
var n = parseInt(val, 10);
|
||||
config[section][key] = isNaN(n) ? val : n;
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
if (!config) { setStatus("No config loaded.", true); return; }
|
||||
try {
|
||||
// Sync steps checkboxes before save (in case table was rebuilt)
|
||||
syncStepsFromTable();
|
||||
|
||||
var json = JSON.stringify(config, null, 2);
|
||||
var f = fso.CreateTextFile(configPath, true, true);
|
||||
f.Write(json);
|
||||
f.Close();
|
||||
setStatus("Saved: " + configPath + " [" + now() + "]");
|
||||
} catch(e) {
|
||||
setStatus("Save error: " + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function syncStepsFromTable() {
|
||||
if (!config.steps) { config.steps = {}; }
|
||||
var rows = document.getElementById("steps-tbody").getElementsByTagName("tr");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var chk = rows[i].getElementsByTagName("input")[0];
|
||||
if (chk && STEP_DEFS[i]) {
|
||||
config.steps[STEP_DEFS[i].key] = chk.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showTab(name) {
|
||||
var panels = document.getElementsByClassName("tab-panel");
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
panels[i].className = "tab-panel";
|
||||
}
|
||||
var tabs = document.getElementsByClassName("tab");
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
tabs[i].className = "tab";
|
||||
}
|
||||
document.getElementById("tab-" + name).className = "tab-panel active";
|
||||
var allTabs = document.getElementById("tabs").getElementsByClassName("tab");
|
||||
var nameMap = { steps: 0, software: 1, settings: 2 };
|
||||
if (nameMap[name] !== undefined) {
|
||||
allTabs[nameMap[name]].className = "tab active";
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip(evt, text) {
|
||||
var t = document.getElementById("tooltip");
|
||||
t.innerText = text;
|
||||
t.style.display = "block";
|
||||
positionTooltip(evt);
|
||||
}
|
||||
|
||||
function positionTooltip(evt) {
|
||||
var t = document.getElementById("tooltip");
|
||||
var x = evt.clientX + 12;
|
||||
var y = evt.clientY + 12;
|
||||
if (x + 350 > document.body.clientWidth) { x = evt.clientX - 350; }
|
||||
if (y + 80 > document.body.clientHeight) { y = evt.clientY - 80; }
|
||||
t.style.left = x + "px";
|
||||
t.style.top = y + "px";
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
document.getElementById("tooltip").style.display = "none";
|
||||
}
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
var el = document.getElementById("status");
|
||||
el.innerText = msg;
|
||||
el.style.color = isError ? "#d08080" : "#7aabbd";
|
||||
}
|
||||
|
||||
function now() {
|
||||
var d = new Date();
|
||||
return d.getHours() + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
||||
}
|
||||
|
||||
function pad(n) { return n < 10 ? "0" + n : String(n); }
|
||||
|
||||
function escapeQ(s) {
|
||||
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, """);
|
||||
}
|
||||
|
||||
window.onload = function() { init(); };
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
49
flash/config/config.json
Normal file
49
flash/config/config.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"steps": {
|
||||
"adminAccount": true,
|
||||
"bloatware": true,
|
||||
"software": true,
|
||||
"systemRegistry": true,
|
||||
"defaultProfile": true,
|
||||
"personalization": true,
|
||||
"scheduledTasks": true,
|
||||
"desktopInfo": true,
|
||||
"activation": true
|
||||
},
|
||||
"deployment": {
|
||||
"timezone": "Central Europe Standard Time",
|
||||
"locale": "cs-CZ"
|
||||
},
|
||||
"adminAccount": {
|
||||
"username": "adminx9",
|
||||
"password": "AdminX9.AdminX9",
|
||||
"description": "X9 MSP admin account"
|
||||
},
|
||||
"activation": {
|
||||
"productKey": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
|
||||
"kmsServer": ""
|
||||
},
|
||||
"software": {
|
||||
"install": [
|
||||
{ "name": "7-Zip", "wingetId": "7zip.7zip" },
|
||||
{ "name": "Adobe Acrobat Reader","wingetId": "Adobe.Acrobat.Reader.64-bit" },
|
||||
{ "name": "OpenVPN Connect", "wingetId": "OpenVPNTechnologies.OpenVPNConnect" }
|
||||
]
|
||||
},
|
||||
"bloatware": {
|
||||
"keepPackages": [
|
||||
"Microsoft.WindowsCalculator"
|
||||
]
|
||||
},
|
||||
"desktopInfo": {
|
||||
"enabled": true,
|
||||
"position": "bottomRight",
|
||||
"fontSize": 12,
|
||||
"fontColor": "#FFFFFF",
|
||||
"backgroundColor": "transparent"
|
||||
},
|
||||
"pdfDefault": {
|
||||
"forceAdobeReader": true,
|
||||
"scheduledTaskEnabled": true
|
||||
}
|
||||
}
|
||||
94
flash/scripts/00-admin-account.ps1
Normal file
94
flash/scripts/00-admin-account.ps1
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Read account config
|
||||
# -----------------------------------------------------------------------
|
||||
$accountName = "adminx9"
|
||||
$accountPass = "AdminX9.AdminX9"
|
||||
$accountDesc = "X9 MSP admin account"
|
||||
|
||||
if ($Config -and $Config.adminAccount) {
|
||||
if ($Config.adminAccount.username) { $accountName = $Config.adminAccount.username }
|
||||
if ($Config.adminAccount.password) { $accountPass = $Config.adminAccount.password }
|
||||
if ($Config.adminAccount.description) { $accountDesc = $Config.adminAccount.description }
|
||||
}
|
||||
|
||||
Write-Log "Creating admin account: $accountName" -Level INFO
|
||||
|
||||
$securePass = ConvertTo-SecureString $accountPass -AsPlainText -Force
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Create or update account
|
||||
# -----------------------------------------------------------------------
|
||||
$existing = Get-LocalUser -Name $accountName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existing) {
|
||||
Write-Log " Account already exists - updating password" -Level INFO
|
||||
try {
|
||||
Set-LocalUser -Name $accountName -Password $securePass -PasswordNeverExpires $true
|
||||
Enable-LocalUser -Name $accountName
|
||||
Write-Log " Account updated: $accountName" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to update account: $_" -Level ERROR
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
New-LocalUser -Name $accountName `
|
||||
-Password $securePass `
|
||||
-Description $accountDesc `
|
||||
-PasswordNeverExpires `
|
||||
-UserMayNotChangePassword `
|
||||
-ErrorAction Stop | Out-Null
|
||||
Write-Log " Account created: $accountName" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to create account: $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Add to Administrators group
|
||||
# -----------------------------------------------------------------------
|
||||
try {
|
||||
$adminsGroup = (Get-LocalGroup | Where-Object { $_.SID -eq "S-1-5-32-544" }).Name
|
||||
$members = Get-LocalGroupMember -Group $adminsGroup -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "*$accountName" }
|
||||
if (-not $members) {
|
||||
Add-LocalGroupMember -Group $adminsGroup -Member $accountName -ErrorAction Stop
|
||||
Write-Log " Added to $adminsGroup" -Level OK
|
||||
} else {
|
||||
Write-Log " Already in $adminsGroup" -Level INFO
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to add to Administrators: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Hide account from login screen
|
||||
# -----------------------------------------------------------------------
|
||||
try {
|
||||
$specialPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
|
||||
if (-not (Test-Path $specialPath)) {
|
||||
New-Item -Path $specialPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $specialPath -Name $accountName -Value 0 -Type DWord -Force
|
||||
Write-Log " Account hidden from login screen" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to hide account from login screen: $_" -Level ERROR
|
||||
}
|
||||
|
||||
Write-Log "Step 0a - Admin account complete" -Level OK
|
||||
185
flash/scripts/01-bloatware.ps1
Normal file
185
flash/scripts/01-bloatware.ps1
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1a - AppX packages
|
||||
# -----------------------------------------------------------------------
|
||||
$AppxToRemove = @(
|
||||
"Microsoft.Microsoft3DViewer"
|
||||
"Microsoft.BingSearch"
|
||||
"Microsoft.WindowsCamera"
|
||||
"Clipchamp.Clipchamp"
|
||||
"Microsoft.WindowsAlarms"
|
||||
"Microsoft.Copilot"
|
||||
"Microsoft.549981C3F5F10"
|
||||
"Microsoft.Windows.DevHome"
|
||||
"MicrosoftCorporationII.MicrosoftFamily"
|
||||
"Microsoft.WindowsFeedbackHub"
|
||||
"Microsoft.Edge.GameAssist"
|
||||
"Microsoft.GetHelp"
|
||||
"Microsoft.Getstarted"
|
||||
"microsoft.windowscommunicationsapps"
|
||||
"Microsoft.WindowsMaps"
|
||||
"Microsoft.MixedReality.Portal"
|
||||
"Microsoft.BingNews"
|
||||
"Microsoft.MicrosoftOfficeHub"
|
||||
"Microsoft.Office.OneNote"
|
||||
"Microsoft.OutlookForWindows"
|
||||
"Microsoft.Paint"
|
||||
"Microsoft.MSPaint"
|
||||
"Microsoft.People"
|
||||
"Microsoft.Windows.Photos"
|
||||
"Microsoft.PowerAutomateDesktop"
|
||||
"MicrosoftCorporationII.QuickAssist"
|
||||
"Microsoft.SkypeApp"
|
||||
"Microsoft.ScreenSketch"
|
||||
"Microsoft.MicrosoftSolitaireCollection"
|
||||
"Microsoft.MicrosoftStickyNotes"
|
||||
"MicrosoftTeams"
|
||||
"MSTeams"
|
||||
"Microsoft.Todos"
|
||||
"Microsoft.WindowsSoundRecorder"
|
||||
"Microsoft.Wallet"
|
||||
"Microsoft.BingWeather"
|
||||
"Microsoft.WindowsTerminal"
|
||||
"Microsoft.Xbox.TCUI"
|
||||
"Microsoft.XboxApp"
|
||||
"Microsoft.XboxGameOverlay"
|
||||
"Microsoft.XboxGamingOverlay"
|
||||
"Microsoft.XboxIdentityProvider"
|
||||
"Microsoft.XboxSpeechToTextOverlay"
|
||||
"Microsoft.GamingApp"
|
||||
"Microsoft.YourPhone"
|
||||
"Microsoft.ZuneMusic"
|
||||
"Microsoft.ZuneVideo"
|
||||
"7EE7776C.LinkedInforWindows"
|
||||
)
|
||||
|
||||
# Packages to always keep
|
||||
$KeepPackages = @("Microsoft.WindowsCalculator")
|
||||
if ($Config -and $Config.bloatware -and $Config.bloatware.keepPackages) {
|
||||
$KeepPackages += $Config.bloatware.keepPackages
|
||||
}
|
||||
$KeepPackages = $KeepPackages | Select-Object -Unique
|
||||
|
||||
Write-Log "1a - Removing AppX packages" -Level STEP
|
||||
|
||||
foreach ($pkg in $AppxToRemove) {
|
||||
if ($KeepPackages -contains $pkg) {
|
||||
Write-Log " KEEP $pkg" -Level INFO
|
||||
continue
|
||||
}
|
||||
|
||||
# Installed packages (current user + all users)
|
||||
$installed = Get-AppxPackage -Name $pkg -AllUsers -ErrorAction SilentlyContinue
|
||||
if ($installed) {
|
||||
try {
|
||||
$installed | Remove-AppxPackage -AllUsers -ErrorAction Stop
|
||||
Write-Log " Removed AppxPackage: $pkg" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to remove AppxPackage $pkg - $_" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
# Provisioned packages (for new users)
|
||||
$provisioned = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DisplayName -eq $pkg }
|
||||
if ($provisioned) {
|
||||
try {
|
||||
$provisioned | Remove-AppxProvisionedPackage -Online -ErrorAction Stop | Out-Null
|
||||
Write-Log " Removed provisioned: $pkg" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to remove provisioned $pkg - $_" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $installed -and -not $provisioned) {
|
||||
Write-Log " Not found (already removed): $pkg" -Level INFO
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1b - Windows Capabilities
|
||||
# -----------------------------------------------------------------------
|
||||
$CapabilitiesToRemove = @(
|
||||
"Print.Fax.Scan"
|
||||
"Language.Handwriting"
|
||||
"Browser.InternetExplorer"
|
||||
"MathRecognizer"
|
||||
"OneCoreUAP.OneSync"
|
||||
"OpenSSH.Client"
|
||||
"Microsoft.Windows.MSPaint"
|
||||
"Microsoft.Windows.PowerShell.ISE"
|
||||
"App.Support.QuickAssist"
|
||||
"Microsoft.Windows.SnippingTool"
|
||||
"App.StepsRecorder"
|
||||
"Hello.Face"
|
||||
"Media.WindowsMediaPlayer"
|
||||
"Microsoft.Windows.WordPad"
|
||||
)
|
||||
|
||||
Write-Log "1b - Removing Windows Capabilities" -Level STEP
|
||||
|
||||
$installedCaps = Get-WindowsCapability -Online -ErrorAction SilentlyContinue
|
||||
|
||||
foreach ($cap in $CapabilitiesToRemove) {
|
||||
# Match by prefix (e.g. Hello.Face matches Hello.Face.20134.0.0.0)
|
||||
$matches = $installedCaps | Where-Object {
|
||||
$_.Name -like "$cap*" -and $_.State -eq "Installed"
|
||||
}
|
||||
if ($matches) {
|
||||
foreach ($c in $matches) {
|
||||
try {
|
||||
Remove-WindowsCapability -Online -Name $c.Name -ErrorAction Stop | Out-Null
|
||||
Write-Log " Removed capability: $($c.Name)" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to remove capability $($c.Name) - $_" -Level WARN
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Log " Not found or not installed: $cap" -Level INFO
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1c - Windows Optional Features
|
||||
# -----------------------------------------------------------------------
|
||||
$FeaturesToDisable = @(
|
||||
"MediaPlayback"
|
||||
"MicrosoftWindowsPowerShellV2Root"
|
||||
"Microsoft-RemoteDesktopConnection"
|
||||
"Recall"
|
||||
"Microsoft-SnippingTool"
|
||||
)
|
||||
|
||||
Write-Log "1c - Disabling Windows Optional Features" -Level STEP
|
||||
|
||||
foreach ($feat in $FeaturesToDisable) {
|
||||
$feature = Get-WindowsOptionalFeature -Online -FeatureName $feat -ErrorAction SilentlyContinue
|
||||
if ($feature -and $feature.State -eq "Enabled") {
|
||||
try {
|
||||
Disable-WindowsOptionalFeature -Online -FeatureName $feat -NoRestart -ErrorAction Stop | Out-Null
|
||||
Write-Log " Disabled feature: $feat" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to disable feature $feat - $_" -Level WARN
|
||||
}
|
||||
} else {
|
||||
Write-Log " Not enabled or not found: $feat" -Level INFO
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "Step 1 complete" -Level OK
|
||||
122
flash/scripts/02-software.ps1
Normal file
122
flash/scripts/02-software.ps1
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Check winget availability
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Checking winget availability" -Level INFO
|
||||
|
||||
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if (-not $winget) {
|
||||
# Try to find winget in known locations
|
||||
$wingetPaths = @(
|
||||
"$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
|
||||
"$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe"
|
||||
)
|
||||
foreach ($p in $wingetPaths) {
|
||||
$found = Get-Item $p -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($found) { $winget = $found.FullName; break }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $winget) {
|
||||
Write-Log "winget not found - software installation skipped" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log "winget found: $($winget.Source -or $winget)" -Level OK
|
||||
|
||||
# Accept agreements upfront
|
||||
& winget source update --accept-source-agreements 2>&1 | Out-Null
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Install packages from config
|
||||
# -----------------------------------------------------------------------
|
||||
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
|
||||
Write-Log "No software list in config - skipping installs" -Level WARN
|
||||
} else {
|
||||
foreach ($pkg in $Config.software.install) {
|
||||
Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
|
||||
$result = & winget install --id $pkg.wingetId `
|
||||
--silent `
|
||||
--accept-package-agreements `
|
||||
--accept-source-agreements `
|
||||
--disable-interactivity `
|
||||
2>&1
|
||||
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Log " Installed OK: $($pkg.name)" -Level OK
|
||||
} elseif ($exitCode -eq -1978335189) {
|
||||
# 0x8A150011 = already installed
|
||||
Write-Log " Already installed: $($pkg.name)" -Level OK
|
||||
} else {
|
||||
Write-Log " Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
|
||||
Write-Log " Output: $($result -join ' ')" -Level ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Set Adobe Reader as default PDF app
|
||||
# -----------------------------------------------------------------------
|
||||
$forcePdf = $true
|
||||
if ($Config -and $Config.pdfDefault) {
|
||||
$forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
|
||||
}
|
||||
|
||||
if ($forcePdf) {
|
||||
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
|
||||
|
||||
# Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
|
||||
$acroPaths = @(
|
||||
"$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
|
||||
"$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
|
||||
)
|
||||
$acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $acroExe) {
|
||||
Write-Log " Adobe PDF viewer not found - PDF default not set" -Level WARN
|
||||
} else {
|
||||
Write-Log " Found: $acroExe" -Level INFO
|
||||
|
||||
# Set file type association via HKCR (system-wide, requires admin)
|
||||
$progId = "AcroExch.Document.DC"
|
||||
$openCmd = "`"$acroExe`" `"%1`""
|
||||
|
||||
# HKCR\.pdf -> progId
|
||||
if (-not (Test-Path "HKCR:\.pdf")) {
|
||||
New-Item -Path "HKCR:\.pdf" -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId
|
||||
|
||||
# HKCR\AcroExch.Document.DC\shell\open\command
|
||||
$cmdPath = "HKCR:\$progId\shell\open\command"
|
||||
if (-not (Test-Path $cmdPath)) {
|
||||
New-Item -Path $cmdPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd
|
||||
|
||||
# Note: HKCU UserChoice requires a Windows-computed Hash value to be valid.
|
||||
# Direct ProgId write without Hash is silently reset by Windows on next access.
|
||||
# HKCR write above provides the system-wide default; per-user default is
|
||||
# handled by the PDF-DefaultApp scheduled task via HKCR on every logon.
|
||||
|
||||
Write-Log " PDF default set to AcroRd32 (HKCR)" -Level OK
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "Step 2 complete" -Level OK
|
||||
324
flash/scripts/03-system-registry.ps1
Normal file
324
flash/scripts/03-system-registry.ps1
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class RegPrivilege {
|
||||
[DllImport("advapi32.dll", ExactSpelling=true, SetLastError=true)]
|
||||
static extern bool AdjustTokenPrivileges(IntPtr htok, bool disAll, ref TokPriv1Luid newState, int len, IntPtr prev, IntPtr relen);
|
||||
[DllImport("kernel32.dll", ExactSpelling=true)]
|
||||
static extern IntPtr GetCurrentProcess();
|
||||
[DllImport("advapi32.dll", ExactSpelling=true, SetLastError=true)]
|
||||
static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
|
||||
[StructLayout(LayoutKind.Sequential, Pack=1)]
|
||||
struct TokPriv1Luid { public int Count; public long Luid; public int Attr; }
|
||||
const int TOKEN_QUERY = 0x8;
|
||||
const int TOKEN_ADJUST = 0x20;
|
||||
const int SE_PRIVILEGE_ENABLED = 2;
|
||||
public static bool Enable(string privilege) {
|
||||
IntPtr htok = IntPtr.Zero;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST | TOKEN_QUERY, ref htok)) return false;
|
||||
TokPriv1Luid tp; tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_ENABLED;
|
||||
if (!LookupPrivilegeValue(null, privilege, ref tp.Luid)) return false;
|
||||
return AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
|
||||
function Grant-RegWriteAccess {
|
||||
param([string]$Path)
|
||||
# Grants Administrators FullControl on a TrustedInstaller-owned registry key.
|
||||
# Enables SeTakeOwnershipPrivilege + SeRestorePrivilege to override ACL.
|
||||
try {
|
||||
[RegPrivilege]::Enable("SeTakeOwnershipPrivilege") | Out-Null
|
||||
[RegPrivilege]::Enable("SeRestorePrivilege") | Out-Null
|
||||
|
||||
$hive = $Path -replace '^(HKLM|HKCU|HKU|HKCR|HKCC):\\.*', '$1'
|
||||
$subkey = $Path -replace '^(HKLM|HKCU|HKU|HKCR|HKCC):\\', ''
|
||||
$rootKey = switch ($hive) {
|
||||
"HKLM" { [Microsoft.Win32.Registry]::LocalMachine }
|
||||
"HKCU" { [Microsoft.Win32.Registry]::CurrentUser }
|
||||
"HKCR" { [Microsoft.Win32.Registry]::ClassesRoot }
|
||||
}
|
||||
|
||||
# Take ownership (requires SeTakeOwnershipPrivilege)
|
||||
$key = $rootKey.OpenSubKey($subkey,
|
||||
[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
|
||||
[System.Security.AccessControl.RegistryRights]::TakeOwnership)
|
||||
if ($key) {
|
||||
$acl = $key.GetAccessControl([System.Security.AccessControl.AccessControlSections]::None)
|
||||
$acl.SetOwner([System.Security.Principal.NTAccount]"BUILTIN\Administrators")
|
||||
$key.SetAccessControl($acl)
|
||||
$key.Close()
|
||||
}
|
||||
|
||||
# Grant FullControl to Administrators (requires ChangePermissions)
|
||||
$key = $rootKey.OpenSubKey($subkey,
|
||||
[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
|
||||
[System.Security.AccessControl.RegistryRights]::ChangePermissions)
|
||||
if ($key) {
|
||||
$acl = $key.GetAccessControl()
|
||||
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
|
||||
"BUILTIN\Administrators",
|
||||
[System.Security.AccessControl.RegistryRights]::FullControl,
|
||||
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit,ObjectInherit",
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow)
|
||||
$acl.SetAccessRule($rule)
|
||||
$key.SetAccessControl($acl)
|
||||
$key.Close()
|
||||
}
|
||||
Write-Log " ACL fixed for $Path" -Level INFO
|
||||
}
|
||||
catch {
|
||||
Write-Log " Grant-RegWriteAccess failed for $Path - $_" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
function Set-Reg {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Name,
|
||||
$Value,
|
||||
[string]$Type = "DWord"
|
||||
)
|
||||
try {
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -Force -ErrorAction Stop
|
||||
Write-Log " SET $Path\$Name = $Value" -Level OK
|
||||
}
|
||||
catch {
|
||||
# Retry 1: grant write access via ACL manipulation
|
||||
try {
|
||||
Grant-RegWriteAccess -Path $Path
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -Force -ErrorAction Stop
|
||||
Write-Log " SET $Path\$Name = $Value (after ACL fix)" -Level OK
|
||||
return
|
||||
}
|
||||
catch { }
|
||||
|
||||
# Retry 2: write via scheduled task running as SYSTEM
|
||||
# SYSTEM has full registry access regardless of key ACL
|
||||
try {
|
||||
$regType = switch ($Type) {
|
||||
"DWord" { "REG_DWORD" }
|
||||
"String" { "REG_SZ" }
|
||||
"ExpandString"{ "REG_EXPAND_SZ" }
|
||||
"MultiString" { "REG_MULTI_SZ" }
|
||||
"QWord" { "REG_QWORD" }
|
||||
default { "REG_DWORD" }
|
||||
}
|
||||
# Convert registry PS path to reg.exe path
|
||||
$regPath = $Path -replace '^HKLM:\\', 'HKLM\' `
|
||||
-replace '^HKCU:\\', 'HKCU\' `
|
||||
-replace '^HKCR:\\', 'HKCR\'
|
||||
$tempScript = "$env:TEMP\set-reg-system-$([System.IO.Path]::GetRandomFileName()).ps1"
|
||||
"reg add `"$regPath`" /v `"$Name`" /t $regType /d $Value /f" |
|
||||
Set-Content -Path $tempScript -Encoding UTF8
|
||||
|
||||
$taskName = "TempRegFix-$([System.IO.Path]::GetRandomFileName())"
|
||||
$action = New-ScheduledTaskAction -Execute "cmd.exe" `
|
||||
-Argument "/c reg add `"$regPath`" /v `"$Name`" /t $regType /d $Value /f"
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 30)
|
||||
$task = New-ScheduledTask -Action $action -Principal $principal -Settings $settings
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Start-Sleep -Seconds 2
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-Item $tempScript -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Verify it was written
|
||||
$written = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name
|
||||
if ($null -ne $written) {
|
||||
Write-Log " SET $Path\$Name = $Value (via SYSTEM task)" -Level OK
|
||||
} else {
|
||||
Write-Log " FAILED $Path\$Name - SYSTEM task ran but value not found" -Level ERROR
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log " FAILED $Path\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-Reg {
|
||||
param([string]$Path, [string]$Name)
|
||||
try {
|
||||
if (Test-Path $Path) {
|
||||
Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " REMOVED $Path\$Name" -Level OK
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log " FAILED removing $Path\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "3 - Applying HKLM system registry tweaks" -Level STEP
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Bypass Network Requirement on OOBE (BypassNRO)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" `
|
||||
-Name "BypassNRO" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Disable auto-install of Teams (Chat)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" `
|
||||
-Name "ConfigureChatAutoInstall" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Disable Cloud Optimized Content (ads in Start menu etc.)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
|
||||
-Name "DisableCloudOptimizedContent" -Value 1
|
||||
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
|
||||
-Name "DisableWindowsConsumerFeatures" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Disable Widgets (News and Interests)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" `
|
||||
-Name "AllowNewsAndInterests" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Microsoft Edge - hide First Run Experience
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" `
|
||||
-Name "HideFirstRunExperience" -Value 1
|
||||
|
||||
# Also disable Edge desktop shortcut creation after install
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" `
|
||||
-Name "CreateDesktopShortcutDefault" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Password - no expiration
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log " Setting password max age to UNLIMITED" -Level INFO
|
||||
$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log " Password max age set to UNLIMITED" -Level OK
|
||||
} else {
|
||||
Write-Log " Failed to set password max age: $pwResult" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Time zone
|
||||
# -----------------------------------------------------------------------
|
||||
$tz = "Central Europe Standard Time"
|
||||
if ($Config -and $Config.deployment -and $Config.deployment.timezone) {
|
||||
$tz = $Config.deployment.timezone
|
||||
}
|
||||
Write-Log " Setting time zone: $tz" -Level INFO
|
||||
try {
|
||||
Set-TimeZone -Id $tz -ErrorAction Stop
|
||||
Write-Log " Time zone set: $tz" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to set time zone: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# OneDrive - prevent setup and remove shortcuts
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log " Disabling OneDrive" -Level INFO
|
||||
|
||||
# Disable OneDrive via policy
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive" `
|
||||
-Name "DisableFileSyncNGSC" -Value 1
|
||||
|
||||
# Remove OneDriveSetup.exe if present
|
||||
$oneDrivePaths = @(
|
||||
"$env:SystemRoot\System32\OneDriveSetup.exe"
|
||||
"$env:SystemRoot\SysWOW64\OneDriveSetup.exe"
|
||||
)
|
||||
foreach ($odPath in $oneDrivePaths) {
|
||||
if (Test-Path $odPath) {
|
||||
try {
|
||||
# Uninstall first
|
||||
& $odPath /uninstall 2>&1 | Out-Null
|
||||
Write-Log " OneDrive uninstalled via $odPath" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " OneDrive uninstall failed: $_" -Level WARN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Remove OneDrive Start Menu shortcut
|
||||
$odLnk = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk"
|
||||
if (Test-Path $odLnk) {
|
||||
Remove-Item $odLnk -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Removed OneDrive Start Menu shortcut" -Level OK
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Outlook (new) - disable auto-install via UScheduler
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log " Disabling Outlook (new) auto-install" -Level INFO
|
||||
|
||||
$uschedulerPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate"
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate"
|
||||
)
|
||||
foreach ($uPath in $uschedulerPaths) {
|
||||
if (Test-Path $uPath) {
|
||||
try {
|
||||
Remove-Item -Path $uPath -Recurse -Force
|
||||
Write-Log " Removed UScheduler key: $uPath" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to remove UScheduler key: $_" -Level WARN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Disable GameDVR
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
|
||||
-Name "AllowGameDVR" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Disable Recall (Windows AI feature)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
|
||||
-Name "DisableAIDataAnalysis" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
|
||||
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
|
||||
# this policy key ensures the setting survives Windows Updates.
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
|
||||
-Name "SearchOnTaskbarMode" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Start menu - hide Recommended section (Win11)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" `
|
||||
-Name "HideRecommendedSection" -Value 1
|
||||
|
||||
Write-Log "Step 3 complete" -Level OK
|
||||
324
flash/scripts/04-default-profile.ps1
Normal file
324
flash/scripts/04-default-profile.ps1
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Helper - apply a registry setting to both Default hive and current HKCU
|
||||
# -----------------------------------------------------------------------
|
||||
function Grant-HiveWriteAccess {
|
||||
param([string]$HivePath) # full path e.g. "Registry::HKU\DefaultProfile\Software\..."
|
||||
# Grants Administrators FullControl on a loaded hive key with restricted ACL.
|
||||
try {
|
||||
$acl = Get-Acl -Path $HivePath -ErrorAction Stop
|
||||
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
|
||||
"BUILTIN\Administrators",
|
||||
[System.Security.AccessControl.RegistryRights]::FullControl,
|
||||
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit,ObjectInherit",
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
)
|
||||
$acl.SetAccessRule($rule)
|
||||
Set-Acl -Path $HivePath -AclObject $acl -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Log " Grant-HiveWriteAccess failed for $HivePath - $_" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
function Set-ProfileReg {
|
||||
param(
|
||||
[string]$SubKey, # relative to HKCU (e.g. "Software\Microsoft\...")
|
||||
[string]$Name,
|
||||
$Value,
|
||||
[string]$Type = "DWord"
|
||||
)
|
||||
|
||||
# Apply to loaded Default hive
|
||||
$defPath = "Registry::HKU\DefaultProfile\$SubKey"
|
||||
try {
|
||||
if (-not (Test-Path $defPath)) {
|
||||
New-Item -Path $defPath -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $defPath -Name $Name -Value $Value -Type $Type -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
# Retry after granting write access to parent key
|
||||
try {
|
||||
$parentPath = $defPath -replace '\\[^\\]+$', ''
|
||||
if (Test-Path $parentPath) { Grant-HiveWriteAccess -HivePath $parentPath }
|
||||
if (-not (Test-Path $defPath)) {
|
||||
New-Item -Path $defPath -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $defPath -Name $Name -Value $Value -Type $Type -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Log " DEFAULT HIVE failed $SubKey\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# Apply to current user as well
|
||||
$hkcuPath = "HKCU:\$SubKey"
|
||||
try {
|
||||
if (-not (Test-Path $hkcuPath)) {
|
||||
New-Item -Path $hkcuPath -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $hkcuPath -Name $Name -Value $Value -Type $Type -Force -ErrorAction Stop
|
||||
Write-Log " SET $SubKey\$Name = $Value" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " HKCU failed $SubKey\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-ProfileReg {
|
||||
param([string]$SubKey, [string]$Name)
|
||||
|
||||
$defPath = "Registry::HKU\DefaultProfile\$SubKey"
|
||||
try {
|
||||
if (Test-Path $defPath) {
|
||||
Remove-ItemProperty -Path $defPath -Name $Name -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
$hkcuPath = "HKCU:\$SubKey"
|
||||
try {
|
||||
if (Test-Path $hkcuPath) {
|
||||
Remove-ItemProperty -Path $hkcuPath -Name $Name -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " REMOVED $SubKey\$Name" -Level OK
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log " FAILED removing $SubKey\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Load Default profile hive
|
||||
# -----------------------------------------------------------------------
|
||||
$hivePath = "C:\Users\Default\NTUSER.DAT"
|
||||
$hiveKey = "DefaultProfile"
|
||||
|
||||
Write-Log "Loading Default hive: $hivePath" -Level INFO
|
||||
|
||||
# Unload first in case previous run left it mounted
|
||||
& reg unload "HKU\$hiveKey" 2>&1 | Out-Null
|
||||
|
||||
$loadResult = & reg load "HKU\$hiveKey" $hivePath 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to load Default hive: $loadResult" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
Write-Log "Default hive loaded" -Level OK
|
||||
|
||||
try {
|
||||
# -----------------------------------------------------------------------
|
||||
# Taskbar settings (Win10 + Win11)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Applying taskbar settings" -Level STEP
|
||||
|
||||
$tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
|
||||
|
||||
# Win11: align taskbar to left (0 = left, 1 = center)
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarAl" -Value 0
|
||||
|
||||
# Hide Search box / button - Win10/11 (0 = hidden, 1 = icon, 2 = full box)
|
||||
# Note: Win11 uses Search subkey, Win10 uses Explorer\Advanced - set both
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Search" `
|
||||
-Name "SearchboxTaskbarMode" -Value 0
|
||||
Set-ProfileReg -SubKey $tbPath -Name "SearchboxTaskbarMode" -Value 0
|
||||
|
||||
# Hide Task View button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowTaskViewButton" -Value 0
|
||||
|
||||
# Hide Widgets button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarDa" -Value 0
|
||||
|
||||
# Hide Chat / Teams button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarMn" -Value 0
|
||||
|
||||
# Hide Copilot button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0
|
||||
|
||||
# Show file extensions in Explorer
|
||||
Set-ProfileReg -SubKey $tbPath -Name "HideFileExt" -Value 0
|
||||
|
||||
# Open Explorer to This PC instead of Quick Access
|
||||
Set-ProfileReg -SubKey $tbPath -Name "LaunchTo" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# System tray - show all icons
|
||||
# -----------------------------------------------------------------------
|
||||
# EnableAutoTray = 0 works on Win10; Win11 ignores it but set anyway
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
|
||||
-Name "EnableAutoTray" -Value 0
|
||||
|
||||
# Win11 workaround: clear cached tray icon streams so all icons appear on next login
|
||||
# Windows rebuilds the streams with all icons visible when no cache exists
|
||||
$trayNotifyKey = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
|
||||
if (Test-Path $trayNotifyKey) {
|
||||
Remove-ItemProperty -Path $trayNotifyKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
|
||||
Remove-ItemProperty -Path $trayNotifyKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Cleared TrayNotify icon streams (Win11 systray workaround)" -Level OK
|
||||
}
|
||||
|
||||
# Also clear in Default hive so new users start with clean state
|
||||
$defTrayKey = "Registry::HKU\DefaultProfile\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
|
||||
if (Test-Path $defTrayKey) {
|
||||
Remove-ItemProperty -Path $defTrayKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
|
||||
Remove-ItemProperty -Path $defTrayKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Cleared TrayNotify icon streams in Default hive" -Level OK
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Desktop icons - show This PC
|
||||
# -----------------------------------------------------------------------
|
||||
# CLSID {20D04FE0-3AEA-1069-A2D8-08002B30309D} = This PC / Computer
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" `
|
||||
-Name "{20D04FE0-3AEA-1069-A2D8-08002B30309D}" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Start menu settings
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Applying Start menu settings" -Level STEP
|
||||
|
||||
# Disable Bing search suggestions in Start menu
|
||||
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
|
||||
-Name "DisableSearchBoxSuggestions" -Value 1
|
||||
|
||||
# Win11: empty Start menu pins
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" `
|
||||
-Name "ConfigureStartPins" `
|
||||
-Value '{"pinnedList":[]}' `
|
||||
-Type "String"
|
||||
|
||||
# Hide "Recently added" apps in Start menu
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
|
||||
-Name "Start_TrackProgs" -Value 0
|
||||
|
||||
# Hide recently opened files/docs from Start menu
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
|
||||
-Name "Start_TrackDocs" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Copilot - disable
|
||||
# -----------------------------------------------------------------------
|
||||
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
|
||||
-Name "TurnOffWindowsCopilot" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# GameDVR - disable
|
||||
# -----------------------------------------------------------------------
|
||||
Set-ProfileReg -SubKey "System\GameConfigStore" `
|
||||
-Name "GameDVR_Enabled" -Value 0
|
||||
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
|
||||
-Name "AppCaptureEnabled" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Num Lock on startup
|
||||
# -----------------------------------------------------------------------
|
||||
Set-ProfileReg -SubKey "Control Panel\Keyboard" `
|
||||
-Name "InitialKeyboardIndicators" -Value 2 -Type "String"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Accent color on title bars
|
||||
# -----------------------------------------------------------------------
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" `
|
||||
-Name "ColorPrevalence" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# OneDrive - remove RunOnce key from Default profile
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Removing OneDrive from Default profile RunOnce" -Level INFO
|
||||
Remove-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Run" `
|
||||
-Name "OneDriveSetup"
|
||||
|
||||
Remove-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\RunOnce" `
|
||||
-Name "Delete Cached Standalone Update Binary"
|
||||
|
||||
# Remove OneDrive from Explorer namespace (left panel)
|
||||
$oneDriveClsid = "{018D5C66-4533-4307-9B53-224DE2ED1FE6}"
|
||||
$nsPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$oneDriveClsid"
|
||||
$defNsPath = "Registry::HKU\DefaultProfile\$nsPath"
|
||||
if (Test-Path $defNsPath) {
|
||||
Remove-Item -Path $defNsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Removed OneDrive from Explorer namespace (Default)" -Level OK
|
||||
}
|
||||
$hkcuNsPath = "HKCU:\$nsPath"
|
||||
if (Test-Path $hkcuNsPath) {
|
||||
Remove-Item -Path $hkcuNsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Removed OneDrive from Explorer namespace (HKCU)" -Level OK
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Empty taskbar pinned apps (Win10/11)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Clearing taskbar pinned apps layout" -Level INFO
|
||||
|
||||
$taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
|
||||
if (-not (Test-Path $taskbarLayoutDir)) {
|
||||
New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$taskbarLayoutXml = @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LayoutModificationTemplate
|
||||
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
|
||||
xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
|
||||
xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
|
||||
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
|
||||
Version="1">
|
||||
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
|
||||
<defaultlayout:TaskbarLayout>
|
||||
<taskbar:TaskbarPinList>
|
||||
</taskbar:TaskbarPinList>
|
||||
</defaultlayout:TaskbarLayout>
|
||||
</CustomTaskbarLayoutCollection>
|
||||
</LayoutModificationTemplate>
|
||||
"@
|
||||
$taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
|
||||
Write-Log " Taskbar LayoutModification.xml written" -Level OK
|
||||
|
||||
}
|
||||
finally {
|
||||
# -----------------------------------------------------------------------
|
||||
# Unload Default hive - always, even on error
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Unloading Default hive" -Level INFO
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$unloadResult = & reg unload "HKU\$hiveKey" 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log "Default hive unloaded" -Level OK
|
||||
} else {
|
||||
Write-Log "Failed to unload Default hive: $unloadResult" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Restart Explorer to apply taskbar/tray changes to current session
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Restarting Explorer to apply taskbar changes" -Level INFO
|
||||
try {
|
||||
Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Process explorer
|
||||
Write-Log "Explorer restarted" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log "Explorer restart failed (non-fatal): $_" -Level WARN
|
||||
}
|
||||
|
||||
Write-Log "Step 4 complete" -Level OK
|
||||
172
flash/scripts/05-personalization.ps1
Normal file
172
flash/scripts/05-personalization.ps1
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# Accent color #223B47 stored as ABGR DWORD: 0xFF473B22
|
||||
# A=FF B=47 G=3B R=22 -> 0xFF473B22 = 4283612962
|
||||
$AccentColorABGR = 0xFF473B22
|
||||
|
||||
# Gradient colors (Windows generates these automatically but we set them explicitly)
|
||||
# AccentPalette is 32 bytes - 8 shades of the accent color (BGRA each)
|
||||
# We use the same color for all shades as a safe default
|
||||
$AccentColorHex = "#223B47"
|
||||
|
||||
function Set-Reg {
|
||||
param([string]$Path, [string]$Name, $Value, [string]$Type = "DWord")
|
||||
try {
|
||||
if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null }
|
||||
Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -Force
|
||||
Write-Log " SET $Path\$Name = $Value" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " FAILED $Path\$Name - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
function Apply-ThemeSettings {
|
||||
param([string]$HiveRoot) # "HKCU:" or "Registry::HKU\DefaultProfile"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# System theme - Dark (taskbar, Start, action center)
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" `
|
||||
-Name "SystemUsesLightTheme" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# App theme - Light
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" `
|
||||
-Name "AppsUseLightTheme" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Accent color on Start and taskbar
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" `
|
||||
-Name "ColorPrevalence" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Transparency effects - disabled
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" `
|
||||
-Name "EnableTransparency" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Accent color
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
|
||||
-Name "AccentColor" -Value $AccentColorABGR -Type "DWord"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
|
||||
-Name "ColorizationColor" -Value $AccentColorABGR -Type "DWord"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
|
||||
-Name "ColorizationAfterglow" -Value $AccentColorABGR -Type "DWord"
|
||||
|
||||
# Accent color on title bars and borders
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
|
||||
-Name "ColorPrevalence" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Wallpaper - solid color #223B47 (fallback before DesktopInfo runs)
|
||||
# -----------------------------------------------------------------------
|
||||
# Background color as decimal RGB
|
||||
Set-Reg -Path "$HiveRoot\Control Panel\Colors" `
|
||||
-Name "Background" -Value "34 59 71" -Type "String"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
|
||||
-Name "WallpaperStyle" -Value "0" -Type "String"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
|
||||
-Name "TileWallpaper" -Value "0" -Type "String"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Desktop icons - show This PC
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" `
|
||||
-Name "{20D04FE0-3AEA-1069-A2D8-08002B30309D}" -Value 0
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Load Default hive
|
||||
# -----------------------------------------------------------------------
|
||||
$hivePath = "C:\Users\Default\NTUSER.DAT"
|
||||
$hiveKey = "DefaultProfile"
|
||||
|
||||
Write-Log "Loading Default hive for personalization" -Level INFO
|
||||
|
||||
& reg unload "HKU\$hiveKey" 2>&1 | Out-Null
|
||||
$loadResult = & reg load "HKU\$hiveKey" $hivePath 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "Failed to load Default hive: $loadResult" -Level ERROR
|
||||
Write-Log "Applying personalization to current user only" -Level WARN
|
||||
|
||||
Write-Log "Applying theme to current user (HKCU)" -Level STEP
|
||||
Apply-ThemeSettings -HiveRoot "HKCU:"
|
||||
|
||||
# Set wallpaper via SystemParametersInfo for current user
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class WallpaperHelper {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
||||
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
|
||||
}
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
[WallpaperHelper]::SystemParametersInfo(20, 0, "", 3) | Out-Null
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Log "Applying theme to Default hive" -Level STEP
|
||||
Apply-ThemeSettings -HiveRoot "Registry::HKU\DefaultProfile"
|
||||
|
||||
Write-Log "Applying theme to current user (HKCU)" -Level STEP
|
||||
Apply-ThemeSettings -HiveRoot "HKCU:"
|
||||
}
|
||||
finally {
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$unloadResult = & reg unload "HKU\$hiveKey" 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log "Default hive unloaded" -Level OK
|
||||
} else {
|
||||
Write-Log "Failed to unload Default hive: $unloadResult" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Apply wallpaper (solid color) to current desktop session
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Setting desktop wallpaper to solid color" -Level INFO
|
||||
|
||||
try {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class WallpaperHelper {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
||||
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
|
||||
}
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
|
||||
# SPI_SETDESKTOPWALLPAPER=20, SPIF_UPDATEINIFILE|SPIF_SENDCHANGE=3
|
||||
# Empty string = solid color defined in Control Panel\Colors\Background
|
||||
[WallpaperHelper]::SystemParametersInfo(20, 0, "", 3) | Out-Null
|
||||
Write-Log " Desktop wallpaper updated" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to update wallpaper: $_" -Level WARN
|
||||
}
|
||||
|
||||
Write-Log "Step 5 complete" -Level OK
|
||||
194
flash/scripts/06-scheduled-tasks.ps1
Normal file
194
flash/scripts/06-scheduled-tasks.ps1
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
$ScriptDir = "C:\Windows\Setup\Scripts"
|
||||
if (-not (Test-Path $ScriptDir)) {
|
||||
New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null
|
||||
}
|
||||
|
||||
function Register-Task {
|
||||
param(
|
||||
[string]$TaskName,
|
||||
[string]$Description,
|
||||
[object]$Action,
|
||||
[object[]]$Triggers,
|
||||
[string]$RunLevel = "Highest"
|
||||
)
|
||||
try {
|
||||
# Remove existing task with same name
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-StartWhenAvailable
|
||||
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId "Users" `
|
||||
-RunLevel $RunLevel
|
||||
|
||||
$task = New-ScheduledTask -Action $Action `
|
||||
-Trigger $Triggers `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description $Description
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
|
||||
Write-Log " Registered task: $TaskName" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to register task $TaskName - $_" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: ShowAllTrayIcons
|
||||
# Runs on logon: clears TrayNotify icon cache and restarts Explorer so all
|
||||
# tray icons are visible on first login (Win10: EnableAutoTray=0, Win11: cache clear)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering task: ShowAllTrayIcons" -Level STEP
|
||||
|
||||
$showTrayScript = "$ScriptDir\ShowAllTrayIcons.ps1"
|
||||
@'
|
||||
# Win10: disable auto-hiding of tray icons
|
||||
$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer"
|
||||
Set-ItemProperty -Path $regPath -Name "EnableAutoTray" -Value 0 -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Win11: clear icon stream cache so all icons become visible after Explorer restart
|
||||
$trayPath = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
|
||||
if (Test-Path $trayPath) {
|
||||
Remove-ItemProperty -Path $trayPath -Name "IconStreams" -Force -ErrorAction SilentlyContinue
|
||||
Remove-ItemProperty -Path $trayPath -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Restart Explorer to apply changes
|
||||
Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 1500
|
||||
if (-not (Get-Process explorer -ErrorAction SilentlyContinue)) {
|
||||
Start-Process explorer
|
||||
}
|
||||
'@ | Set-Content -Path $showTrayScript -Encoding UTF8 -Force
|
||||
|
||||
$showTrayAction = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$showTrayScript`""
|
||||
$showTrayTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
Register-Task -TaskName "ShowAllTrayIcons" `
|
||||
-Description "Show all system tray icons for current user" `
|
||||
-Action $showTrayAction `
|
||||
-Triggers $showTrayTrigger
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: PDF-DefaultApp
|
||||
# Runs on every logon, restores .pdf -> Adobe Reader association
|
||||
# Guards against Edge overwriting it
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering task: PDF-DefaultApp" -Level STEP
|
||||
|
||||
$pdfScript = "$ScriptDir\PDF-DefaultApp.ps1"
|
||||
@'
|
||||
# Restore .pdf -> Adobe Reader HKCR association (system-wide).
|
||||
# Runs as SYSTEM so it can write to HKCR regardless of Edge updates.
|
||||
# Note: HKCU UserChoice requires Windows Hash validation and cannot be
|
||||
# set reliably via registry; HKCR provides the system-wide fallback.
|
||||
$acroPaths = @(
|
||||
"$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
|
||||
"$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe"
|
||||
)
|
||||
$acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $acroExe) { exit 0 }
|
||||
|
||||
$progId = "AcroExch.Document.DC"
|
||||
$openCmd = "`"$acroExe`" `"%1`""
|
||||
|
||||
# HKCR\.pdf
|
||||
if (-not (Test-Path "HKCR:\.pdf")) { New-Item -Path "HKCR:\.pdf" -Force | Out-Null }
|
||||
$current = (Get-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -ErrorAction SilentlyContinue)."(Default)"
|
||||
if ($current -ne $progId) {
|
||||
Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId -Force
|
||||
}
|
||||
|
||||
# HKCR\AcroExch.Document.DC\shell\open\command
|
||||
$cmdPath = "HKCR:\$progId\shell\open\command"
|
||||
if (-not (Test-Path $cmdPath)) { New-Item -Path $cmdPath -Force | Out-Null }
|
||||
Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd -Force
|
||||
'@ | Set-Content -Path $pdfScript -Encoding UTF8 -Force
|
||||
|
||||
$pdfAction = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$pdfScript`""
|
||||
$pdfTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
# Runs as SYSTEM to allow HKCR writes (system-wide file association)
|
||||
$pdfPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
$pdfSettings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-StartWhenAvailable
|
||||
$pdfTask = New-ScheduledTask -Action $pdfAction `
|
||||
-Trigger $pdfTrigger `
|
||||
-Settings $pdfSettings `
|
||||
-Principal $pdfPrincipal `
|
||||
-Description "Restore Adobe Reader as default PDF app on logon"
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName "PDF-DefaultApp" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Register-ScheduledTask -TaskName "PDF-DefaultApp" -InputObject $pdfTask -Force | Out-Null
|
||||
Write-Log " Registered task: PDF-DefaultApp" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to register task PDF-DefaultApp - $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: UnlockStartLayout
|
||||
# Runs once after deployment to unlock the Start menu layout
|
||||
# so users can still customize it later
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering task: UnlockStartLayout" -Level STEP
|
||||
|
||||
$unlockScript = "$ScriptDir\UnlockStartLayout.ps1"
|
||||
@'
|
||||
# Remove Start layout lock so users can modify it
|
||||
$layoutXml = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml"
|
||||
if (Test-Path $layoutXml) {
|
||||
Remove-Item $layoutXml -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Unregister self after running once
|
||||
Unregister-ScheduledTask -TaskName "UnlockStartLayout" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
'@ | Set-Content -Path $unlockScript -Encoding UTF8 -Force
|
||||
|
||||
$unlockAction = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$unlockScript`""
|
||||
# Trigger: 5 minutes after system startup, once
|
||||
$unlockTrigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$unlockTrigger.Delay = "PT5M"
|
||||
|
||||
$unlockPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
$unlockSettings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10) `
|
||||
-StartWhenAvailable
|
||||
$unlockTask = New-ScheduledTask -Action $unlockAction `
|
||||
-Trigger $unlockTrigger `
|
||||
-Settings $unlockSettings `
|
||||
-Principal $unlockPrincipal `
|
||||
-Description "Unlock Start menu layout 5 min after first boot"
|
||||
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName "UnlockStartLayout" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Register-ScheduledTask -TaskName "UnlockStartLayout" -InputObject $unlockTask -Force | Out-Null
|
||||
Write-Log " Registered task: UnlockStartLayout" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to register task UnlockStartLayout - $_" -Level ERROR
|
||||
}
|
||||
|
||||
Write-Log "Step 6 complete" -Level OK
|
||||
217
flash/scripts/07-desktop-info.ps1
Normal file
217
flash/scripts/07-desktop-info.ps1
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
$ScriptDir = "C:\Windows\Setup\Scripts"
|
||||
$RenderScript = "$ScriptDir\DesktopInfo-Render.ps1"
|
||||
$BmpPath = "$ScriptDir\desktopinfo.bmp"
|
||||
|
||||
if (-not (Test-Path $ScriptDir)) {
|
||||
New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Read display settings from config
|
||||
# -----------------------------------------------------------------------
|
||||
$fontSize = 13
|
||||
$fontColor = "#FFFFFF"
|
||||
$position = "bottomRight"
|
||||
|
||||
if ($Config -and $Config.desktopInfo) {
|
||||
if ($Config.desktopInfo.fontSize) { $fontSize = [int]$Config.desktopInfo.fontSize }
|
||||
if ($Config.desktopInfo.fontColor) { $fontColor = $Config.desktopInfo.fontColor }
|
||||
if ($Config.desktopInfo.position) { $position = $Config.desktopInfo.position }
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Write the rendering script (runs on every logon as the user)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Writing DesktopInfo render script to $RenderScript" -Level INFO
|
||||
|
||||
$renderContent = @"
|
||||
# DesktopInfo-Render.ps1
|
||||
# Collects system info and renders it onto the desktop wallpaper.
|
||||
# Runs on every user logon via Scheduled Task.
|
||||
|
||||
`$ErrorActionPreference = "Continue"
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class WallpaperApi {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
||||
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
|
||||
}
|
||||
'@ -ErrorAction SilentlyContinue
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Collect system info
|
||||
# -----------------------------------------------------------------------
|
||||
`$hostname = `$env:COMPUTERNAME
|
||||
`$username = `$env:USERNAME
|
||||
`$ipAddress = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { `$_.IPAddress -ne "127.0.0.1" -and `$_.PrefixOrigin -ne "WellKnown" } |
|
||||
Select-Object -First 1).IPAddress
|
||||
if (-not `$ipAddress) { `$ipAddress = "N/A" }
|
||||
|
||||
`$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
|
||||
`$osName = if (`$osInfo) { `$osInfo.Caption -replace "Microsoft ", "" } else { "Windows" }
|
||||
`$osBuild = if (`$osInfo) { `$osInfo.BuildNumber } else { "" }
|
||||
|
||||
# Deployment date = when script was first run, stored in registry
|
||||
`$deployRegPath = "HKLM:\SOFTWARE\X9\Deployment"
|
||||
`$deployDate = (Get-ItemProperty -Path `$deployRegPath -Name "DeployDate" -ErrorAction SilentlyContinue).DeployDate
|
||||
if (-not `$deployDate) { `$deployDate = "N/A" }
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Build info lines
|
||||
# -----------------------------------------------------------------------
|
||||
`$lines = @(
|
||||
"Computer : `$hostname"
|
||||
"User : `$username"
|
||||
"IP : `$ipAddress"
|
||||
"OS : `$osName (build `$osBuild)"
|
||||
"Deployed : `$deployDate"
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Render bitmap
|
||||
# -----------------------------------------------------------------------
|
||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
||||
|
||||
`$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
||||
`$width = if (`$screen) { `$screen.Bounds.Width } else { 1920 }
|
||||
`$height = if (`$screen) { `$screen.Bounds.Height } else { 1080 }
|
||||
|
||||
`$bmp = New-Object System.Drawing.Bitmap(`$width, `$height)
|
||||
`$g = [System.Drawing.Graphics]::FromImage(`$bmp)
|
||||
|
||||
# Background: solid accent color #223B47
|
||||
`$bgColor = [System.Drawing.ColorTranslator]::FromHtml("#223B47")
|
||||
`$g.Clear(`$bgColor)
|
||||
|
||||
# Font and colors
|
||||
`$fontFamily = "Consolas"
|
||||
`$fontSize = $fontSize
|
||||
`$font = New-Object System.Drawing.Font(`$fontFamily, `$fontSize, [System.Drawing.FontStyle]::Regular)
|
||||
`$brush = New-Object System.Drawing.SolidBrush([System.Drawing.ColorTranslator]::FromHtml("$fontColor"))
|
||||
`$shadowBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(180, 0, 0, 0))
|
||||
|
||||
# Measure text block
|
||||
`$lineHeight = `$font.GetHeight(`$g) + 4
|
||||
`$blockH = `$lines.Count * `$lineHeight
|
||||
`$maxWidth = (`$lines | ForEach-Object { `$g.MeasureString(`$_, `$font).Width } | Measure-Object -Maximum).Maximum
|
||||
|
||||
# Position
|
||||
`$margin = 24
|
||||
`$pos = "$position"
|
||||
`$x = switch -Wildcard (`$pos) {
|
||||
"*Right" { `$width - `$maxWidth - `$margin }
|
||||
"*Left" { `$margin }
|
||||
default { `$margin }
|
||||
}
|
||||
`$y = switch -Wildcard (`$pos) {
|
||||
"bottom*" { `$height - `$blockH - `$margin }
|
||||
"top*" { `$margin }
|
||||
default { `$height - `$blockH - `$margin }
|
||||
}
|
||||
|
||||
# Draw shadow then text
|
||||
foreach (`$line in `$lines) {
|
||||
`$g.DrawString(`$line, `$font, `$shadowBrush, (`$x + 1), (`$y + 1))
|
||||
`$g.DrawString(`$line, `$font, `$brush, `$x, `$y)
|
||||
`$y += `$lineHeight
|
||||
}
|
||||
|
||||
`$g.Dispose()
|
||||
|
||||
# Save BMP
|
||||
`$bmpPath = "$BmpPath"
|
||||
`$bmp.Save(`$bmpPath, [System.Drawing.Imaging.ImageFormat]::Bmp)
|
||||
`$bmp.Dispose()
|
||||
|
||||
# Set as wallpaper
|
||||
# SPI_SETDESKTOPWALLPAPER=20, SPIF_UPDATEINIFILE|SPIF_SENDCHANGE=3
|
||||
[WallpaperApi]::SystemParametersInfo(20, 0, `$bmpPath, 3) | Out-Null
|
||||
"@
|
||||
|
||||
$renderContent | Set-Content -Path $RenderScript -Encoding UTF8 -Force
|
||||
Write-Log "Render script written" -Level OK
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Store deployment date in registry (used by render script)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Storing deployment date in registry" -Level INFO
|
||||
try {
|
||||
if (-not (Test-Path "HKLM:\SOFTWARE\X9\Deployment")) {
|
||||
New-Item -Path "HKLM:\SOFTWARE\X9\Deployment" -Force | Out-Null
|
||||
}
|
||||
$existingDate = (Get-ItemProperty -Path "HKLM:\SOFTWARE\X9\Deployment" `
|
||||
-Name "DeployDate" -ErrorAction SilentlyContinue).DeployDate
|
||||
if (-not $existingDate) {
|
||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\X9\Deployment" `
|
||||
-Name "DeployDate" `
|
||||
-Value (Get-Date -Format "yyyy-MM-dd") `
|
||||
-Force
|
||||
Write-Log " DeployDate set: $(Get-Date -Format 'yyyy-MM-dd')" -Level OK
|
||||
} else {
|
||||
Write-Log " DeployDate already set: $existingDate" -Level INFO
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to set DeployDate: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Register scheduled task: DesktopInfo
|
||||
# Runs the render script on every user logon
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering task: DesktopInfo" -Level STEP
|
||||
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName "DesktopInfo" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$RenderScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-StartWhenAvailable
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId "Users" -RunLevel Limited
|
||||
|
||||
$task = New-ScheduledTask -Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description "Render system info onto desktop wallpaper on logon"
|
||||
|
||||
Register-ScheduledTask -TaskName "DesktopInfo" -InputObject $task -Force | Out-Null
|
||||
Write-Log "Task DesktopInfo registered" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log "Failed to register DesktopInfo task: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Run once immediately for current user
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Running DesktopInfo render now for current user" -Level INFO
|
||||
try {
|
||||
& powershell.exe -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File $RenderScript
|
||||
Write-Log "DesktopInfo rendered" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log "DesktopInfo render failed: $_" -Level WARN
|
||||
}
|
||||
|
||||
Write-Log "Step 7 complete" -Level OK
|
||||
109
flash/scripts/08-activation.ps1
Normal file
109
flash/scripts/08-activation.ps1
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# KMS Generic Volume License Keys (GVLK)
|
||||
# Source: https://docs.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys
|
||||
# These are official Microsoft-published keys for use with KMS infrastructure.
|
||||
# Replace with your MAK/retail key for standalone activation.
|
||||
# -----------------------------------------------------------------------
|
||||
$KmsKeys = @{
|
||||
# Windows 11
|
||||
"Windows 11 Pro" = "W269N-WFGWX-YVC9B-4J6C9-T83GX"
|
||||
"Windows 11 Pro N" = "MH37W-N47XK-V7XM9-C7227-GCQG9"
|
||||
"Windows 11 Pro Education" = "6TP4R-GNPTD-KYYHQ-7B7DP-J447Y"
|
||||
"Windows 11 Education" = "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2"
|
||||
"Windows 11 Enterprise" = "NPPR9-FWDCX-D2C8J-H872K-2YT43"
|
||||
# Windows 10
|
||||
"Windows 10 Pro" = "W269N-WFGWX-YVC9B-4J6C9-T83GX"
|
||||
"Windows 10 Pro N" = "MH37W-N47XK-V7XM9-C7227-GCQG9"
|
||||
"Windows 10 Education" = "NW6C2-QMPVW-D7KKK-3GKT6-VCFB2"
|
||||
"Windows 10 Enterprise" = "NPPR9-FWDCX-D2C8J-H872K-2YT43"
|
||||
"Windows 10 Home" = "TX9XD-98N7V-6WMQ6-BX7FG-H8Q99"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Check current activation status
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Checking Windows activation status" -Level INFO
|
||||
|
||||
$licenseStatus = (Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND Name LIKE 'Windows%'" -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1).LicenseStatus
|
||||
# LicenseStatus: 0=Unlicensed, 1=Licensed, 2=OOBGrace, 3=OOTGrace, 4=NonGenuineGrace, 5=Notification, 6=ExtendedGrace
|
||||
|
||||
if ($licenseStatus -eq 1) {
|
||||
Write-Log " Windows is already activated - skipping" -Level OK
|
||||
} else {
|
||||
Write-Log " Activation status: $licenseStatus (not activated)" -Level WARN
|
||||
|
||||
# Detect Windows edition
|
||||
$osCaption = (Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption
|
||||
Write-Log " Detected OS: $osCaption" -Level INFO
|
||||
|
||||
# Check if a key is configured in config
|
||||
$customKey = $null
|
||||
if ($Config -and $Config.activation -and $Config.activation.productKey) {
|
||||
$customKey = $Config.activation.productKey
|
||||
}
|
||||
|
||||
if ($customKey -and $customKey -ne "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX") {
|
||||
# Use key from config
|
||||
$keyToUse = $customKey
|
||||
Write-Log " Using product key from config" -Level INFO
|
||||
} else {
|
||||
# Find matching GVLK key by OS name
|
||||
$keyToUse = $null
|
||||
foreach ($entry in $KmsKeys.GetEnumerator()) {
|
||||
if ($osCaption -like "*$($entry.Key)*") {
|
||||
$keyToUse = $entry.Value
|
||||
Write-Log " Matched GVLK key for: $($entry.Key)" -Level INFO
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $keyToUse) {
|
||||
Write-Log " No matching key found for: $osCaption" -Level WARN
|
||||
Write-Log " Skipping activation - set activation.productKey in config.json" -Level WARN
|
||||
} else {
|
||||
# Install key
|
||||
Write-Log " Installing product key..." -Level INFO
|
||||
$ipkResult = & cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /ipk $keyToUse 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log " Key installed" -Level OK
|
||||
} else {
|
||||
Write-Log " Key install result: $ipkResult" -Level WARN
|
||||
}
|
||||
|
||||
# Set KMS server if configured
|
||||
if ($Config -and $Config.activation -and $Config.activation.kmsServer) {
|
||||
$kmsServer = $Config.activation.kmsServer
|
||||
Write-Log " Setting KMS server: $kmsServer" -Level INFO
|
||||
& cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /skms $kmsServer 2>&1 | Out-Null
|
||||
}
|
||||
|
||||
# Attempt activation
|
||||
Write-Log " Attempting activation..." -Level INFO
|
||||
$atoResult = & cscript //nologo "$env:SystemRoot\System32\slmgr.vbs" /ato 2>&1
|
||||
$atoOutput = $atoResult -join " "
|
||||
|
||||
if ($atoOutput -match "successfully" -or $atoOutput -match "uspesn") {
|
||||
Write-Log " Activation successful" -Level OK
|
||||
} else {
|
||||
Write-Log " Activation result: $atoOutput" -Level WARN
|
||||
Write-Log " Activation may require a KMS server or valid MAK key" -Level WARN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "Step 8 - Activation complete" -Level OK
|
||||
|
|
@ -110,14 +110,12 @@ if ($forcePdf) {
|
|||
}
|
||||
Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd
|
||||
|
||||
# Also set in HKCU for current user (UserChoice)
|
||||
$ucPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice"
|
||||
if (-not (Test-Path $ucPath)) {
|
||||
New-Item -Path $ucPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $ucPath -Name "ProgId" -Value $progId
|
||||
# Note: HKCU UserChoice requires a Windows-computed Hash value to be valid.
|
||||
# Direct ProgId write without Hash is silently reset by Windows on next access.
|
||||
# HKCR write above provides the system-wide default; per-user default is
|
||||
# handled by the PDF-DefaultApp scheduled task via HKCR on every logon.
|
||||
|
||||
Write-Log " PDF default set to AcroRd32" -Level OK
|
||||
Write-Log " PDF default set to AcroRd32 (HKCR)" -Level OK
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,14 @@ Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
|
|||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
|
||||
-Name "DisableAIDataAnalysis" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
|
||||
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
|
||||
# this policy key ensures the setting survives Windows Updates.
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
|
||||
-Name "SearchOnTaskbarMode" -Value 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Start menu - hide Recommended section (Win11)
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function Register-Task {
|
|||
[string]$TaskName,
|
||||
[string]$Description,
|
||||
[object]$Action,
|
||||
[object]$Trigger,
|
||||
[object[]]$Triggers,
|
||||
[string]$RunLevel = "Highest"
|
||||
)
|
||||
try {
|
||||
|
|
@ -36,7 +36,7 @@ function Register-Task {
|
|||
-RunLevel $RunLevel
|
||||
|
||||
$task = New-ScheduledTask -Action $Action `
|
||||
-Trigger $Trigger `
|
||||
-Trigger $Triggers `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description $Description
|
||||
|
|
@ -51,29 +51,40 @@ function Register-Task {
|
|||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: ShowAllTrayIcons
|
||||
# Runs on logon + every 1 minute, sets EnableAutoTray=0 so all tray icons
|
||||
# are always visible (Win11 hides them by default)
|
||||
# Runs on logon: clears TrayNotify icon cache and restarts Explorer so all
|
||||
# tray icons are visible on first login (Win10: EnableAutoTray=0, Win11: cache clear)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering task: ShowAllTrayIcons" -Level STEP
|
||||
|
||||
$showTrayScript = "$ScriptDir\ShowAllTrayIcons.ps1"
|
||||
@'
|
||||
# Win10: disable auto-hiding of tray icons
|
||||
$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer"
|
||||
Set-ItemProperty -Path $regPath -Name "EnableAutoTray" -Value 0 -Force
|
||||
Set-ItemProperty -Path $regPath -Name "EnableAutoTray" -Value 0 -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Win11: clear icon stream cache so all icons become visible after Explorer restart
|
||||
$trayPath = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
|
||||
if (Test-Path $trayPath) {
|
||||
Remove-ItemProperty -Path $trayPath -Name "IconStreams" -Force -ErrorAction SilentlyContinue
|
||||
Remove-ItemProperty -Path $trayPath -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Restart Explorer to apply changes
|
||||
Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 1500
|
||||
if (-not (Get-Process explorer -ErrorAction SilentlyContinue)) {
|
||||
Start-Process explorer
|
||||
}
|
||||
'@ | Set-Content -Path $showTrayScript -Encoding UTF8 -Force
|
||||
|
||||
$showTrayAction = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$showTrayScript`""
|
||||
$showTrayTrigger = @(
|
||||
$(New-ScheduledTaskTrigger -AtLogOn),
|
||||
$(New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 1) -Once -At (Get-Date))
|
||||
)
|
||||
$showTrayTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
Register-Task -TaskName "ShowAllTrayIcons" `
|
||||
-Description "Show all system tray icons for current user" `
|
||||
-Action $showTrayAction `
|
||||
-Trigger $showTrayTrigger[0]
|
||||
-Triggers $showTrayTrigger
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: PDF-DefaultApp
|
||||
|
|
@ -84,7 +95,10 @@ Write-Log "Registering task: PDF-DefaultApp" -Level STEP
|
|||
|
||||
$pdfScript = "$ScriptDir\PDF-DefaultApp.ps1"
|
||||
@'
|
||||
# Restore .pdf -> Adobe Reader association
|
||||
# Restore .pdf -> Adobe Reader HKCR association (system-wide).
|
||||
# Runs as SYSTEM so it can write to HKCR regardless of Edge updates.
|
||||
# Note: HKCU UserChoice requires Windows Hash validation and cannot be
|
||||
# set reliably via registry; HKCR provides the system-wide fallback.
|
||||
$acroPaths = @(
|
||||
"$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
"${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
||||
|
|
@ -96,26 +110,43 @@ $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|||
if (-not $acroExe) { exit 0 }
|
||||
|
||||
$progId = "AcroExch.Document.DC"
|
||||
$openCmd = "`"$acroExe`" `"%1`""
|
||||
|
||||
# Check current association
|
||||
$current = (Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice" `
|
||||
-Name "ProgId" -ErrorAction SilentlyContinue).ProgId
|
||||
|
||||
# HKCR\.pdf
|
||||
if (-not (Test-Path "HKCR:\.pdf")) { New-Item -Path "HKCR:\.pdf" -Force | Out-Null }
|
||||
$current = (Get-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -ErrorAction SilentlyContinue)."(Default)"
|
||||
if ($current -ne $progId) {
|
||||
$ucPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice"
|
||||
if (-not (Test-Path $ucPath)) { New-Item -Path $ucPath -Force | Out-Null }
|
||||
Set-ItemProperty -Path $ucPath -Name "ProgId" -Value $progId -Force
|
||||
Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId -Force
|
||||
}
|
||||
|
||||
# HKCR\AcroExch.Document.DC\shell\open\command
|
||||
$cmdPath = "HKCR:\$progId\shell\open\command"
|
||||
if (-not (Test-Path $cmdPath)) { New-Item -Path $cmdPath -Force | Out-Null }
|
||||
Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd -Force
|
||||
'@ | Set-Content -Path $pdfScript -Encoding UTF8 -Force
|
||||
|
||||
$pdfAction = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$pdfScript`""
|
||||
$pdfTrigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
Register-Task -TaskName "PDF-DefaultApp" `
|
||||
-Description "Restore Adobe Reader as default PDF app on logon" `
|
||||
-Action $pdfAction `
|
||||
-Trigger $pdfTrigger
|
||||
# Runs as SYSTEM to allow HKCR writes (system-wide file association)
|
||||
$pdfPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
$pdfSettings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-StartWhenAvailable
|
||||
$pdfTask = New-ScheduledTask -Action $pdfAction `
|
||||
-Trigger $pdfTrigger `
|
||||
-Settings $pdfSettings `
|
||||
-Principal $pdfPrincipal `
|
||||
-Description "Restore Adobe Reader as default PDF app on logon"
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName "PDF-DefaultApp" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Register-ScheduledTask -TaskName "PDF-DefaultApp" -InputObject $pdfTask -Force | Out-Null
|
||||
Write-Log " Registered task: PDF-DefaultApp" -Level OK
|
||||
}
|
||||
catch {
|
||||
Write-Log " Failed to register task PDF-DefaultApp - $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Task: UnlockStartLayout
|
||||
|
|
|
|||
Loading…
Reference in a new issue