PS scripts, web platform, Forgejo CI, xetup.exe launcher

Initial deployment suite for X9.cz MSP Windows 10/11 deployment:
- PowerShell scripts 00-11: admin account, bloatware removal, software (winget+Atera),
  system registry tweaks, default profile, personalization, scheduled tasks,
  BackInfo desktop info, Windows activation, PC identity/rename, network, Dell Update
- Web platform: xetup.x9.cz (nginx), spec/annotation page, /dl shortlink, GitHub mirror
- Forgejo Actions CI: auto-build xetup.exe on push, publish to releases/latest
- Go xetup.exe: embeds all scripts/assets, per-feature checkboxes, load/save config
This commit is contained in:
Filip Zubik 2026-04-16 14:49:41 +02:00
commit c42943cfa8
60 changed files with 10202 additions and 0 deletions

14
.claude/MEMORY.md Normal file
View file

@ -0,0 +1,14 @@
# Memory Index
## User
- [Filip Zubik - X9.cz](user_filip.md) - MSP owner/developer, Czech communication, pragmatic approach
## Feedback
- [Communication preferences](feedback_preferences.md) - Czech, discussion first, interactive reviews, phased approach
## Project
- [Xetup project state](project_xetup_state.md) - Current state, architecture decisions, what's next (as of 2026-04-15)
- [Technical findings](project_technical_findings.md) - Deep code analysis, UCPD issue, Win11 compatibility, tools research
## Reference
- [Colleague specs and review](reference_colleague_specs.md) - Where to find colleague's input documents and review results

14
.claude/memory/MEMORY.md Normal file
View file

@ -0,0 +1,14 @@
# Memory Index
## User
- [Filip Zubik - X9.cz](user_filip.md) - MSP owner/developer, Czech communication, pragmatic approach
## Feedback
- [Communication preferences](feedback_preferences.md) - Czech, discussion first, interactive reviews, phased approach
## Project
- [Xetup project state](project_xetup_state.md) - Current state, architecture decisions, what's next (as of 2026-04-15)
- [Technical findings](project_technical_findings.md) - Deep code analysis, UCPD issue, Win11 compatibility, tools research
## Reference
- [Colleague specs and review](reference_colleague_specs.md) - Where to find colleague's input documents and review results

View file

@ -0,0 +1,15 @@
---
name: Communication and workflow preferences
description: How Filip wants to work - Czech communication, discussion before coding, interactive reviews
type: feedback
---
- Communicate in Czech (code/comments/logs stay English)
- When Filip says "nekodujme, udelame diskusi" - he wants discussion first, not jumping to code
- Prefers interactive HTML pages for reviews/decisions (not just markdown)
- Likes phased approaches - start simple, evolve
- Values colleague's field experience over theoretical best practices
- When specs conflict, ask rather than assume
- Filip appreciates out-of-the-box thinking (Go binaries, web platforms) but wants practical solutions
- Don't over-engineer for 20 machines/month scale
- Filip works on macOS, targets Windows - cross-compilation matters

View file

@ -0,0 +1,57 @@
---
name: Technical findings from deep code analysis and research (2026-04-15)
description: Detailed technical analysis results - Win11 compatibility issues, UCPD driver, code quality assessment, and modernization opportunities
type: project
---
## Code quality assessment (all scripts in windows-deployment-new/scripts/)
- Overall: solid, production-ready for Win10/11 22H2
- 3-level registry fallback in 03-system-registry.ps1: direct write → ACL fix (SeTakeOwnershipPrivilege) → SYSTEM scheduled task
- Proper hive handling: GC.Collect + WaitForPendingFinalizers + 500ms sleep before reg unload, always in finally block
- Error handling: $ErrorActionPreference = "Continue", try/catch everywhere, WARN level for non-critical failures
- Logging: every step to C:\Windows\Setup\Scripts\Deploy.log with color-coded console output
## Critical issues found
### 1. UCPD.sys (User Choice Protection Driver)
- Kernel-mode driver since Feb 2024, v4.3 as of early 2026
- Blocks direct registry writes to UserChoice for .pdf, .htm, .html etc.
- Our HKCR approach works as system-wide fallback but isn't clean
- Fix: disable UCPD service + scheduled task during deployment, set associations, re-enable
- Or use SetUserFTA tool (~$20, kolbi.cz)
### 2. System tray EnableAutoTray=0 broken on 24H2
- Win11 23H2/24H2 ignores this registry key
- Icon stream cache clearing is a workaround but not 100%
- No reliable registry-only solution exists for 24H2
### 3. OneDrive removal too aggressive
- 03-system-registry.ps1 lines 244-273: uninstalls + deletes OneDriveSetup.exe
- 04-default-profile.ps1 lines 240-261: removes RunOnce keys + Explorer namespace
- Must remove these blocks entirely
### 4. Edge policies incomplete
- Currently only: HideFirstRunExperience, CreateDesktopShortcutDefault
- Need to add: BrowserSignin=0, CopilotPageContext=0, NewTabPageContentEnabled=0, StandaloneHubsSidebarEnabled=0, ShowRecommendationsEnabled=0, DefaultBrowserSettingsCampaignEnabled=0, and ~10 more
### 5. ConfigureStartPins applyOnce
- New in 24H2 (KB5062660): {"pinnedList":[], "applyOnce": true}
- Applies layout once, then users can customize
- Better than our current approach (XML lock + UnlockStartLayout task)
## Win10/Win11 compatibility matrix
- All core registry keys work on both versions
- Win11-specific keys (TaskbarAl, ShowCopilotButton, TaskbarDa, TaskbarMn) harmlessly create empty keys on Win10
- Scripts handle version differences through graceful degradation
## Config.json issues
- desktopInfo settings (position, fontSize, color) are defined but ignored by 07-desktop-info.ps1
- deployment.locale is not used anywhere
- Software list has only 3 packages (TODO in SPEC)
## Tools landscape (researched 2026-04-15)
- Chris Titus WinUtil: PS-based, `irm christitus.com/win | iex`, has Win11 Creator tab
- Win11Debloat (Raphire): got GUI in Feb 2026, configurable via Apps.json
- Sophia Script: 150+ tweaks, most granular but slower
- Go binary advantages: bypasses execution policy, single file, no dependencies, cross-compile from macOS
- Charmbracelet stack (bubbletea/huh/lipgloss): best for TUI forms in Go

View file

@ -0,0 +1,73 @@
---
name: Xetup project state as of 2026-04-15
description: Current state of the xetup Windows deployment project - architecture decisions, what exists, what's planned
type: project
---
## What xetup is
Automated Windows 10/11 setup for X9.cz MSP clients. Replaces ~3 hours of manual work with a single script/tool.
## Current repo structure (cleaned 2026-04-15)
```
xetup/
├── review.html ← interactive review page v2 (with colleague comments)
├── xetup-review.md ← exported review v1 results from colleague
├── xetup-win-setup-spec.md ← original spec from colleague
├── xetup-win-setup-novinky.md ← v2 additions from colleague (taskbar pins, explorer, network, admin desc)
├── W11.pdf ← reference PDF
└── windows-deployment-new/ ← the active codebase
├── Deploy-Windows.ps1 ← master script
├── CLAUDE.md / SPEC.md
├── config/config.json
├── assets/
│ ├── Backinfo/ ← BackInfo.exe + ini + ps1 (ready to use)
│ └── Logo/ ← X9 ico + jpeg (moved here 2026-04-15)
└── scripts/
├── 00-admin-account.ps1
├── 01-bloatware.ps1
├── 02-software.ps1
├── 03-system-registry.ps1
├── 04-default-profile.ps1
├── 05-personalization.ps1
├── 06-scheduled-tasks.ps1
├── 07-desktop-info.ps1 ← TO BE REPLACED by BackInfo
└── 08-activation.ps1
```
## Key decisions made (2026-04-15 session)
1. **BackInfo wins over custom DesktopInfo** - colleague prefers it (INI config, auto-update, centered text). Our 07-desktop-info.ps1 will be deleted.
2. **OneDrive must NOT be removed** - current code aggressively deletes it, breaks M365. Must fix 03-system-registry.ps1 and 04-default-profile.ps1.
3. **RDP must NOT be removed** - was in SPEC but never implemented (good). Remove from SPEC entirely.
4. **Colleague's spec has priority** over our implementation when they overlap.
5. **adminx9 account: no password** (changed from config-driven password), FullName = "X9.cz s.r.o."
6. **Nextcloud not needed** - assets are in repo, only Atera MSI downloads from web.
7. **Flash2 not integrated** - it's the old version of this tool, just for inspiration.
8. **Atera Agent**: curl from `https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337` then `msiexec /i setup.msi /qn`
## Architecture direction (decided 2026-04-15)
- **Go TUI launcher** (xetup.exe) - single binary, embeds PS scripts + assets
- Charmbracelet stack: bubbletea, huh, lipgloss
- Self-update from web (version.json check)
- **Web platform** at xetup.x9.cz:
- Forgejo for git hosting + issues + CI
- Auto-generated documentation from spec.yaml
- Comments via Forgejo Issues API
- Landing page + download + changelog
- Deployment reporting dashboard (later)
- **spec.yaml** as single source of truth for both exe and docs
## What needs to happen next
1. Create spec.yaml from all gathered specs + review
2. Initialize Go project structure
3. Fix PS scripts (OneDrive removal, admin password, BackInfo integration)
4. Set up repo (GitHub initially, Forgejo later)
5. First Go build with TUI form
6. CI pipeline (GitHub Actions)
7. Web landing page
## Technical findings from deep analysis
- UCPD kernel driver (since Feb 2024) blocks PDF default association via UserChoice - need to disable UCPD during deployment
- System tray "show all icons" broken in Win11 24H2 - EnableAutoTray=0 ignored
- Edge needs ~15 more policy keys than we currently set
- ConfigureStartPins has new applyOnce property in 24H2
- Current code quality is solid: 3-level registry fallback, proper hive handling with GC+finally

View file

@ -0,0 +1,24 @@
---
name: Colleague spec documents and review results
description: Where to find colleague's input - specs, review comments, and novinky files in repo root
type: reference
---
## Input documents from colleague (in repo root)
- `xetup-win-setup-spec.md` - original full spec (15 sections covering directory structure, personalization, taskbar, power, proxy, PC rename, bloatware, SW install, BackInfo, Edge, bootstrap launcher)
- `xetup-win-setup-novinky.md` - v2 additions: A) taskbar pinned apps (admin vs user XML layout), B) Explorer settings (ShowRecent/Frequent off, FullPath), C) network discovery + private network, D) admin account FullName
- `xetup-review.md` - exported review v1 with colleague's comments
- `W11.pdf` - reference PDF from colleague
## Key colleague comments from review v1
- Nextcloud not needed for assets
- adminX9 without password
- Flash2 is just the old version, for inspiration only
- OneDrive and RDP problems reported from first version
- Atera: specific curl URL provided with cid=31 and aeid parameter
- Logo files added to repo (now in assets/Logo/)
## Review v2
- `review.html` in repo root - interactive review page with all steps organized into 9 groups
- Uses localStorage prefix `xr2-` for state
- 27 steps total, incorporates all colleague feedback + novinky

View file

@ -0,0 +1,17 @@
---
name: Filip Zubik - X9.cz
description: MSP owner/developer at X9.cz, builds Windows deployment automation, prefers Czech communication, pragmatic approach
type: user
---
- Runs X9.cz - MSP (Managed Service Provider) deploying ~20 Windows machines/month
- Developer + business owner - makes architectural decisions
- Works on macOS, deploys to Windows
- Prefers Czech for communication, English for code/comments/logs
- Pragmatic - wants working solutions, not over-engineering
- Open to modern approaches (Go binaries, TUI, web platforms)
- Has a colleague ("kolega") who is the hands-on technician doing the actual deployments
- Colleague provides real-world specs and feedback from field experience
- Filip values colleague's input - "spec kolegy ma prednost"
- Likes interactive review workflows (HTML review pages with approve/reject/discuss)
- Thinks long-term - wants living documentation, feedback loops, continuous improvement

View file

@ -0,0 +1,76 @@
name: release
on:
push:
branches: [main]
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'scripts/**'
- 'assets/**'
- 'embed.go'
- '.forgejo/workflows/release.yml'
jobs:
build-and-release:
# Runner label 'ubuntu-latest' maps to golang:1.24-alpine container (see runner config)
runs-on: ubuntu-latest
defaults:
run:
shell: sh
working-directory: /repo
steps:
- name: Setup
working-directory: /
run: |
apk add --no-cache git curl jq mingw-w64-gcc
git clone --depth=1 \
"http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \
/repo
cd /repo
git checkout "${{ github.sha }}"
- name: Build xetup.exe
run: |
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
- name: Publish latest release
env:
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
API: http://xetup-forgejo:3000/api/v1
REPO: ${{ github.repository }}
run: |
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
# Delete existing 'latest' release and tag to recreate cleanly
RID=$(curl -sf -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/tags/latest" | jq -r '.id // empty')
if [ -n "$RID" ]; then
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$RID" || true
fi
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/tags/latest" || true
# Create new 'latest' release
RID=$(curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "{\"tag_name\":\"latest\",\"name\":\"latest\",\"body\":\"Auto-built from ${SHORT}\",\"prerelease\":true}" \
| jq -r '.id')
# Upload xetup.exe
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
"$API/repos/$REPO/releases/$RID/assets?name=xetup.exe" \
--data-binary @xetup.exe
echo "Released xetup.exe (commit ${SHORT})"

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# Secrets
.env
*.key
# Logs
*.log
# Windows
Thumbs.db
Desktop.ini
# macOS
.DS_Store
# Temp
*.tmp
*.bak
# Build artifacts
flash.zip
# Large reference files
W11.pdf

175
CLAUDE.md Normal file
View file

@ -0,0 +1,175 @@
# CLAUDE.md - Instructions for Claude Code
## Project context
MSP deployment script for X9.cz - automated preparation of new Windows 10/11 computers for clients.
Replaces ~3 hours of manual setup with a single PowerShell script (evolving toward Go TUI launcher).
**Key parameters:**
- Target OS: Windows 10 and Windows 11 (x64), including unsupported HW
- Execution: as Administrator on already-installed Windows (not WinPE/autounattend)
- Volume: ~20 machines per month, various clients
- Operator: MSP technician on-site at client
---
## Communication
- Communicate with the user in Czech
- Code, comments, log messages: English only (no diacritics rule still applies)
---
## Repo structure
```
windows-deployment-new/
├── CLAUDE.md <- this file
├── SPEC.md <- technical specification
├── Deploy-Windows.ps1 <- master script (entry point)
├── scripts/
│ ├── 00-admin-account.ps1 <- create hidden admin account
│ ├── 01-bloatware.ps1 <- remove AppX, Capabilities, Features
│ ├── 02-software.ps1 <- winget installs + Adobe PDF default
│ ├── 03-system-registry.ps1 <- HKLM tweaks
│ ├── 04-default-profile.ps1 <- C:\Users\Default\NTUSER.DAT changes
│ ├── 05-personalization.ps1 <- colors, wallpaper, theme
│ ├── 06-scheduled-tasks.ps1 <- register scheduled tasks
│ ├── 07-desktop-info.ps1 <- TO BE DELETED (replaced by BackInfo)
│ └── 08-activation.ps1 <- Windows activation via slmgr
├── config/
│ └── config.json <- per-client config
├── assets/
│ ├── Backinfo/ <- BackInfo.exe + .ini + backinfo_W11.ps1
│ └── Logo/ <- X9-ikona.ico, X9-logo.jpeg
└── tests/
└── Test-Deployment.ps1 <- post-deployment verification
```
---
## Conventions and rules
### PowerShell
- Always `#Requires -RunAsAdministrator` in master script
- `$ErrorActionPreference = "Continue"` - script must survive partial failures
- Log every step to `C:\Windows\Setup\Scripts\Deploy.log`
- Logging via `Write-Log` function defined in master script
- `Invoke-Step` function wraps every step - catches errors, logs, continues
- Comments in English, code in English
- NO diacritics - no accented characters anywhere: not in comments, not in user messages, not in log output
- NO emoticons - not in comments, not in output messages
- Reason: encoding issues across systems, log readability, compatibility
### Master script structure
```powershell
# 1. Load config.json
# 2. Run individual scripts in order
# 3. Print summary report at end (OK/ERROR counts)
```
### Master script switches
| Switch | Behavior |
|---|---|
| `-SkipBloatware` | Skip step 1 |
| `-SkipSoftware` | Skip step 2 |
| `-SkipDefaultProfile` | Skip step 4 |
| `-DryRun` | Run without changes, log only |
### Testing
- Test VM: Windows 10/11 x64 on VMware ESXi (X9.cz internal infrastructure)
- Before each test: take snapshot
- After test: revert snapshot
- Dev environment: x64 VM only - NOT ARM (no Parallels/Apple Silicon for testing)
---
## Important notes
### BackInfo (replaces custom DesktopInfo)
BackInfo.exe IS used. Located in assets/Backinfo/. Deployment:
1. Copy assets/Backinfo/ to C:\Program Files\Backinfo\
2. Run backinfo_W11.ps1 (detects OS, writes registry, creates Startup shortcut)
3. BackInfo.exe auto-starts on every logon, reads INI, renders BMP with system info
- Configurable via BackInfo.ini (fonts, positions, data sources)
- Displays: hostname (centered, large), username, OS, HW info, network info
- DELETE 07-desktop-info.ps1 - no longer needed
### Adobe Reader as default PDF app
- After install: set .pdf -> AcroRd32 association
- Scheduled task PDF-DefaultApp restores association on every logon (guard against Edge overwriting it)
- NOTE: UCPD.sys (kernel driver since Feb 2024) blocks UserChoice writes. Consider disabling UCPD during deployment.
### Default Profile
- Changes to C:\Users\Default\NTUSER.DAT via reg load / reg unload
- Applies to all new users - critical for MSP deployment
- Currently logged-in user gets changes via direct write to HKCU
### Winget
- Always use --accept-package-agreements --accept-source-agreements
- Check winget availability before running installs
- Log result of every install
### Atera Agent
- Download: `Invoke-WebRequest -Uri "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337" -OutFile setup.msi`
- Install: `msiexec /i setup.msi /qn`
### Admin account (adminx9)
- NO PASSWORD (changed from previous version)
- FullName = "X9.cz s.r.o." (via ADSI)
- Hidden from login screen
- Added to Administrators group
---
## DO NOT
- Do not use $ErrorActionPreference = "Stop" - script must survive partial failure
- Do not remove Calculator (Microsoft.WindowsCalculator) - intentionally kept
- Do not use ARM VM for testing
- Do not write scripts depending on specific username - script is universal
- Do not use hardcoded paths that do not exist on clean Windows
- NO diacritics - no accented characters in any part of any script
- NO emoticons - none in comments, log messages or output
- Do not remove OneDrive - must remain installable for M365
- Do not remove RDP/RDS - must remain functional
- Do not remove Microsoft-RemoteDesktopConnection from Optional Features
---
## Planned changes (from review v2, 2026-04-15)
### Must fix
- [ ] Remove OneDrive uninstall from 03-system-registry.ps1 and 04-default-profile.ps1
- [ ] Remove password from admin account, add FullName = "X9.cz s.r.o."
- [ ] Delete 07-desktop-info.ps1, replace with BackInfo deployment step
- [ ] Add powercfg settings (standby-timeout-ac 0, monitor-timeout-ac 60, etc.)
- [ ] Add proxy auto-detect disable (AutoDetect = 0)
- [ ] Add Atera Agent install step
- [ ] Extend Edge policies (~15 more keys)
### New features (from colleague spec v2)
- [ ] Taskbar pinned apps: admin vs user variants via XML layout + -ProfileType parameter
- [ ] Explorer: ShowRecent=0, ShowFrequent=0, FullPath=1 in CabinetState
- [ ] Network discovery: enable ping, set private network profile (post-restart step)
- [ ] PC rename: Rename-Computer as final step before restart
- [ ] C:\X9 directory structure with custom folder icon
### Architecture evolution
- [ ] Go TUI launcher (xetup.exe) embedding PS scripts
- [ ] spec.yaml as single source of truth
- [ ] Web platform at xetup.x9.cz (Forgejo + docs + comments)
- [ ] Self-update mechanism in xetup.exe
---
## Open questions
| # | Question | Status |
|---|---|---|
| 1 | BackInfo replacement | DONE - using BackInfo.exe from assets/ |
| 2 | Complete SW list for winget | TODO - list incomplete |
| 3 | Per-client variability via config.json | FUTURE |
| 4 | Admin account adminx9 | DECIDED - no password, FullName "X9.cz s.r.o." |
| 5 | UCPD driver workaround for PDF default | TODO - disable during deployment |
| 6 | Atera MFA bypass | OPEN - does aeid parameter avoid MFA? |

276
Deploy-Windows.ps1 Normal file
View file

@ -0,0 +1,276 @@
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[switch]$SkipBloatware,
[switch]$SkipSoftware,
[switch]$SkipDefaultProfile,
[switch]$DryRun,
[ValidateSet("default","admin","user")]
[string]$ProfileType = "default"
)
$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
backinfo = $true
network = $true
pcIdentity = $true
activation = $true
dellUpdate = $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 -ProfileType $ProfileType
}
} 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 - BackInfo
# -----------------------------------------------------------------------
if ($stepsEnabled['backinfo']) {
Invoke-Step -Name "Step 7 - BackInfo" -Action {
& "$ScriptRoot\scripts\07-backinfo.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 7 - BackInfo" }
# -----------------------------------------------------------------------
# Step 9 - Network
# -----------------------------------------------------------------------
if ($stepsEnabled['network']) {
Invoke-Step -Name "Step 9 - Network" -Action {
& "$ScriptRoot\scripts\10-network.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 9 - Network" }
# -----------------------------------------------------------------------
# Step 11 - Dell Command | Update (auto-skipped on non-Dell hardware)
# -----------------------------------------------------------------------
if ($stepsEnabled['dellUpdate']) {
Invoke-Step -Name "Step 11 - Dell Command | Update" -Action {
& "$ScriptRoot\scripts\11-dell-update.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 11 - Dell Command | Update" }
# -----------------------------------------------------------------------
# Step 10 - PC identity (rename + C:\X9) - runs last, rename needs restart
# -----------------------------------------------------------------------
if ($stepsEnabled['pcIdentity']) {
Invoke-Step -Name "Step 10 - PC identity" -Action {
& "$ScriptRoot\scripts\09-pc-identity.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 10 - PC identity" }
# -----------------------------------------------------------------------
# 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
}

137
Remove-ClaudeCode.ps1 Normal file
View file

@ -0,0 +1,137 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Claude Code - odstraneni citlivych dat a volitelne cele instalace
.USAGE
# Jen citliva data (API key + repo):
.\Remove-ClaudeCode.ps1 -RepoPath "C:\Projects\windows-deployment"
# Vse vcetne Claude Code a Node.js:
.\Remove-ClaudeCode.ps1 -RepoPath "C:\Projects\windows-deployment" -Full
#>
param(
[Parameter(Mandatory)]
[string] $RepoPath,
[switch] $Full
)
$ErrorActionPreference = "Continue"
function Write-Step { param([string]$Msg) Write-Host "`n[REMOVE] $Msg" -ForegroundColor Yellow }
function Write-OK { param([string]$Msg) Write-Host " OK: $Msg" -ForegroundColor Green }
function Write-Skip { param([string]$Msg) Write-Host " SKIP: $Msg" -ForegroundColor DarkGray }
Write-Host "`n========================================" -ForegroundColor Red
Write-Host " Claude Code Cleanup" -ForegroundColor Red
Write-Host "========================================`n" -ForegroundColor Red
# ------------------------------------------------------------
# 1. API KEY
# ------------------------------------------------------------
Write-Step "Removing ANTHROPIC_API_KEY..."
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "Machine")
$env:ANTHROPIC_API_KEY = $null
Write-OK "API key removed from environment variables"
# Claude Code si uklada API key take v ~/.claude
$claudeConfig = Join-Path $HOME ".claude"
if (Test-Path $claudeConfig) {
Remove-Item $claudeConfig -Recurse -Force
Write-OK "Removed ~/.claude config directory"
} else {
Write-Skip "~/.claude not found"
}
# ------------------------------------------------------------
# 2. REPO
# ------------------------------------------------------------
Write-Step "Removing repository at $RepoPath..."
if (Test-Path $RepoPath) {
# Nejdriv over ze je to skutecne git repo - pojistka
$gitDir = Join-Path $RepoPath ".git"
if (Test-Path $gitDir) {
Remove-Item $RepoPath -Recurse -Force
Write-OK "Repository removed"
} else {
Write-Host " WARN: $RepoPath does not look like a git repo. Skipping for safety." -ForegroundColor Yellow
}
} else {
Write-Skip "Repo path not found: $RepoPath"
}
# ------------------------------------------------------------
# 3. GIT CREDENTIALS (pokud byly ulozeny)
# ------------------------------------------------------------
Write-Step "Clearing git credentials for repo..."
try {
git credential reject | Out-Null
} catch {}
# Windows Credential Manager - GitHub tokeny
try {
$creds = cmdkey /list 2>$null | Select-String "github"
foreach ($cred in $creds) {
$target = ($cred -split '\s+') | Where-Object { $_ -like "*github*" } | Select-Object -First 1
if ($target) {
cmdkey /delete:$target | Out-Null
Write-OK "Removed credential: $target"
}
}
} catch {
Write-Skip "No GitHub credentials found in Credential Manager"
}
# ------------------------------------------------------------
# 4. VOLITELNE - Claude Code + Node.js
# ------------------------------------------------------------
if ($Full) {
Write-Step "Uninstalling Claude Code..."
try {
npm uninstall -g @anthropic-ai/claude-code
Write-OK "Claude Code uninstalled"
} catch {
Write-Skip "Claude Code not installed via npm or npm not available"
}
Write-Step "Uninstalling Node.js..."
try {
winget uninstall OpenJS.NodeJS.LTS --silent
Write-OK "Node.js uninstalled"
} catch {
Write-Skip "Node.js not found via winget - remove manually if needed"
}
}
# ------------------------------------------------------------
# 5. POWERSHELL HISTORY (muze obsahovat API key z parametru)
# ------------------------------------------------------------
Write-Step "Clearing PowerShell history..."
$historyPath = (Get-PSReadlineOption).HistorySavePath
if ($historyPath -and (Test-Path $historyPath)) {
# Vymaz pouze radky obsahujici ApiKey / sk-ant
$lines = Get-Content $historyPath | Where-Object { $_ -notmatch 'ApiKey|sk-ant-|ANTHROPIC' }
$lines | Set-Content $historyPath
Write-OK "Sensitive lines removed from PS history"
} else {
Write-Skip "PS history file not found"
}
# ------------------------------------------------------------
# SUMMARY
# ------------------------------------------------------------
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " Cleanup complete!" -ForegroundColor Green
if ($Full) {
Write-Host " Removed: API key, repo, ~/.claude, Node.js, Claude Code" -ForegroundColor White
} else {
Write-Host " Removed: API key, repo, ~/.claude config" -ForegroundColor White
Write-Host " Node.js and Claude Code kept (use -Full to remove)" -ForegroundColor DarkGray
}
Write-Host "========================================`n" -ForegroundColor Green

125
Run.cmd Normal file
View 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

293
SPEC.md Normal file
View file

@ -0,0 +1,293 @@
# MSP Windows Deployment - Specification (SPEC.md)
> Version: 0.2 (draft)
> Author: X9.cz
> Purpose: Automated preparation of new Windows 10/11 computers for clients
---
## Overview
Script replaces ~3 hours of manual computer setup. Run once as Administrator on
already-installed Windows, performs everything automatically, saves result to Default
Profile so settings apply to every subsequent user.
---
## Prerequisites
- Windows 10 or Windows 11 (x64)
- Run as Administrator
- Internet connection (for winget installs)
- Computer received either as clean OEM install or with manufacturer pre-installed Windows
---
## What the script does NOT do
- Does not install Windows (not an autounattend.xml for clean install)
- Does not create images
- Does not manage the computer ongoing (one-time deployment)
---
## Script structure
Script is divided into steps. Each step logs its result. Steps can be skipped with switches.
---
## STEP 0a - Admin account
Creates local admin account `adminx9`:
- Password from `config.json` (`adminAccount.password`)
- Added to Administrators group
- Password never expires, user cannot change password
- Hidden from Windows login screen (SpecialAccounts\UserList = 0)
---
## STEP 0b - Windows activation
Activates Windows using product key from config:
- Key from `config.json` (`activation.productKey`) - set to real MAK/retail key for production
- Falls back to GVLK (KMS client key) matched by detected OS edition
- Optional KMS server via `activation.kmsServer`
- If already activated, skips silently
---
## STEP 1 - Bloatware removal
### 1a - AppX packages (UWP apps)
Removed for all users (-AllUsers) and from provisioned packages (so they do not return for new users).
| Package | Description |
|---|---|
| Microsoft.Microsoft3DViewer | 3D Viewer |
| Microsoft.BingSearch | Bing Search |
| Microsoft.WindowsCamera | Camera |
| Clipchamp.Clipchamp | Clipchamp video editor |
| Microsoft.WindowsAlarms | Clock / Alarm |
| Microsoft.Copilot | Copilot AI |
| Microsoft.549981C3F5F10 | Cortana |
| Microsoft.Windows.DevHome | Dev Home |
| MicrosoftCorporationII.MicrosoftFamily | Family Safety |
| Microsoft.WindowsFeedbackHub | Feedback Hub |
| Microsoft.Edge.GameAssist | Game Assist |
| Microsoft.GetHelp | Help |
| Microsoft.Getstarted | Tips / Get Started |
| microsoft.windowscommunicationsapps | Mail and Calendar |
| Microsoft.WindowsMaps | Maps |
| Microsoft.MixedReality.Portal | Mixed Reality |
| Microsoft.BingNews | News |
| Microsoft.MicrosoftOfficeHub | Office Hub |
| Microsoft.Office.OneNote | OneNote |
| Microsoft.OutlookForWindows | Outlook (new) |
| Microsoft.Paint | Paint (new UWP) |
| Microsoft.MSPaint | Paint (legacy) |
| Microsoft.People | People |
| Microsoft.Windows.Photos | Photos |
| Microsoft.PowerAutomateDesktop | Power Automate |
| MicrosoftCorporationII.QuickAssist | Quick Assist |
| Microsoft.SkypeApp | Skype |
| Microsoft.ScreenSketch | Snipping Tool |
| Microsoft.MicrosoftSolitaireCollection | Solitaire |
| Microsoft.MicrosoftStickyNotes | Sticky Notes |
| MicrosoftTeams / MSTeams | Teams (personal) |
| Microsoft.Todos | To Do |
| Microsoft.WindowsSoundRecorder | Voice Recorder |
| Microsoft.Wallet | Wallet |
| Microsoft.BingWeather | Weather |
| Microsoft.WindowsTerminal | Windows Terminal |
| Microsoft.Xbox.TCUI | Xbox UI |
| Microsoft.XboxApp | Xbox |
| Microsoft.XboxGameOverlay | Xbox Game Overlay |
| Microsoft.XboxGamingOverlay | Xbox Gaming Overlay |
| Microsoft.XboxIdentityProvider | Xbox Identity |
| Microsoft.XboxSpeechToTextOverlay | Xbox Speech |
| Microsoft.GamingApp | Gaming App |
| Microsoft.YourPhone | Phone Link |
| Microsoft.ZuneMusic | Music |
| Microsoft.ZuneVideo | Movies and TV |
NOTE: Microsoft.WindowsCalculator is intentionally KEPT.
### 1b - Windows Capabilities
| Capability | Description |
|---|---|
| Print.Fax.Scan | Fax and Scan |
| Language.Handwriting | Handwriting |
| Browser.InternetExplorer | Internet Explorer |
| MathRecognizer | Math Input |
| OneCoreUAP.OneSync | OneSync |
| OpenSSH.Client | OpenSSH client |
| Microsoft.Windows.MSPaint | Paint (Win32) |
| Microsoft.Windows.PowerShell.ISE | PowerShell ISE |
| App.Support.QuickAssist | Quick Assist |
| Microsoft.Windows.SnippingTool | Snipping Tool |
| App.StepsRecorder | Steps Recorder |
| Hello.Face.* | Windows Hello face |
| Media.WindowsMediaPlayer | Windows Media Player |
| Microsoft.Windows.WordPad | WordPad |
### 1c - Windows Optional Features
| Feature | Description |
|---|---|
| MediaPlayback | Media playback |
| MicrosoftWindowsPowerShellV2Root | PowerShell 2.0 |
| Microsoft-RemoteDesktopConnection | RDP client |
| Recall | Windows Recall (AI) |
| Microsoft-SnippingTool | Snipping Tool (feature) |
---
## STEP 2 - Software installation (winget)
| Software | Winget ID | Notes |
|---|---|---|
| 7-Zip | `7zip.7zip` | OK |
| Adobe Acrobat Reader | `Adobe.Acrobat.Reader.64-bit` | OK, see note |
| OpenVPN Connect | `OpenVPNTechnologies.OpenVPNConnect` | OK |
| ... | ... | TODO: complete list |
> Adobe Acrobat Reader: After install, script sets .pdf -> AcroRd32 as default.
> Scheduled task PDF-DefaultApp restores this association on every logon as a guard
> against Edge overwriting it.
> BackInfo: NOT used. Replaced by custom PowerShell scheduled task DesktopInfo.
> See STEP 7.
---
## STEP 3 - System settings (HKLM - applies to whole system)
| Setting | Value | Notes |
|---|---|---|
| Disable NRO (bypass network check) | HKLM\...\OOBE\BypassNRO = 1 | |
| Disable auto-install of Teams | ConfigureChatAutoInstall = 0 | |
| Disable Cloud Optimized Content | DisableCloudOptimizedContent = 1 | |
| Disable Widgets (News and Interests) | HKLM\...\Dsh\AllowNewsAndInterests = 0 | |
| Edge - hide First Run Experience | HKLM\Policies\Edge\HideFirstRunExperience = 1 | |
| Passwords - no expiration | net accounts /maxpwage:UNLIMITED | |
| Time zone | Central Europe Standard Time | |
| OneDrive - remove | Delete OneDriveSetup.exe + Start Menu lnk | |
| Outlook (new) - disable auto-install | Delete UScheduler registry key | |
| Disable GameDVR | AppCaptureEnabled = 0 | |
---
## STEP 4 - Default Profile (NTUSER.DAT)
Settings applied to C:\Users\Default\NTUSER.DAT - inherited by every new user on first logon.
Method: script loads Default hive (reg load), makes changes, unloads (reg unload).
| Setting | Key / Value | Description |
|---|---|---|
| Taskbar - align left | TaskbarAl = 0 | Win11 default is center |
| Taskbar - hide Search box | SearchboxTaskbarMode = 0 | |
| Taskbar - hide Copilot button | ShowCopilotButton = 0 | |
| Taskbar - hide Task View button | ShowTaskViewButton = 0 | |
| Taskbar - hide Widgets | TaskbarDa = 0 | |
| Taskbar - hide Chat/Teams button | TaskbarMn = 0 | |
| Taskbar - show all tray icons | Scheduled task ShowAllTrayIcons | Runs on every logon |
| Taskbar - empty pinlist | TaskbarLayoutModification.xml | Removes default pinned apps |
| Explorer - show file extensions | HideFileExt = 0 | |
| Explorer - open to This PC | LaunchTo = 1 | Instead of Quick Access |
| Start menu - empty pins | ConfigureStartPins = {"pinnedList":[]} | Win11 |
| Start menu - disable Bing results | DisableSearchBoxSuggestions = 1 | |
| Copilot - disable | TurnOffWindowsCopilot = 1 | |
| GameDVR - disable | AppCaptureEnabled = 0 | |
| OneDrive - remove RunOnce key | Delete OneDriveSetup from Run | |
| Num Lock on startup - enable | InitialKeyboardIndicators = 2 | |
| Accent color on title bars | ColorPrevalence = 1 | |
---
## STEP 5 - Personalization (colors, wallpaper)
Applied to both Default Profile and currently logged-in user.
| Setting | Value |
|---|---|
| System theme (taskbar, Start) | Dark |
| App theme | Light |
| Accent color | #223B47 (dark blue-gray) |
| Accent color on Start and taskbar | Yes |
| Accent color on title bars | Yes |
| Transparency | Disabled |
| Wallpaper | Solid color #223B47 (no image) |
NOTE: DesktopInfo scheduled task (STEP 7) will overwrite the wallpaper with a system
info BMP. The solid color here is only a fallback if DesktopInfo is not running.
---
## STEP 6 - Scheduled Tasks
| Task | Trigger | Purpose |
|---|---|---|
| ShowAllTrayIcons | Every logon, every 1 min | Show all icons in system tray (Win11) |
| UnlockStartLayout | Once after layout is applied | Unlock Start menu layout |
| PDF-DefaultApp | Every logon | Restore .pdf -> Adobe Reader if Edge overwrote it |
| DesktopInfo | Every logon | Render system info onto desktop wallpaper |
---
## STEP 7 - DesktopInfo (BackInfo replacement)
Custom PowerShell scheduled task. No external dependencies.
**What it displays:**
- Computer name (hostname)
- IP address
- Windows version and build
- Logged-in username
- Deployment date
**How it works:**
1. PS script collects system info
2. Renders text onto bitmap via WPF / System.Drawing
3. Saves BMP to C:\Windows\Setup\Scripts\desktopinfo.bmp
4. Sets BMP as desktop wallpaper via SystemParametersInfo
5. Runs on every user logon via Scheduled Task
**Why not BackInfo:**
- BackInfo has Win11 rendering issues requiring registry hacks
- External EXE dependency is hard to distribute
- Custom PS solution = full control, no dependencies, works on Win10 and Win11
---
## STEP 8 - Logging and output
- Every step writes to C:\Windows\Setup\Scripts\Deploy.log
- Format: [HH:mm:ss] Step description - OK / ERROR: ...
- At end: summary report (how many steps OK, how many failed)
- Log stays on disk for diagnostics
---
## Script switches
| Switch | Behavior |
|---|---|
| `-SkipBloatware` | Skip step 1 |
| `-SkipSoftware` | Skip step 2 |
| `-SkipDefaultProfile` | Skip step 4 |
| `-DryRun` | Run through steps without changes, log only |
---
## Open questions
| # | Question | Status |
|---|---|---|
| 1 | BackInfo replacement | DONE - custom PS scheduled task DesktopInfo |
| 2 | Complete SW list for winget | TODO |
| 3 | Per-client variability via config.json | FUTURE |
| 4 | Admin account adminx9 - script or manual? | DONE - script (00-admin-account.ps1) |

140
Setup-ClaudeCode.ps1 Normal file
View file

@ -0,0 +1,140 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Claude Code - rychla instalace a nastaveni prostredi
.USAGE
# Windows PS:
.\Setup-ClaudeCode.ps1 -ApiKey "sk-ant-..." -RepoUrl "https://github.com/org/repo"
# S volitelnym cilove adresarem:
.\Setup-ClaudeCode.ps1 -ApiKey "sk-ant-..." -RepoUrl "https://github.com/org/repo" -WorkDir "C:\Projects"
#>
param(
[Parameter(Mandatory)]
[string] $ApiKey,
[Parameter(Mandatory)]
[string] $RepoUrl,
[string] $WorkDir = "$HOME\Projects"
)
$ErrorActionPreference = "Stop"
function Write-Step { param([string]$Msg) Write-Host "`n[SETUP] $Msg" -ForegroundColor Cyan }
function Write-OK { param([string]$Msg) Write-Host " OK: $Msg" -ForegroundColor Green }
function Write-Fail { param([string]$Msg) Write-Host " ERR: $Msg" -ForegroundColor Red; exit 1 }
# ------------------------------------------------------------
# 1. NODE.JS
# ------------------------------------------------------------
Write-Step "Checking Node.js..."
$nodeOk = $false
try {
$nodeVer = node --version 2>$null
if ($nodeVer -match 'v(\d+)' -and [int]$Matches[1] -ge 18) {
Write-OK "Node.js $nodeVer already installed"
$nodeOk = $true
}
} catch {}
if (-not $nodeOk) {
Write-Step "Installing Node.js via winget..."
try {
winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements --silent
# Reload PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
Write-OK "Node.js installed"
} catch {
Write-Fail "Node.js install failed: $_. Install manually from https://nodejs.org"
}
}
# ------------------------------------------------------------
# 2. CLAUDE CODE
# ------------------------------------------------------------
Write-Step "Checking Claude Code..."
$ccOk = $false
try {
$ccVer = claude --version 2>$null
Write-OK "Claude Code $ccVer already installed"
$ccOk = $true
} catch {}
if (-not $ccOk) {
Write-Step "Installing Claude Code..."
try {
npm install -g @anthropic-ai/claude-code
Write-OK "Claude Code installed"
} catch {
Write-Fail "Claude Code install failed: $_"
}
}
# ------------------------------------------------------------
# 3. API KEY
# ------------------------------------------------------------
Write-Step "Setting ANTHROPIC_API_KEY..."
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $ApiKey, "User")
$env:ANTHROPIC_API_KEY = $ApiKey
Write-OK "API key set (User environment variable)"
# ------------------------------------------------------------
# 4. GIT
# ------------------------------------------------------------
Write-Step "Checking Git..."
try {
git --version | Out-Null
Write-OK "Git available"
} catch {
Write-Step "Installing Git via winget..."
try {
winget install Git.Git --accept-package-agreements --accept-source-agreements --silent
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
Write-OK "Git installed"
} catch {
Write-Fail "Git install failed: $_. Install manually from https://git-scm.com"
}
}
# ------------------------------------------------------------
# 5. CLONE REPO
# ------------------------------------------------------------
Write-Step "Cloning repository..."
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
$repoName = ($RepoUrl -split '/')[-1] -replace '\.git$', ''
$targetPath = Join-Path $WorkDir $repoName
if (Test-Path $targetPath) {
Write-OK "Repo already exists at $targetPath — pulling latest..."
Push-Location $targetPath
git pull
Pop-Location
} else {
git clone $RepoUrl $targetPath
Write-OK "Cloned to $targetPath"
}
# ------------------------------------------------------------
# 6. LAUNCH
# ------------------------------------------------------------
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " Setup complete!" -ForegroundColor Green
Write-Host " Repo: $targetPath" -ForegroundColor White
Write-Host " Run: cd '$targetPath' && claude" -ForegroundColor White
Write-Host "========================================`n" -ForegroundColor Green
$launch = Read-Host "Launch Claude Code now? (Y/n)"
if ($launch -ne 'n') {
Set-Location $targetPath
claude
}

Binary file not shown.

View file

@ -0,0 +1,182 @@
;; This INI file should use the following format
;;
;; [General]
;; BackgroundColor = <COLORREF value> ; The background color to use (default = 0 (black))
;; AutoBackground = [0 | 1] ; Use background color of current desktop (default = 0)
;; BackgroundBitmap = <path to BMP file> ; Overrides AutoBackground and BackgroundColor values. Loads background bitmap from BMP file
;; XOffset = <horizontal offset in pixels> ; Horizontal offest of the entire text block from the bitmap's center. Can be negative. Default = 0
;; YOffset = <vertical offset in pixels> ; Vertical offest of the entire text block from the bitmap's center. Can be negative. Default = 0
;; Output = <file name> ; Name of output bitmap file (default = "", use popup message)
;; UpdateDesktop = [0 | 1] ; Update background desktop bitmap (default = 0)
;; ForceDesktopCenter = [0 | 1] ; Force the desktop to display the bitmap as cenetered (instead of tiled / streched). Default = 1
;; LineSpacing = <value> ; Line spacing (default = 3)
;; SuppressErrors = [0 | 1] ; If 1, errors are NOT displayed (default = 0)
;;
;; [LineN] ; Text settings for line N, where N between [1..20]
;; Type = [CompName | UserName | SysVer | ; Type of information to display on the line
;; SysInfo | NetInfo | FileVer |
;; RegValue | FreeText |
UpdateTime | Unused]
;; ; CompName - Computer name
;; ; UserName - User name
;; ; SysVer - Operating system version
;; ; SysInfo - Hardware information
;; ; NetInfo - Network information
;; ; FileVer - Version of a file specified in 'FileName' option
;; ; RegValue - Registry string value.
;; ; Reg root from 'RegRoot' (e.g. HKLM)
;; ; Reg path from 'RegPath' (e.g. SOFTWARE\Microsoft\Windows NT\CurrentVersion)
;; ; Reg value from 'RegValue' (e.g. CurrentType)
;; ; Reg title from 'RegTitle' (e.g. "The value of X is")
;; ; FreeText - Text specified in 'Text' will be displayed as is
;; ; UpdateTime - The date and time the bitmap was created
;; ; Unused - Line will not be displayed
;;
;; Font = <Face name> ; Font name (default = "Arial")
;; Size = <Font size> ; Font size (default = 22)
;; Color = <COLORREF value> ; Font color (default = WHITE)
;; Bold = [0 | 1] ; Font boldness (default = 0)
;; Italic = [0 | 1] ; Font italicness (default = 0)
;; Alignment = [Left | Right | Center] ; Font alignment (default = Left)
;;
;; ShadowX = <X offset value> ; Shadow X offset (positive only, 0 = No X shadow. Default = 0)
;; ShadowY = <Y offset value> ; Shadow Y offset (positive only, 0 = No Y shadow. Default = 0)
;; ShadowColor = <COLORREF value> ; Shadow Color (default = 0 (black))
;;
;; RegRoot = [HKLM | HKCU] ; Registry root to use for 'Type' = 'RegValue'
;; RegPath = <Registry path to read from> ; Registry path to use for 'Type' = 'RegValue'
;; RegValue = <Registry value to read from> ; Registry value to use for 'Type' = 'RegValue'. Must be of type REG_SZ
;; RegTitle = <Display title of read value> ; Registry value to use for 'Type' = 'RegValue'
;;
;; Text = <free text to display> ; Free text to display. Used if 'Type' = 'FreeText'
;;
;; FilePath = <full path to file> ; Path to file to display version for. Used if 'Type' = 'FileVer'
;; FileName = <display name of file> ; Display name of file specified in 'FilePath'. Used if 'Type' = 'FileVer'
;;
[General]
BackgroundColor = 2097152
AutoBackground = 1
Output = %temp%\backinfo.bmp
UpdateDesktop = 1
LineSpacing = 2
ForceDesktopCenter = 1
SuppressErrors = 1
[Line1]
Font = Trebuchet MS
Size = 42
Color = 16777215
Bold = 1
Italic = 0
Alignment = Center
ShadowX = 2
ShadowY = 2
ShadowColor = 4210752
Type = CompName
[Line2]
Font = Trebuchet MS
Size = 20
Color = 10526880
Bold = 0
Italic = 0
Alignment = Center
ShadowX = 0
ShadowY = 0
ShadowColor = 4210752
Type = UserName
[Line3]
Font = Trebuchet MS
Size = 20
Color = 10526880
Bold = 1
Italic = 0
Alignment = Center
ShadowX = 0
ShadowY = 0
ShadowColor = 4210752
Type = RegValue
RegRoot = HKLM
RegPath = SOFTWARE\BackInfo
RegValue = OSName
RegTitle = OS:
[Line4]
Font = Trebuchet MS
Size = 20
Color = 10526880
Bold = 0
Italic = 0
Alignment = Center
ShadowX = 0
ShadowY = 0
ShadowColor = 4210752
Type = SysInfo
[Line5]
Font = Trebuchet MS
Size = 20
Color = 10526880
Bold = 0
Italic = 0
Alignment = Center
ShadowX = 0
ShadowY = 0
ShadowColor = 4210752
Type = NetInfo
;;
;; SAMPLE - how to display free text lines
;;
; [Line6]
; Font = Trebuchet MS
; Size = 20
; Color = 10526880
; Bold = 0
; Italic = 0
; Alignment = Center
; ShadowX = 0
; ShadowY = 0
; ShadowColor = 4210752
; Type = FreeText
; Text = System path is %windir%
;;;
;; SAMPLE - how to display file version
;;
; [Line7]
; Font = Trebuchet MS
; Size = 20
; Color = 10526880
; Bold = 0
; Italic = 0
; Alignment = Center
; ShadowX = 0
; ShadowY = 0
; ShadowColor = 4210752
; Type = FileVer
; FilePath = %ProgramFiles%\backinfo\backinfo.exe
; FileName = backinfo.exe
;;
;; SAMPLE - how to display registry value
;;
; [Line8]
; Font = Trebuchet MS
; Size = 20
; Color = 10526880
; Bold = 0
; Italic = 0
; Alignment = Center
; ShadowX = 0
; ShadowY = 0
; ShadowColor = 4210752
; Type = RegValue
; RegRoot = HKLM
; RegPath = SOFTWARE\Microsoft\Windows NT\CurrentVersion
; RegValue = CurrentType
; RegTitle = OS type

View file

@ -0,0 +1,60 @@
# ================================
# BackInfo OS detection script
# Writes OS name for BGInfo/BackInfo
# ================================
Set-ExecutionPolicy Unrestricted
$cvPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
$cv = Get-ItemProperty -Path $cvPath
# --- Detect OS by build number ---
$build = [int]$cv.CurrentBuild
if ($build -ge 22000) {
$osName = "Windows 11"
} else {
$osName = "Windows 10"
}
# --- Detect edition ---
switch ($cv.EditionID) {
"Professional" { $edition = "Pro" }
"ProfessionalN" { $edition = "Pro N" }
"Core" { $edition = "Home" }
"CoreN" { $edition = "Home N" }
"Enterprise" { $edition = "Enterprise" }
"Education" { $edition = "Education" }
default { $edition = $cv.EditionID }
}
$finalOSName = "$osName $edition"
# --- Registry paths for BackInfo (64bit + 32bit) ---
$regPaths = @(
"HKLM:\SOFTWARE\BackInfo",
"HKLM:\SOFTWARE\WOW6432Node\BackInfo"
)
foreach ($path in $regPaths) {
if (-not (Test-Path $path)) {
New-Item -Path $path -Force | Out-Null
}
New-ItemProperty `
-Path $path `
-Name "OSName" `
-Value $finalOSName `
-PropertyType String `
-Force | Out-Null
}
# --- Optional output for logging ---
Write-Output "BackInfo OSName set to: $finalOSName"
$SourceFilePath = "C:\Program Files\BackInfo\BackInfo.exe"
$ShortcutPath = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\BackInfo.lnk"
$WScriptObj = New-Object -ComObject ("WScript.Shell")
$shortcut = $WscriptObj.CreateShortcut($ShortcutPath)
$shortcut.TargetPath = $SourceFilePath
$shortcut.Save()

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

63
cmd/xetup/main.go Normal file
View file

@ -0,0 +1,63 @@
// Command xetup is the GUI launcher for Windows deployment.
//
// All PowerShell scripts and assets are embedded in the binary and extracted
// to a temp directory at runtime. The GUI collects configuration, streams
// live log output while the scripts run, and shows a final summary.
//
// Cross-compile for Windows (requires MinGW):
//
// CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
// GOOS=windows GOARCH=amd64 \
// go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup
package main
import (
"fmt"
"log"
"os"
"path/filepath"
xetupembed "git.xetup.x9.cz/x9/xetup"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/gui"
"git.xetup.x9.cz/x9/xetup/internal/runner"
)
func main() {
// Load config (falls back to defaults when config.json is missing)
cfgPath := config.ConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err)
}
// Temp working directory cleaned up on exit
tmpDir, err := os.MkdirTemp("", "xetup-*")
if err != nil {
log.Fatalf("Cannot create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Extract embedded scripts and assets
if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
log.Fatalf("Failed to extract scripts: %v", err)
}
if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
log.Fatalf("Failed to extract assets: %v", err)
}
// Write runtime config JSON so PowerShell scripts can read it
cfgRuntimePath, err := runner.WriteConfig(cfg, tmpDir)
if err != nil {
log.Fatalf("Failed to write runtime config: %v", err)
}
runCfg := runner.RunConfig{
ScriptsDir: filepath.Join(tmpDir, "scripts"),
ConfigPath: cfgRuntimePath,
LogFile: `C:\Windows\Setup\Scripts\Deploy.log`,
ProfileType: cfg.Deployment.ProfileType,
}
gui.Run(cfg, runCfg, cfgPath)
}

632
config-editor.hta Normal file
View 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, "&quot;");
}
window.onload = function() { init(); };
</script>
</body>
</html>

49
config/config.json Normal file
View 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
}
}

72
docker-compose.yml Normal file
View file

@ -0,0 +1,72 @@
services:
forgejo:
image: codeberg.org/forgejo/forgejo:9
container_name: xetup-forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
# Forgejo config via env
- FORGEJO__server__ROOT_URL=https://git.xetup.x9.cz
- FORGEJO__server__DOMAIN=git.xetup.x9.cz
- FORGEJO__server__SSH_DOMAIN=git.xetup.x9.cz
- FORGEJO__server__SSH_PORT=2222
- FORGEJO__server__LFS_START_SERVER=true
- FORGEJO__database__DB_TYPE=sqlite3
- FORGEJO__service__DISABLE_REGISTRATION=true
- FORGEJO__service__REQUIRE_SIGNIN_VIEW=false
- FORGEJO__ui__DEFAULT_THEME=forgejo-dark
- FORGEJO__repository__DEFAULT_BRANCH=main
- FORGEJO__actions__ENABLED=true
- FORGEJO__indexer__REPO_INDEXER_ENABLED=true
- FORGEJO__cors__ENABLED=true
- FORGEJO__cors__ALLOW_DOMAIN=xetup.x9.cz
- FORGEJO__cors__METHODS=GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
- FORGEJO__cors__HEADERS=Authorization,Content-Type
- FORGEJO__cors__MAX_AGE=10m
volumes:
- forgejo-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3100:3000" # Web UI (behind reverse proxy)
- "2222:22" # Git SSH
networks:
- xetup
runner:
image: code.forgejo.org/forgejo/runner:6.3.1
container_name: xetup-runner
restart: unless-stopped
entrypoint: ["/bin/sh", "-c", "forgejo-runner daemon --config /etc/runner/config.yml"]
user: "0:996" # root:docker - needed for /var/run/docker.sock access
depends_on:
- forgejo
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
- ./runner-config.yml:/etc/runner/config.yml:ro
networks:
- xetup
web:
image: nginx:alpine
container_name: xetup-web
restart: unless-stopped
volumes:
- ./web:/usr/share/nginx/html:ro
- ./web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "3200:80" # Web (xetup.x9.cz via reverse proxy)
networks:
- xetup
volumes:
forgejo-data:
runner-data:
networks:
xetup:
name: xetup

111
docs/xetup-review.md Normal file
View file

@ -0,0 +1,111 @@
# Xetup - Review vysledek
Datum: 15. 4. 2026
## 1. Priprava prostredi
[ ] **Adresar C:\X9 + ikona slozky** (CEKA)
Vytvoreni adresarove struktury C:\X9 se slozkami install, vlastni ikonou a logem.
[ ] **Nextcloud - stahovani souboru** (CEKA)
Stahovani Backinfo, Atera, Flash2, X9-ikona.ico, X9-logo.jpg z Nextcloudu do C:\X9\install.
> nextcloud tam netřeba, Backinfo máme ve vlastní složce
[?] **Admin ucet (adminx9)** (K DISKUSI)
Vytvoreni skryteho lokalniho admin uctu adminx9 pro MSP spravce.
> adminX9 bude bez hesla
[ ] **Aktivace Windows** (CEKA)
Aktivace Windows pomoci klice z configu nebo GVLK (KMS). Spec kolegy toto nezminuje.
## 2. Odstranovani bloatware
[ ] **AppX balicky (UWP aplikace)** (CEKA)
Odebirame ~65 preinstalovanych aplikaci (Solitaire, Xbox, Teams, Copilot, Mail...). Kalkulacka zustava.
[ ] **Windows Capabilities** (CEKA)
Odebirame: Fax, IE, WordPad, PowerShell ISE, Steps Recorder, WMP, Handwriting...
[ ] **Optional Features** (CEKA)
Vypnuti: MediaPlayback, PowerShell 2.0, Recall (AI), SnippingTool.
[?] **Flash2 integrace** (K DISKUSI)
Flash2 je nastroj kolegy pro debloating. Integrovat, nebo pouzit nase kroky 2a-2c?
> flash2 je v podstatě tento setup, minulá verze :-) takže je hlavně pro inspiraci a zahrnutí
[?] **OneDrive - NEMAZAT** (K DISKUSI)
Nas skript agresivne maze OneDrive vcetne instalatoru. Spec kolegy OneDrive neresi = nechat!
> nevím jestli v poslední verzi už to bylo opravené, ale v první verzi se onedrive zabíjel nějakým regeditem nebo scheduled taskem
[?] **RDP/RDS - NEODEBIRAT** (K DISKUSI)
SPEC mel RDP klient k odebirani. Kolega to nechce - RDP musi zustat funkcni.
> nevím jestli v poslední verzi už to bylo opravené, ale v první verzi se rdp problematizovalo nějakým regeditem nebo scheduled taskem
## 3. Instalace software
[ ] **Winget balicky (7-Zip, Adobe, OpenVPN)** (CEKA)
Silent instalace 7-Zip, Adobe Acrobat Reader, OpenVPN Connect pres winget.
[?] **Atera Agent (MSI)** (K DISKUSI)
Silent instalace Atera monitoring agenta z C:\X9\install\atera-agent\.
> použít tohle a nejlíp najít parametr, u kterého ATERA nebude chtí MFA kod z mailu:-)
curl -L -o setup.msi "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337" && msiexec /i setup.msi /qn
[ ] **BackInfo (info na plose)** (CEKA)
BackInfo.exe zobrazi hostname, user, OS, HW, sit uprostred plochy. Konfigurovatelny pres INI.
## 4. Vzhled a personalizace
[ ] **Barvy a motiv** (CEKA)
Tmavy system, svetle aplikace, accent #223B47, plna barva pozadi.
[ ] **Ikona Tento pocitac na plose** (CEKA)
Zobrazit ikonu Tento pocitac na plose.
[?] **Avatar uctu (X9 logo)** (K DISKUSI)
Nastaveni X9-logo.jpg jako profiloveho obrazku admin uctu.
> přidal jsem logo a ico do rootu do složky LOGO
## 5. Hlavni panel a Start menu
[ ] **Taskbar - zarovnani, skryti prvku** (CEKA)
Zarovnani vlevo, skryti Search, Task View, Widgets, Chat, Copilot.
[ ] **System tray - zobrazit vsechny ikony** (CEKA)
EnableAutoTray=0 + mazani icon cache + scheduled task ShowAllTrayIcons.
[ ] **Prazdny taskbar pinlist + Start menu** (CEKA)
Prazdny LayoutModification.xml (zadne pripnute apps), prazdne Start menu pins.
## 6. Systemova nastaveni
[ ] **HKLM registry tweaky** (CEKA)
BypassNRO, vypnuti Teams/Widgets/Copilot/GameDVR/Recall, hesla bez expirace, casova zona.
[ ] **Default Profile (NTUSER.DAT)** (CEKA)
Nastaveni pro vsechny budouci uzivatele: Explorer, Num Lock, GameDVR, Copilot...
[ ] **Napajeni (powercfg)** (CEKA)
Spanek nikdy na siti, obrazovka 60min/15min, spanek baterie 60min.
[ ] **Scheduled tasks** (CEKA)
ShowAllTrayIcons, PDF-DefaultApp, UnlockStartLayout.
## 7. Sit a Edge
[ ] **Proxy - vypnout auto-detect** (CEKA)
Vypnuti automatickeho zjistovani proxy serveru.
[ ] **MS Edge - rozsirene nastaveni** (CEKA)
Striktni tracking protection, Google vyhledavac, panel oblibenych, toolbar tlacitka.
## 8. Finalizace
[ ] **Prejmenování PC** (CEKA)
Rename-Computer na nazev z parametru -ComputerName. Vyzaduje restart.
[ ] **Bootstrap spoustec (irm | iex)** (CEKA)
Jednoradkovy spoustec z webu: irm https://xetup.x9.cz/setup.ps1 | iex

View file

@ -0,0 +1,163 @@
# Xetup Novinky oproti původní specifikaci (W11.pdf v3)
---
## A. Hlavní panel pinnované aplikace (diferenciace admin vs user)
Nový požadavek: různé sady připnutých aplikací podle typu profilu.
**Admin profil připnout na taskbar:**
- Nastavení (`ms-settings:`)
- Správa počítače (`compmgmt.msc`)
- Služby (`services.msc`)
- PowerShell (`pwsh.exe` nebo `powershell.exe`)
- Průzkumník Windows (`explorer.exe`)
- MS Edge (`msedge.exe`)
**User profil připnout na taskbar:**
- Průzkumník Windows (`explorer.exe`)
- MS Edge (`msedge.exe`)
**Implementace:**
Přímé pinnování přes registry/PS je v moderním Win11 neoficiální Microsoft odstranil `Pin-Application` cmdlet. Nejspolehlivější postup je XML layout policy:
```xml
<!-- TaskbarLayoutModification.xml admin varianta -->
<?xml version="1.0" encoding="utf-8"?>
<LayoutModificationTemplate xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout" Version="1">
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
<defaultlayout:TaskbarLayout xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout">
<taskbar:TaskbarPinList>
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\File Explorer.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
</taskbar:TaskbarPinList>
</defaultlayout:TaskbarLayout>
</CustomTaskbarLayoutCollection>
</LayoutModificationTemplate>
```
```powershell
# Aplikovat layout policy (HKLM = platí pro všechny uživatele)
# Admin varianta
$xmlPathAdmin = "C:\X9\TaskbarAdmin.xml"
$xmlPathUser = "C:\X9\TaskbarUser.xml"
# Pro Default Profile (user) zapsat před prvním přihlášením uživatele
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" `
/v "LayoutXMLPath" /t REG_SZ /d $xmlPathUser /f
```
> **Upozornění:** `LayoutXMLPath` policy je dostupná ve Win11 22H2+. Před nasazením ověřit verzi. Shortcuty pro `compmgmt.msc` a `services.msc` je nutné vytvořit ručně jako `.lnk` soubory, protože XML přijímá pouze `.lnk` cesty.
---
## B. Průzkumník Windows nastavení
Tři změny oproti původní specifikaci:
| Nastavení | Hodnota |
|---|---|
| Otevřít Průzkumník pro | Tento počítač (místo Rychlý přístup) |
| Nedávné soubory / složky / Office.com | Vše vypnuto |
| Zobrazit úplnou cestu v záhlaví | Zapnuto |
**Registry (aplikovat do Default Profile hive):**
```registry
; Otevřít pro "Tento počítač" místo Rychlého přístupu
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced
- LaunchTo = 1 (DWORD) ; 1 = Tento počítač, 2 = Rychlý přístup
; Vypnout historii posledních souborů a složek
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
- ShowRecent = 0 (DWORD)
- ShowFrequent = 0 (DWORD)
; Zobrazit úplnou cestu v záhlaví
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState
- FullPath = 1 (DWORD)
```
```powershell
# Blok pro vložení do sekce reg load/unload (Default Profile)
$hive = "HKU\DefaultUser"
Set-ItemProperty "Registry::$hive\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "LaunchTo" -Value 1 -Type DWord
Set-ItemProperty "Registry::$hive\Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowRecent" -Value 0 -Type DWord
Set-ItemProperty "Registry::$hive\Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowFrequent" -Value 0 -Type DWord
New-Item -Path "Registry::$hive\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" `
-Force | Out-Null
Set-ItemProperty "Registry::$hive\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" `
-Name "FullPath" -Value 1 -Type DWord
```
---
## C. Síťové zjišťování zapnout ping, přepnout na privátní síť
Nový krok: po přejmenování/připojení do domény zapnout zjišťování sítě a přepnout profil sítě na privátní (bez toho nefunguje ping na stanici).
```powershell
# Zapnout Network Discovery a File Sharing
netsh advfirewall firewall set rule group="Network Discovery" new enable=Yes
netsh advfirewall firewall set rule group="File and Printer Sharing" new enable=Yes
# Přepnout aktuální síťový profil na Private (= "Ne, chci síť změnit na privátní")
# Funguje pro první aktivní síťové rozhraní
$adapter = Get-NetConnectionProfile | Select-Object -First 1
Set-NetConnectionProfile -InterfaceIndex $adapter.InterfaceIndex -NetworkCategory Private
```
> **Scope:** Toto nastavení se aplikuje na aktuální síťové připojení v době spuštění skriptu nelze předem uložit do Default Profile, protože se váže na konkrétní síťový adaptér/GUID. Skript musí být spuštěn po připojení k síti.
> **Závislost:** Pokud se stanice teprve připojuje do domény, spustit tento blok až po restartu a domain-join.
---
## D. Popis účtu adminx9
Nový krok: nastavit pole "Jméno a příjmení" u lokálního účtu `adminx9` na hodnotu `X9.cz s.r.o.`
Provádí se přes Správu počítače → Místní uživatelé a skupiny → Uživatelé → adminx9 → Vlastnosti → záložka Obecné.
**Automatizace:**
```powershell
# Nastavit Full Name pro lokální účet adminx9
$user = [ADSI]"WinNT://$env:COMPUTERNAME/adminx9,user"
$user.FullName = "X9.cz s.r.o."
$user.SetInfo()
```
> **Scope:** Platí pouze pro aktuální (admin) účet na tomto stroji není součástí Default Profile. Spustit před předáním stanice klientovi.
---
## Integrace do hlavního skriptu
Nové kroky přidat do architektury spuštění:
```
Deploy-Windows.ps1
├── ... (existující kroky)
├── NEW: Popis účtu adminx9 = "X9.cz s.r.o."
├── NEW: Průzkumník registry do Default Profile hive
├── NEW: Taskbar XML layout (admin nebo user varianta dle parametru)
├── 9. Přejmenování PC + restart
└── NEW: Po restartu zapnout síťové zjišťování + privátní profil
```
**Nový parametr skriptu:**
```powershell
[ValidateSet("admin","user")]
[string]$ProfileType = "user" # řídí TaskbarLayout XML i zarovnání panelu
```

View file

@ -0,0 +1,347 @@
# Xetup Specifikace automatizovaného nastavení Windows 10/11
> **Účel:** Eliminovat ~3 hodiny ručního nastavování nových Windows stanic pro klienty X9.cz
> **Cíl:** Jeden PowerShell skript spustitelný z admina, výsledek uložen v Default Profile (aplikuje se všem budoucím uživatelům)
> **Repo:** `C:\x9\xetup`
> **Vstup:** Parametrizovaný spouštěč název PC, doména (volitelná), typ profilu (admin/user)
---
## 1. Struktura adresářů
```
C:\X9\
├── install\
│ ├── Backinfo\
│ ├── flash2\
│ ├── atera-agent\
│ ├── 7zip.exe
│ ├── AdobeReader.exe
│ └── OpenVPNConnect.exe
├── X9-ikona.ico
└── X9-logo.jpg
```
**Kroky:**
- [ ] Vytvořit `C:\X9` a `C:\X9\install`
- [ ] Stáhnout z Nextcloudu: Backinfo, Atera agent, Flash2, ikonu X9, logo X9
- [ ] Stáhnout z internetu: 7-zip, Adobe Reader, OpenVPN Connect
- [ ] Roztřídit soubory do správných adresářů dle výše
> **Automatizace:** `Invoke-WebRequest` pro stažení + `Copy-Item` pro přesun. Nextcloud vyžaduje autentizaci credentials parametrem nebo uloženým tokenem.
---
## 2. Přizpůsobení Pozadí a barvy (Default Profile)
Všechna nastavení ukládat do `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes` + Default Profile hive (`C:\Users\Default\NTUSER.DAT`).
| Nastavení | Hodnota |
|---|---|
| Typ pozadí | Plná barva |
| Barva pozadí | `#223B47` |
| Barevný režim | Vlastní |
| Režim Windows | Tmavý |
| Režim aplikací | Světlý |
| Barva motivu | Ruční `#223B47` |
| Barva v Start menu | Zapnuto |
| Barva v záhlavích oken | Zapnuto |
**Registry klíče (NTUSER.DAT / Default Profile):**
```
HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
- AppsUseLightTheme = 1 (DWORD)
- SystemUsesLightTheme = 0 (DWORD)
- ColorPrevalence = 1 (DWORD)
HKCU\Control Panel\Desktop
- Wallpaper = "" (prázdný string = plná barva)
- WallpaperStyle = 0
HKCU\Control Panel\Colors
- Background = "34 59 71" (RGB hodnota #223B47)
```
> **Poznámka:** Barvu motivu (`AccentColor`) nastavit přes `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent` hodnota `AccentColorMenu` = `0xFF3B2322` (ABGR formát).
---
## 3. Přizpůsobení Motivy (ikony na ploše)
- [ ] Zobrazit ikonu **Tento počítač** na ploše
```registry
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel
- {20D04FE0-3AEA-1069-A2D8-08002B30309D} = 0 (DWORD) ; Tento počítač
```
> Aplikovat do Default Profile hive.
---
## 4. Hlavní panel (Taskbar)
| Nastavení | Hodnota |
|---|---|
| Vyhledávací pole | Skryto |
| Zobrazení úkolů (Task View) | Vypnuto |
| Widgety | Vypnuto |
| Zarovnání (uživatel) | Vlevo |
| Zarovnání (admin) | Na střed |
| Systémové ikony (overflow) | Vše viditelné |
**Registry (Win11):**
```registry
HKCU\Software\Microsoft\Windows\CurrentVersion\Search
- SearchboxTaskbarMode = 0 (DWORD) ; skrýt hledání
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced
- ShowTaskViewButton = 0 (DWORD)
- TaskbarAl = 0 (DWORD) ; 0=vlevo, 1=střed
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\People
- PeopleBand = 0 (DWORD)
HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Feeds
- EnableFeeds = 0 (DWORD) ; widgety
```
**Viditelnost systémových ikon (overflow area):**
```registry
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
- EnableAutoTray = 0 (DWORD) ; zobrazit vše, nevracet do přetečení
```
> **Problém:** Automatické zobrazení VŠECH budoucích ikon (nový SW po instalaci) nelze 100% garantovat přes registry `EnableAutoTray=0` je nejbližší řešení. Individuální ikony aplikací závisí na tom, co si každá aplikace sama zapíše.
---
## 5. Připnutí složky X9 na hlavní panel
- [ ] Složce `C:\X9` nastavit vlastní ikonu (`X9-ikona.ico`)
- [ ] Připnout na panel rychlého spuštění (Quick Access / Taskbar)
```powershell
# Nastavení vlastní ikony složky (desktop.ini)
$iniPath = "C:\X9\desktop.ini"
Set-Content $iniPath "[.ShellClassInfo]`nIconResource=C:\X9\X9-ikona.ico,0`n[ViewState]`nMode=`nVid=`nFolderType=Generic"
attrib +s +h $iniPath
attrib +s "C:\X9"
# Připnutí na taskbar vyžaduje Shell COM objekt nebo workaround přes VBScript
```
> **Upozornění:** Připnutí složky na taskbar je v moderním Win11 omezené Microsoft tuto možnost odstranil. Alternativa: připnout jako Quick Access v Průzkumníku nebo vytvořit shortcut na ploše.
---
## 6. Profil účtu avatar
- [ ] Nahrát `X9-logo.jpg` jako profilový obrázek admin účtu
```powershell
$accountPicPath = "$env:APPDATA\Microsoft\Windows\AccountPictures"
New-Item -ItemType Directory -Force -Path $accountPicPath
Copy-Item "C:\X9\X9-logo.jpg" "$accountPicPath\X9-logo.jpg"
# Nastavit jako výchozí profilový obrázek přes registry
```
> **Scope:** Toto platí jen pro aktuální (admin) účet, nikoliv Default Profile je to záměr.
---
## 7. Napájení
| Nastavení | Hodnota |
|---|---|
| Spánek při napájení ze sítě | Nikdy |
| Vypnutí obrazovky (síť) | 1 hodina |
| Vypnutí obrazovky (baterie) | 15 minut |
| Spánek na baterii | 1 hodina |
| Zavření víka | Neautomatizovat |
```powershell
powercfg /change standby-timeout-ac 0 # nikdy - síť
powercfg /change monitor-timeout-ac 60 # 60 min - síť
powercfg /change monitor-timeout-dc 15 # 15 min - baterie
powercfg /change standby-timeout-dc 60 # 60 min - baterie
```
---
## 8. Síť Proxy server
- [ ] Vypnout automatické zjišťování nastavení proxy
```registry
HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings
- AutoDetect = 0 (DWORD)
HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings
- AutoDetect = 0 (DWORD)
```
---
## 9. Přejmenování počítače
```powershell
param([string]$NewName)
Rename-Computer -NewName $NewName -Force -Restart
```
> Restart je nutný. Připojení do domény probíhá zvlášť přes stejné rozhraní není součástí tohoto skriptu (volitelný parametr `-Domain`).
---
## 10. Odinstalace bloatware
Aktuálně řešeno přes **Flash2** integrovat volání Flash2 instalačky nebo zachovat jako samostatný krok.
Alternativně vlastní seznam přes winget/AppX:
```powershell
# Příklady AppX balíčků k odebrání (rozšířit dle potřeby)
$bloatware = @(
"Microsoft.BingWeather",
"Microsoft.GetHelp",
"Microsoft.Getstarted",
"Microsoft.MicrosoftSolitaireCollection",
"Microsoft.People",
"Microsoft.WindowsFeedbackHub",
"Microsoft.Xbox.TCUI",
"Microsoft.XboxApp",
"Microsoft.ZuneMusic",
"Microsoft.ZuneVideo"
)
foreach ($app in $bloatware) {
Get-AppxPackage -Name $app -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $app | Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
}
```
> `Remove-AppxProvisionedPackage` zajistí, že se bloatware neobjeví ani novým uživatelům (Default Profile).
---
## 11. Nastavení uživatelského profilu (Default Profile)
Klíčový mechanismus: **načíst `C:\Users\Default\NTUSER.DAT` jako dočasný hive**, aplikovat registry změny, odpojit hive.
```powershell
reg load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT"
# ... zde všechny Set-ItemProperty operace s cestou HKU:\DefaultUser\...
reg unload "HKU\DefaultUser"
```
> Tímto způsobem se veškerá nastavení (barvy, taskbar, Edge, atd.) aplikují všem budoucím uživatelům při prvním přihlášení.
---
## 12. Instalace SW
| Aplikace | Zdroj | Metoda |
|---|---|---|
| 7-Zip | Internet / winget | `winget install 7zip.7zip` |
| Adobe Reader | Internet / winget | `winget install Adobe.Acrobat.Reader.64-bit` |
| OpenVPN Connect | Internet / winget | `winget install OpenVPNTechnologies.OpenVPNConnect` |
| Atera Agent | Nextcloud (`C:\X9\install`) | MSI silent install |
| Backinfo | Nextcloud (`C:\X9\install`) | PS skript |
```powershell
winget install --id 7zip.7zip --silent --accept-package-agreements --accept-source-agreements
winget install --id Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
winget install --id OpenVPNTechnologies.OpenVPNConnect --silent --accept-package-agreements --accept-source-agreements
```
---
## 13. Backinfo
```powershell
Copy-Item "C:\X9\install\Backinfo" "C:\Program Files\Backinfo" -Recurse
Set-ExecutionPolicy Unrestricted -Force
& "C:\Program Files\Backinfo\backinfo_W11.ps1"
Set-ExecutionPolicy Restricted -Force
```
> Backinfo zapíše do registru verzi W11 a nastaví autostart po přihlášení.
---
## 14. MS Edge nastavení
Nastavit přes registry (platí pro všechny uživatele pokud aplikováno do Default Profile nebo HKLM).
| Nastavení | Hodnota |
|---|---|
| Ochrana sledování | Striktní (`2`) |
| Panel oblíbených | Vždy zobrazit |
| Výchozí vyhledávač | Google |
| Tlačítka panelu nástrojů | Historie, Aplikace, Stažené soubory, Výkon |
```registry
HKLM\SOFTWARE\Policies\Microsoft\Edge
- TrackingPrevention = 3 (DWORD) ; Striktní
- FavoritesBarEnabled = 1 (DWORD) ; Oblíbené vždy
- DefaultSearchProviderEnabled = 1
- DefaultSearchProviderName = "Google"
- DefaultSearchProviderSearchURL = "https://www.google.com/search?q={searchTerms}"
; Toolbar tlačítka individuální nastavení přes Edge policy nebo NTUSER.DAT
```
> **Poznámka:** Edge nastavení přes GPO/registry mají přednost před uživatelskými preferencemi. Pro Default Profile alternativně upravit `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Preferences` šablonou méně spolehlivé.
---
## 15. Spouštěč parametry hlavního skriptu
```powershell
# Deploy-Windows.ps1
param(
[Parameter(Mandatory=$true)]
[string]$ComputerName,
[string]$Domain = "", # prázdné = pracovní skupina
[ValidateSet("admin","user")]
[string]$TaskbarAlign = "user", # admin=střed, user=vlevo
[string]$NextcloudUrl = "",
[string]$NextcloudUser = "",
[string]$NextcloudPass = ""
)
```
---
## Otevřené otázky / TODO
- [ ] Jak řešit stahování z Nextcloudu bez interaktivního přihlášení? (token vs. credentials parametr)
- [ ] Flash2 integrovat nebo volat jako subprocess?
- [ ] Atera Agent silent install parametry MSI?
- [ ] Přejmenování PC + připojení do domény jako jeden průchod nebo dva samostatné kroky?
- [ ] Testování: Win10 vs Win11 některé registry klíče se liší (hlavně Taskbar)
- [ ] Složka X9 na Taskbaru Win11 nepodporuje připnutí složky; nahradit shortcutem na ploše?
---
## Architektura spuštění (navržená)
```
irm https://xetup.x9.cz/setup.ps1 | iex
└── Stáhne Deploy-Windows.ps1 z repa
└── Vyzve na parametry (nebo převezme z CLI)
└── Spustí Deploy-Windows.ps1 -ExecutionPolicy Bypass
├── 1. Vytvoří adresáře
├── 2. Stáhne soubory (Nextcloud + web)
├── 3. Odinstaluje bloatware (AppX + Flash2)
├── 4. Nainstaluje SW (winget)
├── 5. Aplikuje registry do Default Profile (reg load/unload)
├── 6. Nastaví napájení
├── 7. Nastaví proxy
├── 8. Spustí Backinfo
├── 9. Přejmenuje PC
└── 10. Restart
```

16
embed.go Normal file
View file

@ -0,0 +1,16 @@
// Package xetup exposes embedded PowerShell scripts and assets for use by
// cmd/xetup. Placing the embed declarations here (at the module root) gives
// the //go:embed directives a clear, stable relative path to the content.
package xetup
import "embed"
// Scripts holds all PowerShell scripts from the scripts/ directory.
//
//go:embed scripts
var Scripts embed.FS
// Assets holds all deployment assets from the assets/ directory.
//
//go:embed assets
var Assets embed.FS

40
go.mod Normal file
View file

@ -0,0 +1,40 @@
module git.xetup.x9.cz/x9/xetup
go 1.24.0
require fyne.io/fyne/v2 v2.7.3
require (
fyne.io/systray v1.12.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.3.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

80
go.sum Normal file
View file

@ -0,0 +1,80 @@
fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE=
fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc=
github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

145
internal/config/config.go Normal file
View file

@ -0,0 +1,145 @@
package config
import (
"encoding/json"
"os"
"path/filepath"
)
// Config mirrors config.json structure.
type Config struct {
Deployment Deployment `json:"deployment"`
AdminAccount AdminAccount `json:"adminAccount"`
Activation Activation `json:"activation"`
Software Software `json:"software"`
Steps map[string]bool `json:"steps"`
Features Features `json:"features"`
}
type Deployment struct {
PCName string `json:"pcName"`
PCDescription string `json:"pcDescription"`
Timezone string `json:"timezone"`
ProfileType string `json:"profileType"` // default | admin | user
}
type AdminAccount struct {
Username string `json:"username"`
}
type Activation struct {
ProductKey string `json:"productKey"`
KMSServer string `json:"kmsServer"`
}
type SoftwareItem struct {
Name string `json:"name"`
WingetID string `json:"wingetId"`
}
type Software struct {
Install []SoftwareItem `json:"install"`
}
// Features holds per-step, per-feature toggle flags.
// Keys: stepID -> featureID -> enabled.
// A missing key defaults to true (feature enabled).
type Features map[string]map[string]bool
// DefaultConfig returns a config with sensible defaults.
func DefaultConfig() Config {
return Config{
Deployment: Deployment{
Timezone: "Central Europe Standard Time",
ProfileType: "default",
},
AdminAccount: AdminAccount{
Username: "adminx9",
},
Activation: Activation{
ProductKey: "",
},
Software: Software{
Install: []SoftwareItem{
{Name: "7-Zip", WingetID: "7zip.7zip"},
{Name: "Adobe Acrobat Reader 64-bit", WingetID: "Adobe.Acrobat.Reader.64-bit"},
{Name: "OpenVPN Connect", WingetID: "OpenVPNTechnologies.OpenVPNConnect"},
},
},
Steps: map[string]bool{
"adminAccount": true,
"bloatware": true,
"software": true,
"systemRegistry": true,
"defaultProfile": true,
"personalization": true,
"scheduledTasks": true,
"backinfo": true,
"activation": true,
"dellUpdate": true,
"network": true,
"pcIdentity": true,
},
Features: Features{
"software": {
"wingetInstalls": true,
"pdfDefault": true,
"ateraAgent": true,
},
"systemRegistry": {
"systemTweaks": true,
"edgePolicies": true,
"oneDriveUninstall": true,
"powercfg": true,
"proxyDisable": true,
},
"defaultProfile": {
"taskbarTweaks": true,
"startMenuTweaks": true,
"explorerTweaks": true,
},
"dellUpdate": {
"drivers": true,
"bios": true,
},
},
}
}
// Load reads config.json from the given path.
// If the file does not exist, returns DefaultConfig without error.
func Load(path string) (Config, error) {
cfg := DefaultConfig()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return cfg, nil
}
if err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
// Save writes config to the given path (creates directories if needed).
func Save(cfg Config, path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// ConfigPath returns the default config.json path (next to the executable).
func ConfigPath() string {
exe, err := os.Executable()
if err != nil {
return "config.json"
}
return filepath.Join(filepath.Dir(exe), "config.json")
}

363
internal/gui/gui.go Normal file
View file

@ -0,0 +1,363 @@
// Package gui implements the Fyne-based graphical interface for xetup.
//
// Three phases, one window:
// 1. Config form PC name, product key, profile, step selection,
// load/save config buttons for per-client presets
// 2. Live run real-time log streamed from PowerShell scripts
// 3. Summary per-step OK / ERROR / SKIPPED with elapsed time
package gui
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/runner"
)
// Run opens the xetup window and blocks until the user closes it.
// cfgPath is the default config.json path (next to the exe).
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
// Force software (CPU/GDI) rendering so the app works on VMs and machines
// without proper OpenGL support (VMware SVGA, Hyper-V basic display, etc.).
// The UI is simple enough that GPU acceleration gives no benefit.
os.Setenv("FYNE_RENDERER", "software") //nolint:errcheck
a := app.New()
a.Settings().SetTheme(theme.DarkTheme())
w := a.NewWindow("xetup — Windows deployment")
w.Resize(fyne.NewSize(740, 680))
w.SetMaster() // closing this window quits the app
showForm(w, cfg, runCfg, cfgPath)
w.ShowAndRun()
}
// --------------------------------------------------------------------------
// Phase 1 Config form
// --------------------------------------------------------------------------
func showForm(w fyne.Window, cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
// ── Text inputs ─────────────────────────────────────────────────────────
pcName := widget.NewEntry()
pcName.SetPlaceHolder("napr. NB-KLIENT-01 (prazdne = neprejmenovat)")
pcName.SetText(cfg.Deployment.PCName)
pcDesc := widget.NewEntry()
pcDesc.SetPlaceHolder("napr. PC recepce")
pcDesc.SetText(cfg.Deployment.PCDescription)
productKey := widget.NewEntry()
productKey.SetPlaceHolder("prazdne = OA3 / GVLK fallback")
productKey.SetText(cfg.Activation.ProductKey)
profileSel := widget.NewSelect([]string{"default", "admin", "user"}, nil)
profileSel.SetSelected(cfg.Deployment.ProfileType)
// ── Step checkboxes ─────────────────────────────────────────────────────
items := runner.AllSelectableItems()
checks := make([]*widget.Check, len(items))
checkObjs := make([]fyne.CanvasObject, len(items))
for i, item := range items {
c := widget.NewCheck(item.Label, nil)
c.SetChecked(itemEnabled(cfg, item))
checks[i] = c
checkObjs[i] = c
}
stepsScroll := container.NewVScroll(container.NewVBox(checkObjs...))
stepsScroll.SetMinSize(fyne.NewSize(0, 290))
// ── collectCfg reads current form state into a Config ───────────────────
collectCfg := func() config.Config {
out := cfg // start from loaded config (preserves fields not shown in form)
out.Deployment.PCName = pcName.Text
out.Deployment.PCDescription = pcDesc.Text
out.Activation.ProductKey = productKey.Text
out.Deployment.ProfileType = profileSel.Selected
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checks[i].Checked
}
_, features := buildStepsAndFeatures(selected)
out.Features = features
return out
}
// ── Toolbar: Load / Save config ─────────────────────────────────────────
jsonFilter := storage.NewExtensionFileFilter([]string{".json"})
loadBtn := widget.NewButton("Nacist config...", func() {
d := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
if err != nil || rc == nil {
return
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
dialog.ShowError(err, w)
return
}
newCfg := config.DefaultConfig()
if err := json.Unmarshal(data, &newCfg); err != nil {
dialog.ShowError(err, w)
return
}
// Reload the entire form with the new config
showForm(w, newCfg, runCfg, rc.URI().Path())
}, w)
d.SetFilter(jsonFilter)
d.Show()
})
saveBtn := widget.NewButton("Ulozit config...", func() {
d := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
if err != nil || wc == nil {
return
}
defer wc.Close()
data, err := json.MarshalIndent(collectCfg(), "", " ")
if err != nil {
dialog.ShowError(err, w)
return
}
if _, err := wc.Write(data); err != nil {
dialog.ShowError(err, w)
}
}, w)
d.SetFilter(jsonFilter)
d.SetFileName("config.json")
d.Show()
})
// ── SPUSTIT ─────────────────────────────────────────────────────────────
startBtn := widget.NewButton(" SPUSTIT ", func() {
finalCfg := collectCfg()
runCfg.ProfileType = finalCfg.Deployment.ProfileType
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checks[i].Checked
}
steps, features := buildStepsAndFeatures(selected)
finalCfg.Features = features
_ = config.Save(finalCfg, cfgPath) // auto-save to default path
showRun(w, runCfg, steps)
})
startBtn.Importance = widget.HighImportance
// ── Layout ───────────────────────────────────────────────────────────────
form := widget.NewForm(
widget.NewFormItem("PC jmeno", pcName),
widget.NewFormItem("Popis PC", pcDesc),
widget.NewFormItem("Product Key", productKey),
widget.NewFormItem("Profil", profileSel),
)
toolbar := container.NewHBox(loadBtn, saveBtn)
w.SetContent(container.NewBorder(
form,
container.NewVBox(
widget.NewSeparator(),
container.NewBorder(nil, nil, toolbar, container.NewCenter(startBtn)),
),
nil, nil,
container.NewVBox(
widget.NewSeparator(),
widget.NewLabel("Kroky a nastaveni (odskrtnete co nechcete spustit):"),
stepsScroll,
),
))
}
// --------------------------------------------------------------------------
// Phase 2 Live run view
// --------------------------------------------------------------------------
func showRun(w fyne.Window, runCfg runner.RunConfig, steps []runner.Step) {
statusLabel := widget.NewLabel("Spoustim...")
// Virtualised list efficient for thousands of log lines
var (
mu sync.Mutex
logLines []string
)
logList := widget.NewList(
func() int {
mu.Lock()
defer mu.Unlock()
return len(logLines)
},
func() fyne.CanvasObject {
return widget.NewLabel("")
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
mu.Lock()
defer mu.Unlock()
if id < len(logLines) {
obj.(*widget.Label).SetText(logLines[id])
}
},
)
var cancelFn context.CancelFunc
stopBtn := widget.NewButton(" ZASTAVIT ", func() {
if cancelFn != nil {
cancelFn()
}
})
stopBtn.Importance = widget.DangerImportance
w.SetContent(container.NewBorder(
container.NewVBox(statusLabel, widget.NewSeparator()),
container.NewCenter(container.NewPadded(stopBtn)),
nil, nil,
logList,
))
ctx, cancel := context.WithCancel(context.Background())
cancelFn = cancel
r := runner.New(
runCfg,
func(l runner.LogLine) {
mu.Lock()
logLines = append(logLines, l.Text)
mu.Unlock()
logList.Refresh()
logList.ScrollToBottom()
},
func(res runner.Result) {
statusLabel.SetText(fmt.Sprintf(
"Krok %s %s: %s", res.Step.Num, res.Step.Name, res.Status,
))
},
)
go func() {
results := r.Run(ctx, steps)
showDone(w, results)
}()
}
// --------------------------------------------------------------------------
// Phase 3 Summary
// --------------------------------------------------------------------------
func showDone(w fyne.Window, results []runner.Result) {
ok, errs, skipped := 0, 0, 0
rows := make([]fyne.CanvasObject, 0, len(results))
for _, res := range results {
var icon string
switch res.Status {
case "OK":
ok++
icon = "OK "
case "ERROR":
errs++
icon = "ERR "
default:
skipped++
icon = " "
}
text := icon + res.Step.Num + " " + res.Step.Name
if res.Elapsed > 0 {
text += fmt.Sprintf(" (%s)", res.Elapsed.Round(time.Second))
}
rows = append(rows, widget.NewLabel(text))
}
summary := widget.NewLabel(fmt.Sprintf(
"OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped,
))
summary.TextStyle = fyne.TextStyle{Bold: true}
closeBtn := widget.NewButton(" ZAVRIT ", func() {
fyne.CurrentApp().Quit()
})
w.SetContent(container.NewBorder(
widget.NewLabelWithStyle("Hotovo", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
container.NewVBox(
widget.NewSeparator(),
container.NewCenter(summary),
container.NewCenter(container.NewPadded(closeBtn)),
),
nil, nil,
container.NewVScroll(container.NewVBox(rows...)),
))
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
// itemEnabled returns the initial checked state for a checkbox row,
// reading from the loaded config (defaults to true / enabled when absent).
func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
if item.FeatureID == "" {
if v, ok := cfg.Steps[item.StepID]; ok {
return v
}
return true
}
if feats, ok := cfg.Features[item.StepID]; ok {
if v, ok2 := feats[item.FeatureID]; ok2 {
return v
}
}
return true
}
// buildStepsAndFeatures converts the flat checkbox map into the structures
// that runner.Runner and config.Config expect.
func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
items := runner.AllSelectableItems()
features := make(config.Features)
stepOn := make(map[string]bool)
for _, item := range items {
if item.FeatureID == "" {
// Simple step: enabled iff its own checkbox is checked
stepOn[item.StepID] = selected[item.Key]
} else {
// Feature checkbox: at least one checked feature enables the step
if features[item.StepID] == nil {
features[item.StepID] = make(map[string]bool)
}
features[item.StepID][item.FeatureID] = selected[item.Key]
if selected[item.Key] {
stepOn[item.StepID] = true
}
}
}
allSteps := runner.AllSteps()
steps := make([]runner.Step, len(allSteps))
for i, s := range allSteps {
s.Enabled = stepOn[s.ID]
steps[i] = s
}
return steps, features
}

336
internal/runner/runner.go Normal file
View file

@ -0,0 +1,336 @@
// Package runner executes PowerShell deployment scripts and streams log output.
package runner
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
)
// Step describes a single deployment step.
type Step struct {
ID string // e.g. "adminAccount"
Num string // display number e.g. "00"
Name string
ScriptName string // e.g. "00-admin-account.ps1"
Enabled bool
}
// AllSteps returns the ordered list of deployment steps.
func AllSteps() []Step {
return []Step{
{ID: "adminAccount", Num: "00", Name: "Admin ucet", ScriptName: "00-admin-account.ps1"},
{ID: "bloatware", Num: "01", Name: "Bloatware removal", ScriptName: "01-bloatware.ps1"},
{ID: "software", Num: "02", Name: "Software (winget)", ScriptName: "02-software.ps1"},
{ID: "systemRegistry", Num: "03", Name: "System Registry (HKLM)", ScriptName: "03-system-registry.ps1"},
{ID: "defaultProfile", Num: "04", Name: "Default Profile", ScriptName: "04-default-profile.ps1"},
{ID: "personalization", Num: "05", Name: "Personalizace", ScriptName: "05-personalization.ps1"},
{ID: "scheduledTasks", Num: "06", Name: "Scheduled Tasks", ScriptName: "06-scheduled-tasks.ps1"},
{ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"},
{ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"},
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
{ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.ps1"},
{ID: "pcIdentity", Num: "10", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
}
}
// Feature is a single toggleable sub-item within a deployment step.
type Feature struct {
ID string
Label string
}
// StepFeatures returns per-step feature lists. Steps absent from this map
// have no sub-features and are controlled at the step level only.
func StepFeatures() map[string][]Feature {
return map[string][]Feature{
"software": {
{ID: "wingetInstalls", Label: "Instalace SW ze seznamu (winget)"},
{ID: "pdfDefault", Label: "Adobe Reader jako vychozi PDF"},
{ID: "ateraAgent", Label: "Atera RMM agent"},
},
"systemRegistry": {
{ID: "systemTweaks", Label: "Windows tweaky (Widgets, GameDVR, Recall...)"},
{ID: "edgePolicies", Label: "Edge policies (tlacitka, vyhledavac, telemetrie)"},
{ID: "oneDriveUninstall", Label: "OneDrive uninstall (consumer pre-install)"},
{ID: "powercfg", Label: "Nastaveni napajeni (timeout AC/DC)"},
{ID: "proxyDisable", Label: "Zakaz WPAD proxy auto-detect"},
},
"defaultProfile": {
{ID: "taskbarTweaks", Label: "Taskbar zarovnani, tlacitka, layout XML"},
{ID: "startMenuTweaks", Label: "Start menu cisteni pinu, Bing, Copilot"},
{ID: "explorerTweaks", Label: "Explorer pripony, LaunchTo, ShowRecent"},
},
"dellUpdate": {
{ID: "drivers", Label: "Dell drivery + firmware"},
{ID: "bios", Label: "Dell BIOS update"},
},
}
}
// SelectableItem is a single toggleable row in the TUI checklist.
// It represents either a whole step (FeatureID == "") or a specific feature.
type SelectableItem struct {
Key string // "stepID" or "stepID.featureID"
StepID string
FeatureID string // empty for step-level items
Label string
Num string
}
// AllSelectableItems returns the flat ordered list of all TUI toggle rows.
// Steps with features are expanded to individual feature rows.
// Steps without features appear as a single step-level row.
func AllSelectableItems() []SelectableItem {
steps := AllSteps()
features := StepFeatures()
var items []SelectableItem
for _, s := range steps {
feats, hasFeatures := features[s.ID]
if !hasFeatures {
items = append(items, SelectableItem{
Key: s.ID,
StepID: s.ID,
Label: s.Num + " " + s.Name,
Num: s.Num,
})
} else {
for _, f := range feats {
items = append(items, SelectableItem{
Key: s.ID + "." + f.ID,
StepID: s.ID,
FeatureID: f.ID,
Label: s.Num + " " + f.Label,
Num: s.Num,
})
}
}
}
return items
}
// RunConfig holds runtime parameters passed to each script.
type RunConfig struct {
ScriptsDir string
ConfigPath string
LogFile string
ProfileType string
}
// Result is the outcome of a single step.
type Result struct {
Step Step
Status string // "OK", "ERROR", "SKIPPED"
Elapsed time.Duration
}
// LogLine is a single output line from a running script.
type LogLine struct {
StepID string
Text string
Level string // INFO, OK, ERROR, WARN, STEP - parsed from [LEVEL] prefix
}
// Runner executes deployment steps sequentially.
type Runner struct {
cfg RunConfig
onLog func(LogLine)
onResult func(Result)
cancel context.CancelFunc
}
// New creates a Runner. onLog is called for each output line, onResult after each step.
func New(cfg RunConfig, onLog func(LogLine), onResult func(Result)) *Runner {
return &Runner{cfg: cfg, onLog: onLog, onResult: onResult}
}
// Run executes enabled steps sequentially. Blocks until done or context cancelled.
func (r *Runner) Run(ctx context.Context, steps []Step) []Result {
ctx, cancel := context.WithCancel(ctx)
r.cancel = cancel
defer cancel()
// Write config JSON to temp file so scripts can read it
cfgArg := r.cfg.ConfigPath
var results []Result
for _, step := range steps {
if !step.Enabled {
res := Result{Step: step, Status: "SKIPPED"}
r.onResult(res)
results = append(results, res)
continue
}
start := time.Now()
err := r.runScript(ctx, step, cfgArg)
elapsed := time.Since(start)
status := "OK"
if err != nil {
if ctx.Err() != nil {
status = "CANCELLED"
} else {
status = "ERROR"
}
}
res := Result{Step: step, Status: status, Elapsed: elapsed}
r.onResult(res)
results = append(results, res)
if ctx.Err() != nil {
break
}
}
return results
}
// Stop cancels the running deployment.
func (r *Runner) Stop() {
if r.cancel != nil {
r.cancel()
}
}
func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error {
scriptPath := filepath.Join(r.cfg.ScriptsDir, step.ScriptName)
// Build argument list
args := []string{
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", scriptPath,
"-LogFile", r.cfg.LogFile,
}
// Pass config object as JSON string (script reads it inline)
if cfgArg != "" {
args = append(args, "-Config", fmt.Sprintf("(Get-Content '%s' | ConvertFrom-Json)", cfgArg))
}
// ProfileType for step 04
if step.ID == "defaultProfile" && r.cfg.ProfileType != "" {
args = append(args, "-ProfileType", r.cfg.ProfileType)
}
cmd := exec.CommandContext(ctx, "powershell.exe", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout // merge stderr into stdout
if err := cmd.Start(); err != nil {
return err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
r.onLog(LogLine{
StepID: step.ID,
Text: line,
Level: parseLevel(line),
})
}
return cmd.Wait()
}
// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
func parseLevel(line string) string {
if strings.Contains(line, "] [OK]") {
return "OK"
}
if strings.Contains(line, "] [ERROR]") {
return "ERROR"
}
if strings.Contains(line, "] [WARN]") {
return "WARN"
}
if strings.Contains(line, "] [STEP]") {
return "STEP"
}
return "INFO"
}
// ExtractScripts unpacks embedded scripts to a temp directory.
// Returns the directory path. Caller is responsible for cleanup.
func ExtractScripts(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
entries, err := fs.ReadDir("scripts")
if err != nil {
return fmt.Errorf("read embedded scripts: %w", err)
}
scriptsDir := filepath.Join(tmpDir, "scripts")
if err := os.MkdirAll(scriptsDir, 0755); err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue
}
// embed.FS always uses forward slashes regardless of OS
data, err := fs.ReadFile(path.Join("scripts", e.Name()))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(scriptsDir, e.Name()), data, 0644); err != nil {
return err
}
}
return nil
}
// ExtractAssets unpacks embedded assets to tmpDir/assets.
func ExtractAssets(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
return extractDir(fs, "assets", tmpDir)
}
func extractDir(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, src, dstBase string) error {
entries, err := fs.ReadDir(src)
if err != nil {
return err
}
dst := filepath.Join(dstBase, filepath.FromSlash(src))
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
for _, e := range entries {
// embed.FS always uses forward slashes regardless of OS
srcPath := path.Join(src, e.Name())
dstPath := filepath.Join(dstBase, filepath.FromSlash(srcPath))
if e.IsDir() {
if err := extractDir(fs, srcPath, dstBase); err != nil {
return err
}
continue
}
data, err := fs.ReadFile(srcPath)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
}
return nil
}
// WriteConfig serialises cfg to a temp JSON file and returns its path.
func WriteConfig(cfg interface{}, tmpDir string) (string, error) {
path := filepath.Join(tmpDir, "config-runtime.json")
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return "", err
}
return path, os.WriteFile(path, data, 0644)
}

834
review.html Normal file
View file

@ -0,0 +1,834 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xetup - Review v2</title>
<style>
:root {
--bg: #0f1117;
--card: #1a1d27;
--border: #2a2d3a;
--text: #e0e0e0;
--muted: #888;
--accent: #223B47;
--green: #2ea043;
--green-bg: rgba(46,160,67,.12);
--red: #da3633;
--red-bg: rgba(218,54,51,.12);
--yellow: #d29922;
--yellow-bg: rgba(210,153,34,.12);
--blue: #58a6ff;
--blue-bg: rgba(88,166,255,.12);
--purple: #a371f7;
--purple-bg: rgba(163,113,247,.12);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
padding: 2rem;
max-width: 960px;
margin: 0 auto;
line-height: 1.5;
}
h1 { font-size: 1.6rem; margin-bottom: .3rem; color: #fff; }
.subtitle { color: var(--muted); margin-bottom: 1.5rem; font-size: .9rem; }
.stats {
display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap;
}
.stat {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: .6rem 1rem; font-size: .85rem; min-width: 100px; text-align: center;
}
.stat .num { font-size: 1.4rem; font-weight: 700; display: block; }
.stat.s-pending .num { color: var(--muted); }
.stat.s-approved .num { color: var(--green); }
.stat.s-rejected .num { color: var(--red); }
.stat.s-discuss .num { color: var(--yellow); }
.group { margin-bottom: 2rem; }
.group-header {
display: flex; align-items: center; gap: .6rem;
margin-bottom: .7rem; padding-bottom: .4rem; border-bottom: 1px solid var(--border);
}
.group-icon {
font-size: 1rem; width: 26px; height: 26px;
display: flex; align-items: center; justify-content: center;
border-radius: 6px; background: rgba(255,255,255,.05); font-weight: 700;
}
.group-title { font-size: 1.05rem; font-weight: 600; color: #fff; }
.group-count { color: var(--muted); font-size: .85rem; }
.step {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
margin-bottom: .6rem; overflow: hidden; transition: border-color .2s;
}
.step.status-approved { border-left: 3px solid var(--green); }
.step.status-rejected { border-left: 3px solid var(--red); }
.step.status-discuss { border-left: 3px solid var(--yellow); }
.step-header {
display: flex; align-items: center; padding: .7rem 1rem;
cursor: pointer; gap: .6rem; user-select: none;
}
.step-header:hover { background: rgba(255,255,255,.03); }
.step-title { font-weight: 600; flex: 1; font-size: .92rem; }
.step-source {
font-size: .65rem; padding: .12rem .4rem; border-radius: 3px; white-space: nowrap;
}
.source-impl { background: var(--blue-bg); color: var(--blue); }
.source-spec { background: var(--yellow-bg); color: var(--yellow); }
.source-new { background: var(--purple-bg); color: var(--purple); }
.source-both { background: var(--green-bg); color: var(--green); }
.source-problem { background: var(--red-bg); color: var(--red); }
.source-resolved { background: rgba(255,255,255,.06); color: var(--muted); }
.status-badge {
font-size: .65rem; padding: .1rem .35rem; border-radius: 3px; font-weight: 600; display: none;
}
.step.status-approved .status-badge { display: inline; background: var(--green-bg); color: var(--green); }
.step.status-rejected .status-badge { display: inline; background: var(--red-bg); color: var(--red); }
.step.status-discuss .status-badge { display: inline; background: var(--yellow-bg); color: var(--yellow); }
.chevron {
color: var(--muted); transition: transform .2s; font-size: .75rem;
}
.step.open .chevron { transform: rotate(90deg); }
.step-body {
display: none; padding: 0 1rem 1rem 1rem; font-size: .86rem;
}
.step.open .step-body { display: block; }
.step-body p { margin-bottom: .4rem; }
.detail-label {
color: var(--muted); font-size: .75rem; text-transform: uppercase;
letter-spacing: .05em; margin-top: .7rem; margin-bottom: .25rem;
}
.step-body ul { margin-left: 1.2rem; margin-bottom: .4rem; }
.step-body li { margin-bottom: .2rem; }
.step-body code {
background: rgba(255,255,255,.08); padding: .08rem .3rem; border-radius: 3px;
font-size: .8rem; font-family: 'SF Mono', 'Fira Code', monospace;
}
.step-body pre {
background: rgba(0,0,0,.3); padding: .5rem .7rem; border-radius: 6px;
overflow-x: auto; font-size: .78rem; margin: .4rem 0;
font-family: 'SF Mono', 'Fira Code', monospace; line-height: 1.4;
}
.issue {
background: var(--red-bg); border: 1px solid rgba(218,54,51,.3);
border-radius: 6px; padding: .45rem .65rem; margin: .4rem 0; font-size: .83rem;
}
.issue strong { color: var(--red); }
.note {
background: var(--blue-bg); border: 1px solid rgba(88,166,255,.3);
border-radius: 6px; padding: .45rem .65rem; margin: .4rem 0; font-size: .83rem;
}
.note strong { color: var(--blue); }
.resolved {
background: rgba(255,255,255,.04); border: 1px solid var(--border);
border-radius: 6px; padding: .45rem .65rem; margin: .4rem 0; font-size: .83rem;
}
.resolved strong { color: var(--green); }
.prev-comment {
background: var(--yellow-bg); border: 1px solid rgba(210,153,34,.3);
border-radius: 6px; padding: .45rem .65rem; margin: .4rem 0; font-size: .83rem;
}
.prev-comment strong { color: var(--yellow); }
.actions { display: flex; gap: .4rem; margin-top: .7rem; flex-wrap: wrap; }
.btn {
padding: .3rem .65rem; border: 1px solid var(--border); border-radius: 6px;
background: transparent; color: var(--text); cursor: pointer;
font-size: .78rem; transition: all .15s;
}
.btn:hover { background: rgba(255,255,255,.06); }
.btn-approve { border-color: var(--green); color: var(--green); }
.btn-approve:hover, .btn-approve.active { background: var(--green-bg); }
.btn-reject { border-color: var(--red); color: var(--red); }
.btn-reject:hover, .btn-reject.active { background: var(--red-bg); }
.btn-discuss { border-color: var(--yellow); color: var(--yellow); }
.btn-discuss:hover, .btn-discuss.active { background: var(--yellow-bg); }
.comment-area { margin-top: .5rem; display: none; }
.comment-area.visible { display: block; }
.comment-area textarea {
width: 100%; background: rgba(0,0,0,.3); border: 1px solid var(--border);
border-radius: 6px; color: var(--text); padding: .45rem; font-size: .83rem;
font-family: inherit; resize: vertical; min-height: 55px;
}
.comment-area textarea:focus { outline: none; border-color: var(--blue); }
.export-bar {
position: sticky; bottom: 0; background: var(--card);
border: 1px solid var(--border); border-radius: 10px;
padding: .7rem 1rem; margin-top: 1.5rem;
display: flex; align-items: center; gap: .8rem;
justify-content: space-between; flex-wrap: wrap; z-index: 10;
}
.btn-export {
padding: .45rem 1rem; background: var(--accent); color: #fff; border: none;
border-radius: 6px; cursor: pointer; font-size: .85rem; font-weight: 600;
}
.btn-export:hover { opacity: .85; }
.btn-minor {
padding: .35rem .7rem; background: transparent; border: 1px solid var(--border);
color: var(--muted); border-radius: 6px; cursor: pointer; font-size: .78rem;
}
.btn-minor:hover { color: var(--text); border-color: var(--text); }
@media (max-width: 600px) {
body { padding: 1rem; }
.stats { gap: .5rem; }
.stat { min-width: 70px; padding: .4rem .5rem; }
}
</style>
</head>
<body>
<h1>Xetup - Windows Deployment Review v2</h1>
<p class="subtitle">Vcetne novinek od kolegy + zapracovane komentare z review v1. Schval / zamitni / okomentuj.</p>
<div class="stats">
<div class="stat s-pending"><span class="num" id="cnt-pending">0</span>Ceka</div>
<div class="stat s-approved"><span class="num" id="cnt-approved">0</span>OK</div>
<div class="stat s-rejected"><span class="num" id="cnt-rejected">0</span>Ne</div>
<div class="stat s-discuss"><span class="num" id="cnt-discuss">0</span>Diskuse</div>
</div>
<div id="app"></div>
<div class="export-bar">
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="btn-minor" onclick="toggleAll()">Rozbalit/sbalit vse</button>
<button class="btn-minor" onclick="expandDetails()">Otevrit detaily</button>
<button class="btn-minor" onclick="clearAll()">Reset vsech hlasu</button>
</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="btn-export" onclick="copyToClipboard()">Kopirovat Markdown</button>
<button class="btn-export" onclick="exportMarkdown()" style="background:#333;border:1px solid var(--border)">Stahnout .md</button>
</div>
</div>
<script>
const groups = [
{
id: "prep",
icon: "1",
title: "Priprava prostredi",
steps: [
{
id: "v2-dirs",
title: "Adresar C:\\X9 + ikona slozky",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Vytvoreni C:\\X9, kopirovani assets (BackInfo, Logo, ikona). Vlastni ikona slozky pres desktop.ini.",
detail: `<p class="detail-label">Struktura</p>
<pre>C:\\X9\\
+-- install\\ (staging pro Atera MSI atd.)
+-- X9-ikona.ico (ikona slozky)
+-- X9-logo.jpg (avatar uctu)</pre>
<p class="detail-label">Ikona slozky</p>
<pre>[.ShellClassInfo]
IconResource=C:\\X9\\X9-ikona.ico,0
attrib +s +h "C:\\X9\\desktop.ini"
attrib +s "C:\\X9"</pre>
<div class="resolved"><strong>Z review v1:</strong> Nextcloud neni potreba - Backinfo mame v assets/ repa, logo taky. Stahovat se bude jen Atera MSI (viz krok 3b).</div>
<div class="note"><strong>Pozn:</strong> Pripnuti slozky na taskbar Win11 nepodporuje. Shortcut na plose nebo Quick Access.</div>`
},
{
id: "v2-admin",
title: "Admin ucet (adminx9)",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Vytvoreni skryteho admin uctu adminx9 BEZ HESLA. FullName = \"X9.cz s.r.o.\"",
detail: `<p class="detail-label">Co delame</p>
<ul>
<li>Vytvoreni <code>adminx9</code></li>
<li><strong>Bez hesla</strong> (zmena oproti v1, kde bylo heslo z config.json)</li>
<li>Pridani do Administrators</li>
<li>Heslo nevyprsi, uzivatel nesmi menit</li>
<li>Skryti z login screen (SpecialAccounts\\UserList = 0)</li>
</ul>
<div class="prev-comment"><strong>Review v1:</strong> "adminX9 bude bez hesla"</div>
<p class="detail-label">NOVINKA: Popis uctu</p>
<pre>\$user = [ADSI]"WinNT://\$env:COMPUTERNAME/adminx9,user"
\$user.FullName = "X9.cz s.r.o."
\$user.SetInfo()</pre>
<p>Nastavi pole "Jmeno a prijmeni" na <code>X9.cz s.r.o.</code> - viditelne ve Sprave pocitace.</p>
<p class="detail-label">Skript</p>
<p><code>scripts/00-admin-account.ps1</code> (upravit: odstranit heslo, pridat FullName)</p>`
},
{
id: "v2-activation",
title: "Aktivace Windows",
source: "impl", sourceLabel: "Nase implementace",
summary: "Aktivace Windows pomoci klice z configu nebo GVLK (KMS). Spec kolegy toto nezminuje.",
detail: `<p class="detail-label">Co delame</p>
<ul>
<li>Kontrola stavu aktivace</li>
<li>Pouziti klice z <code>config.json</code> nebo GVLK fallback</li>
<li>Volitelne nastaveni KMS serveru</li>
<li><code>slmgr.vbs /ipk</code> + <code>/ato</code></li>
</ul>
<div class="note"><strong>Pozn:</strong> Spec kolegy aktivaci vubec neresi. Ponechat? Nebo vyradit a resit rucne?</div>
<p class="detail-label">Skript</p>
<p><code>scripts/08-activation.ps1</code></p>`
}
]
},
{
id: "debloat",
icon: "2",
title: "Odstranovani bloatware",
steps: [
{
id: "v2-appx",
title: "AppX balicky (UWP aplikace)",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Odebirame ~65 preinstalovanych aplikaci. Kalkulacka zustava.",
detail: `<p class="detail-label">Hlavni balicky</p>
<pre>BingSearch, Camera, Clipchamp, Alarms, Copilot, Cortana,
DevHome, Family, FeedbackHub, GetHelp, Getstarted, Mail,
Maps, BingNews, OfficeHub, OneNote, Outlook, Paint, People,
Photos, PowerAutomate, QuickAssist, Skype, ScreenSketch,
Solitaire, StickyNotes, Teams, Todos, SoundRecorder, Wallet,
BingWeather, Terminal, Xbox (6x), GamingApp, YourPhone,
ZuneMusic, ZuneVideo, LinkedIn</pre>
<p class="detail-label">Zachovavame</p>
<ul><li><code>Microsoft.WindowsCalculator</code></li></ul>
<div class="resolved"><strong>Z review v1:</strong> "Flash2 je v podstate tento setup, minula verze - hlavne pro inspiraci a zahrnuti." = Flash2 neintegrujeme, nas seznam je kompletnejsi.</div>
<p class="detail-label">Skript</p>
<p><code>scripts/01-bloatware.ps1</code></p>`
},
{
id: "v2-caps",
title: "Windows Capabilities",
source: "impl", sourceLabel: "Implementovano",
summary: "Odebirame: Fax, IE, WordPad, PowerShell ISE, Steps Recorder, WMP, Handwriting...",
detail: `<pre>Print.Fax.Scan, Language.Handwriting, Browser.InternetExplorer,
MathRecognizer, OneCoreUAP.OneSync, OpenSSH.Client,
Microsoft.Windows.MSPaint, PowerShell.ISE,
QuickAssist, SnippingTool, StepsRecorder,
Hello.Face.*, WindowsMediaPlayer, WordPad</pre>`
},
{
id: "v2-features",
title: "Optional Features",
source: "impl", sourceLabel: "Implementovano",
summary: "Vypnuti: MediaPlayback, PowerShell 2.0, Recall (AI), SnippingTool. RDP se NEODEBIRA.",
detail: `<pre>MediaPlayback, MicrosoftWindowsPowerShellV2Root,
Recall (Windows AI), Microsoft-SnippingTool</pre>
<div class="resolved"><strong>Vyreseno:</strong> RDP klient (<code>Microsoft-RemoteDesktopConnection</code>) se NEODEBIRA. Puvodne byl ve SPEC, kolega potvrdil ze musi zustat.</div>`
},
{
id: "v2-onedrive",
title: "OneDrive - oprava",
source: "problem", sourceLabel: "OPRAVIT",
summary: "Nas skript agresivne maze OneDrive. Nutno opravit - nechat OneDrive instalovatelny.",
detail: `<p class="detail-label">Co aktualne spatne delame</p>
<ul>
<li><code>OneDriveSetup.exe /uninstall</code> + mazani exe</li>
<li>Mazani Start menu shortcutu</li>
<li>Mazani RunOnce klicu a Explorer namespace z Default Profile</li>
</ul>
<div class="prev-comment"><strong>Review v1:</strong> "nevim jestli v posledni verzi uz to bylo opravene, ale v prvni verzi se onedrive zabijel nejakym regeditem nebo scheduled taskem"</div>
<p class="detail-label">Stav v kodu</p>
<p>V aktualnim <code>03-system-registry.ps1</code> (radky 244-273) + <code>04-default-profile.ps1</code> (radky 240-261) je OneDrive stale agresivne mazany.</p>
<p class="detail-label">Reseni</p>
<p>Odstranit cely OneDrive blok z obou skriptu. OneDrive musi zustat instalovatelny pro M365.</p>`
},
{
id: "v2-rdp",
title: "RDP/RDS - overeni",
source: "problem", sourceLabel: "OVERIT",
summary: "RDP se v kodu neodebira (neni v seznamu). Overit, ze zadny registry tweak neblokuje RDS.",
detail: `<div class="prev-comment"><strong>Review v1:</strong> "nevim jestli v posledni verzi uz to bylo opravene, ale v prvni verzi se rdp problematizovalo nejakym regeditem nebo scheduled taskem"</div>
<p class="detail-label">Stav</p>
<p>V aktualnim kodu <code>01-bloatware.ps1</code> RDP NENI v seznamu k odebirani. Ale je mozne, ze nektery registry tweak v <code>03-system-registry.ps1</code> neprimo ovlivnuje RDS.</p>
<p class="detail-label">TODO</p>
<p>Prozkoumat vsechny HKLM zmeny a overit, ze zadna neblokuje Remote Desktop Services. Otestovat na VM.</p>`
}
]
},
{
id: "software",
icon: "3",
title: "Instalace software",
steps: [
{
id: "v2-winget",
title: "Winget balicky (7-Zip, Adobe, OpenVPN)",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Silent instalace 7-Zip, Adobe Acrobat Reader, OpenVPN Connect pres winget.",
detail: `<table style="width:100%;font-size:.83rem;border-collapse:collapse;">
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem .4rem"><code>7zip.7zip</code></td><td>7-Zip</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem .4rem"><code>Adobe.Acrobat.Reader.64-bit</code></td><td>Adobe Reader</td></tr>
<tr><td style="padding:.25rem .4rem"><code>OpenVPNTechnologies.OpenVPNConnect</code></td><td>OpenVPN Connect</td></tr>
</table>
<p class="detail-label">Adobe jako vychozi PDF</p>
<ul>
<li>Po instalaci: <code>.pdf -&gt; AcroRd32</code> pres HKCR</li>
<li>Scheduled task <code>PDF-DefaultApp</code> obnovi asociaci pri kazdem prihlaseni</li>
</ul>
<p class="detail-label">Skript</p>
<p><code>scripts/02-software.ps1</code></p>`
},
{
id: "v2-atera",
title: "Atera Agent",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Stahnout MSI z Atera API a provest silent install. MFA problem k reseni.",
detail: `<div class="prev-comment"><strong>Review v1 - kolega dodal presny postup:</strong></div>
<pre>curl -L -o setup.msi "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
msiexec /i setup.msi /qn</pre>
<p class="detail-label">PowerShell ekvivalent</p>
<pre>\$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&amp;aeid=50b72e7113e54a63ac76b96c54c7e337"
\$ateraMsi = "C:\\X9\\install\\atera-setup.msi"
Invoke-WebRequest -Uri \$ateraUrl -OutFile \$ateraMsi
Start-Process msiexec -ArgumentList "/i \$ateraMsi /qn" -Wait</pre>
<div class="note"><strong>Otevrena otazka:</strong> Kolega rika "nejlip najit parametr, u ktereho ATERA nebude chtit MFA kod z mailu". Overit, jestli URL s aeid parametrem to obchazi.</div>`
},
{
id: "v2-backinfo",
title: "BackInfo (info na plose)",
source: "spec", sourceLabel: "Spec kolegy",
summary: "BackInfo.exe - hostname, user, OS, HW, sit uprostred plochy. Mame v assets/, jen zkopirovat a spustit.",
detail: `<p class="detail-label">Instalace</p>
<ol>
<li>Zkopirovat <code>assets/Backinfo/</code> do <code>C:\\Program Files\\Backinfo\\</code></li>
<li>Spustit <code>backinfo_W11.ps1</code>:
<ul>
<li>Detekuje Win10/Win11 + edici</li>
<li>Zapise <code>HKLM:\\SOFTWARE\\BackInfo\\OSName</code></li>
<li>Vytvori shortcut do Startup (<code>StartUp\\BackInfo.lnk</code>)</li>
</ul>
</li>
<li>BackInfo.exe se pak spousti automaticky po kazdem prihlaseni</li>
</ol>
<p class="detail-label">INI</p>
<pre>AutoBackground=1, ForceDesktopCenter=1
Line1: CompName (42pt, bold, white, center)
Line2: UserName (20pt, gray, center)
Line3: OS z registru (20pt, bold, gray)
Line4: HW SysInfo (20pt, gray)
Line5: Network NetInfo (20pt, gray)</pre>
<div class="resolved"><strong>Vyreseno:</strong> BackInfo uz je v <code>assets/Backinfo/</code>. Nahrazuje nas custom 07-desktop-info.ps1 (smazat).</div>`
}
]
},
{
id: "appearance",
icon: "4",
title: "Vzhled a personalizace",
steps: [
{
id: "v2-theme",
title: "Barvy a motiv",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Tmavy system, svetle aplikace, accent #223B47, plna barva pozadi.",
detail: `<table style="width:100%;font-size:.83rem;border-collapse:collapse;">
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Rezim Windows</td><td><strong>Tmavy</strong></td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Rezim aplikaci</td><td><strong>Svetly</strong></td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Accent barva</td><td><strong>#223B47</strong> <span style="display:inline-block;width:13px;height:13px;background:#223B47;border-radius:3px;vertical-align:middle;border:1px solid #555"></span></td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Accent v Start/taskbar</td><td>Ano</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Accent v zahlavi oken</td><td>Ano</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Pruhlednost</td><td>Vypnuta</td></tr>
<tr><td style="padding:.25rem">Pozadi</td><td><strong>Plna barva #223B47</strong> (BackInfo prepise BMP)</td></tr>
</table>
<p class="detail-label">Skript</p>
<p><code>scripts/05-personalization.ps1</code></p>`
},
{
id: "v2-desktop",
title: "Ikona Tento pocitac na plose",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Zobrazit ikonu Tento pocitac na plose (Default Profile + HKCU).",
detail: `<pre>HKCU\\...\\HideDesktopIcons\\NewStartPanel
{20D04FE0-3AEA-1069-A2D8-08002B30309D} = 0</pre>`
},
{
id: "v2-avatar",
title: "Avatar uctu (X9 logo)",
source: "spec", sourceLabel: "Spec kolegy",
summary: "X9-logo.jpg jako profilovy obrazek admin uctu. Logo mame v assets/Logo/.",
detail: `<pre>\$accountPicPath = "\$env:APPDATA\\Microsoft\\Windows\\AccountPictures"
Copy-Item "C:\\X9\\X9-logo.jpg" "\$accountPicPath\\X9-logo.jpg"</pre>
<div class="resolved"><strong>Vyreseno:</strong> Logo (ico + jpeg) presunuto do <code>assets/Logo/</code>. Plati jen pro admin ucet.</div>`
}
]
},
{
id: "taskbar",
icon: "5",
title: "Hlavni panel a Start menu",
steps: [
{
id: "v2-tb-layout",
title: "Taskbar - zarovnani, skryti prvku",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Skryti Search, Task View, Widgets, Chat, Copilot. Zarovnani dle parametru -ProfileType.",
detail: `<p class="detail-label">Skryte prvky</p>
<ul>
<li>Search box (SearchboxTaskbarMode=0)</li>
<li>Task View (ShowTaskViewButton=0)</li>
<li>Widgets (TaskbarDa=0)</li>
<li>Chat/Teams (TaskbarMn=0)</li>
<li>Copilot (ShowCopilotButton=0)</li>
</ul>
<p class="detail-label">Zarovnani</p>
<ul>
<li><code>-ProfileType user</code> = vlevo (TaskbarAl=0)</li>
<li><code>-ProfileType admin</code> = na stred (TaskbarAl=1)</li>
</ul>
<p class="detail-label">Skript</p>
<p><code>scripts/04-default-profile.ps1</code></p>`
},
{
id: "v2-tb-pins",
title: "NOVINKA: Taskbar pinnovane aplikace (admin vs user)",
source: "new", sourceLabel: "NOVINKA v2",
summary: "Ruzne sady pripnutych aplikaci podle -ProfileType. Admin: Settings, ComputerMgmt, Services, PS, Explorer, Edge. User: Explorer, Edge.",
detail: `<p class="detail-label">Admin profil - pripnout</p>
<ul>
<li>Nastaveni (<code>ms-settings:</code>)</li>
<li>Sprava pocitace (<code>compmgmt.msc</code>)</li>
<li>Sluzby (<code>services.msc</code>)</li>
<li>PowerShell</li>
<li>Pruzkumnik Windows</li>
<li>MS Edge</li>
</ul>
<p class="detail-label">User profil - pripnout</p>
<ul>
<li>Pruzkumnik Windows</li>
<li>MS Edge</li>
</ul>
<p class="detail-label">Implementace</p>
<p>XML layout policy (<code>LayoutXMLPath</code>). Pro <code>compmgmt.msc</code> a <code>services.msc</code> nutno vytvorit .lnk soubory, XML prijima jen .lnk cesty.</p>
<pre>&lt;CustomTaskbarLayoutCollection PinListPlacement="Replace"&gt;
&lt;taskbar:TaskbarPinList&gt;
&lt;taskbar:DesktopApp DesktopApplicationLinkPath="...\\PowerShell.lnk"/&gt;
&lt;taskbar:DesktopApp DesktopApplicationLinkPath="...\\File Explorer.lnk"/&gt;
&lt;taskbar:DesktopApp DesktopApplicationLinkPath="...\\Microsoft Edge.lnk"/&gt;
&lt;/taskbar:TaskbarPinList&gt;
&lt;/CustomTaskbarLayoutCollection&gt;</pre>
<div class="note"><strong>Pozn:</strong> <code>LayoutXMLPath</code> policy je dostupna ve Win11 22H2+. Nutno overit verzi pred nasazenim.</div>`
},
{
id: "v2-tray",
title: "System tray - zobrazit vsechny ikony",
source: "both", sourceLabel: "Implementovano",
summary: "EnableAutoTray=0 + mazani icon cache + scheduled task ShowAllTrayIcons.",
detail: `<ul>
<li><code>EnableAutoTray = 0</code> (Win10)</li>
<li>Mazani TrayNotify icon streams (Win11 workaround)</li>
<li>Scheduled task <code>ShowAllTrayIcons</code> pri kazdem prihlaseni</li>
</ul>`
},
{
id: "v2-start",
title: "Start menu - prazdne piny, bez Bing",
source: "impl", sourceLabel: "Implementovano",
summary: "Prazdny LayoutModification.xml, prazdne Start pins, vypnuty Bing suggestions.",
detail: `<ul>
<li><code>ConfigureStartPins = {"pinnedList":[]}</code></li>
<li><code>DisableSearchBoxSuggestions = 1</code></li>
<li>Scheduled task <code>UnlockStartLayout</code> po 5 min odemkne pro uzivatele</li>
</ul>`
}
]
},
{
id: "explorer",
icon: "6",
title: "Pruzkumnik Windows",
steps: [
{
id: "v2-explorer-base",
title: "Zakladni nastaveni (implementovano)",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "Otevirat do Tento pocitac, zobrazit pripony souboru.",
detail: `<pre>LaunchTo = 1 (Tento pocitac misto Rychly pristup)
HideFileExt = 0 (zobrazit pripony)</pre>
<p class="detail-label">Skript</p>
<p><code>scripts/04-default-profile.ps1</code></p>`
},
{
id: "v2-explorer-new",
title: "NOVINKA: Dalsi nastaveni Pruzkumniku",
source: "new", sourceLabel: "NOVINKA v2",
summary: "Vypnout nedavne soubory/slozky, zobrazit uplnou cestu v zahlavi.",
detail: `<p class="detail-label">Nova nastaveni</p>
<table style="width:100%;font-size:.83rem;border-collapse:collapse;">
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Nedavne soubory</td><td><strong>Vypnuto</strong> (ShowRecent=0)</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Caste slozky</td><td><strong>Vypnuto</strong> (ShowFrequent=0)</td></tr>
<tr><td style="padding:.25rem">Uplna cesta v zahlavi</td><td><strong>Zapnuto</strong> (FullPath=1)</td></tr>
</table>
<p class="detail-label">Registry</p>
<pre>HKCU\\...\\Explorer
ShowRecent = 0 (DWORD)
ShowFrequent = 0 (DWORD)
HKCU\\...\\Explorer\\CabinetState
FullPath = 1 (DWORD)</pre>
<p>Aplikovat do Default Profile hive + HKCU.</p>`
}
]
},
{
id: "system",
icon: "7",
title: "Systemova nastaveni",
steps: [
{
id: "v2-registry",
title: "HKLM registry tweaky",
source: "both", sourceLabel: "Implementovano + Spec",
summary: "BypassNRO, vypnuti Teams/Widgets/Copilot/GameDVR/Recall, hesla bez expirace, casova zona.",
detail: `<ul>
<li>Bypass NRO (OOBE sit)</li>
<li>Vypnuti auto-instalace Teams</li>
<li>Vypnuti Cloud Optimized Content</li>
<li>Vypnuti Widgets</li>
<li>Edge: HideFirstRunExperience, zadny desktop shortcut</li>
<li>Hesla bez expirace</li>
<li>Casova zona: Central Europe Standard Time</li>
<li>Vypnuti GameDVR + Recall</li>
<li>Skryti Search (HKLM policy)</li>
<li>Skryti Recommended v Start menu</li>
<li>Vypnuti auto-instalace Outlooku</li>
</ul>
<p class="detail-label">Skript</p>
<p><code>scripts/03-system-registry.ps1</code></p>`
},
{
id: "v2-defprofile",
title: "Default Profile (NTUSER.DAT)",
source: "both", sourceLabel: "Implementovano",
summary: "Nastaveni pro budouci uzivatele: Explorer, Num Lock, GameDVR, Copilot...",
detail: `<pre>reg load "HKU\\DefaultProfile" "C:\\Users\\Default\\NTUSER.DAT"
... zmeny ...
reg unload "HKU\\DefaultProfile"</pre>
<ul>
<li>Explorer: pripony, Tento pocitac, ShowRecent/Frequent off, FullPath</li>
<li>Num Lock zapnut</li>
<li>Copilot vypnut</li>
<li>GameDVR vypnut</li>
<li>Start menu: prazdne piny, bez Bing</li>
</ul>`
},
{
id: "v2-power",
title: "Napajeni (powercfg)",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Spanek nikdy na siti, obrazovka 60min/15min, spanek baterie 60min.",
detail: `<pre>powercfg /change standby-timeout-ac 0 # spanek sit: nikdy
powercfg /change monitor-timeout-ac 60 # obrazovka sit: 60 min
powercfg /change monitor-timeout-dc 15 # obrazovka bat: 15 min
powercfg /change standby-timeout-dc 60 # spanek bat: 60 min</pre>
<div class="note"><strong>Pozn:</strong> Zavreni vika - nechat vychozi.</div>`
},
{
id: "v2-tasks",
title: "Scheduled tasks",
source: "both", sourceLabel: "Implementovano",
summary: "ShowAllTrayIcons, PDF-DefaultApp, UnlockStartLayout.",
detail: `<table style="width:100%;font-size:.83rem;border-collapse:collapse;">
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem"><strong>ShowAllTrayIcons</strong></td><td>Logon: systray ikony</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem"><strong>PDF-DefaultApp</strong></td><td>Logon: .pdf -&gt; Adobe (SYSTEM)</td></tr>
<tr><td style="padding:.25rem"><strong>UnlockStartLayout</strong></td><td>5 min po startu, pak se smaze</td></tr>
</table>
<p class="detail-label">Skripty</p>
<p><code>C:\\Windows\\Setup\\Scripts\\</code></p>`
}
]
},
{
id: "network",
icon: "8",
title: "Sit a Edge",
steps: [
{
id: "v2-proxy",
title: "Proxy - vypnout auto-detect",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Vypnuti automatickeho zjistovani proxy serveru.",
detail: `<pre>HKCU\\...\\Internet Settings\\AutoDetect = 0
HKLM\\...\\Internet Settings\\AutoDetect = 0</pre>
<p>Aplikovat do Default Profile i HKCU.</p>`
},
{
id: "v2-edge",
title: "MS Edge - rozsirene nastaveni",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Striktni tracking protection, Google vyhledavac, panel oblibenych.",
detail: `<table style="width:100%;font-size:.83rem;border-collapse:collapse;">
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Tracking</td><td><strong>Striktni</strong> (TrackingPrevention=3)</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Oblibene</td><td><strong>Vzdy videt</strong> (FavoritesBarEnabled=1)</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:.25rem">Vyhledavac</td><td><strong>Google</strong></td></tr>
<tr><td style="padding:.25rem">Toolbar</td><td>Historie, Aplikace, Stazene, Vykon</td></tr>
</table>
<pre>HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge
TrackingPrevention = 3
FavoritesBarEnabled = 1
DefaultSearchProviderEnabled = 1
DefaultSearchProviderName = "Google"
DefaultSearchProviderSearchURL = "https://www.google.com/search?q={searchTerms}"</pre>`
},
{
id: "v2-network",
title: "NOVINKA: Sitove zjistovani + privatni sit",
source: "new", sourceLabel: "NOVINKA v2",
summary: "Zapnout Network Discovery, File Sharing, prepnout sit na Private (aby fungoval ping).",
detail: `<pre># Zapnout Network Discovery a File Sharing
netsh advfirewall firewall set rule group="Network Discovery" new enable=Yes
netsh advfirewall firewall set rule group="File and Printer Sharing" new enable=Yes
# Prepnout sitovy profil na Private
\$adapter = Get-NetConnectionProfile | Select-Object -First 1
Set-NetConnectionProfile -InterfaceIndex \$adapter.InterfaceIndex -NetworkCategory Private</pre>
<div class="note"><strong>Dulezite:</strong> Vaze se na aktualni sitovy adapter - nelze ulozit do Default Profile. Spustit az po pripojeni k siti. Pokud domain-join, az po restartu.</div>`
}
]
},
{
id: "finish",
icon: "9",
title: "Finalizace",
steps: [
{
id: "v2-rename",
title: "Prejmenování PC",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Rename-Computer na nazev z parametru -ComputerName. Posledni krok pred restartem.",
detail: `<pre>Rename-Computer -NewName $ComputerName -Force -Restart</pre>
<ul>
<li>Restart je nutny</li>
<li>Pripojeni do domeny = volitelne, nezavisle</li>
</ul>`
},
{
id: "v2-bootstrap",
title: "Bootstrap spoustec (irm | iex)",
source: "spec", sourceLabel: "Spec kolegy",
summary: "Jednoradkovy spoustec: irm https://xetup.x9.cz/setup.ps1 | iex",
detail: `<pre>irm https://xetup.x9.cz/setup.ps1 | iex</pre>
<p class="detail-label">Parametry</p>
<pre>-ComputerName (povinny)
-Domain (volitelny)
-ProfileType "admin"|"user" (default "user")
ridi: taskbar zarovnani + pinnovane apps</pre>
<div class="note"><strong>Novy parametr -ProfileType</strong> (z novinek v2): nahrazuje puvodni -TaskbarAlign. Ridi zarovnani taskbaru I sadu pinnovanych aplikaci.</div>`
}
]
}
];
// --- Rendering ---
const STORAGE_PREFIX = 'xr2-';
function render() {
const app = document.getElementById('app');
app.innerHTML = '';
groups.forEach(g => {
const div = document.createElement('div');
div.className = 'group';
div.innerHTML = `
<div class="group-header">
<div class="group-icon">${g.icon}</div>
<span class="group-title">${g.title}</span>
<span class="group-count">(${g.steps.length})</span>
</div>`;
g.steps.forEach(s => {
const saved = load(s.id);
const sc = saved.status ? `status-${saved.status}` : '';
const badge = {approved:'OK',rejected:'NE',discuss:'?'}[saved.status]||'';
const el = document.createElement('div');
el.className = `step ${sc}`;
el.dataset.id = s.id;
el.innerHTML = `
<div class="step-header" onclick="toggle(this)">
<span class="step-title">${s.title}</span>
<span class="status-badge">${badge}</span>
<span class="step-source source-${s.source}">${s.sourceLabel}</span>
<span class="chevron">&#9654;</span>
</div>
<div class="step-body">
<p>${s.summary}</p>
<details style="margin-top:.5rem">
<summary style="cursor:pointer;color:var(--blue);font-size:.83rem;user-select:none">Zobrazit detaily</summary>
<div style="margin-top:.4rem">${s.detail}</div>
</details>
<div class="actions">
<button class="btn btn-approve ${saved.status==='approved'?'active':''}" onclick="setStatus('${s.id}','approved',this)">Schvalit</button>
<button class="btn btn-reject ${saved.status==='rejected'?'active':''}" onclick="setStatus('${s.id}','rejected',this)">Zamitnout</button>
<button class="btn btn-discuss ${saved.status==='discuss'?'active':''}" onclick="setStatus('${s.id}','discuss',this)">K diskusi</button>
</div>
<div class="comment-area ${saved.comment?'visible':''}">
<textarea placeholder="Poznamka / komentar..." oninput="saveComment('${s.id}',this.value)">${saved.comment||''}</textarea>
</div>
</div>`;
div.appendChild(el);
});
app.appendChild(div);
});
updateCounts();
}
function toggle(h){h.closest('.step').classList.toggle('open')}
let allOpen=false;
function toggleAll(){allOpen=!allOpen;document.querySelectorAll('.step').forEach(s=>s.classList.toggle('open',allOpen))}
function expandDetails(){document.querySelectorAll('.step-body details').forEach(d=>d.open=true);document.querySelectorAll('.step').forEach(s=>s.classList.add('open'));allOpen=true}
function setStatus(id,status,btn){
const st=load(id);
st.status=st.status===status?'':status;
save(id,st);
const step=btn.closest('.step');
step.className=`step ${st.status?'status-'+st.status:''} open`;
step.querySelector('.status-badge').textContent={approved:'OK',rejected:'NE',discuss:'?'}[st.status]||'';
step.querySelectorAll('.actions .btn').forEach(b=>b.classList.remove('active'));
if(st.status)btn.classList.add('active');
const ca=step.querySelector('.comment-area');
if(st.status==='discuss'||st.status==='rejected'){ca.classList.add('visible');ca.querySelector('textarea').focus()}
updateCounts();
}
function saveComment(id,v){const s=load(id);s.comment=v;save(id,s)}
function load(id){try{return JSON.parse(localStorage.getItem(STORAGE_PREFIX+id))||{}}catch{return{}}}
function save(id,s){localStorage.setItem(STORAGE_PREFIX+id,JSON.stringify(s))}
function allSteps(){return groups.flatMap(g=>g.steps)}
function updateCounts(){
let c={pending:0,approved:0,rejected:0,discuss:0};
allSteps().forEach(s=>{const st=load(s.id).status;if(st&&c[st]!==undefined)c[st]++;else c.pending++});
document.getElementById('cnt-pending').textContent=c.pending;
document.getElementById('cnt-approved').textContent=c.approved;
document.getElementById('cnt-rejected').textContent=c.rejected;
document.getElementById('cnt-discuss').textContent=c.discuss;
}
function clearAll(){if(!confirm('Opravdu smazat vsechny hlasy a komentare?'))return;allSteps().forEach(s=>localStorage.removeItem(STORAGE_PREFIX+s.id));render()}
function genMd(){
let md=`# Xetup - Review v2 vysledek\n\nDatum: ${new Date().toLocaleDateString('cs-CZ')}\n\n`;
const ic={approved:'[OK]',rejected:'[X]',discuss:'[?]',pending:'[ ]'};
const lb={approved:'SCHVALENO',rejected:'ZAMITNUTO',discuss:'K DISKUSI',pending:'CEKA'};
groups.forEach(g=>{
md+=`## ${g.icon}. ${g.title}\n\n`;
g.steps.forEach(s=>{
const st=load(s.id);const status=st.status||'pending';
md+=`${ic[status]} **${s.title}** (${lb[status]})\n`;
md+=`${s.summary}\n`;
if(st.comment)md+=`> ${st.comment}\n`;
md+=`\n`;
});
});
return md;
}
function exportMarkdown(){const b=new Blob([genMd()],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='xetup-review-v2.md';a.click()}
function copyToClipboard(){navigator.clipboard.writeText(genMd()).then(()=>{const b=event.target;const o=b.textContent;b.textContent='Skopirovano!';setTimeout(()=>b.textContent=o,1500)})}
render();
</script>
</body>
</html>

21
runner-config.yml Normal file
View file

@ -0,0 +1,21 @@
log:
level: info
runner:
file: /data/.runner
capacity: 1
timeout: 3h
fetch_timeout: 5s
fetch_interval: 2s
report_interval: 1s
cache:
enabled: true
container:
network: xetup
privileged: false
valid_volumes:
- '**'
docker_host: "-"
force_pull: false

View file

@ -0,0 +1,125 @@
<#
.SYNOPSIS
Creates the adminx9 local administrator account for MSP use.
.DESCRIPTION
Creates a hidden local administrator account 'adminx9' used by X9.cz technicians
for remote management and on-site administration. The account has no password by
design - it is invisible to regular users and only accessible to technicians who
know it exists. FullName is set to "X9.cz s.r.o." so it is identifiable in
system tools. Password policy is set so it never expires.
.ITEMS
vytvorit-lokalni-ucet-adminx9: Creates the account via [ADSI] WinNT provider. No password by design - the account is hidden from users and used only by MSP technicians for remote administration.
pridat-do-skupiny-administrators: Adds adminx9 to the local Administrators group via net localgroup. Required for full system management rights.
skryt-z-login-obrazovky-specialaccounts-: Sets HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList\adminx9 = 0. Removes the user tile from Windows login and lock screen completely.
heslo-nevypirsi-uzivatel-nesmeni-heslo: Sets ADS_UF_DONT_EXPIRE_PASSWD and ADS_UF_PASSWD_CANT_CHANGE flags via ADSI userFlags. The account never locks out or requires password maintenance.
zadne-heslo-aktualne-nastavovano-z-confi: Account created with empty password. Previous version used config.json password - removed because plaintext passwords in config files are a security risk.
fullname-x9-cz-s-r-o-via-adsi: Sets FullName property via [ADSI] so the account shows as "X9.cz s.r.o." in User Accounts panel, Event Viewer, and audit logs.
#>
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
}
# -----------------------------------------------------------------------
# Account config - no password by design
# -----------------------------------------------------------------------
$accountName = "adminx9"
$accountDesc = "X9 MSP admin account"
$accountFullName = "X9.cz s.r.o."
if ($Config -and $Config.adminAccount) {
if ($Config.adminAccount.username) { $accountName = $Config.adminAccount.username }
}
Write-Log "Creating admin account: $accountName" -Level INFO
# Empty password - account is hidden from login screen, no password needed
$emptyPass = [System.Security.SecureString]::new()
# -----------------------------------------------------------------------
# Create or update account
# -----------------------------------------------------------------------
$existing = Get-LocalUser -Name $accountName -ErrorAction SilentlyContinue
if ($existing) {
Write-Log " Account already exists - clearing password" -Level INFO
try {
Set-LocalUser -Name $accountName -Password $emptyPass -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 $emptyPass `
-Description $accountDesc `
-PasswordNeverExpires `
-UserMayNotChangePassword `
-ErrorAction Stop | Out-Null
Write-Log " Account created: $accountName" -Level OK
}
catch {
Write-Log " Failed to create account: $_" -Level ERROR
}
}
# -----------------------------------------------------------------------
# Set FullName via ADSI
# -----------------------------------------------------------------------
try {
$adsiUser = [ADSI]"WinNT://./$accountName,user"
$adsiUser.FullName = $accountFullName
$adsiUser.SetInfo()
Write-Log " FullName set to: $accountFullName" -Level OK
}
catch {
Write-Log " Failed to set FullName: $_" -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

199
scripts/01-bloatware.ps1 Normal file
View file

@ -0,0 +1,199 @@
<#
.SYNOPSIS
Removes pre-installed bloatware: AppX packages, Capabilities, and Optional Features.
.DESCRIPTION
Removes Microsoft-bundled apps and features not needed in a business MSP deployment.
Removal is done for all users (-AllUsers) and from the provisioning store so new
users do not get them either. Calculator is intentionally kept.
.ITEMS
appx-balicky-odstraneni-pro-vsechny-uziv: Uses Remove-AppxPackage -AllUsers and Remove-AppxProvisionedPackage. The provisioned removal prevents apps from reinstalling for new user profiles. Covers ~35 apps including Cortana, Copilot, Teams personal, Xbox, Skype, News, Weather, Maps.
zachovano-microsoft-windowscalculator: Calculator is explicitly excluded. Lightweight utility frequently used by technicians and end users. Removing it would require manual reinstall from Store.
windows-capabilities-fax-ie-openssh-wmp-: Removed via Remove-WindowsCapability: Fax & Scan, Internet Explorer mode, OpenSSH client, Windows Media Player (legacy), WordPad, Handwriting recognition, Steps Recorder, Math Input Panel, Quick Assist.
windows-optional-features-ps-2-0-mediapl: Disabled via Disable-WindowsOptionalFeature: PowerShell 2.0 (security risk - allows unsigned script execution bypass on older hosts), MediaPlayback, Windows Recall (AI screenshot surveillance), Snipping Tool optional component.
#>
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"
"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

213
scripts/02-software.ps1 Normal file
View file

@ -0,0 +1,213 @@
<#
.SYNOPSIS
Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.
.DESCRIPTION
Uses winget to install the standard X9.cz MSP software bundle. Checks winget
availability before running. Each install is logged. After Adobe Acrobat Reader,
temporarily stops the UCPD driver (User Choice Protection Driver, present since
Win11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets
.pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP
monitoring, remote access, and ticketing integration.
.ITEMS
7-zip-7zip-7zip: Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.
adobe-acrobat-reader-64-bit-adobe-acroba: Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.
openvpn-connect-openvpntechnologies-open: Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.
atera-agent-install: Atera RMM agent installed via msiexec /qn. Download: Invoke-WebRequest from https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337. Agent enables MSP monitoring, remote access, and ticketing integration with the Atera dashboard.
adobe-pdf-default-pdf-acrord32-po-instal: Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.
ucpd-sys-kernel-driver-od-feb-2024-bloku: UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\.pdf -> Start-Service ucpd. Implemented in this script.
#>
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
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# 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 (Get-Feature $Config "software" "wingetInstalls") {
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
}
}
}
} else {
Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "pdfDefault") {
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
# Stop UCPD driver before writing file association.
# UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
# on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
# the HKCR association to be written reliably.
$ucpdStopped = $false
$ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
if ($ucpdSvc) {
try {
Stop-Service -Name "ucpd" -Force -ErrorAction Stop
$ucpdStopped = $true
Write-Log " UCPD driver stopped" -Level OK
}
catch {
Write-Log " Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
}
}
# 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
Write-Log " PDF default set to AcroRd32 (HKCR)" -Level OK
}
# Restart UCPD after writing association
if ($ucpdStopped) {
try {
Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
Write-Log " UCPD driver restarted" -Level OK
}
catch {
Write-Log " Could not restart UCPD: $_" -Level WARN
}
}
} else {
Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
if (Get-Feature $Config "software" "ateraAgent") {
Write-Log "Installing Atera RMM Agent" -Level INFO
$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"
try {
Write-Log " Downloading Atera agent..." -Level INFO
Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
Write-Log " Download complete" -Level OK
$msiResult = & msiexec /i $ateraMsi /qn 2>&1
Start-Sleep -Seconds 5
$ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
if (Test-Path $ateraExe) {
Write-Log " Atera agent installed" -Level OK
} else {
Write-Log " Atera agent install may have failed - binary not found at expected path" -Level WARN
Write-Log " msiexec output: $($msiResult -join ' ')" -Level WARN
}
}
catch {
Write-Log " Atera agent download/install failed: $_" -Level ERROR
}
finally {
Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}
} else {
Write-Log "ateraAgent feature disabled - skipping" -Level INFO
}
Write-Log "Step 2 complete" -Level OK

View file

@ -0,0 +1,438 @@
<#
.SYNOPSIS
Applies system-wide registry settings and power configuration (HKLM).
.DESCRIPTION
Sets machine-wide registry tweaks under HKLM that apply to all users. Disables
unwanted telemetry and cloud features, configures Edge policies, sets power plan
timeouts, and disables proxy auto-detect. Uninstalls the pre-installed OneDrive
consumer version via OneDriveSetup.exe /uninstall - intentional for a clean MSP
deployment baseline. No DisableFileSyncNGSC policy key is set, so M365 installation
can install and run its own OneDrive version without restriction.
.ITEMS
bypass-nro-oobe-bypassnro-1: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\BypassNRO = 1. Bypasses the "Let's connect you to a network" OOBE screen. Enables offline Windows setup without forcing a Microsoft account login.
zakaz-auto-instalace-teams: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications\ConfigureChatAutoInstall = 0. Prevents Windows from auto-installing Teams Personal during OOBE or after Cumulative Updates.
zakaz-cloud-optimized-content: ContentDeliveryManager\DisableCloudOptimizedContent = 1. Stops Windows from pushing sponsored app suggestions, tips from Microsoft servers, and "Get even more from Windows" prompts.
zakaz-widgets-news-and-interests: HKLM\SOFTWARE\Policies\Microsoft\Dsh\AllowNewsAndInterests = 0. Disables the Widgets taskbar button and panel (news feed, weather, stocks). Not relevant for business deployments.
hesla-bez-expirace-net-accounts-maxpwage: net accounts /maxpwage:UNLIMITED. Sets the local password expiration policy to never. MSP-managed machines handle password rotation via other means (Atera, domain policy, manual).
casova-zona-central-europe-standard-time: Set-TimeZone -Id "Central Europe Standard Time". UTC+1 (UTC+2 in summer DST). Applied system-wide. Critical for correct log timestamps, scheduled task timing, and calendar sync.
zakaz-gamedvr: HKLM\SOFTWARE\Policies\Microsoft\Windows\GameDVR\AppCaptureEnabled = 0. Disables Xbox Game Bar screen capture overlay. Reduces background resource usage and eliminates unintended capture prompts on business machines.
edge-skryt-first-run-experience: HideFirstRunExperience=1 + DefaultBrowserSettingEnabled=0. Suppresses Edge welcome wizard and default browser prompts on first launch.
edge-policies-panel-oblibeny-vyhledavac: FavoritesBarEnabled=1 (always show), DefaultSearchProviderEnabled=1, DefaultSearchProviderName=Google, ManagedSearchEngines removes other providers.
edge-policies-tlacitka-zobrazit: DownloadsButtonEnabled=1, HistoryButtonEnabled=1.
edge-policies-tlacitka-skryt: HomeButtonEnabled=0, SplitScreenEnabled=0, EdgeEDropEnabled=0 (Drop), WebCaptureEnabled=0 (Screenshot), ShareAllowed=0.
edge-policies-obsah-a-telemetrie: NewTabPageContentEnabled=0, ShowRecommendationsEnabled=0, SpotlightExperiencesAndRecommendationsEnabled=0, PersonalizationReportingEnabled=0, EdgeShoppingAssistantEnabled=0, ShowMicrosoftRewards=0, HubsSidebarEnabled=0, SearchSuggestEnabled=0, DiagnosticData=0, FeedbackSurveysEnabled=0, EdgeCollectionsEnabled=0.
onedrive-uninstall-intentional: Uninstalls the pre-installed OneDrive consumer version via OneDriveSetup.exe /uninstall and removes Start Menu shortcut. Intentional for clean MSP deployment baseline. No DisableFileSyncNGSC policy key is set - M365 installation can reinstall and run OneDrive normally. Only the stock consumer pre-install is removed.
powercfg-nastaveni-spotreba-energie: powercfg /change: standby-timeout-ac 0 (never sleep on AC), monitor-timeout-ac 60 (screen off after 60 min on AC), standby-timeout-dc 30 (sleep after 30 min on battery), monitor-timeout-dc 15 (screen off after 15 min on battery). Applied to active power plan.
proxy-auto-detect-zakaz-autodetect-0: HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\AutoDetect = 0. Disables WPAD (Web Proxy Auto-Discovery). Eliminates startup delays from WPAD DNS lookup and prevents MITM via rogue WPAD on untrusted networks.
#>
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
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
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
# -----------------------------------------------------------------------
# Always-on: password and timezone (fundamental system settings)
# -----------------------------------------------------------------------
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
}
$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
}
# -----------------------------------------------------------------------
# System tweaks (Windows features, cloud noise, Xbox, Recall, Search UI)
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "systemTweaks") {
Write-Log " Applying system tweaks" -Level INFO
# 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
# Disable Outlook (new) 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
} else {
Write-Log "systemTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Microsoft Edge policies
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "edgePolicies") {
Write-Log " Applying Edge policies" -Level INFO
$edgePath = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
# UI / first run
Set-Reg -Path $edgePath -Name "HideFirstRunExperience" -Value 1
Set-Reg -Path $edgePath -Name "DefaultBrowserSettingEnabled" -Value 0
# New tab page / recommendations
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SpotlightExperiencesAndRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "PersonalizationReportingEnabled" -Value 0
# Shopping / rewards / sidebar
Set-Reg -Path $edgePath -Name "EdgeShoppingAssistantEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowMicrosoftRewards" -Value 0
Set-Reg -Path $edgePath -Name "HubsSidebarEnabled" -Value 0
# Search suggestions
Set-Reg -Path $edgePath -Name "SearchSuggestEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ImportOnEachLaunch" -Value 0
# Telemetry / feedback
Set-Reg -Path $edgePath -Name "DiagnosticData" -Value 0
Set-Reg -Path $edgePath -Name "FeedbackSurveysEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeCollectionsEnabled" -Value 0
# Toolbar buttons - show
Set-Reg -Path $edgePath -Name "FavoritesBarEnabled" -Value 1 # Favorites bar always visible
Set-Reg -Path $edgePath -Name "DownloadsButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "HistoryButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "PerformanceButtonEnabled" -Value 1 # Sleeping Tabs / Performance
# Toolbar buttons - hide
Set-Reg -Path $edgePath -Name "HomeButtonEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SplitScreenEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeEDropEnabled" -Value 0 # Drop
Set-Reg -Path $edgePath -Name "WebCaptureEnabled" -Value 0 # Screenshot
Set-Reg -Path $edgePath -Name "ShareAllowed" -Value 0 # Share
# Default search engine: Google
# SearchProviderEnabled must be 1, SearchProviderName + URL set the provider
Set-Reg -Path $edgePath -Name "DefaultSearchProviderEnabled" -Value 1 -Type "DWord"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderName" -Value "Google" -Type "String"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderSearchURL" `
-Value "https://www.google.com/search?q={searchTerms}" -Type "String"
# Remove other search engines (empty list = no other providers besides default)
Set-Reg -Path $edgePath -Name "ManagedSearchEngines" `
-Value '[{"is_default":true,"name":"Google","search_url":"https://www.google.com/search?q={searchTerms}","keyword":"google.com"}]' `
-Type "String"
# Disable desktop shortcut on install/update
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" `
-Name "CreateDesktopShortcutDefault" -Value 0
} else {
Write-Log "edgePolicies feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# OneDrive - uninstall from clean Windows (no policy block)
# NOTE: No policy key is set intentionally - M365 installation can reinstall
# and run OneDrive normally. Policy DisableFileSyncNGSC would prevent that.
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
Write-Log " Uninstalling OneDrive" -Level INFO
$oneDrivePaths = @(
"$env:SystemRoot\System32\OneDriveSetup.exe"
"$env:SystemRoot\SysWOW64\OneDriveSetup.exe"
)
foreach ($odPath in $oneDrivePaths) {
if (Test-Path $odPath) {
try {
& $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
}
} else {
Write-Log "oneDriveUninstall feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Power configuration
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "powercfg") {
Write-Log " Applying power configuration" -Level INFO
$powercfgArgs = @(
@("/change", "standby-timeout-ac", "0"), # never sleep on AC
@("/change", "monitor-timeout-ac", "60"), # screen off after 60 min on AC
@("/change", "standby-timeout-dc", "30"), # sleep after 30 min on battery
@("/change", "monitor-timeout-dc", "15") # screen off after 15 min on battery
)
foreach ($a in $powercfgArgs) {
$result = & powercfg @a 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " powercfg $($a -join ' ')" -Level OK
} else {
Write-Log " powercfg $($a -join ' ') failed: $result" -Level WARN
}
}
} else {
Write-Log "powercfg feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Proxy auto-detect disable (WPAD)
# -----------------------------------------------------------------------
if (Get-Feature $Config "systemRegistry" "proxyDisable") {
Write-Log " Disabling WPAD proxy auto-detect" -Level INFO
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" `
-Name "AutoDetect" -Value 0
} else {
Write-Log "proxyDisable feature disabled - skipping" -Level INFO
}
Write-Log "Step 3 complete" -Level OK

View file

@ -0,0 +1,375 @@
<#
.SYNOPSIS
Applies registry settings to the Default User profile and the current logged-in user.
.DESCRIPTION
Loads C:\Users\Default\NTUSER.DAT as a temporary hive (HKU\DefaultProfile), applies
all settings, then unloads it. Every new user account created on this machine inherits
these settings on first logon. The same settings are applied directly to the current
user's HKCU. Does NOT block OneDrive re-launch - the Explorer namespace CLSID and RunOnce entries have been removed.
.ITEMS
taskbar-zarovnat-vlevo-taskbaral-0: TaskbarAl = 0 in Explorer\Advanced. Windows 11 default is center-aligned (TaskbarAl = 1). Left alignment matches Windows 10 muscle memory and is strongly preferred by business users transitioning from Win10.
taskbar-skryt-search-copilot-task-view-w: Hides Search box (SearchboxTaskbarMode=0), Copilot button (ShowCopilotButton=0), Task View (ShowTaskViewButton=0), Widgets (TaskbarDa=0), Chat/Teams (TaskbarMn=0). Reduces taskbar clutter to just pinned apps and running processes.
taskbar-zobrazit-vsechny-ikonky-v-tray-s: Registers scheduled task that sets EnableAutoTray=0 on logon (repeat every 1 min). Windows 11 periodically re-hides tray icons - this task forces all icons visible so users can see VPN status, antivirus, backup, etc.
taskbar-vyprazdnit-pinlist-taskbarlayout: Deploys TaskbarLayoutModification.xml. ProfileType=default: empty pins (clean slate). ProfileType=admin: Explorer+PowerShell+Edge. ProfileType=user: Explorer+Edge. Lock is removed by UnlockStartLayout task 5 min after first boot so users can customize.
explorer-zobrazovat-pripony-souboru-hide: HideFileExt = 0 in Explorer\Advanced. Shows file extensions (.docx, .exe, .pdf, .ps1) in File Explorer. Essential for recognizing file types, avoiding phishing (fake .pdf.exe), and general IT work.
explorer-otevrit-na-this-pc-launchto-1: LaunchTo = 1. File Explorer opens to "This PC" (drives view) instead of Quick Access. More useful on fresh machines where Quick Access history is empty and irrelevant.
start-menu-vyprazdnit-piny-win11: ConfigureStartPins = {"pinnedList":[]} applied via registry. Removes all default Start menu tiles (Edge, Teams, Store, Office, Solitaire, etc.) from the Windows 11 Start grid. User starts with an empty, clean Start menu.
start-menu-zakaz-bing-vyhledavani: DisableSearchBoxSuggestions = 1 in Software\Policies\Microsoft\Windows. Disables web search, Bing suggestions, and online results in Start menu search. Search returns only local apps, files, and settings.
copilot-zakaz-turnoffwindowscopilot-1: TurnOffWindowsCopilot = 1 in SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot. Disables the Windows Copilot sidebar entirely. Not suitable for most client environments (data privacy, AI usage policies).
numlock-zapnout-pri-startu-initialkeyboa: InitialKeyboardIndicators = 2 in Default profile. Ensures NumLock is enabled when Windows starts. Standard expectation for users working with numeric data - prevents confusion on data entry.
accent-barva-na-titulnich-listech-colorp: ColorPrevalence = 1 in Personalize key. Shows the X9.cz accent color (#223B47) on window title bars and borders. Gives all windows a consistent branded appearance.
onedrive-runonce-klic-je-tady-smazat: REMOVED. The RunOnce key deletion and Explorer namespace CLSID removal were deleted - those registry tweaks prevented a freshly installed OneDrive (e.g. for M365) from launching. OneDrive AppX uninstall in step 01 is intentional; blocking re-launch is not.
explorer-showrecent-0-showfrequent-0: ShowRecent=0 and ShowFrequent=0 in HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer. Hides Recent files and Frequent folders from Quick Access. Privacy improvement and cleaner File Explorer on fresh deployments.
explorer-fullpath-1-cabinetstate: FullPath=1 in HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState. Displays the full directory path (e.g. C:\Users\jan\Documents\Projekty) in the File Explorer title bar instead of just the folder name.
#>
param(
[object]$Config,
[string]$LogFile,
[ValidateSet("default","admin","user")]
[string]$ProfileType = "default"
)
$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
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# 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 tweaks (alignment, buttons, tray, layout XML)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "taskbarTweaks") {
Write-Log "Applying taskbar tweaks" -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 all tray 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
$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
}
$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
# Taskbar pinned apps layout (Win10/11)
# ProfileType: default = empty, admin = Explorer+PS+Edge, user = Explorer+Edge
# Note: TaskbarLayoutModification.xml locks the taskbar temporarily.
# UnlockStartLayout scheduled task removes the lock 5 min after first boot
# so users can then customize pins freely.
# Win11 24H2+ may require ProvisionedLayoutModification.xml format instead.
Write-Log " Writing taskbar layout (ProfileType=$ProfileType)" -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
}
$pinList = switch ($ProfileType) {
"admin" {
@'
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
'@
}
"user" {
@'
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
'@
}
default { "" } # empty = clean slate
}
$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>
$pinList
</taskbar:TaskbarPinList>
</defaultlayout:TaskbarLayout>
</CustomTaskbarLayoutCollection>
</LayoutModificationTemplate>
"@
$taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
Write-Log " Taskbar LayoutModification.xml written (profile: $ProfileType)" -Level OK
# NumLock 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
} else {
Write-Log "taskbarTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Start menu tweaks (pins, Bing, Copilot, GameDVR)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "startMenuTweaks") {
Write-Log "Applying Start menu tweaks" -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
# Disable Copilot
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
-Name "TurnOffWindowsCopilot" -Value 1
# Disable GameDVR
Set-ProfileReg -SubKey "System\GameConfigStore" `
-Name "GameDVR_Enabled" -Value 0
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
-Name "AppCaptureEnabled" -Value 0
} else {
Write-Log "startMenuTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Explorer tweaks (file extensions, LaunchTo, ShowRecent, FullPath)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "explorerTweaks") {
Write-Log "Applying Explorer tweaks" -Level STEP
$advPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
# Show file extensions in Explorer
Set-ProfileReg -SubKey $advPath -Name "HideFileExt" -Value 0
# Open Explorer to This PC instead of Quick Access
Set-ProfileReg -SubKey $advPath -Name "LaunchTo" -Value 1
# Hide Recent files from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowRecent" -Value 0
# Hide Frequent folders from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowFrequent" -Value 0
# Show full path in Explorer title bar
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" `
-Name "FullPath" -Value 1
} else {
Write-Log "explorerTweaks feature disabled - skipping" -Level INFO
}
}
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

View file

@ -0,0 +1,190 @@
<#
.SYNOPSIS
Sets system colors, wallpaper, and visual theme.
.DESCRIPTION
Applies X9.cz visual identity: dark taskbar/Start with accent color #223B47
(deep blue-gray), light app mode, no transparency. Wallpaper is set to a solid
color matching the accent. BackInfo.exe (Step 07) overwrites the wallpaper with
a live system info BMP on every logon - solid color is only the fallback.
.ITEMS
system-tema-taskbar-start-dark: SystemUsesLightTheme=0 in Themes\Personalize. Dark mode for shell (taskbar, Start menu, Action Center, notification area). Does NOT affect application windows - those stay light. Reduces eye strain in dim environments.
aplikacni-tema-light: AppsUseLightTheme=1. Application windows (File Explorer, Settings, Calculator, etc.) use white/light backgrounds. Majority of business applications (Office, browsers) also respect this and show light mode.
accent-barva-223b47-tmave-modroseda: AccentColor DWORD = 0xFF473B22 (stored as ABGR: A=FF, B=47, G=3B, R=22). The deep blue-gray #223B47 is the X9.cz brand color, also used as the solid wallpaper background.
accent-barva-na-start-a-taskbaru-ano: ColorPrevalence=1. Applies accent color to taskbar background and Start menu surface. The taskbar becomes the brand color instead of default black, creating a distinct recognizable look on X9.cz-deployed machines.
pruhlednost-vypnuta: EnableTransparency=0. Disables Aero translucency on taskbar and Start. Improves text readability on the taskbar, reduces subtle GPU usage, and looks more professional/consistent on business machines.
tapeta-jednobarevna-223b47-bez-obrazku: Wallpaper set to solid color #223B47 via SystemParametersInfo(SPI_SETDESKWALLPAPER). BackInfo.exe generates a BMP with hostname, username, OS, network info and sets it as wallpaper on every logon. Solid color = fallback only.
#>
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

View file

@ -0,0 +1,205 @@
<#
.SYNOPSIS
Registers logon scheduled tasks to maintain per-user settings that Windows resets.
.DESCRIPTION
Creates scheduled tasks under Task Scheduler that run at user logon (and optionally
on a timer) to enforce settings that Windows tends to revert. Tasks are registered
in the Default profile task store so new user accounts inherit them automatically.
Note: PDF-DefaultApp task has been removed - PDF default is set once during deployment.
.ITEMS
showalltrayicons-pri-logonu-kazdou-1-min: Task 'ShowAllTrayIcons': runs at logon, repeats every 1 minute. Sets HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\EnableAutoTray=0. Windows 11 re-enables auto-hiding of tray icons after updates and sometimes after logon - the 1-min repeat ensures permanent override.
unlockstartlayout-jednou-po-aplikaci-lay: Task 'UnlockStartLayout': runs once, 30 seconds after logon. Clears the Start menu layout lock bit that is set when ConfigureStartPins is applied. Without this, users cannot pin or unpin apps from Start after deployment.
pdf-defaultapp-pri-kazdem-logonu: REMOVED. PDF default is set once during deployment (step 02) with UCPD service stopped. The scheduled task is no longer needed.
#>
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
# -----------------------------------------------------------------------
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

126
scripts/07-backinfo.ps1 Normal file
View file

@ -0,0 +1,126 @@
<#
.SYNOPSIS
Deploys BackInfo.exe to C:\Program Files\Backinfo\ and configures auto-start.
.DESCRIPTION
Copies the BackInfo folder from assets to Program Files, writes the OS name
to the registry (HKLM\SOFTWARE\BackInfo\OSName) so BackInfo can display it,
and creates a Startup shortcut so BackInfo launches on every user logon.
BackInfo renders a BMP wallpaper overlay with hostname, username, OS, HW info,
and network info - configured via BackInfo.ini.
.ITEMS
kopirovat-assets-backinfo-do-program-fil: Copies all files from assets\Backinfo\ to C:\Program Files\Backinfo\. Includes BackInfo.exe, BackInfo.ini, and backinfo_W11.ps1. Creates the target directory if it does not exist.
registry-osname-hklm-software-backinfo: Detects Windows build number and edition, writes OSName string to HKLM\SOFTWARE\BackInfo\OSName (and WOW6432Node). BackInfo.ini references %OSName% to display the correct OS on the wallpaper.
startup-shortcut-backinfo-exe: Creates a shortcut at C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\BackInfo.lnk pointing to C:\Program Files\Backinfo\BackInfo.exe. Ensures BackInfo starts for every user on logon.
07-desktop-info-ps1-smazat-nahrazeno: 07-desktop-info.ps1 is superseded by this script. BackInfo.exe is the preferred approach - stable on Win10 and Win11, configurable via INI, already present in assets.
#>
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
}
# -----------------------------------------------------------------------
# Copy BackInfo assets to Program Files
# -----------------------------------------------------------------------
$assetsBackinfo = Join-Path $PSScriptRoot "..\assets\Backinfo"
$destBackinfo = "C:\Program Files\Backinfo"
Write-Log "Deploying BackInfo to $destBackinfo" -Level INFO
if (-not (Test-Path $assetsBackinfo)) {
Write-Log " Assets not found: $assetsBackinfo" -Level ERROR
exit 1
}
try {
if (-not (Test-Path $destBackinfo)) {
New-Item -ItemType Directory -Path $destBackinfo -Force | Out-Null
}
Copy-Item -Path "$assetsBackinfo\*" -Destination $destBackinfo -Recurse -Force
Write-Log " Copied BackInfo assets to $destBackinfo" -Level OK
}
catch {
Write-Log " Failed to copy BackInfo assets: $_" -Level ERROR
exit 1
}
# -----------------------------------------------------------------------
# Detect OS name and write to registry (BackInfo reads this via %OSName%)
# -----------------------------------------------------------------------
Write-Log "Detecting OS for BackInfo registry" -Level INFO
try {
$cvPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
$cv = Get-ItemProperty -Path $cvPath
$build = [int]$cv.CurrentBuild
$osBase = if ($build -ge 22000) { "Windows 11" } else { "Windows 10" }
$edition = switch ($cv.EditionID) {
"Professional" { "Pro" }
"ProfessionalN" { "Pro N" }
"Core" { "Home" }
"CoreN" { "Home N" }
"Enterprise" { "Enterprise" }
"Education" { "Education" }
default { $cv.EditionID }
}
$osName = "$osBase $edition"
foreach ($regPath in @("HKLM:\SOFTWARE\BackInfo", "HKLM:\SOFTWARE\WOW6432Node\BackInfo")) {
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
Set-ItemProperty -Path $regPath -Name "OSName" -Value $osName -Type String -Force
}
Write-Log " OSName set to: $osName" -Level OK
}
catch {
Write-Log " Failed to set BackInfo registry: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# Create Startup shortcut for all users
# -----------------------------------------------------------------------
Write-Log "Creating BackInfo startup shortcut" -Level INFO
try {
$backInfoExe = "$destBackinfo\BackInfo.exe"
$shortcutPath = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\BackInfo.lnk"
$wsh = New-Object -ComObject WScript.Shell
$shortcut = $wsh.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $backInfoExe
$shortcut.WorkingDirectory = $destBackinfo
$shortcut.Description = "BackInfo system info wallpaper"
$shortcut.Save()
Write-Log " Startup shortcut created: $shortcutPath" -Level OK
}
catch {
Write-Log " Failed to create startup shortcut: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# Launch BackInfo now to render initial wallpaper
# -----------------------------------------------------------------------
Write-Log "Launching BackInfo for initial render" -Level INFO
try {
Start-Process -FilePath "$destBackinfo\BackInfo.exe" -ErrorAction Stop
Write-Log " BackInfo launched" -Level OK
}
catch {
Write-Log " BackInfo launch failed (non-fatal): $_" -Level WARN
}
Write-Log "Step 7 complete" -Level OK

252
scripts/07-desktop-info.ps1 Normal file
View file

@ -0,0 +1,252 @@
<#
.SYNOPSIS
DEPRECATED - delete this script. Replaced by BackInfo.exe.
.DESCRIPTION
Original custom PowerShell approach to render system info onto the desktop wallpaper
using WPF (System.Windows.Media / System.Drawing). Superseded by BackInfo.exe which
is already present in assets/Backinfo/ and handles Win10/Win11 natively.
ACTION REQUIRED: Delete this file. Add a BackInfo deployment step to the master script.
.ITEMS
07-desktop-info-ps1-smazat-stary-pristup: DELETE THIS FILE. The WPF rendering approach had compatibility issues on some Windows editions and required maintaining complex PS rendering code. BackInfo.exe is a mature, stable replacement already bundled in assets/Backinfo/.
zkopirovat-assets-backinfo-do-c-program-: NEW STEP (in master script): Copy assets/Backinfo/ to C:\Program Files\Backinfo\ on the target machine. Includes BackInfo.exe, BackInfo.ini (display config), and backinfo_W11.ps1 (setup helper).
spustit-backinfo-w11-ps1-detekce-os-regi: Run backinfo_W11.ps1 after file copy. Detects Win10 vs Win11, writes the required registry key for wallpaper rendering compatibility, and creates a Startup shortcut in the All Users Startup folder.
backinfo-exe-v-assets-backinfo-k-dispozi: BackInfo.exe reads BackInfo.ini on each run. INI configures: font size and family, position of each info block, which data sources to show (hostname, username, OS version, CPU, RAM, disk, IP address, domain).
backinfo-auto-start-pri-kazdem-logonu-vi: The Startup shortcut created by backinfo_W11.ps1 ensures BackInfo.exe runs on every user logon. It re-reads live system data each time, so the wallpaper BMP always shows current information (username changes, IP changes, etc.).
#>
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
}
# -----------------------------------------------------------------------
# Write the rendering script (runs on every logon as the user)
# Layout: hostname (large bold, centered), then detail lines centered
# -----------------------------------------------------------------------
Write-Log "Writing DesktopInfo render script to $RenderScript" -Level INFO
$renderContent = @'
# DesktopInfo-Render.ps1
# Collects system info and renders it centered on the desktop wallpaper.
# Runs on every user logon via Scheduled Task.
$ErrorActionPreference = "Continue"
$LogFile = "C:\Windows\Setup\Scripts\desktopinfo.log"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
Add-Content -Path $LogFile -Value "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" -Encoding UTF8
}
Write-Log "DesktopInfo render started" -Level INFO
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
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
# -----------------------------------------------------------------------
Write-Log "Collecting system info"
$hostname = $env:COMPUTERNAME
$userDomain = $env:USERDOMAIN
$userName = $env:USERNAME
$loggedUser = if ($userDomain -and $userDomain -ne $hostname) { "$userDomain\$userName" } else { "$hostname\$userName" }
$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$osName = if ($osInfo) { $osInfo.Caption -replace "^Microsoft\s*", "" } else { "Windows" }
$ramGB = if ($osInfo) { [math]::Round($osInfo.TotalVisibleMemorySize / 1024 / 1024, 1) } else { "?" }
$cpuInfo = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
$cpuCount = if ($cpuInfo) { $cpuInfo.NumberOfLogicalProcessors } else { "?" }
$cpuSpeed = if ($cpuInfo) { $cpuInfo.MaxClockSpeed } else { "?" }
$ips = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.PrefixOrigin -ne "WellKnown" } |
Select-Object -ExpandProperty IPAddress) -join ", "
if (-not $ips) { $ips = "N/A" }
$csInfo = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
$domain = if ($csInfo -and $csInfo.PartOfDomain) { $csInfo.Domain } `
elseif ($csInfo -and $csInfo.Workgroup) { $csInfo.Workgroup.ToLower() } `
else { "N/A" }
Write-Log "hostname=$hostname user=$loggedUser os=$osName ram=$($ramGB)GB cpu=${cpuCount}x${cpuSpeed}MHz ips=$ips domain=$domain"
# -----------------------------------------------------------------------
# Screen dimensions
# -----------------------------------------------------------------------
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
$width = if ($screen) { $screen.Bounds.Width } else { 1920 }
$height = if ($screen) { $screen.Bounds.Height } else { 1080 }
Write-Log "screen=${width}x${height}"
# -----------------------------------------------------------------------
# Create bitmap and graphics context
# -----------------------------------------------------------------------
$bmp = New-Object System.Drawing.Bitmap($width, $height)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
$g.Clear([System.Drawing.ColorTranslator]::FromHtml("#556364"))
# -----------------------------------------------------------------------
# Fonts and brushes
# -----------------------------------------------------------------------
$fontName = "Segoe UI"
$fontTitle = New-Object System.Drawing.Font($fontName, 36, [System.Drawing.FontStyle]::Bold)
$fontBold = New-Object System.Drawing.Font($fontName, 14, [System.Drawing.FontStyle]::Bold)
$fontReg = New-Object System.Drawing.Font($fontName, 14, [System.Drawing.FontStyle]::Regular)
$brushWhite = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
$brushGray = New-Object System.Drawing.SolidBrush([System.Drawing.ColorTranslator]::FromHtml("#C8D2D2"))
# -----------------------------------------------------------------------
# Lines: text, font, brush
# -----------------------------------------------------------------------
$texts = @(
$hostname
"Logged on user: $loggedUser"
"OS: $osName"
"CPU: $cpuCount at $cpuSpeed MHz RAM: $($ramGB)GB"
"IPv4 address: $ips Machine domain: $domain"
)
$fonts = @( $fontTitle, $fontReg, $fontBold, $fontReg, $fontReg )
$brushes = @( $brushWhite, $brushGray, $brushGray, $brushGray, $brushGray )
# -----------------------------------------------------------------------
# Measure total block height, then center vertically
# -----------------------------------------------------------------------
$lineSpacing = 8
$heights = @()
for ($i = 0; $i -lt $texts.Count; $i++) {
$heights += [int]($g.MeasureString($texts[$i], $fonts[$i]).Height)
}
$totalH = ($heights | Measure-Object -Sum).Sum + $lineSpacing * ($texts.Count - 1)
$currentY = [int](($height - $totalH) / 2)
# -----------------------------------------------------------------------
# Draw each line centered horizontally
# -----------------------------------------------------------------------
for ($i = 0; $i -lt $texts.Count; $i++) {
$sz = $g.MeasureString($texts[$i], $fonts[$i])
$x = [int](($width - $sz.Width) / 2)
$g.DrawString($texts[$i], $fonts[$i], $brushes[$i], [float]$x, [float]$currentY)
$currentY += $heights[$i] + $lineSpacing
}
$g.Dispose()
# -----------------------------------------------------------------------
# Save and set as wallpaper
# -----------------------------------------------------------------------
$bmpPath = "C:\Windows\Setup\Scripts\desktopinfo.bmp"
Write-Log "Saving BMP: $bmpPath"
$bmp.Save($bmpPath, [System.Drawing.Imaging.ImageFormat]::Bmp)
$bmp.Dispose()
# Clear Windows wallpaper cache so it reloads from our BMP
# Without this, Windows reuses TranscodedWallpaper and ignores the updated file
$transcodedPath = "$env:APPDATA\Microsoft\Windows\Themes\TranscodedWallpaper"
if (Test-Path $transcodedPath) {
Remove-Item $transcodedPath -Force -ErrorAction SilentlyContinue
Write-Log "Cleared TranscodedWallpaper cache"
}
# SPI_SETDESKTOPWALLPAPER=20, SPIF_UPDATEINIFILE|SPIF_SENDCHANGE=3
$result = [WallpaperApi]::SystemParametersInfo(20, 0, $bmpPath, 3)
Write-Log "SystemParametersInfo result: $result"
Write-Log "DesktopInfo render complete" -Level INFO
'@
$renderContent | Set-Content -Path $RenderScript -Encoding UTF8 -Force
Write-Log "Render script written" -Level OK
# -----------------------------------------------------------------------
# Store deployment date in registry (used for reference)
# -----------------------------------------------------------------------
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
$trigger.Delay = "PT20S" # wait for network to be available
$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

146
scripts/08-activation.ps1 Normal file
View file

@ -0,0 +1,146 @@
<#
.SYNOPSIS
Activates Windows using a product key from config or KMS GVLK fallback.
.DESCRIPTION
Checks if Windows is already activated (LicenseStatus = 1). If not, reads the
product key from config.json activation.productKey. If no key is present, selects
the appropriate GVLK for the detected Windows edition and activates via KMS.
Optionally configures a specific KMS server if activation.kmsServer is set.
.ITEMS
oa3-bios-uefi-klic-kontrola-embedded-ke: Checks for OA3 embedded product key in BIOS/UEFI firmware via SoftwareLicensingService.OA3xOriginalProductKey WMI query. If a key is found, it is installed via slmgr /ipk and activation is attempted. Most OEM machines (since Win8 OA3) have a digital entitlement key in firmware - this path handles them without requiring a key in config.json.
klic-z-config-json-activation-productkey: Reads activation.productKey from config.json. Installs via slmgr.vbs /ipk <key> and activates via slmgr.vbs /ato. Supports MAK (Multiple Activation Key) for volume licensing without KMS, and retail keys. Takes priority over GVLK fallback.
fallback-na-gvlk-kms-client-key-dle-edic: When no key is in config, detects Windows edition via (Get-WmiObject SoftwareLicensingProduct).Name and maps to Microsoft's published GVLK table. Pro: W269N-WFGWX-YVC9B-4J6C9-T83GX, Enterprise: NPPR9-FWDCX-D2C8J-H872K-2YT43, Home: TX9XD-98N7V-6WMQ6-BX7FG-H8Q99.
volitelny-kms-server-activation-kmsserve: If activation.kmsServer is in config.json, runs slmgr.vbs /skms <server>:<port> before /ato. Used for clients with on-premises KMS infrastructure (common in larger organizations with volume licensing).
preskocit-pokud-jiz-aktivovano: Queries Win32_WindowsLicenseStatus or SoftwareLicensingProduct to check LicenseStatus. Value 1 = Licensed (fully activated). Script skips activation attempt and logs "Windows already activated" to avoid unnecessary slmgr calls.
typ-klice-mak-vs-kms-vs-retail: Key type selection depends on client's Microsoft licensing: MAK = volume license key activates online against Microsoft (limited activations), KMS = requires KMS server on network (VLSC subscription), Retail = individual license from Microsoft Store or OEM.
#>
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 for OA3 embedded BIOS/UEFI product key
# Most OEM machines since Win8 OA3 have a product key embedded in firmware.
# -----------------------------------------------------------------------
Write-Log "Checking for OA3 embedded BIOS/UEFI product key" -Level INFO
$oa3Key = (Get-CimInstance -ClassName SoftwareLicensingService -ErrorAction SilentlyContinue).OA3xOriginalProductKey
if ($oa3Key -and $oa3Key.Trim() -ne '') {
$maskedKey = $oa3Key.Substring(0, [Math]::Min(5, $oa3Key.Length)) + "-XXXXX-XXXXX-XXXXX-XXXXX"
Write-Log " OA3 key found in firmware: $maskedKey" -Level OK
} else {
Write-Log " No OA3 key found in firmware" -Level INFO
$oa3Key = $null
}
# -----------------------------------------------------------------------
# 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
# Key priority: config.json > OA3 firmware > GVLK
$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 (highest priority)
$keyToUse = $customKey
Write-Log " Using product key from config" -Level INFO
} elseif ($oa3Key) {
# Use OA3 key from firmware
$keyToUse = $oa3Key
Write-Log " Using OA3 key from firmware" -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

137
scripts/09-pc-identity.ps1 Normal file
View file

@ -0,0 +1,137 @@
<#
.SYNOPSIS
Sets PC identity: computer name, description, and creates C:\X9 folder structure.
.DESCRIPTION
Renames the computer if deployment.pcName is set in config.json. Sets the
computer description (visible in System properties and network neighborhood).
Creates C:\X9\ directory structure with subdirectories for logs, scripts and
assets. Copies X9 icon and creates Desktop.ini so the folder shows a custom
icon in Explorer. Computer rename requires a restart - this step runs last
before the final summary.
.ITEMS
rename-computer-dle-config-deployment-pcn: Renames the computer via Rename-Computer if config.json deployment.pcName is set and differs from the current name. Rename takes effect after restart. If pcName is empty, rename is skipped and the current name is preserved.
popis-pocitace-computer-description: Sets the computer description shown in System Properties and Network Neighborhood. Read from config.json deployment.pcDescription, default "X9 deployment". Written to HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters\SrvComment.
vytvorit-cx9-adresar: Creates C:\X9\ with subdirectories Logs\, Scripts\, Assets\. Used for deployment logs, custom per-client scripts, and client-specific configuration assets.
cx9-vlastni-ikonka-desktop-ini: Copies X9-ikona.ico to C:\X9\ and creates Desktop.ini with IconResource entry. Sets System+Hidden attributes on Desktop.ini and ReadOnly on C:\X9\ so Explorer displays the custom folder icon.
#>
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
}
# -----------------------------------------------------------------------
# C:\X9 directory structure
# -----------------------------------------------------------------------
Write-Log "Creating C:\X9 directory structure" -Level INFO
$x9Root = "C:\X9"
$x9Dirs = @("$x9Root\Logs", "$x9Root\Scripts", "$x9Root\Assets")
foreach ($dir in $x9Dirs) {
try {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Write-Log " Dir: $dir" -Level OK
}
catch {
Write-Log " Failed to create $dir - $_" -Level ERROR
}
}
# -----------------------------------------------------------------------
# Copy X9 icon and create Desktop.ini for custom folder appearance
# -----------------------------------------------------------------------
$assetsLogo = Join-Path $PSScriptRoot "..\assets\Logo"
$icoSrc = Get-ChildItem -Path $assetsLogo -Filter "*.ico" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($icoSrc) {
$icoDest = "$x9Root\X9-ikona.ico"
try {
Copy-Item -Path $icoSrc.FullName -Destination $icoDest -Force
Write-Log " Copied icon: $icoDest" -Level OK
}
catch {
Write-Log " Failed to copy icon: $_" -Level WARN
}
$desktopIni = "$x9Root\desktop.ini"
try {
@"
[.ShellClassInfo]
IconResource=X9-ikona.ico,0
[ViewState]
Mode=
Vid=
FolderType=Generic
"@ | Set-Content -Path $desktopIni -Encoding Unicode -Force
# desktop.ini must be System+Hidden; folder must be ReadOnly for Explorer to show the icon
(Get-Item $desktopIni -Force).Attributes = "System,Hidden"
(Get-Item $x9Root).Attributes = "ReadOnly,Directory"
Write-Log " Desktop.ini created for custom folder icon" -Level OK
}
catch {
Write-Log " Failed to create desktop.ini: $_" -Level WARN
}
} else {
Write-Log " No .ico found in assets\Logo - custom folder icon skipped" -Level WARN
}
# -----------------------------------------------------------------------
# Computer description
# -----------------------------------------------------------------------
$pcDesc = "X9 deployment"
if ($Config -and $Config.deployment -and $Config.deployment.pcDescription) {
$pcDesc = $Config.deployment.pcDescription
}
Write-Log "Setting computer description: $pcDesc" -Level INFO
try {
Set-ItemProperty `
-Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" `
-Name "SrvComment" -Value $pcDesc -Type String -Force
Write-Log " Computer description set" -Level OK
}
catch {
Write-Log " Failed to set computer description: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# Rename computer (must be last - requires restart to take effect)
# -----------------------------------------------------------------------
$pcName = $null
if ($Config -and $Config.deployment -and $Config.deployment.pcName) {
$pcName = $Config.deployment.pcName.Trim()
}
if ($pcName -and $pcName -ne "") {
$currentName = $env:COMPUTERNAME
if ($currentName -eq $pcName) {
Write-Log "Computer name already '$pcName' - no rename needed" -Level OK
} else {
Write-Log "Renaming computer: '$currentName' -> '$pcName'" -Level INFO
try {
Rename-Computer -NewName $pcName -Force -ErrorAction Stop
Write-Log " Computer renamed to '$pcName' (restart required)" -Level OK
}
catch {
Write-Log " Failed to rename computer: $_" -Level ERROR
}
}
} else {
Write-Log "No pcName in config - computer rename skipped" -Level INFO
}
Write-Log "Step 9 complete" -Level OK

111
scripts/10-network.ps1 Normal file
View file

@ -0,0 +1,111 @@
<#
.SYNOPSIS
Sets network profile to Private, enables ping, and enables Network Discovery.
.DESCRIPTION
Sets all connected network adapter profiles from Public to Private. Private
profile enables file sharing, network discovery, and other LAN features.
Enables ICMP echo (ping) via Windows Firewall for diagnostic purposes.
Enables the Network Discovery firewall rule group for the Private profile
so this PC is visible to other computers on the local network.
.ITEMS
nastavit-sitovy-profil-private: Sets all connected network profiles to Private via Set-NetConnectionProfile. Public profile blocks most LAN features. Private is required for file sharing, printer sharing, and network discovery. Applied to all currently connected adapters.
povolit-ping-icmp-firewall: Enables "File and Printer Sharing (Echo Request)" firewall rules for ICMPv4 and ICMPv6. ICMP echo is disabled by default on clean Windows. Required for network diagnostics, monitoring tools, and basic connectivity verification.
zapnout-network-discovery: Enables the Network Discovery firewall rule group (FPS-NB_Name-In-UDP, LLMNR, etc.) for Private and Domain profiles via Set-NetFirewallRule. Allows this PC to appear in Network Neighborhood and browse other machines.
#>
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
}
# -----------------------------------------------------------------------
# Set network profiles to Private
# -----------------------------------------------------------------------
Write-Log "Setting network profiles to Private" -Level INFO
try {
$profiles = Get-NetConnectionProfile -ErrorAction Stop
foreach ($profile in $profiles) {
if ($profile.NetworkCategory -ne "Private") {
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex `
-NetworkCategory Private -ErrorAction SilentlyContinue
Write-Log " $($profile.Name): Public -> Private" -Level OK
} else {
Write-Log " $($profile.Name): already Private" -Level INFO
}
}
}
catch {
Write-Log " Failed to set network profiles: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# Enable ICMP echo (ping) - ICMPv4 and ICMPv6
# -----------------------------------------------------------------------
Write-Log "Enabling ICMP echo (ping)" -Level INFO
$icmpRules = @(
"FPS-ICMP4-ERQ-In", # File and Printer Sharing (Echo Request - ICMPv4-In)
"FPS-ICMP6-ERQ-In", # File and Printer Sharing (Echo Request - ICMPv6-In)
"CoreNet-ICMP4-DU-In",
"CoreNet-ICMP6-DU-In"
)
foreach ($rule in $icmpRules) {
try {
$r = Get-NetFirewallRule -Name $rule -ErrorAction SilentlyContinue
if ($r) {
Enable-NetFirewallRule -Name $rule -ErrorAction SilentlyContinue
Write-Log " Enabled: $rule" -Level OK
}
}
catch {
Write-Log " Rule not found or error: $rule - $_" -Level WARN
}
}
# Also enable by display name for robustness across Windows versions
try {
Get-NetFirewallRule -DisplayGroup "File and Printer Sharing" -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like "*Echo*" } |
Enable-NetFirewallRule -ErrorAction SilentlyContinue
Write-Log " Enabled File and Printer Sharing Echo rules" -Level OK
}
catch {
Write-Log " Could not enable Echo rules via DisplayGroup: $_" -Level WARN
}
# -----------------------------------------------------------------------
# Enable Network Discovery firewall rules
# -----------------------------------------------------------------------
Write-Log "Enabling Network Discovery" -Level INFO
try {
# Enable all Network Discovery rules for Private profile
Get-NetFirewallRule -DisplayGroup "Network Discovery" -ErrorAction Stop |
Where-Object { $_.Profile -match "Private|Any" } |
Enable-NetFirewallRule -ErrorAction SilentlyContinue
Write-Log " Network Discovery rules enabled (Private)" -Level OK
}
catch {
Write-Log " Failed to enable Network Discovery rules: $_" -Level ERROR
}
# Enable via netsh as fallback (covers older Windows builds)
$netshResult = & netsh advfirewall firewall set rule group="Network Discovery" new enable=Yes 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " Network Discovery enabled via netsh" -Level OK
} else {
Write-Log " netsh Network Discovery: $netshResult" -Level WARN
}
Write-Log "Step 10 complete" -Level OK

152
scripts/11-dell-update.ps1 Normal file
View file

@ -0,0 +1,152 @@
<#
.SYNOPSIS
Detects Dell hardware, installs Dell Command | Update, and applies all available updates.
.DESCRIPTION
Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without
error so the same deployment script works on any hardware. On Dell machines, installs
Dell Command | Update (Universal) via winget and immediately runs /applyUpdates with
-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are
staged at this point and finalize automatically during the restart that closes the
deployment. The operator does not need to run a separate update pass after setup.
.ITEMS
detekce-dell-hw-win32-computersystem: Reads Win32_ComputerSystem.Manufacturer. If the string does not contain "Dell", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.
instalace-dell-command-update-via-winget: Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.
spusteni-vsech-aktualizaci-drivery-firmware-bios: Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.
bios-firmware-staging-reboot: BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed.
#>
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
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 }
}
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Detect Dell hardware
# -----------------------------------------------------------------------
Write-Log "Checking hardware manufacturer" -Level INFO
try {
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
$manufacturer = $cs.Manufacturer
$model = $cs.Model
Write-Log " Manufacturer: $manufacturer Model: $model" -Level INFO
}
catch {
Write-Log " Failed to query Win32_ComputerSystem: $_" -Level ERROR
return
}
if ($manufacturer -notmatch "Dell") {
Write-Log "Not a Dell machine ($manufacturer) - step skipped" -Level WARN
return
}
Write-Log "Dell confirmed: $model" -Level OK
# -----------------------------------------------------------------------
# Install Dell Command | Update via winget
# -----------------------------------------------------------------------
Write-Log "Installing Dell Command | Update (Universal)..." -Level STEP
$wingetArgs = @(
"install",
"--id", "Dell.CommandUpdate.Universal",
"--silent",
"--accept-package-agreements",
"--accept-source-agreements"
)
$wingetOutput = & winget @wingetArgs 2>&1
$wingetExit = $LASTEXITCODE
$wingetOutput | ForEach-Object { Write-Log " [winget] $_" -Level INFO }
if ($wingetExit -ne 0 -and $wingetExit -ne 1638) { # 1638 = already installed
Write-Log " winget exit code $wingetExit - checking if DCU is already present" -Level WARN
}
# Locate dcu-cli.exe (path is the same for x64 and Universal edition)
$dcuCandidates = @(
"C:\Program Files\Dell\CommandUpdate\dcu-cli.exe",
"C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe"
)
$dcuCli = $dcuCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $dcuCli) {
Write-Log " dcu-cli.exe not found - cannot run updates" -Level ERROR
return
}
Write-Log " dcu-cli.exe found: $dcuCli" -Level OK
# -----------------------------------------------------------------------
# Run updates - categories controlled by feature flags
# -reboot=disable -> no mid-deployment reboot; BIOS/firmware staged for next restart
# -----------------------------------------------------------------------
$runDrivers = Get-Feature $Config "dellUpdate" "drivers"
$runBios = Get-Feature $Config "dellUpdate" "bios"
if (-not $runDrivers -and -not $runBios) {
Write-Log "Both drivers and bios features disabled - skipping update run" -Level INFO
} else {
# Build update type list from enabled features
$updateTypes = @()
if ($runDrivers) {
$updateTypes += "driver"
$updateTypes += "firmware"
}
if ($runBios) {
$updateTypes += "bios"
}
$updateTypeArg = $updateTypes -join ","
Write-Log "Running Dell Command | Update (updateType=$updateTypeArg, no auto-reboot)..." -Level STEP
Write-Log " This may take several minutes depending on available updates" -Level INFO
$dcuOutput = & $dcuCli /applyUpdates -silent -reboot=disable "-updateType=$updateTypeArg" 2>&1
$exitCode = $LASTEXITCODE
$dcuOutput | ForEach-Object { Write-Log " [DCU] $_" -Level INFO }
Write-Log " DCU exit code: $exitCode" -Level INFO
# Dell Command | Update exit codes:
# 0 = completed, no updates required or updates applied (no reboot needed)
# 1 = updates applied, reboot required to finalize BIOS/firmware
# 5 = no applicable updates found for this system
# others = error or partial failure
switch ($exitCode) {
0 { Write-Log "Dell Command | Update: complete (no reboot required)" -Level OK }
1 { Write-Log "Dell Command | Update: updates staged - BIOS/firmware will finalize on restart" -Level OK }
5 { Write-Log "Dell Command | Update: no applicable updates for this model" -Level OK }
default { Write-Log "Dell Command | Update: exit code $exitCode - review DCU log in C:\ProgramData\Dell\UpdateService\Logs" -Level WARN }
}
}
Write-Log "Step 11 complete" -Level OK

157
setup.ps1 Normal file
View file

@ -0,0 +1,157 @@
# setup.ps1 - Claude Code bootstrap for Windows
# Usage:
# irm https://gist.githubusercontent.com/YOUR_GIST_URL/raw/setup.ps1 | iex
#
# Or with parameters (paste as one line):
# $env:CC_API_KEY="sk-ant-..."; $env:CC_REPO="https://github.com/org/repo"; irm https://gist.../raw/setup.ps1 | iex
$ErrorActionPreference = "Stop"
function Write-Step { param([string]$Msg) Write-Host "[SETUP] $Msg" -ForegroundColor Cyan }
function Write-OK { param([string]$Msg) Write-Host " OK: $Msg" -ForegroundColor Green }
function Write-Fail { param([string]$Msg) Write-Host " ERR: $Msg" -ForegroundColor Red; exit 1 }
Write-Host ""
Write-Host " Claude Code Bootstrap - X9.cz" -ForegroundColor Cyan
Write-Host " ==============================" -ForegroundColor Cyan
Write-Host ""
# ------------------------------------------------------------
# API KEY
# ------------------------------------------------------------
$apiKey = $env:CC_API_KEY
if (-not $apiKey) {
$apiKey = Read-Host "Enter Anthropic API key (sk-ant-...)"
}
if (-not $apiKey -or -not $apiKey.StartsWith("sk-")) {
Write-Fail "Invalid API key"
}
# ------------------------------------------------------------
# REPO URL
# ------------------------------------------------------------
$repoUrl = $env:CC_REPO
if (-not $repoUrl) {
$repoUrl = Read-Host "Enter repo URL (https://github.com/org/repo)"
}
if (-not $repoUrl) {
Write-Fail "No repo URL provided"
}
$workDir = if ($env:CC_WORKDIR) { $env:CC_WORKDIR } else { "$HOME\Projects" }
# ------------------------------------------------------------
# NODE.JS
# ------------------------------------------------------------
Write-Step "Checking Node.js..."
$nodeOk = $false
try {
$nodeVer = & node --version 2>$null
if ($nodeVer -match 'v(\d+)' -and [int]$Matches[1] -ge 18) {
Write-OK "Node.js $nodeVer"
$nodeOk = $true
}
} catch {}
if (-not $nodeOk) {
Write-Step "Installing Node.js via winget..."
try {
winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements --silent
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
Write-OK "Node.js installed"
} catch {
Write-Fail "Node.js install failed. Install manually: https://nodejs.org"
}
}
# ------------------------------------------------------------
# GIT
# ------------------------------------------------------------
Write-Step "Checking Git..."
$gitOk = $false
try { git --version | Out-Null; $gitOk = $true; Write-OK "Git available" } catch {}
if (-not $gitOk) {
Write-Step "Installing Git via winget..."
try {
winget install Git.Git --accept-package-agreements --accept-source-agreements --silent
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
Write-OK "Git installed"
} catch {
Write-Fail "Git install failed. Install manually: https://git-scm.com"
}
}
# ------------------------------------------------------------
# CLAUDE CODE
# ------------------------------------------------------------
Write-Step "Checking Claude Code..."
$ccOk = $false
try { $ccVer = claude --version 2>$null; Write-OK "Claude Code $ccVer"; $ccOk = $true } catch {}
if (-not $ccOk) {
Write-Step "Installing Claude Code..."
try {
npm install -g @anthropic-ai/claude-code
Write-OK "Claude Code installed"
} catch {
Write-Fail "Claude Code install failed"
}
}
# ------------------------------------------------------------
# API KEY - ulozit
# ------------------------------------------------------------
Write-Step "Saving API key..."
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $apiKey, "User")
$env:ANTHROPIC_API_KEY = $apiKey
# Vymazat z env aby nezustal v historii procesu
Remove-Item Env:\CC_API_KEY -ErrorAction SilentlyContinue
Write-OK "API key saved"
# ------------------------------------------------------------
# CLONE / PULL REPO
# ------------------------------------------------------------
Write-Step "Setting up repository..."
New-Item -ItemType Directory -Path $workDir -Force | Out-Null
$repoName = ($repoUrl -split '/')[-1] -replace '\.git$', ''
$targetPath = Join-Path $workDir $repoName
if (Test-Path (Join-Path $targetPath ".git")) {
Write-Step "Repo exists, pulling latest..."
Push-Location $targetPath
git pull
Pop-Location
} else {
git clone $repoUrl $targetPath
Write-OK "Cloned to $targetPath"
}
# ------------------------------------------------------------
# CLEAN PS HISTORY - odstranit radky s API key
# ------------------------------------------------------------
try {
$histPath = (Get-PSReadlineOption).HistorySavePath
if ($histPath -and (Test-Path $histPath)) {
$clean = Get-Content $histPath | Where-Object { $_ -notmatch 'sk-ant-|CC_API_KEY|ANTHROPIC' }
$clean | Set-Content $histPath
}
} catch {}
# ------------------------------------------------------------
# LAUNCH
# ------------------------------------------------------------
Write-Host ""
Write-Host " ==============================" -ForegroundColor Green
Write-Host " Ready! Repo: $targetPath" -ForegroundColor Green
Write-Host " ==============================" -ForegroundColor Green
Write-Host ""
Set-Location $targetPath
claude

214
setup.sh Executable file
View file

@ -0,0 +1,214 @@
#!/usr/bin/env bash
# setup.sh - Claude Code bootstrap for macOS / Linux
# Usage:
# curl -fsSL https://gist.githubusercontent.com/YOUR_GIST_URL/raw/setup.sh | bash
#
# Or with parameters:
# CC_API_KEY="sk-ant-..." CC_REPO="https://github.com/org/repo" bash <(curl -fsSL https://gist.../raw/setup.sh)
set -e
CYAN='\033[0;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
GRAY='\033[0;90m'
NC='\033[0m'
step() { echo -e "\n${CYAN}[SETUP] $1${NC}"; }
ok() { echo -e " ${GREEN}OK: $1${NC}"; }
fail() { echo -e " ${RED}ERR: $1${NC}"; exit 1; }
skip() { echo -e " ${GRAY}SKIP: $1${NC}"; }
echo ""
echo -e "${CYAN} Claude Code Bootstrap - X9.cz"
echo -e " ==============================${NC}"
echo ""
# ------------------------------------------------------------
# API KEY
# ------------------------------------------------------------
API_KEY="${CC_API_KEY:-}"
if [ -z "$API_KEY" ]; then
read -rsp "Enter Anthropic API key (sk-ant-...): " API_KEY
echo ""
fi
if [[ ! "$API_KEY" == sk-* ]]; then
fail "Invalid API key"
fi
# ------------------------------------------------------------
# REPO URL
# ------------------------------------------------------------
REPO_URL="${CC_REPO:-}"
if [ -z "$REPO_URL" ]; then
read -rp "Enter repo URL (https://github.com/org/repo): " REPO_URL
fi
if [ -z "$REPO_URL" ]; then
fail "No repo URL provided"
fi
WORK_DIR="${CC_WORKDIR:-$HOME/Projects}"
# ------------------------------------------------------------
# DETECT OS
# ------------------------------------------------------------
OS="$(uname -s)"
step "Detected OS: $OS"
# ------------------------------------------------------------
# NODE.JS
# ------------------------------------------------------------
step "Checking Node.js..."
node_ok=false
if command -v node &>/dev/null; then
NODE_VER=$(node --version | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VER" -ge 18 ]; then
ok "Node.js $(node --version)"
node_ok=true
fi
fi
if [ "$node_ok" = false ]; then
step "Installing Node.js..."
case "$OS" in
Darwin)
if command -v brew &>/dev/null; then
brew install node
else
step "Installing Homebrew first..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node
fi
;;
Linux)
# Node via NodeSource
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
if command -v apt-get &>/dev/null; then
sudo apt-get install -y nodejs
elif command -v dnf &>/dev/null; then
sudo dnf install -y nodejs
elif command -v yum &>/dev/null; then
sudo yum install -y nodejs
else
fail "Cannot detect package manager. Install Node.js manually: https://nodejs.org"
fi
;;
*)
fail "Unsupported OS: $OS. Install Node.js manually: https://nodejs.org"
;;
esac
ok "Node.js installed"
fi
# ------------------------------------------------------------
# GIT
# ------------------------------------------------------------
step "Checking Git..."
if command -v git &>/dev/null; then
ok "Git available"
else
step "Installing Git..."
case "$OS" in
Darwin) brew install git ;;
Linux)
if command -v apt-get &>/dev/null; then sudo apt-get install -y git
elif command -v dnf &>/dev/null; then sudo dnf install -y git
elif command -v yum &>/dev/null; then sudo yum install -y git
else fail "Cannot install Git automatically"
fi
;;
esac
ok "Git installed"
fi
# ------------------------------------------------------------
# CLAUDE CODE
# ------------------------------------------------------------
step "Checking Claude Code..."
if command -v claude &>/dev/null; then
ok "Claude Code $(claude --version)"
else
step "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code
ok "Claude Code installed"
fi
# ------------------------------------------------------------
# API KEY - ulozit
# ------------------------------------------------------------
step "Saving API key..."
SHELL_RC=""
case "$SHELL" in
*/zsh) SHELL_RC="$HOME/.zshrc" ;;
*/bash)
if [ "$OS" = "Darwin" ]; then
SHELL_RC="$HOME/.bash_profile"
else
SHELL_RC="$HOME/.bashrc"
fi
;;
*) SHELL_RC="$HOME/.profile" ;;
esac
# Odstrante stary zaznam pokud existuje
if [ -f "$SHELL_RC" ]; then
grep -v 'ANTHROPIC_API_KEY' "$SHELL_RC" > "${SHELL_RC}.tmp" && mv "${SHELL_RC}.tmp" "$SHELL_RC"
fi
echo "export ANTHROPIC_API_KEY=\"$API_KEY\"" >> "$SHELL_RC"
export ANTHROPIC_API_KEY="$API_KEY"
unset CC_API_KEY
ok "API key saved to $SHELL_RC"
# ------------------------------------------------------------
# CLONE / PULL REPO
# ------------------------------------------------------------
step "Setting up repository..."
mkdir -p "$WORK_DIR"
REPO_NAME=$(basename "$REPO_URL" .git)
TARGET_PATH="$WORK_DIR/$REPO_NAME"
if [ -d "$TARGET_PATH/.git" ]; then
step "Repo exists, pulling latest..."
git -C "$TARGET_PATH" pull
else
git clone "$REPO_URL" "$TARGET_PATH"
ok "Cloned to $TARGET_PATH"
fi
# ------------------------------------------------------------
# CLEAN SHELL HISTORY - odstranit radky s API key
# ------------------------------------------------------------
HIST_FILE="${HISTFILE:-$HOME/.bash_history}"
if [ -f "$HIST_FILE" ]; then
grep -v 'sk-ant-\|CC_API_KEY\|ANTHROPIC' "$HIST_FILE" > "${HIST_FILE}.tmp" && mv "${HIST_FILE}.tmp" "$HIST_FILE"
fi
# zsh history
ZSH_HIST="$HOME/.zsh_history"
if [ -f "$ZSH_HIST" ]; then
grep -v 'sk-ant-\|CC_API_KEY\|ANTHROPIC' "$ZSH_HIST" > "${ZSH_HIST}.tmp" && mv "${ZSH_HIST}.tmp" "$ZSH_HIST"
fi
# ------------------------------------------------------------
# LAUNCH
# ------------------------------------------------------------
echo ""
echo -e "${GREEN} =============================="
echo -e " Ready! Repo: $TARGET_PATH"
echo -e " ==============================${NC}"
echo ""
echo -e " Run: ${CYAN}source $SHELL_RC && cd $TARGET_PATH && claude${NC}"
echo ""
# Pokud je skript spusten primo (ne pres pipe), rovnou spustit
if [ -t 0 ]; then
cd "$TARGET_PATH"
# shellcheck disable=SC1090
source "$SHELL_RC"
claude
fi

265
tests/Test-Deployment.ps1 Normal file
View file

@ -0,0 +1,265 @@
#Requires -RunAsAdministrator
# Post-deployment verification script.
# Checks that all deployment steps completed correctly.
# Outputs a pass/fail report.
$ErrorActionPreference = "Continue"
$PassCount = 0
$FailCount = 0
$WarnCount = 0
function Test-Check {
param(
[string]$Name,
[scriptblock]$Check,
[switch]$WarnOnly
)
try {
$result = & $Check
if ($result) {
Write-Host " [PASS] $Name" -ForegroundColor Green
$script:PassCount++
} else {
if ($WarnOnly) {
Write-Host " [WARN] $Name" -ForegroundColor Yellow
$script:WarnCount++
} else {
Write-Host " [FAIL] $Name" -ForegroundColor Red
$script:FailCount++
}
}
}
catch {
Write-Host " [FAIL] $Name (exception: $_)" -ForegroundColor Red
$script:FailCount++
}
}
function Get-RegValue {
param([string]$Path, [string]$Name)
try {
return (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop).$Name
}
catch { return $null }
}
Write-Host ""
Write-Host "========================================"
Write-Host " Deployment Verification"
Write-Host " Computer: $env:COMPUTERNAME"
Write-Host " Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
Write-Host "========================================"
# -----------------------------------------------------------------------
# Log file
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Log ---"
Test-Check "Deploy.log exists" {
Test-Path "C:\Windows\Setup\Scripts\Deploy.log"
}
# -----------------------------------------------------------------------
# Admin account
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Admin account ---"
Test-Check "Account adminx9 exists" {
Get-LocalUser -Name "adminx9" -ErrorAction SilentlyContinue
}
Test-Check "Account adminx9 is enabled" {
(Get-LocalUser -Name "adminx9" -ErrorAction SilentlyContinue).Enabled -eq $true
}
Test-Check "Account adminx9 in Administrators" {
$adminsGroup = (Get-LocalGroup | Where-Object { $_.SID -eq "S-1-5-32-544" }).Name
Get-LocalGroupMember -Group $adminsGroup -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "*adminx9" }
}
Test-Check "Account adminx9 hidden from login screen" {
$specialPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
(Get-ItemProperty -Path $specialPath -Name "adminx9" -ErrorAction SilentlyContinue).adminx9 -eq 0
}
# -----------------------------------------------------------------------
# Activation
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Activation ---"
Test-Check "Windows activated" {
$status = (Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND Name LIKE 'Windows%'" -ErrorAction SilentlyContinue |
Select-Object -First 1).LicenseStatus
$status -eq 1
} -WarnOnly
# -----------------------------------------------------------------------
# Software
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Software ---"
Test-Check "7-Zip installed" {
(Get-AppxPackage -Name "7zip.7zip" -ErrorAction SilentlyContinue) -or
(Test-Path "${env:ProgramFiles}\7-Zip\7z.exe") -or
(Test-Path "${env:ProgramFiles(x86)}\7-Zip\7z.exe")
}
Test-Check "Adobe Acrobat installed" {
(Test-Path "$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe") -or
(Test-Path "${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe") -or
(Test-Path "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe") -or
(Test-Path "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe")
}
Test-Check "OpenVPN Connect installed" {
(Test-Path "$env:ProgramFiles\OpenVPN Connect\OpenVPNConnect.exe") -or
(Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" `
-ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "OpenVPN*" })
} -WarnOnly
# -----------------------------------------------------------------------
# Bloatware
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Bloatware removal ---"
$bloatwareToCheck = @(
"Microsoft.549981C3F5F10" # Cortana
"Microsoft.BingNews"
"MicrosoftTeams"
"Microsoft.XboxApp"
"Microsoft.YourPhone"
"Microsoft.ZuneMusic"
"Microsoft.GamingApp"
)
foreach ($pkg in $bloatwareToCheck) {
Test-Check "Removed: $pkg" {
$installed = Get-AppxPackage -Name $pkg -AllUsers -ErrorAction SilentlyContinue
-not $installed
} -WarnOnly
}
Test-Check "Calculator kept" {
Get-AppxPackage -Name "Microsoft.WindowsCalculator" -AllUsers -ErrorAction SilentlyContinue
}
# -----------------------------------------------------------------------
# System registry (HKLM)
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- System registry ---"
Test-Check "BypassNRO set" {
(Get-RegValue "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" "BypassNRO") -eq 1
}
Test-Check "Teams auto-install disabled" {
(Get-RegValue "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" "ConfigureChatAutoInstall") -eq 0
}
Test-Check "Widgets disabled" {
(Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" "AllowNewsAndInterests") -eq 0
}
Test-Check "Edge First Run hidden" {
(Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Edge" "HideFirstRunExperience") -eq 1
}
Test-Check "OneDrive disabled via policy" {
(Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive" "DisableFileSyncNGSC") -eq 1
}
Test-Check "GameDVR disabled" {
(Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" "AllowGameDVR") -eq 0
}
Test-Check "Time zone set" {
(Get-TimeZone).Id -eq "Central Europe Standard Time"
}
Test-Check "Deployment date in registry" {
(Get-RegValue "HKLM:\SOFTWARE\X9\Deployment" "DeployDate") -ne $null
}
# -----------------------------------------------------------------------
# Current user (HKCU) - personalization
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- User settings (current user) ---"
Test-Check "Dark system theme" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "SystemUsesLightTheme") -eq 0
}
Test-Check "Light app theme" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "AppsUseLightTheme") -eq 1
}
Test-Check "Transparency disabled" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "EnableTransparency") -eq 0
}
Test-Check "Taskbar aligned left" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "TaskbarAl") -eq 0
} -WarnOnly
Test-Check "File extensions visible" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "HideFileExt") -eq 0
}
Test-Check "Explorer opens to This PC" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "LaunchTo") -eq 1
}
Test-Check "This PC icon on desktop" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" "{20D04FE0-3AEA-1069-A2D8-08002B30309D}") -eq 0
}
Test-Check "Start menu Recommended section hidden" {
(Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" "HideRecommendedSection") -eq 1
}
Test-Check "Start menu recently added hidden" {
(Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "Start_TrackProgs") -eq 0
}
# -----------------------------------------------------------------------
# Scheduled tasks
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- Scheduled tasks ---"
$tasks = @("ShowAllTrayIcons", "PDF-DefaultApp", "DesktopInfo", "UnlockStartLayout")
foreach ($t in $tasks) {
Test-Check "Task registered: $t" {
Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue
}
}
# -----------------------------------------------------------------------
# DesktopInfo
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "--- DesktopInfo ---"
Test-Check "Render script exists" {
Test-Path "C:\Windows\Setup\Scripts\DesktopInfo-Render.ps1"
}
Test-Check "BMP file exists" {
Test-Path "C:\Windows\Setup\Scripts\desktopinfo.bmp"
} -WarnOnly
# -----------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------
Write-Host ""
Write-Host "========================================"
Write-Host " PASS: $PassCount FAIL: $FailCount WARN: $WarnCount"
Write-Host "========================================"
if ($FailCount -gt 0) {
Write-Host "Deployment verification FAILED. Review items above." -ForegroundColor Red
exit 1
} else {
Write-Host "Deployment verification PASSED." -ForegroundColor Green
exit 0
}

87
tools/extract-docs.py Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
extract-docs.py - Extract documentation from PS script headers and generate web/data/descriptions.json
Usage: python3 tools/extract-docs.py
Run from repo root. Reads scripts/*.ps1, writes web/data/descriptions.json.
PS script header format:
<#
.SYNOPSIS
One line description
.DESCRIPTION
Multi-line description
.ITEMS
slug-of-spec-row: Description of what this item does
another-slug: Another description
#>
"""
import os, re, json
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), '..', 'scripts')
OUTPUT_FILE = os.path.join(os.path.dirname(__file__), '..', 'web', 'data', 'descriptions.json')
def parse_script(path):
with open(path, encoding='utf-8') as f:
content = f.read()
# Extract <# ... #> block
m = re.search(r'<#(.*?)#>', content, re.DOTALL)
if not m:
return None
block = m.group(1)
def extract_section(name):
pattern = r'\.' + name + r'\s*\n(.*?)(?=\n\s*\.[A-Z]|\Z)'
sm = re.search(pattern, block, re.DOTALL)
if not sm:
return ''
return re.sub(r'\n[ \t]+', '\n', sm.group(1)).strip()
synopsis = extract_section('SYNOPSIS').replace('\n', ' ').strip()
description = extract_section('DESCRIPTION').strip()
items_raw = extract_section('ITEMS')
items = {}
for line in items_raw.splitlines():
line = line.strip()
if not line or ':' not in line:
continue
slug, _, desc = line.partition(':')
slug = slug.strip()
desc = desc.strip()
if slug and desc:
items[slug] = desc
return {
'synopsis': synopsis,
'description': description,
'items': items,
}
def main():
result = {}
for fname in sorted(os.listdir(SCRIPTS_DIR)):
if not fname.endswith('.ps1'):
continue
script_id = fname.replace('.ps1', '')
path = os.path.join(SCRIPTS_DIR, fname)
parsed = parse_script(path)
if parsed:
result[script_id] = parsed
item_count = len(parsed['items'])
print(f'OK {fname}: {item_count} items')
else:
print(f'--- {fname}: no doc header found')
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f'\nWritten: {OUTPUT_FILE}')
print(f'Scripts documented: {len(result)}')
total_items = sum(len(v["items"]) for v in result.values())
print(f'Total item descriptions: {total_items}')
if __name__ == '__main__':
main()

160
web/data/descriptions.json Normal file
View file

@ -0,0 +1,160 @@
{
"00-admin-account": {
"synopsis": "Creates the adminx9 local administrator account for MSP use.",
"description": "Creates a hidden local administrator account 'adminx9' used by X9.cz technicians\nfor remote management and on-site administration. The account has no password by\ndesign - it is invisible to regular users and only accessible to technicians who\nknow it exists. FullName is set to \"X9.cz s.r.o.\" so it is identifiable in\nsystem tools. Password policy is set so it never expires.",
"items": {
"vytvorit-lokalni-ucet-adminx9": "Creates the account via [ADSI] WinNT provider. No password by design - the account is hidden from users and used only by MSP technicians for remote administration.",
"pridat-do-skupiny-administrators": "Adds adminx9 to the local Administrators group via net localgroup. Required for full system management rights.",
"skryt-z-login-obrazovky-specialaccounts-": "Sets HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\SpecialAccounts\\UserList\\adminx9 = 0. Removes the user tile from Windows login and lock screen completely.",
"heslo-nevypirsi-uzivatel-nesmeni-heslo": "Sets ADS_UF_DONT_EXPIRE_PASSWD and ADS_UF_PASSWD_CANT_CHANGE flags via ADSI userFlags. The account never locks out or requires password maintenance.",
"zadne-heslo-aktualne-nastavovano-z-confi": "Account created with empty password. Previous version used config.json password - removed because plaintext passwords in config files are a security risk.",
"fullname-x9-cz-s-r-o-via-adsi": "Sets FullName property via [ADSI] so the account shows as \"X9.cz s.r.o.\" in User Accounts panel, Event Viewer, and audit logs."
}
},
"01-bloatware": {
"synopsis": "Removes pre-installed bloatware: AppX packages, Capabilities, and Optional Features.",
"description": "Removes Microsoft-bundled apps and features not needed in a business MSP deployment.\nRemoval is done for all users (-AllUsers) and from the provisioning store so new\nusers do not get them either. Calculator is intentionally kept.",
"items": {
"appx-balicky-odstraneni-pro-vsechny-uziv": "Uses Remove-AppxPackage -AllUsers and Remove-AppxProvisionedPackage. The provisioned removal prevents apps from reinstalling for new user profiles. Covers ~35 apps including Cortana, Copilot, Teams personal, Xbox, Skype, News, Weather, Maps.",
"zachovano-microsoft-windowscalculator": "Calculator is explicitly excluded. Lightweight utility frequently used by technicians and end users. Removing it would require manual reinstall from Store.",
"windows-capabilities-fax-ie-openssh-wmp-": "Removed via Remove-WindowsCapability: Fax & Scan, Internet Explorer mode, OpenSSH client, Windows Media Player (legacy), WordPad, Handwriting recognition, Steps Recorder, Math Input Panel, Quick Assist.",
"windows-optional-features-ps-2-0-mediapl": "Disabled via Disable-WindowsOptionalFeature: PowerShell 2.0 (security risk - allows unsigned script execution bypass on older hosts), MediaPlayback, Windows Recall (AI screenshot surveillance), Snipping Tool optional component."
}
},
"02-software": {
"synopsis": "Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.",
"description": "Uses winget to install the standard X9.cz MSP software bundle. Checks winget\navailability before running. Each install is logged. After Adobe Acrobat Reader,\ntemporarily stops the UCPD driver (User Choice Protection Driver, present since\nWin11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets\n.pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP\nmonitoring, remote access, and ticketing integration.",
"items": {
"7-zip-7zip-7zip": "Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.",
"adobe-acrobat-reader-64-bit-adobe-acroba": "Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.",
"openvpn-connect-openvpntechnologies-open": "Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.",
"atera-agent-install": "Atera RMM agent installed via msiexec /qn. Download: Invoke-WebRequest from https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337. Agent enables MSP monitoring, remote access, and ticketing integration with the Atera dashboard.",
"adobe-pdf-default-pdf-acrord32-po-instal": "Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.",
"ucpd-sys-kernel-driver-od-feb-2024-bloku": "UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\\.pdf -> Start-Service ucpd. Implemented in this script."
}
},
"03-system-registry": {
"synopsis": "Applies system-wide registry settings and power configuration (HKLM).",
"description": "Sets machine-wide registry tweaks under HKLM that apply to all users. Disables\nunwanted telemetry and cloud features, configures Edge policies, sets power plan\ntimeouts, and disables proxy auto-detect. Uninstalls the pre-installed OneDrive\nconsumer version via OneDriveSetup.exe /uninstall - intentional for a clean MSP\ndeployment baseline. No DisableFileSyncNGSC policy key is set, so M365 installation\ncan install and run its own OneDrive version without restriction.",
"items": {
"bypass-nro-oobe-bypassnro-1": "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OOBE\\BypassNRO = 1. Bypasses the \"Let's connect you to a network\" OOBE screen. Enables offline Windows setup without forcing a Microsoft account login.",
"zakaz-auto-instalace-teams": "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Communications\\ConfigureChatAutoInstall = 0. Prevents Windows from auto-installing Teams Personal during OOBE or after Cumulative Updates.",
"zakaz-cloud-optimized-content": "ContentDeliveryManager\\DisableCloudOptimizedContent = 1. Stops Windows from pushing sponsored app suggestions, tips from Microsoft servers, and \"Get even more from Windows\" prompts.",
"zakaz-widgets-news-and-interests": "HKLM\\SOFTWARE\\Policies\\Microsoft\\Dsh\\AllowNewsAndInterests = 0. Disables the Widgets taskbar button and panel (news feed, weather, stocks). Not relevant for business deployments.",
"hesla-bez-expirace-net-accounts-maxpwage": "net accounts /maxpwage:UNLIMITED. Sets the local password expiration policy to never. MSP-managed machines handle password rotation via other means (Atera, domain policy, manual).",
"casova-zona-central-europe-standard-time": "Set-TimeZone -Id \"Central Europe Standard Time\". UTC+1 (UTC+2 in summer DST). Applied system-wide. Critical for correct log timestamps, scheduled task timing, and calendar sync.",
"zakaz-gamedvr": "HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\GameDVR\\AppCaptureEnabled = 0. Disables Xbox Game Bar screen capture overlay. Reduces background resource usage and eliminates unintended capture prompts on business machines.",
"edge-skryt-first-run-experience": "HideFirstRunExperience=1 + DefaultBrowserSettingEnabled=0. Suppresses Edge welcome wizard and default browser prompts on first launch.",
"edge-policies-panel-oblibeny-vyhledavac": "FavoritesBarEnabled=1 (always show), DefaultSearchProviderEnabled=1, DefaultSearchProviderName=Google, ManagedSearchEngines removes other providers.",
"edge-policies-tlacitka-zobrazit": "DownloadsButtonEnabled=1, HistoryButtonEnabled=1.",
"edge-policies-tlacitka-skryt": "HomeButtonEnabled=0, SplitScreenEnabled=0, EdgeEDropEnabled=0 (Drop), WebCaptureEnabled=0 (Screenshot), ShareAllowed=0.",
"edge-policies-obsah-a-telemetrie": "NewTabPageContentEnabled=0, ShowRecommendationsEnabled=0, SpotlightExperiencesAndRecommendationsEnabled=0, PersonalizationReportingEnabled=0, EdgeShoppingAssistantEnabled=0, ShowMicrosoftRewards=0, HubsSidebarEnabled=0, SearchSuggestEnabled=0, DiagnosticData=0, FeedbackSurveysEnabled=0, EdgeCollectionsEnabled=0.",
"onedrive-uninstall-intentional": "Uninstalls the pre-installed OneDrive consumer version via OneDriveSetup.exe /uninstall and removes Start Menu shortcut. Intentional for clean MSP deployment baseline. No DisableFileSyncNGSC policy key is set - M365 installation can reinstall and run OneDrive normally. Only the stock consumer pre-install is removed.",
"powercfg-nastaveni-spotreba-energie": "powercfg /change: standby-timeout-ac 0 (never sleep on AC), monitor-timeout-ac 60 (screen off after 60 min on AC), standby-timeout-dc 30 (sleep after 30 min on battery), monitor-timeout-dc 15 (screen off after 15 min on battery). Applied to active power plan.",
"proxy-auto-detect-zakaz-autodetect-0": "HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\AutoDetect = 0. Disables WPAD (Web Proxy Auto-Discovery). Eliminates startup delays from WPAD DNS lookup and prevents MITM via rogue WPAD on untrusted networks."
}
},
"04-default-profile": {
"synopsis": "Applies registry settings to the Default User profile and the current logged-in user.",
"description": "Loads C:\\Users\\Default\\NTUSER.DAT as a temporary hive (HKU\\DefaultProfile), applies\nall settings, then unloads it. Every new user account created on this machine inherits\nthese settings on first logon. The same settings are applied directly to the current\nuser's HKCU. Does NOT block OneDrive re-launch - the Explorer namespace CLSID and RunOnce entries have been removed.",
"items": {
"taskbar-zarovnat-vlevo-taskbaral-0": "TaskbarAl = 0 in Explorer\\Advanced. Windows 11 default is center-aligned (TaskbarAl = 1). Left alignment matches Windows 10 muscle memory and is strongly preferred by business users transitioning from Win10.",
"taskbar-skryt-search-copilot-task-view-w": "Hides Search box (SearchboxTaskbarMode=0), Copilot button (ShowCopilotButton=0), Task View (ShowTaskViewButton=0), Widgets (TaskbarDa=0), Chat/Teams (TaskbarMn=0). Reduces taskbar clutter to just pinned apps and running processes.",
"taskbar-zobrazit-vsechny-ikonky-v-tray-s": "Registers scheduled task that sets EnableAutoTray=0 on logon (repeat every 1 min). Windows 11 periodically re-hides tray icons - this task forces all icons visible so users can see VPN status, antivirus, backup, etc.",
"taskbar-vyprazdnit-pinlist-taskbarlayout": "Deploys TaskbarLayoutModification.xml. ProfileType=default: empty pins (clean slate). ProfileType=admin: Explorer+PowerShell+Edge. ProfileType=user: Explorer+Edge. Lock is removed by UnlockStartLayout task 5 min after first boot so users can customize.",
"explorer-zobrazovat-pripony-souboru-hide": "HideFileExt = 0 in Explorer\\Advanced. Shows file extensions (.docx, .exe, .pdf, .ps1) in File Explorer. Essential for recognizing file types, avoiding phishing (fake .pdf.exe), and general IT work.",
"explorer-otevrit-na-this-pc-launchto-1": "LaunchTo = 1. File Explorer opens to \"This PC\" (drives view) instead of Quick Access. More useful on fresh machines where Quick Access history is empty and irrelevant.",
"start-menu-vyprazdnit-piny-win11": "ConfigureStartPins = {\"pinnedList\":[]} applied via registry. Removes all default Start menu tiles (Edge, Teams, Store, Office, Solitaire, etc.) from the Windows 11 Start grid. User starts with an empty, clean Start menu.",
"start-menu-zakaz-bing-vyhledavani": "DisableSearchBoxSuggestions = 1 in Software\\Policies\\Microsoft\\Windows. Disables web search, Bing suggestions, and online results in Start menu search. Search returns only local apps, files, and settings.",
"copilot-zakaz-turnoffwindowscopilot-1": "TurnOffWindowsCopilot = 1 in SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsCopilot. Disables the Windows Copilot sidebar entirely. Not suitable for most client environments (data privacy, AI usage policies).",
"numlock-zapnout-pri-startu-initialkeyboa": "InitialKeyboardIndicators = 2 in Default profile. Ensures NumLock is enabled when Windows starts. Standard expectation for users working with numeric data - prevents confusion on data entry.",
"accent-barva-na-titulnich-listech-colorp": "ColorPrevalence = 1 in Personalize key. Shows the X9.cz accent color (#223B47) on window title bars and borders. Gives all windows a consistent branded appearance.",
"onedrive-runonce-klic-je-tady-smazat": "REMOVED. The RunOnce key deletion and Explorer namespace CLSID removal were deleted - those registry tweaks prevented a freshly installed OneDrive (e.g. for M365) from launching. OneDrive AppX uninstall in step 01 is intentional; blocking re-launch is not.",
"explorer-showrecent-0-showfrequent-0": "ShowRecent=0 and ShowFrequent=0 in HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer. Hides Recent files and Frequent folders from Quick Access. Privacy improvement and cleaner File Explorer on fresh deployments.",
"explorer-fullpath-1-cabinetstate": "FullPath=1 in HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CabinetState. Displays the full directory path (e.g. C:\\Users\\jan\\Documents\\Projekty) in the File Explorer title bar instead of just the folder name."
}
},
"05-personalization": {
"synopsis": "Sets system colors, wallpaper, and visual theme.",
"description": "Applies X9.cz visual identity: dark taskbar/Start with accent color #223B47\n(deep blue-gray), light app mode, no transparency. Wallpaper is set to a solid\ncolor matching the accent. BackInfo.exe (Step 07) overwrites the wallpaper with\na live system info BMP on every logon - solid color is only the fallback.",
"items": {
"system-tema-taskbar-start-dark": "SystemUsesLightTheme=0 in Themes\\Personalize. Dark mode for shell (taskbar, Start menu, Action Center, notification area). Does NOT affect application windows - those stay light. Reduces eye strain in dim environments.",
"aplikacni-tema-light": "AppsUseLightTheme=1. Application windows (File Explorer, Settings, Calculator, etc.) use white/light backgrounds. Majority of business applications (Office, browsers) also respect this and show light mode.",
"accent-barva-223b47-tmave-modroseda": "AccentColor DWORD = 0xFF473B22 (stored as ABGR: A=FF, B=47, G=3B, R=22). The deep blue-gray #223B47 is the X9.cz brand color, also used as the solid wallpaper background.",
"accent-barva-na-start-a-taskbaru-ano": "ColorPrevalence=1. Applies accent color to taskbar background and Start menu surface. The taskbar becomes the brand color instead of default black, creating a distinct recognizable look on X9.cz-deployed machines.",
"pruhlednost-vypnuta": "EnableTransparency=0. Disables Aero translucency on taskbar and Start. Improves text readability on the taskbar, reduces subtle GPU usage, and looks more professional/consistent on business machines.",
"tapeta-jednobarevna-223b47-bez-obrazku": "Wallpaper set to solid color #223B47 via SystemParametersInfo(SPI_SETDESKWALLPAPER). BackInfo.exe generates a BMP with hostname, username, OS, network info and sets it as wallpaper on every logon. Solid color = fallback only."
}
},
"06-scheduled-tasks": {
"synopsis": "Registers logon scheduled tasks to maintain per-user settings that Windows resets.",
"description": "Creates scheduled tasks under Task Scheduler that run at user logon (and optionally\non a timer) to enforce settings that Windows tends to revert. Tasks are registered\nin the Default profile task store so new user accounts inherit them automatically.\nNote: PDF-DefaultApp task has been removed - PDF default is set once during deployment.",
"items": {
"showalltrayicons-pri-logonu-kazdou-1-min": "Task 'ShowAllTrayIcons': runs at logon, repeats every 1 minute. Sets HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\EnableAutoTray=0. Windows 11 re-enables auto-hiding of tray icons after updates and sometimes after logon - the 1-min repeat ensures permanent override.",
"unlockstartlayout-jednou-po-aplikaci-lay": "Task 'UnlockStartLayout': runs once, 30 seconds after logon. Clears the Start menu layout lock bit that is set when ConfigureStartPins is applied. Without this, users cannot pin or unpin apps from Start after deployment.",
"pdf-defaultapp-pri-kazdem-logonu": "REMOVED. PDF default is set once during deployment (step 02) with UCPD service stopped. The scheduled task is no longer needed."
}
},
"07-backinfo": {
"synopsis": "Deploys BackInfo.exe to C:\\Program Files\\Backinfo\\ and configures auto-start.",
"description": "Copies the BackInfo folder from assets to Program Files, writes the OS name\nto the registry (HKLM\\SOFTWARE\\BackInfo\\OSName) so BackInfo can display it,\nand creates a Startup shortcut so BackInfo launches on every user logon.\nBackInfo renders a BMP wallpaper overlay with hostname, username, OS, HW info,\nand network info - configured via BackInfo.ini.",
"items": {
"kopirovat-assets-backinfo-do-program-fil": "Copies all files from assets\\Backinfo\\ to C:\\Program Files\\Backinfo\\. Includes BackInfo.exe, BackInfo.ini, and backinfo_W11.ps1. Creates the target directory if it does not exist.",
"registry-osname-hklm-software-backinfo": "Detects Windows build number and edition, writes OSName string to HKLM\\SOFTWARE\\BackInfo\\OSName (and WOW6432Node). BackInfo.ini references %OSName% to display the correct OS on the wallpaper.",
"startup-shortcut-backinfo-exe": "Creates a shortcut at C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp\\BackInfo.lnk pointing to C:\\Program Files\\Backinfo\\BackInfo.exe. Ensures BackInfo starts for every user on logon.",
"07-desktop-info-ps1-smazat-nahrazeno": "07-desktop-info.ps1 is superseded by this script. BackInfo.exe is the preferred approach - stable on Win10 and Win11, configurable via INI, already present in assets."
}
},
"07-desktop-info": {
"synopsis": "DEPRECATED - delete this script. Replaced by BackInfo.exe.",
"description": "Original custom PowerShell approach to render system info onto the desktop wallpaper\nusing WPF (System.Windows.Media / System.Drawing). Superseded by BackInfo.exe which\nis already present in assets/Backinfo/ and handles Win10/Win11 natively.\nACTION REQUIRED: Delete this file. Add a BackInfo deployment step to the master script.",
"items": {
"07-desktop-info-ps1-smazat-stary-pristup": "DELETE THIS FILE. The WPF rendering approach had compatibility issues on some Windows editions and required maintaining complex PS rendering code. BackInfo.exe is a mature, stable replacement already bundled in assets/Backinfo/.",
"zkopirovat-assets-backinfo-do-c-program-": "NEW STEP (in master script): Copy assets/Backinfo/ to C:\\Program Files\\Backinfo\\ on the target machine. Includes BackInfo.exe, BackInfo.ini (display config), and backinfo_W11.ps1 (setup helper).",
"spustit-backinfo-w11-ps1-detekce-os-regi": "Run backinfo_W11.ps1 after file copy. Detects Win10 vs Win11, writes the required registry key for wallpaper rendering compatibility, and creates a Startup shortcut in the All Users Startup folder.",
"backinfo-exe-v-assets-backinfo-k-dispozi": "BackInfo.exe reads BackInfo.ini on each run. INI configures: font size and family, position of each info block, which data sources to show (hostname, username, OS version, CPU, RAM, disk, IP address, domain).",
"backinfo-auto-start-pri-kazdem-logonu-vi": "The Startup shortcut created by backinfo_W11.ps1 ensures BackInfo.exe runs on every user logon. It re-reads live system data each time, so the wallpaper BMP always shows current information (username changes, IP changes, etc.)."
}
},
"08-activation": {
"synopsis": "Activates Windows using a product key from config or KMS GVLK fallback.",
"description": "Checks if Windows is already activated (LicenseStatus = 1). If not, reads the\nproduct key from config.json activation.productKey. If no key is present, selects\nthe appropriate GVLK for the detected Windows edition and activates via KMS.\nOptionally configures a specific KMS server if activation.kmsServer is set.",
"items": {
"oa3-bios-uefi-klic-kontrola-embedded-ke": "Checks for OA3 embedded product key in BIOS/UEFI firmware via SoftwareLicensingService.OA3xOriginalProductKey WMI query. If a key is found, it is installed via slmgr /ipk and activation is attempted. Most OEM machines (since Win8 OA3) have a digital entitlement key in firmware - this path handles them without requiring a key in config.json.",
"klic-z-config-json-activation-productkey": "Reads activation.productKey from config.json. Installs via slmgr.vbs /ipk <key> and activates via slmgr.vbs /ato. Supports MAK (Multiple Activation Key) for volume licensing without KMS, and retail keys. Takes priority over GVLK fallback.",
"fallback-na-gvlk-kms-client-key-dle-edic": "When no key is in config, detects Windows edition via (Get-WmiObject SoftwareLicensingProduct).Name and maps to Microsoft's published GVLK table. Pro: W269N-WFGWX-YVC9B-4J6C9-T83GX, Enterprise: NPPR9-FWDCX-D2C8J-H872K-2YT43, Home: TX9XD-98N7V-6WMQ6-BX7FG-H8Q99.",
"volitelny-kms-server-activation-kmsserve": "If activation.kmsServer is in config.json, runs slmgr.vbs /skms <server>:<port> before /ato. Used for clients with on-premises KMS infrastructure (common in larger organizations with volume licensing).",
"preskocit-pokud-jiz-aktivovano": "Queries Win32_WindowsLicenseStatus or SoftwareLicensingProduct to check LicenseStatus. Value 1 = Licensed (fully activated). Script skips activation attempt and logs \"Windows already activated\" to avoid unnecessary slmgr calls.",
"typ-klice-mak-vs-kms-vs-retail": "Key type selection depends on client's Microsoft licensing: MAK = volume license key activates online against Microsoft (limited activations), KMS = requires KMS server on network (VLSC subscription), Retail = individual license from Microsoft Store or OEM."
}
},
"09-pc-identity": {
"synopsis": "Sets PC identity: computer name, description, and creates C:\\X9 folder structure.",
"description": "Renames the computer if deployment.pcName is set in config.json. Sets the\ncomputer description (visible in System properties and network neighborhood).\nCreates C:\\X9\\ directory structure with subdirectories for logs, scripts and\nassets. Copies X9 icon and creates Desktop.ini so the folder shows a custom\nicon in Explorer. Computer rename requires a restart - this step runs last\nbefore the final summary.",
"items": {
"rename-computer-dle-config-deployment-pcn": "Renames the computer via Rename-Computer if config.json deployment.pcName is set and differs from the current name. Rename takes effect after restart. If pcName is empty, rename is skipped and the current name is preserved.",
"popis-pocitace-computer-description": "Sets the computer description shown in System Properties and Network Neighborhood. Read from config.json deployment.pcDescription, default \"X9 deployment\". Written to HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters\\SrvComment.",
"vytvorit-cx9-adresar": "Creates C:\\X9\\ with subdirectories Logs\\, Scripts\\, Assets\\. Used for deployment logs, custom per-client scripts, and client-specific configuration assets.",
"cx9-vlastni-ikonka-desktop-ini": "Copies X9-ikona.ico to C:\\X9\\ and creates Desktop.ini with IconResource entry. Sets System+Hidden attributes on Desktop.ini and ReadOnly on C:\\X9\\ so Explorer displays the custom folder icon."
}
},
"10-network": {
"synopsis": "Sets network profile to Private, enables ping, and enables Network Discovery.",
"description": "Sets all connected network adapter profiles from Public to Private. Private\nprofile enables file sharing, network discovery, and other LAN features.\nEnables ICMP echo (ping) via Windows Firewall for diagnostic purposes.\nEnables the Network Discovery firewall rule group for the Private profile\nso this PC is visible to other computers on the local network.",
"items": {
"nastavit-sitovy-profil-private": "Sets all connected network profiles to Private via Set-NetConnectionProfile. Public profile blocks most LAN features. Private is required for file sharing, printer sharing, and network discovery. Applied to all currently connected adapters.",
"povolit-ping-icmp-firewall": "Enables \"File and Printer Sharing (Echo Request)\" firewall rules for ICMPv4 and ICMPv6. ICMP echo is disabled by default on clean Windows. Required for network diagnostics, monitoring tools, and basic connectivity verification.",
"zapnout-network-discovery": "Enables the Network Discovery firewall rule group (FPS-NB_Name-In-UDP, LLMNR, etc.) for Private and Domain profiles via Set-NetFirewallRule. Allows this PC to appear in Network Neighborhood and browse other machines."
}
},
"11-dell-update": {
"synopsis": "Detects Dell hardware, installs Dell Command | Update, and applies all available updates.",
"description": "Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without\nerror so the same deployment script works on any hardware. On Dell machines, installs\nDell Command | Update (Universal) via winget and immediately runs /applyUpdates with\n-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are\nstaged at this point and finalize automatically during the restart that closes the\ndeployment. The operator does not need to run a separate update pass after setup.",
"items": {
"detekce-dell-hw-win32-computersystem": "Reads Win32_ComputerSystem.Manufacturer. If the string does not contain \"Dell\", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.",
"instalace-dell-command-update-via-winget": "Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.",
"spusteni-vsech-aktualizaci-drivery-firmware-bios": "Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.",
"bios-firmware-staging-reboot": "BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed."
}
}
}

BIN
web/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

18
web/get.ps1 Normal file
View file

@ -0,0 +1,18 @@
# xetup installer - downloads and launches the latest release
# Usage: irm xetup.x9.cz/get.ps1 | iex
$api = 'https://xetup.x9.cz/forgejo-api/repos/x9/xetup/releases?limit=1'
try {
$rel = (Invoke-RestMethod -Uri $api -UseBasicParsing)[0]
$asset = $rel.assets | Where-Object { $_.name -eq 'xetup.exe' } | Select-Object -First 1
if (-not $asset) { Write-Error "xetup.exe not found in latest release"; exit 1 }
$out = "$env:TEMP\xetup.exe"
Write-Host "Downloading xetup $($rel.tag_name)..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $out -UseBasicParsing
Write-Host "Launching..."
Start-Process -FilePath $out -Verb RunAs
} catch {
Write-Error "Failed: $_"
}

271
web/index.html Normal file
View file

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Xetup - Windows deployment pro X9.cz</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
:root {
--bg: #0f1117;
--card: #1a1d27;
--border: #2a2d3a;
--text: #e0e0e0;
--muted: #888;
--accent: #223B47;
--accent-bright: #2d5266;
--green: #2ea043;
--blue: #58a6ff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
align-items: center;
gap: .75rem;
}
.logo-text { font-size: 1.2rem; font-weight: 700; color: #fff; letter-spacing: -.02em; }
.logo-sub { font-size: .8rem; color: var(--muted); margin-left: .2rem; }
header nav { margin-left: auto; display: flex; gap: 1.5rem; }
header nav a { color: var(--muted); text-decoration: none; font-size: .88rem; transition: color .15s; }
header nav a:hover { color: var(--text); }
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
}
.badge {
display: inline-block;
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: .25rem .75rem;
font-size: .75rem;
color: var(--muted);
margin-bottom: 1.5rem;
letter-spacing: .03em;
}
h1 {
font-size: 2.8rem; font-weight: 800; color: #fff;
letter-spacing: -.04em; line-height: 1.1;
margin-bottom: 1rem; max-width: 600px;
}
h1 span { color: var(--blue); }
.tagline {
font-size: 1.1rem; color: var(--muted); max-width: 480px;
line-height: 1.6; margin-bottom: 2.5rem;
}
/* ---- ACTIONS ---- */
.actions {
display: flex; gap: .75rem; flex-wrap: wrap;
justify-content: center; margin-bottom: 1.5rem;
}
.btn-primary {
padding: .6rem 1.4rem; background: var(--accent-bright); color: #fff;
border: 1px solid transparent; border-radius: 8px;
font-size: .95rem; font-weight: 600; text-decoration: none; transition: opacity .15s;
}
.btn-primary:hover { opacity: .85; }
.btn-secondary {
padding: .6rem 1.4rem; background: transparent; color: var(--text);
border: 1px solid var(--border); border-radius: 8px;
font-size: .95rem; text-decoration: none; transition: background .15s;
}
.btn-secondary:hover { background: var(--card); }
/* ---- INSTALL COMMAND ---- */
.install-box {
margin-bottom: 1.5rem;
display: flex; align-items: center; gap: 0;
background: #0d1117; border: 1px solid var(--border);
border-radius: 10px; overflow: hidden;
font-size: .88rem; max-width: 520px; width: 100%;
}
.install-box code {
flex: 1; padding: .65rem 1rem;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
color: var(--blue); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.install-box button {
padding: .65rem .9rem; background: var(--card);
border: none; border-left: 1px solid var(--border);
color: var(--muted); cursor: pointer; font-size: .8rem;
transition: color .15s, background .15s; white-space: nowrap;
}
.install-box button:hover { background: var(--accent); color: #fff; }
.install-box button.copied { color: var(--green); }
/* ---- DOWNLOAD STRIP ---- */
.download-strip {
margin-bottom: 3.5rem;
display: flex; align-items: center; gap: .6rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: .6rem 1.1rem;
font-size: .85rem;
}
.download-strip a {
color: var(--blue); text-decoration: none; font-weight: 600;
display: flex; align-items: center; gap: .35rem;
}
.download-strip a:hover { text-decoration: underline; }
.download-strip .dl-meta { color: var(--muted); font-size: .78rem; }
.download-strip .dl-sep { color: var(--border); }
/* ---- CARDS ---- */
.cards {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; max-width: 720px; width: 100%;
}
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1.2rem; text-align: left; }
.card-icon { font-size: 1.3rem; margin-bottom: .6rem; display: block; }
.card h3 { font-size: .9rem; font-weight: 600; color: #fff; margin-bottom: .3rem; }
.card p { font-size: .82rem; color: var(--muted); line-height: 1.4; }
footer {
border-top: 1px solid var(--border); padding: 1.2rem 2rem;
text-align: center; font-size: .8rem; color: var(--muted);
}
footer a { color: var(--muted); text-decoration: none; }
footer a:hover { color: var(--text); }
</style>
</head>
<body>
<header>
<div style="display:flex;align-items:center;gap:.6rem">
<img src="/x9-logo.jpeg" alt="X9.cz" style="height:28px;width:28px;border-radius:5px;object-fit:cover;">
<span class="logo-text">xetup</span><span class="logo-sub">by X9.cz</span>
</div>
<nav>
<a href="/spec/">Specifikace</a>
<a href="https://git.xetup.x9.cz/x9/xetup">Git</a>
<a href="https://git.xetup.x9.cz/x9/xetup/issues">Issues</a>
</nav>
</header>
<main>
<div class="badge">MSP deployment tool &mdash; X9.cz</div>
<h1>Automaticky nastavene <span>Windows</span> za 20 minut</h1>
<p class="tagline">
Nahrazuje 3 hodiny rucniho nastavovani jednim skriptem.
Win10 + Win11, OEM i cisty install, funguje offline.
</p>
<div class="actions">
<a href="/spec/" class="btn-primary">Zobrazit specifikaci</a>
<a href="https://git.xetup.x9.cz/x9/xetup" class="btn-secondary">Git repozitar</a>
</div>
<div class="install-box">
<code id="install-cmd">curl -Lo xetup.exe xetup.x9.cz/dl</code>
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
</div>
<!-- Dynamic download strip filled by JS from Forgejo releases API -->
<div class="download-strip" id="dl-strip" style="display:none">
<a id="dl-link" href="#" download>
&#11015; xetup.exe
</a>
<span class="dl-sep">&middot;</span>
<span class="dl-meta" id="dl-meta">nacitam...</span>
<span class="dl-sep">&middot;</span>
<a href="https://git.xetup.x9.cz/x9/xetup/releases" style="color:var(--muted);font-size:.78rem;text-decoration:none">
vsechny verze
</a>
</div>
<div class="cards">
<div class="card">
<span class="card-icon">&#9881;</span>
<h3>~20 stroju / mesic</h3>
<p>Ruzni klienti, Win10 i Win11, vcetne nepodporovaneho HW.</p>
</div>
<div class="card">
<span class="card-icon">&#128274;</span>
<h3>Offline provoz</h3>
<p>Scripty + assets jsou soucasti balicku. Site jen pro winget a Atera.</p>
</div>
<div class="card">
<span class="card-icon">&#128196;</span>
<h3>Per-client config</h3>
<p>config.json vedle .exe pro opakovatelne nasazeni u stejneho klienta.</p>
</div>
<div class="card">
<span class="card-icon">&#128640;</span>
<h3>Go TUI launcher</h3>
<p>xetup.exe &mdash; jednotny binarni spoustec s TUI formularom a live logem.</p>
</div>
</div>
</main>
<footer>
&copy; 2026 <a href="https://x9.cz">X9.cz s.r.o.</a>
&nbsp;&middot;&nbsp;
<a href="https://git.xetup.x9.cz/x9/xetup">Forgejo</a>
&nbsp;&middot;&nbsp;
<a href="/spec/">Specifikace</a>
</footer>
<script>
(function() {
const API = '/forgejo-api';
const REPO = 'x9/xetup';
fetch(API + '/repos/' + REPO + '/releases?limit=1')
.then(r => r.json())
.then(releases => {
if (!Array.isArray(releases) || !releases.length) return;
const rel = releases[0];
const asset = (rel.assets || []).find(a => a.name === 'xetup.exe');
if (!asset) return;
const strip = document.getElementById('dl-strip');
const link = document.getElementById('dl-link');
const meta = document.getElementById('dl-meta');
const sizeMB = (asset.size / 1048576).toFixed(1);
link.href = asset.browser_download_url;
link.textContent = '\u2b15 xetup.exe';
meta.textContent = rel.tag_name + ' \u00b7 ' + sizeMB + ' MB \u00b7 Windows x64';
strip.style.display = '';
})
.catch(() => {});
})();
function copyCmd() {
const cmd = document.getElementById('install-cmd').textContent;
const btn = document.getElementById('copy-btn');
navigator.clipboard.writeText(cmd).then(function() {
btn.textContent = 'Skopirovano!';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Kopirovat';
btn.classList.remove('copied');
}, 2000);
});
}
</script>
</body>
</html>

33
web/nginx.conf Normal file
View file

@ -0,0 +1,33 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
location / {
try_files $uri $uri/ $uri.html =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
add_header X-Content-Type-Options nosniff always;
}
# Permanent shortlink to latest xetup.exe never needs updating
location = /dl {
return 302 https://git.xetup.x9.cz/x9/xetup/releases/download/latest/xetup.exe;
}
# Proxy Forgejo API calls so browser doesn't need CORS or direct access to Forgejo
location /forgejo-api/ {
proxy_pass http://xetup-forgejo:3000/api/v1/;
proxy_set_header Host xetup-forgejo;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Cache-Control "no-store" always;
}
error_page 404 /404.html;
}

1355
web/spec/index.html Normal file

File diff suppressed because it is too large Load diff

BIN
web/x9-logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
xetup.exe Executable file

Binary file not shown.