The file /opt/xetup/scripts/12-windows-update.ps1 has been updated successfully.

# -----------------------------------------------------------------------
# 3. Enable autologon for adminx9 (temporary - disabled when updates complete)
# -----------------------------------------------------------------------
Write-Log "Enabling temporary autologon for adminx9..." -Level INFO

$winlogonPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
try {
    Set-ItemProperty -Path $winlogonPath -Name "AutoAdminLogon"  -Value "1"  -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultUserName" -Value "adminx9" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultPassword" -Value "" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultDomainName" -Value "." -Type String -Force
    # Safety cap: max 10 automatic logons in case the task fails to clean up
    Set-ItemProperty -Path $winlogonPath -Name "AutoLogonCount"  -Value 10 -Type DWord -Force
    Write-Log "  Autologon enabled (adminx9, max 10 rounds)" -Level OK
} catch {
    Write-Log "  Failed to enable autologon: $_" -Level WARN
    Write-Log "  Windows Update rounds will require manual login after each reboot" -Level WARN
}

# -----------------------------------------------------------------------
# 4. Scheduled task for post-reboot update rounds (self-deleting)
# -----------------------------------------------------------------------
Write-Log "Registering post-reboot update task..." -Level INFO

$taskName = "X9-WindowsUpdate"

# PowerShell block that runs on each logon until no more updates found.
# When done: disables autologon and removes itself.
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
    # No more updates - disable autologon and remove this task
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue
    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@

try {
    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `
                 -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
    $trigger = New-ScheduledTaskTrigger -AtLogOn
    $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
                  -MultipleInstances IgnoreNew
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

    # Remove existing task first (idempotent)
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
      -Settings $settings -Principal $principal -Force | Out-Null

    Write-Log "  Task '$taskName' registered - runs on each logon until fully updated" -Level OK
} catch {
    Write-Log "  Failed to register scheduled task: $_" -Level WARN
    Write-Log "  Manual Windows Update rounds will be needed after reboot" -Level WARN
}

Write-Log "Step 12 - Windows Update complete" -Level OK

<#
.SYNOPSIS
    Installs all available Windows Updates via PSWindowsUpdate module.

.DESCRIPTION
    First pass: installs all currently available updates without rebooting.
    Then registers a scheduled task "X9-WindowsUpdate" that runs on every
    logon (as SYSTEM) until no more updates are found - handles the typical
    2-3 reboot cycles required on a fresh Windows installation.

    Operator workflow:
      1. xetup completes all steps
      2. Operator reboots manually
      3. On each subsequent logon the scheduled task runs another update pass
      4. Task removes itself automatically when system is fully up to date

.ITEMS
    nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
    spustit-prvni-kolo-windows-update: First update pass without reboot - installs all currently available updates.
    registrovat-scheduled-task-pro-dalsi-kola: Registers X9-WindowsUpdate scheduled task that runs on logon, handles post-reboot update rounds, and self-deletes when no more updates are found.
#>
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"
    $null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
    Write-Output $line
}

Write-Log "=== Step 12 - Windows Update ===" -Level STEP

# -----------------------------------------------------------------------
# 1. NuGet provider + PSWindowsUpdate module
# -----------------------------------------------------------------------
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
try {
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
    $existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
    if ($existing) {
        Write-Log "  PSWindowsUpdate $($existing.Version) already installed" -Level INFO
    } else {
        Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
        Write-Log "  PSWindowsUpdate installed" -Level OK
    }
    Import-Module PSWindowsUpdate -Force
} catch {
    Write-Log "  Module setup failed: $_" -Level ERROR
    exit 1
}

# -----------------------------------------------------------------------
# 2. First update pass (no reboot)
# -----------------------------------------------------------------------
Write-Log "Running first Windows Update pass..." -Level INFO
try {
    $result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
    $installed = @($result | Where-Object { $_ -match 'KB\d+|Downloaded|Installed' })
    if ($installed.Count -gt 0) {
        $result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log "  $_" -Level INFO }
        Write-Log "  First pass complete - reboot required for remaining rounds" -Level OK
    } else {
        Write-Log "  System already up to date" -Level OK
    }
} catch {
    Write-Log "  First pass failed: $_" -Level ERROR
}

# -----------------------------------------------------------------------
# 3. Scheduled task for post-reboot update rounds (self-deleting)
# -----------------------------------------------------------------------
Write-Log "Registering post-reboot update task..." -Level INFO

$taskName = "X9-WindowsUpdate"

# PowerShell block that runs on each logon until no more updates found
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
    # No more updates - remove this task
    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@

try {
    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `
                 -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
    $trigger = New-ScheduledTaskTrigger -AtLogOn
    $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
                  -MultipleInstances IgnoreNew
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

    # Remove existing task first (idempotent)
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
      -Settings $settings -Principal $principal -Force | Out-Null

    Write-Log "  Task '$taskName' registered - runs on each logon until fully updated" -Level OK
} catch {
    Write-Log "  Failed to register scheduled task: $_" -Level WARN
    Write-Log "  Manual Windows Update rounds will be needed after reboot" -Level WARN
}

Write-Log "Step 12 - Windows Update complete" -Level OK
Write-Log "  ACTION REQUIRED: Reboot the machine to complete remaining update rounds" -Level WARN

-# 3. Scheduled task for post-reboot update rounds (self-deleting)

+# 3. Enable autologon for adminx9 (temporary - disabled when updates complete)

+Write-Log "Enabling temporary autologon for adminx9..." -Level INFO

+$winlogonPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"

+    Set-ItemProperty -Path $winlogonPath -Name "AutoAdminLogon"  -Value "1"  -Type String -Force

+    Set-ItemProperty -Path $winlogonPath -Name "DefaultUserName" -Value "adminx9" -Type String -Force

+    Set-ItemProperty -Path $winlogonPath -Name "DefaultPassword" -Value "" -Type String -Force

+    Set-ItemProperty -Path $winlogonPath -Name "DefaultDomainName" -Value "." -Type String -Force

+    # Safety cap: max 10 automatic logons in case the task fails to clean up

+    Set-ItemProperty -Path $winlogonPath -Name "AutoLogonCount"  -Value 10 -Type DWord -Force

+    Write-Log "  Autologon enabled (adminx9, max 10 rounds)" -Level OK

+    Write-Log "  Failed to enable autologon: $_" -Level WARN

+    Write-Log "  Windows Update rounds will require manual login after each reboot" -Level WARN

+# 4. Scheduled task for post-reboot update rounds (self-deleting)

-# PowerShell block that runs on each logon until no more updates found

+# PowerShell block that runs on each logon until no more updates found.

+# When done: disables autologon and removes itself.

-    # No more updates - remove this task

+    # No more updates - disable autologon and remove this task

+    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"

+    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force

+    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue

+    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue

-Write-Log "  ACTION REQUIRED: Reboot the machine to complete remaining update rounds" -Level WARN


[Request interrupted by user]


a pořád na taskbaru chybí explorer :-)


Podívám se na co přesně se zapíše do XML a jestli ta .lnk cesta reálně existuje:


Bash

ls "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\System Tools\" 2>/dev/null || echo "path missing on linux host"

Check if File Explorer.lnk exists in Default profile


Error: Exit code 2
/bin/bash: eval: line 1: unexpected EOF while looking for matching `"'


Logicky: `%APPDATA%\...\System Tools\File Explorer.lnk` — tato cesta musí existovat v Default profilu v době kdy Windows kopíruje profil pro nového uživatele. Na čistém Win11 tam ta .lnk nemusí být. Skript ji musí vytvořit explicitně.


218	        # Taskbar pinned apps layout (Win10/11)
219	        # ProfileType: default = empty, admin = Explorer+PS+Edge, user = Explorer+Edge
220	        # Note: TaskbarLayoutModification.xml locks the taskbar temporarily.
221	        # UnlockStartLayout scheduled task removes the lock 5 min after first boot
222	        # so users can then customize pins freely.
223	        # Win11 24H2+ may require ProvisionedLayoutModification.xml format instead.
224	        Write-Log "  Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO
225
226	        $taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
227	        if (-not (Test-Path $taskbarLayoutDir)) {
228	            New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
229	        }
230
231	        $pinList = switch ($ProfileType) {
232	            "admin" {
233	@'
234	        <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
235	        <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell.lnk"/>
236	        <taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
237	'@
238	            }
239	            "user" {
240	@'
241	        <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
242	        <taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
243	'@
244	            }
245	            default {
246	@'
247	        <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
248	        <taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
249	'@
250	            }  # explicit pins with Replace = no Store, no other defaults
251	        }
252
253	        $taskbarLayoutXml = @"
254	<?xml version="1.0" encoding="utf-8"?>
255	<LayoutModificationTemplate
256	    xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
257	    xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
258	    xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
259	    xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
260	    Version="1">
261	  <CustomTaskbarLayoutCollection PinListPlacement="Replace">
262	    <defaultlayout:TaskbarLayout>
263	      <taskbar:TaskbarPinList>
264	$pinList
265	      </taskbar:TaskbarPinList>
266	    </defaultlayout:TaskbarLayout>
267	  </CustomTaskbarLayoutCollection>
268	</LayoutModificationTemplate>
269	"@
270	        $taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
271	        Write-Log "  Taskbar LayoutModification.xml written (profile: $ProfileType)" -Level OK
272

        # 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 {
@'
        <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"/>
'@
            }  # explicit pins with Replace = no Store, no other defaults
        }

        $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


Jasný problém — skript předpokládá, že `System Tools\File Explorer.lnk` existuje v Default profilu. Na čistém Win11 tam nemusí být. Vytvořím ji explicitně:


Edit

        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
        }

        Write-Log "  Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO

        # Ensure File Explorer shortcut exists in Default profile's Start Menu.
        # On a clean Windows 11 install the System Tools folder may be missing
        # from C:\Users\Default\AppData\Roaming - without it the XML pin is silently skipped.
        $wsh = New-Object -ComObject WScript.Shell
        $defRoaming = "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"

        $explorerLnkDir = "$defRoaming\System Tools"
        $explorerLnk    = "$explorerLnkDir\File Explorer.lnk"
        if (-not (Test-Path $explorerLnk)) {
            if (-not (Test-Path $explorerLnkDir)) { New-Item -ItemType Directory -Path $explorerLnkDir -Force | Out-Null }
            $sc = $wsh.CreateShortcut($explorerLnk)
            $sc.TargetPath = "$env:WINDIR\explorer.exe"
            $sc.Save()
            Write-Log "  Created File Explorer.lnk in Default profile Start Menu" -Level OK
        }

        # Same for PowerShell (admin profile)
        if ($ProfileType -eq "admin") {
            $psLnkDir = "$defRoaming\Windows PowerShell"
            $psLnk    = "$psLnkDir\Windows PowerShell.lnk"
            if (-not (Test-Path $psLnk)) {
                if (-not (Test-Path $psLnkDir)) { New-Item -ItemType Directory -Path $psLnkDir -Force | Out-Null }
                $sc = $wsh.CreateShortcut($psLnk)
                $sc.TargetPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
                $sc.Save()
                Write-Log "  Created Windows PowerShell.lnk in Default profile Start Menu" -Level OK
            }
        }

        $taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
        if (-not (Test-Path $taskbarLayoutDir)) {
            New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
        }


The file /opt/xetup/scripts/04-default-profile.ps1 has been updated successfully.

        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
        }

        Write-Log "  Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO

        # Ensure File Explorer shortcut exists in Default profile's Start Menu.
        # On a clean Windows 11 install the System Tools folder may be missing
        # from C:\Users\Default\AppData\Roaming - without it the XML pin is silently skipped.
        $wsh = New-Object -ComObject WScript.Shell
        $defRoaming = "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"

        $explorerLnkDir = "$defRoaming\System Tools"
        $explorerLnk    = "$explorerLnkDir\File Explorer.lnk"
        if (-not (Test-Path $explorerLnk)) {
            if (-not (Test-Path $explorerLnkDir)) { New-Item -ItemType Directory -Path $explorerLnkDir -Force | Out-Null }
            $sc = $wsh.CreateShortcut($explorerLnk)
            $sc.TargetPath = "$env:WINDIR\explorer.exe"
            $sc.Save()
            Write-Log "  Created File Explorer.lnk in Default profile Start Menu" -Level OK
        }

        # Same for PowerShell (admin profile)
        if ($ProfileType -eq "admin") {
            $psLnkDir = "$defRoaming\Windows PowerShell"
            $psLnk    = "$psLnkDir\Windows PowerShell.lnk"
            if (-not (Test-Path $psLnk)) {
                if (-not (Test-Path $psLnkDir)) { New-Item -ItemType Directory -Path $psLnkDir -Force | Out-Null }
                $sc = $wsh.CreateShortcut($psLnk)
                $sc.TargetPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
                $sc.Save()
                Write-Log "  Created Windows PowerShell.lnk in Default profile Start Menu" -Level OK
            }
        }

        $taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
        if (-not (Test-Path $taskbarLayoutDir)) {
            New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
        }

# Win11 24H2+ may require ProvisionedLayoutModification.xml format instead.

+        # Ensure File Explorer shortcut exists in Default profile's Start Menu.

+        # On a clean Windows 11 install the System Tools folder may be missing

+        # from C:\Users\Default\AppData\Roaming - without it the XML pin is silently skipped.

+        $wsh = New-Object -ComObject WScript.Shell

+        $defRoaming = "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"

+        $explorerLnkDir = "$defRoaming\System Tools"

+        $explorerLnk    = "$explorerLnkDir\File Explorer.lnk"

+        if (-not (Test-Path $explorerLnk)) {

+            if (-not (Test-Path $explorerLnkDir)) { New-Item -ItemType Directory -Path $explorerLnkDir -Force | Out-Null }

+            $sc = $wsh.CreateShortcut($explorerLnk)

+            $sc.TargetPath = "$env:WINDIR\explorer.exe"

+            Write-Log "  Created File Explorer.lnk in Default profile Start Menu" -Level OK

+        # Same for PowerShell (admin profile)

+        if ($ProfileType -eq "admin") {

+            $psLnkDir = "$defRoaming\Windows PowerShell"

+            $psLnk    = "$psLnkDir\Windows PowerShell.lnk"

+            if (-not (Test-Path $psLnk)) {

+                if (-not (Test-Path $psLnkDir)) { New-Item -ItemType Directory -Path $psLnkDir -Force | Out-Null }

+                $sc = $wsh.CreateShortcut($psLnk)

+                $sc.TargetPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"

+                Write-Log "  Created Windows PowerShell.lnk in Default profile Start Menu" -Level OK


Bash

GOOS=windows GOARCH=amd64 go build ./... 2>&1 && git add scripts/04-default-profile.ps1 scripts/12-windows-update.ps1 && git commit -m "$(cat <<'EOF'
fix: create File Explorer.lnk explicitly + autologon for WU rounds

04-default-profile: Create File Explorer.lnk (and PowerShell.lnk for
admin profile) in C:\Users\Default\AppData\Roaming\...\Start Menu
before writing LayoutModification.xml. On a clean Windows 11 install
the System Tools folder is often missing from the Default profile,
which causes the taskbar pin to be silently skipped.

12-windows-update: Enable temporary autologon for adminx9 so the
machine logs in automatically after each update reboot without
operator intervention. AutoLogonCount=10 as safety cap.
Autologon is disabled (and DefaultPassword removed) by the
scheduled task when no more updates are found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Build check, stage and commit


[main 3fb6578] fix: create File Explorer.lnk explicitly + autologon for WU rounds
 2 files changed, 56 insertions(+), 4 deletions(-)


TOKEN=$(sudo docker exec -u git xetup-forgejo forgejo admin user generate-access-token --username x9 --token-name "push-$(date +%s)" --raw --scopes "write:repository" 2>/dev/null | tail -1) && git push "https://x9:${TOKEN}@git.xetup.x9.cz/x9/xetup.git" main 2>&1

Push to Forgejo


To https://git.xetup.x9.cz/x9/xetup.git
   1c7678c..3fb6578  main -> main


Příčina: na čistém Win11 je `C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\System Tools\` prázdná nebo chybí úplně — Windows ji nepopuluje dokud se někdo reálně přihlásí. XML pin na neexistující .lnk se tiše přeskočí.

Oprava: skript teď před zápisem XML explicitně vytvoří `File Explorer.lnk` (a `Windows PowerShell.lnk` pro admin profil) přímo v Default profilu. Ty se pak při prvním přihlášení zkopírují do profilu každého nového uživatele.


a dodělals ten autologon pro adminx9 (zrušit pak zárověň se schtaskem)


Ano, je tam — šlo to do commitu zároveň s Explorer fixem. Ověřím:


grep -n "AutoAdminLogon\|AutoLogonCount\|DefaultPassword\|autologon" /opt/xetup/scripts/12-windows-update.ps1

Check autologon code in script


76:# 3. Enable autologon for adminx9 (temporary - disabled when updates complete)
78:Write-Log "Enabling temporary autologon for adminx9..." -Level INFO
82:    Set-ItemProperty -Path $winlogonPath -Name "AutoAdminLogon"  -Value "1"  -Type String -Force
84:    Set-ItemProperty -Path $winlogonPath -Name "DefaultPassword" -Value "" -Type String -Force
87:    Set-ItemProperty -Path $winlogonPath -Name "AutoLogonCount"  -Value 10 -Type DWord -Force
90:    Write-Log "  Failed to enable autologon: $_" -Level WARN
102:# When done: disables autologon and removes itself.
109:    # No more updates - disable autologon and remove this task
111:    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
112:    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
113:    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue


Je tam vše — šlo to do commitu `3fb6578` zároveň s Explorer fixem, jen ses mě stihl přerušit dřív než jsem to ohlásil.

Shrnutí chování:
- krok 12 zapne autologon pro `adminx9` (bez hesla, `AutoLogonCount=10` jako pojistka)
- po každém rebootu se stroj přihlásí sám → spustí se `X9-WindowsUpdate` task
- když už nejsou žádné updaty: task nastaví `AutoAdminLogon=0`, smaže `DefaultPassword` a `AutoLogonCount`, a odregistruje se


hm... ale jak budu vědšět, že už je to hotový? :-) Nikde není žádný indokátor ...


Dobrý postřeh. Nejlepší kombinace: až je hotovo, task **zamkne obrazovku** (přihlašovací obrazovka = jasný signál "hotovo") a hodí **viditelný soubor na veřejný Desktop**. Obojí bez nutnosti extra SW.


100
101	# PowerShell block that runs on each logon until no more updates found.
102	# When done: disables autologon and removes itself.
103	$updateScript = @'
104	Import-Module PSWindowsUpdate -Force -ErrorAction Stop
105	$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
106	if ($updates) {
107	    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
108	} else {
109	    # No more updates - disable autologon and remove this task
110	    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
111	    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
112	    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
113	    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue
114	    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
115	}
116	'@
117
118	try {
119	    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `

# PowerShell block that runs on each logon until no more updates found.
# When done: disables autologon and removes itself.
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
    # No more updates - disable autologon and remove this task
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue
    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@

try {
    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `


Edit

} else {
    # No more updates - disable autologon and remove this task
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue
    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}

} else {
    # No more updates - disable autologon and clean up
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue

    # Leave a visible marker on the shared Desktop so the operator knows it's done
    $ts      = Get-Date -Format "yyyy-MM-dd HH:mm"
    $doneMsg = "Windows Update dokoncen: $ts`r`nStroj je plne aktualizovan a pripraven k predani klientovi."
    [System.IO.File]::WriteAllText(
        "C:\Users\Public\Desktop\! WU HOTOVO $ts.txt",
        $doneMsg,
        [System.Text.Encoding]::UTF8
    )

    # Lock the workstation - login screen = clear visual signal for the operator.
    # Runs as adminx9 (interactive session) via a one-shot task, self-deletes after 5 min.
    $lockAction    = New-ScheduledTaskAction -Execute "rundll32.exe" -Argument "user32.dll,LockWorkStation"
    $lockTrigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5)
    $lockPrincipal = New-ScheduledTaskPrincipal -UserId "adminx9" -LogonType Interactive -RunLevel Limited
    $lockSettings  = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5)
    Register-ScheduledTask -TaskName "X9-WUDoneLock" -Action $lockAction -Trigger $lockTrigger `
        -Principal $lockPrincipal -Settings $lockSettings -Force -ErrorAction SilentlyContinue | Out-Null

    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}


} else {
    # No more updates - disable autologon and clean up
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue

    # Leave a visible marker on the shared Desktop so the operator knows it's done
    $ts      = Get-Date -Format "yyyy-MM-dd HH:mm"
    $doneMsg = "Windows Update dokoncen: $ts`r`nStroj je plne aktualizovan a pripraven k predani klientovi."
    [System.IO.File]::WriteAllText(
        "C:\Users\Public\Desktop\! WU HOTOVO $ts.txt",
        $doneMsg,
        [System.Text.Encoding]::UTF8
    )

    # Lock the workstation - login screen = clear visual signal for the operator.
    # Runs as adminx9 (interactive session) via a one-shot task, self-deletes after 5 min.
    $lockAction    = New-ScheduledTaskAction -Execute "rundll32.exe" -Argument "user32.dll,LockWorkStation"
    $lockTrigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5)
    $lockPrincipal = New-ScheduledTaskPrincipal -UserId "adminx9" -LogonType Interactive -RunLevel Limited
    $lockSettings  = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5)
    Register-ScheduledTask -TaskName "X9-WUDoneLock" -Action $lockAction -Trigger $lockTrigger `
        -Principal $lockPrincipal -Settings $lockSettings -Force -ErrorAction SilentlyContinue | Out-Null

    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}

<#
.SYNOPSIS
    Installs all available Windows Updates via PSWindowsUpdate module.

.DESCRIPTION
    First pass: installs all currently available updates without rebooting.
    Then registers a scheduled task "X9-WindowsUpdate" that runs on every
    logon (as SYSTEM) until no more updates are found - handles the typical
    2-3 reboot cycles required on a fresh Windows installation.

    Operator workflow:
      1. xetup completes all steps
      2. Operator reboots manually
      3. On each subsequent logon the scheduled task runs another update pass
      4. Task removes itself automatically when system is fully up to date

.ITEMS
    nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
    spustit-prvni-kolo-windows-update: First update pass without reboot - installs all currently available updates.
    registrovat-scheduled-task-pro-dalsi-kola: Registers X9-WindowsUpdate scheduled task that runs on logon, handles post-reboot update rounds, and self-deletes when no more updates are found.
#>
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"
    $null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
    Write-Output $line
}

Write-Log "=== Step 12 - Windows Update ===" -Level STEP

# -----------------------------------------------------------------------
# 1. NuGet provider + PSWindowsUpdate module
# -----------------------------------------------------------------------
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
try {
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
    $existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
    if ($existing) {
        Write-Log "  PSWindowsUpdate $($existing.Version) already installed" -Level INFO
    } else {
        Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
        Write-Log "  PSWindowsUpdate installed" -Level OK
    }
    Import-Module PSWindowsUpdate -Force
} catch {
    Write-Log "  Module setup failed: $_" -Level ERROR
    exit 1
}

# -----------------------------------------------------------------------
# 2. First update pass (no reboot)
# -----------------------------------------------------------------------
Write-Log "Running first Windows Update pass..." -Level INFO
try {
    $result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
    $installed = @($result | Where-Object { $_ -match 'KB\d+|Downloaded|Installed' })
    if ($installed.Count -gt 0) {
        $result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log "  $_" -Level INFO }
        Write-Log "  First pass complete - reboot required for remaining rounds" -Level OK
    } else {
        Write-Log "  System already up to date" -Level OK
    }
} catch {
    Write-Log "  First pass failed: $_" -Level ERROR
}

# -----------------------------------------------------------------------
# 3. Enable autologon for adminx9 (temporary - disabled when updates complete)
# -----------------------------------------------------------------------
Write-Log "Enabling temporary autologon for adminx9..." -Level INFO

$winlogonPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
try {
    Set-ItemProperty -Path $winlogonPath -Name "AutoAdminLogon"  -Value "1"  -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultUserName" -Value "adminx9" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultPassword" -Value "" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultDomainName" -Value "." -Type String -Force
    # Safety cap: max 10 automatic logons in case the task fails to clean up
    Set-ItemProperty -Path $winlogonPath -Name "AutoLogonCount"  -Value 10 -Type DWord -Force
    Write-Log "  Autologon enabled (adminx9, max 10 rounds)" -Level OK
} catch {
    Write-Log "  Failed to enable autologon: $_" -Level WARN
    Write-Log "  Windows Update rounds will require manual login after each reboot" -Level WARN
}

# -----------------------------------------------------------------------
# 4. Scheduled task for post-reboot update rounds (self-deleting)
# -----------------------------------------------------------------------
Write-Log "Registering post-reboot update task..." -Level INFO

$taskName = "X9-WindowsUpdate"

# PowerShell block that runs on each logon until no more updates found.
# When done: disables autologon and removes itself.
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
    # No more updates - disable autologon and remove this task
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue
    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@

try {
    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `
                 -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
    $trigger = New-ScheduledTaskTrigger -AtLogOn
    $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
                  -MultipleInstances IgnoreNew
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

    # Remove existing task first (idempotent)
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
      -Settings $settings -Principal $principal -Force | Out-Null

    Write-Log "  Task '$taskName' registered - runs on each logon until fully updated" -Level OK
} catch {
    Write-Log "  Failed to register scheduled task: $_" -Level WARN
    Write-Log "  Manual Windows Update rounds will be needed after reboot" -Level WARN
}

Write-Log "Step 12 - Windows Update complete" -Level OK

-    # No more updates - disable autologon and remove this task

+    # No more updates - disable autologon and clean up

+    # Leave a visible marker on the shared Desktop so the operator knows it's done

+    $ts      = Get-Date -Format "yyyy-MM-dd HH:mm"

+    $doneMsg = "Windows Update dokoncen: $ts`r`nStroj je plne aktualizovan a pripraven k predani klientovi."

+        "C:\Users\Public\Desktop\! WU HOTOVO $ts.txt",

+    # Lock the workstation - login screen = clear visual signal for the operator.

+    # Runs as adminx9 (interactive session) via a one-shot task, self-deletes after 5 min.

+    $lockAction    = New-ScheduledTaskAction -Execute "rundll32.exe" -Argument "user32.dll,LockWorkStation"

+    $lockTrigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5)

+    $lockPrincipal = New-ScheduledTaskPrincipal -UserId "adminx9" -LogonType Interactive -RunLevel Limited

+    $lockSettings  = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5)

+    Register-ScheduledTask -TaskName "X9-WUDoneLock" -Action $lockAction -Trigger $lockTrigger `

+        -Principal $lockPrincipal -Settings $lockSettings -Force -ErrorAction SilentlyContinue | Out-Null


Bash

GOOS=windows GOARCH=amd64 go build ./... 2>&1 && git add scripts/12-windows-update.ps1 && git commit -m "$(cat <<'EOF'
feat: WU completion indicators - lock screen + desktop file

When X9-WindowsUpdate finds no more updates:
- Creates "! WU HOTOVO yyyy-MM-dd HH:mm.txt" on C:\Users\Public\Desktop
- Locks the workstation via one-shot task running as adminx9
  (login screen = unambiguous visual signal for the operator)
- One-shot lock task self-deletes after 5 minutes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)" && TOKEN=$(sudo docker exec -u git xetup-forgejo forgejo admin user generate-access-token --username x9 --token-name "push-$(date +%s)" --raw --scopes "write:repository" 2>/dev/null | tail -1) && git push "https://x9:${TOKEN}@git.xetup.x9.cz/x9/xetup.git" main 2>&1

Build, commit and push


[main 9feb7ba] feat: WU completion indicators - lock screen + desktop file
 1 file changed, 20 insertions(+), 1 deletion(-)
To https://git.xetup.x9.cz/x9/xetup.git
   3fb6578..9feb7ba  main -> main


Pushnuté.

Jak to bude vypadat pro technika:
1. Spustí xetup → stroj se restartuje → odejde
2. Stroj se přihlásí sám jako adminx9, nainstaluje updaty, restartuje → opakuje
3. Až jsou updaty hotové → na Desktopu přibyde `! WU HOTOVO 2026-04-16 14:35.txt` → stroj se **zamkne** (přihlašovací obrazovka)
4. Technik vidí přihlašovací obrazovku = hotovo, přihlásí se → na Desktopu najde soubor s časem dokončení


a co když ty dell updaty budopu chtít restart? Máme na to taky nějaké řešení? Jako např. xetup s příslušným parametrem v "po spuštění" nebo tak něco?


Dobrý postřeh — Dell drivery/firmware staging potřebují restart stejně jako WU, takže potřebujeme i pro Dell scheduled task + cross-check mezi oběma tasky. Přečtu aktuální stav:


1	<#
2	.SYNOPSIS
3	    Detects Dell hardware, installs Dell Command | Update, and applies all available updates.
4
5	.DESCRIPTION
6	    Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without
7	    error so the same deployment script works on any hardware. On Dell machines, installs
8	    Dell Command | Update (Universal) via winget and immediately runs /applyUpdates with
9	    -reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are
10	    staged at this point and finalize automatically during the restart that closes the
11	    deployment. The operator does not need to run a separate update pass after setup.
12
13	.ITEMS
14	    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.
15	    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.
16	    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.
17	    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.
18	#>
19	param(
20	    [object]$Config,
21	    [string]$LogFile
22	)
23
24	$ErrorActionPreference = "Continue"
25
26	function Write-Log {
27	    param([string]$Message, [string]$Level = "INFO")
28	    $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
29	    $null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
30	    Add-Content -Path $LogFile -Value $line -Encoding UTF8
31	    switch ($Level) {
32	        "OK"    { Write-Host $line -ForegroundColor Green }
33	        "ERROR" { Write-Host $line -ForegroundColor Red }
34	        "WARN"  { Write-Host $line -ForegroundColor Yellow }
35	        "STEP"  { Write-Host $line -ForegroundColor Cyan }
36	        default { Write-Host $line }
37	    }
38	}
39
40	function Get-Feature {
41	    param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
42	    try {
43	        if ($null -eq $Cfg) { return $Default }
44	        $stepFeatures = $Cfg.features.$StepID
45	        if ($null -eq $stepFeatures) { return $Default }
46	        $val = $stepFeatures.$FeatureID
47	        if ($null -eq $val) { return $Default }
48	        return [bool]$val
49	    } catch { return $Default }
50	}
51
52	# -----------------------------------------------------------------------
53	# Detect Dell hardware
54	# -----------------------------------------------------------------------
55	Write-Log "Checking hardware manufacturer" -Level INFO
56
57	try {
58	    $cs           = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
59	    $manufacturer = $cs.Manufacturer
60	    $model        = $cs.Model
61	    Write-Log "  Manufacturer: $manufacturer  Model: $model" -Level INFO
62	}
63	catch {
64	    Write-Log "  Failed to query Win32_ComputerSystem: $_" -Level ERROR
65	    return
66	}
67
68	if ($manufacturer -notmatch "Dell") {
69	    Write-Log "Not a Dell machine ($manufacturer) - step skipped" -Level WARN
70	    return
71	}
72
73	Write-Log "Dell confirmed: $model" -Level OK
74
75	# -----------------------------------------------------------------------
76	# Install Dell Command | Update via winget
77	# -----------------------------------------------------------------------
78	Write-Log "Installing Dell Command | Update (Universal)..." -Level STEP
79
80	$wingetArgs = @(
81	    "install",
82	    "--id",   "Dell.CommandUpdate.Universal",
83	    "--silent",
84	    "--accept-package-agreements",
85	    "--accept-source-agreements"
86	)
87
88	$wingetOutput = & winget @wingetArgs 2>&1
89	$wingetExit   = $LASTEXITCODE
90	$wingetOutput | ForEach-Object { Write-Log "  [winget] $_" -Level INFO }
91
92	if ($wingetExit -ne 0 -and $wingetExit -ne 1638) {   # 1638 = already installed
93	    Write-Log "  winget exit code $wingetExit - checking if DCU is already present" -Level WARN
94	}
95
96	# Locate dcu-cli.exe (path is the same for x64 and Universal edition)
97	$dcuCandidates = @(
98	    "C:\Program Files\Dell\CommandUpdate\dcu-cli.exe",
99	    "C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe"
100	)
101	$dcuCli = $dcuCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
102
103	if (-not $dcuCli) {
104	    Write-Log "  dcu-cli.exe not found - cannot run updates" -Level ERROR
105	    return
106	}
107
108	Write-Log "  dcu-cli.exe found: $dcuCli" -Level OK
109
110	# -----------------------------------------------------------------------
111	# Run updates - categories controlled by feature flags
112	# -reboot=disable  -> no mid-deployment reboot; BIOS/firmware staged for next restart
113	# -----------------------------------------------------------------------
114	$runDrivers = Get-Feature $Config "dellUpdate" "drivers"
115	$runBios    = Get-Feature $Config "dellUpdate" "bios"
116
117	if (-not $runDrivers -and -not $runBios) {
118	    Write-Log "Both drivers and bios features disabled - skipping update run" -Level INFO
119	} else {
120	    # Build update type list from enabled features
121	    $updateTypes = @()
122	    if ($runDrivers) {
123	        $updateTypes += "driver"
124	        $updateTypes += "firmware"
125	    }
126	    if ($runBios) {
127	        $updateTypes += "bios"
128	    }
129	    $updateTypeArg = $updateTypes -join ","
130
131	    Write-Log "Running Dell Command | Update (updateType=$updateTypeArg, no auto-reboot)..." -Level STEP
132	    Write-Log "  This may take several minutes depending on available updates" -Level INFO
133
134	    $dcuOutput = & $dcuCli /applyUpdates -silent -reboot=disable "-updateType=$updateTypeArg" 2>&1
135	    $exitCode  = $LASTEXITCODE
136	    $dcuOutput | ForEach-Object { Write-Log "  [DCU] $_" -Level INFO }
137
138	    Write-Log "  DCU exit code: $exitCode" -Level INFO
139
140	    # Dell Command | Update exit codes:
141	    #  0 = completed, no updates required or updates applied (no reboot needed)
142	    #  1 = updates applied, reboot required to finalize BIOS/firmware
143	    #  5 = no applicable updates found for this system
144	    #  others = error or partial failure
145	    switch ($exitCode) {
146	        0       { Write-Log "Dell Command | Update: complete (no reboot required)" -Level OK }
147	        1       { Write-Log "Dell Command | Update: updates staged - BIOS/firmware will finalize on restart" -Level OK }
148	        5       { Write-Log "Dell Command | Update: no applicable updates for this model" -Level OK }
149	        default { Write-Log "Dell Command | Update: exit code $exitCode - review DCU log in C:\ProgramData\Dell\UpdateService\Logs" -Level WARN }
150	    }
151	}
152
153	Write-Log "Step 11 complete" -Level OK
154

<#
.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"
    $null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
    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


[Request interrupted by user]


jetě nic nedělej, pojďme diskutovat. Co když si prostě xetup bude zapisovat, kde je a po stuštění se znovu spustí, načte si stav a pokračuje? Hm? Pak se to může restartovat, jak chce a pokud bude tohle a autologon, tak to pofrčí ....


Jo, to je podstatně čistší architektura. Teď to mám jako spoustu nezávislých scheduled tasků, kde každý si řeší svoje — to je nepřehledné a těžko se ladí.

Návrh v principu:

**State file** — `C:\Windows\Setup\Scripts\xetup-state.json`
```json
{
  "phase": "post-reboot",
  "pendingSteps": ["windowsUpdate"],
  "completedSteps": ["adminAccount", ..., "dellUpdate"],
  "config": { ... }
}
```

**xetup flow:**
1. Spustí se → zkontroluje jestli existuje state file
2. Pokud ne → normální formulář, uživatel nakonfiguruje
3. Pokud ano → přeskočí formulář, pokračuje od místa kde přestal (skip completed steps)
4. Krok potřebuje restart → xetup uloží stav, zaregistruje sám sebe jako scheduled task při logonu, zapne autologon, restartuje
5. Po rebootu → autologon → xetup se spustí → načte state → pokračuje
6. Všechno hotovo → odstraní scheduled task, vypne autologon, zobrazí summary + zamkne

**Co se zjednoduší:**
- `11-dell-update.ps1` a `12-windows-update.ps1` nepotřebují vlastní scheduled tasky vůbec — jen vrátí "potřebuji restart" a xetup to řeší
- autologon v jednom místě, ne rozházený po skriptech
- completion logic v jednom místě

**Otevřené otázky před implementací:**

1. **Kdy Dell/WU ví, že potřebuje další kolo?** DCU vrací exit code 1 = reboot needed. WU po instalaci = vždy reboot. xetup po rebootu znovu spustí ten samý krok dokud nevyjde "hotovo".

2. **UI v resume mode** — zobrazit formulář nebo rovnou skočit do run view? Asi rovnou run, jen s hlavičkou "Pokračuji po restartu…"

3. **Co když uživatel spustí xetup ručně** a state file existuje z předchozího běhu? Nabídnout "Pokračovat" nebo "Začít znova"?

4. **Steps které potřebují vícenásobné kolo** (WU může chtít 3-4 rebooty) — stačí mít krok jako "opakuj dokud exit 0" nebo potřebuješ explicitní počítadlo?

Jak to vidíš? Chceš to takto rozjet?


Budujeme Windows deployment tool xetup. Právě jsme diskutovali redesign post-reboot orchestrace — xetup by si ukládal stav a po autologonu pokračoval sám, místo samostatných scheduled tasků. Čekáme na Filipovo rozhodnutí, zda to takto implementovat.


nooo ... jenomže co když je xetup spuštěný pod jiným uživatelem? Co když adminx9 neexistuje? Jen diskutuju ... V podstatě by to asi mělo být tak, že pokud už není spuštěn pod adminx9, tak si toho uživatele vytvoří, zaregistruje si autologon pro adminx9m do profilu adminx9 si vytvoří ten schtask ... a jede. A druhá otázka: to gui automaticky ukládá konfigurační jsou, když se dá spustit, že?


Dobrá poznámka — tady jsou dvě situace:

**Problém s uživatelem:**

Před restartem xetup potřebuje garantovat, že po rebootu poběží pod někým, kdo:
- existuje (i na čistém Windows)
- má admin práva
- je předvídatelný

Takže logika před každým restartem:
1. Zkontroluj jestli `adminx9` existuje → pokud ne, vytvoř ho inline (bez hesla, bez viditelnosti na login screen) — je to v podstatě mini-krok 00 jako prerekvizita
2. Nastav autologon pro `adminx9`
3. Zaregistruj scheduled task "spusť xetup.exe z téhle cesty, při logonu adminx9"
4. Ulož state, rebootuj

Pokud xetup už **běží jako adminx9** — žádná extra akce, přímočaré.
Pokud běží jako **někdo jiný** — nevadí, adminx9 se vytvoří/zkontroluje a autologon se nastaví pro něj. Po rebootu to přebere adminx9.

---

**Config při SPUSTIT:**

Ano, ukládá se — v `gui.go` je:
```go
_ = config.Save(finalCfg, cfgPath)
```

State file pro resume by pak obsahoval celou konfiguraci i seznam pending steps, takže po rebootu formulář vůbec nepotřebujeme — rovnou do run phase se stavem "Pokračuji po restartu...".

---

Jediná otázka co vidím: **kde leží xetup.exe po prvním spuštění?** Technik ho stáhne třeba na `C:\Users\Jan\Downloads\xetup.exe`, po rebootu adminx9 ho nenajde. Potřebujeme ho před prvním restartem zkopírovat někam stabilního — třeba `C:\Windows\Setup\Scripts\xetup.exe`.

Dává to smysl takhle?


jo, to už musí technik vědět .. spustit to z nějaké opakovatelné cesty při přechodu mezi profilama. To by si ostatně xetup po startu mohl zkontrolovat a upozornit, žejo. Takže máme vše zodpovězeno?


Ano, máme vše. Shrnutí návrhu:

**Před každým restartem xetup:**
1. Zkontroluje cestu odkud běží — pokud `Downloads`, `Temp` apod., upozorní (ale neblokuje)
2. Zajistí `adminx9` existuje (inline, bez hesla)
3. Nakopíruje sebe do `C:\Windows\Setup\Scripts\xetup.exe`
4. Uloží `xetup-state.json` (config + pending steps + completed steps)
5. Zapne autologon pro `adminx9`
6. Zaregistruje scheduled task: `xetup.exe --resume` při logonu adminx9
7. Restartuje

**Po rebootu (resume mode):**
- Formulář přeskočen, rovnou run phase: „Pokračuji po restartu…"
- Hotové kroky přeskočeny, pokračuje od pending
- Po dokončení všeho: vypne autologon, odstraní task, summary + zamknutí

**Co zmizí:**
- Vlastní scheduled tasky v `11-dell-update.ps1` a `12-windows-update.ps1` — skript jen vrátí potřebu rebootu, xetup orchestruje
- Autologon logika rozházená po skriptech

Jdeme do toho implementovat?


jo aha, on se stejně někam nakopíruje - no tak to nemusí ani upozorňovat ... Takže pokud se už ale pustí pod adminx9 (což se bude reálně dít), tak co?


Přesně — pokud už běží jako adminx9, je vše hotové z minulého kola:
- xetup je na stabilní cestě ✓
- autologon je nastavený ✓
- scheduled task existuje ✓

Takže logika před každým restartem je prostě:

```
if (current user != adminx9):
    ensure adminx9 exists
    copy xetup.exe → C:\Windows\Setup\Scripts\xetup.exe
    enable autologon adminx9
    register scheduled task

// vždy:
save xetup-state.json
reboot
```

Druhé a další kolo (WU round 2, Dell round 2...) — už je adminx9, takže celý blok přeskočí a jde rovnou na uložení stavu + restart.

Jediný rozdíl mezi prvním a dalšími koly je ten `if`. Jinak identická logika. Jdeme implementovat?


jo


Jasně. Přečtu si nejdřív aktuální stav main.go a config.go, pak napíšu vše najednou:


1	// Command xetup is the GUI launcher for Windows deployment.
2	//
3	// All PowerShell scripts and assets are embedded in the binary and extracted
4	// to a temp directory at runtime. The GUI collects configuration, streams
5	// live log output while the scripts run, and shows a final summary.
6	//
7	// Cross-compile for Windows (requires MinGW):
8	//
9	//	CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
10	//	  GOOS=windows GOARCH=amd64 \
11	//	  go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup
12	package main
13
14	import (
15		"fmt"
16		"log"
17		"os"
18		"path/filepath"
19
20		xetupembed "git.xetup.x9.cz/x9/xetup"
21		"git.xetup.x9.cz/x9/xetup/internal/config"
22		"git.xetup.x9.cz/x9/xetup/internal/gui"
23		"git.xetup.x9.cz/x9/xetup/internal/runner"
24	)
25
26	func main() {
27		// Load config (falls back to defaults when config.json is missing)
28		cfgPath := config.ConfigPath()
29		cfg, err := config.Load(cfgPath)
30		if err != nil {
31			fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err)
32		}
33
34		// Temp working directory – cleaned up on exit
35		tmpDir, err := os.MkdirTemp("", "xetup-*")
36		if err != nil {
37			log.Fatalf("Cannot create temp dir: %v", err)
38		}
39		defer os.RemoveAll(tmpDir)
40
41		// Extract embedded scripts and assets
42		if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
43			log.Fatalf("Failed to extract scripts: %v", err)
44		}
45		if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
46			log.Fatalf("Failed to extract assets: %v", err)
47		}
48
49		// Write runtime config JSON so PowerShell scripts can read it
50		cfgRuntimePath, err := runner.WriteConfig(cfg, tmpDir)
51		if err != nil {
52			log.Fatalf("Failed to write runtime config: %v", err)
53		}
54
55		runCfg := runner.RunConfig{
56			ScriptsDir:  filepath.Join(tmpDir, "scripts"),
57			ConfigPath:  cfgRuntimePath,
58			LogFile:     `C:\Windows\Setup\Scripts\Deploy.log`,
59			ProfileType: cfg.Deployment.ProfileType,
60		}
61
62		gui.Run(cfg, runCfg, cfgPath)
63	}
64

// 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)
}


1	package config
2
3	import (
4		"encoding/json"
5		"os"
6		"path/filepath"
7	)
8
9	// Config mirrors config.json structure.
10	type Config struct {
11		Deployment    Deployment             `json:"deployment"`
12		AdminAccount  AdminAccount           `json:"adminAccount"`
13		Activation    Activation             `json:"activation"`
14		Software      Software               `json:"software"`
15		Steps         map[string]bool        `json:"steps"`
16		Features      Features               `json:"features"`
17	}
18
19	type Deployment struct {
20		PCName        string `json:"pcName"`
21		PCDescription string `json:"pcDescription"`
22		Timezone      string `json:"timezone"`
23		ProfileType   string `json:"profileType"` // default | admin | user
24	}
25
26	type AdminAccount struct {
27		Username string `json:"username"`
28	}
29
30	type Activation struct {
31		ProductKey string `json:"productKey"`
32		KMSServer  string `json:"kmsServer"`
33	}
34
35	type SoftwareItem struct {
36		Name     string `json:"name"`
37		WingetID string `json:"wingetId"`
38	}
39
40	type Software struct {
41		Install []SoftwareItem `json:"install"`
42	}
43
44	// Features holds per-step, per-feature toggle flags.
45	// Keys: stepID -> featureID -> enabled.
46	// A missing key defaults to true (feature enabled).
47	type Features map[string]map[string]bool
48
49	// DefaultConfig returns a config with sensible defaults.
50	func DefaultConfig() Config {
51		return Config{
52			Deployment: Deployment{
53				Timezone:    "Central Europe Standard Time",
54				ProfileType: "default",
55			},
56			AdminAccount: AdminAccount{
57				Username: "adminx9",
58			},
59			Activation: Activation{
60				ProductKey: "",
61			},
62			Software: Software{
63				Install: []SoftwareItem{
64					{Name: "7-Zip", WingetID: "7zip.7zip"},
65					{Name: "Adobe Acrobat Reader 64-bit", WingetID: "Adobe.Acrobat.Reader.64-bit"},
66					{Name: "OpenVPN Connect", WingetID: "OpenVPNTechnologies.OpenVPNConnect"},
67				},
68			},
69			Steps: map[string]bool{
70				"adminAccount":    true,
71				"bloatware":       true,
72				"software":        true,
73				"systemRegistry":  true,
74				"defaultProfile":  true,
75				"personalization": true,
76				"scheduledTasks":  true,
77				"backinfo":        true,
78				"activation":      true,
79				"dellUpdate":      true,
80				"windowsUpdate":   true,
81				"network":         true,
82				"pcIdentity":      true,
83			},
84			Features: Features{
85				"software": {
86					"wingetInstalls": true,
87					"pdfDefault":     true,
88					"ateraAgent":     true,
89				},
90				"systemRegistry": {
91					"systemTweaks":      true,
92					"edgePolicies":      true,
93					"oneDriveUninstall": true,
94					"powercfg":          true,
95					"proxyDisable":      true,
96				},
97				"defaultProfile": {
98					"taskbarTweaks":   true,
99					"startMenuTweaks": true,
100					"explorerTweaks":  true,
101				},
102				"dellUpdate": {
103					"drivers": true,
104					"bios":    true,
105				},
106			},
107		}
108	}
109
110	// Load reads config.json from the given path.
111	// If the file does not exist, returns DefaultConfig without error.
112	func Load(path string) (Config, error) {
113		cfg := DefaultConfig()
114		data, err := os.ReadFile(path)
115		if os.IsNotExist(err) {
116			return cfg, nil
117		}
118		if err != nil {
119			return cfg, err
120		}
121		if err := json.Unmarshal(data, &cfg); err != nil {
122			return cfg, err
123		}
124		return cfg, nil
125	}
126
127	// Save writes config to the given path (creates directories if needed).
128	func Save(cfg Config, path string) error {
129		if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
130			return err
131		}
132		data, err := json.MarshalIndent(cfg, "", "  ")
133		if err != nil {
134			return err
135		}
136		return os.WriteFile(path, data, 0644)
137	}
138
139	// ConfigPath returns the default config.json path (next to the executable).
140	func ConfigPath() string {
141		exe, err := os.Executable()
142		if err != nil {
143			return "config.json"
144		}
145		return filepath.Join(filepath.Dir(exe), "config.json")
146	}
147

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,
			"windowsUpdate":   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")
}


Conversation compacted


This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:

This session continued from a previous one. Key requests in order:
- Complete the step progress strip in gui.go (was mid-implementation)
- Fix step ordering bug (09/10 were after 11/12 and had swapped script names)
- Update spec page at xetup.x9.cz/spec/ to reflect current reality
- Add auto-deploy job for spec page changes (git pull → immediate deploy via bind mount)
- Fix BackInfo black background + wrong taskbar color (RGB 130,232,253)
- Add note under download link about antivirus/curl
- Fix Explorer still missing from taskbar (recurring)
- Add autologon for adminx9 during Windows Update rounds (confirmed implemented)
- Add WU completion indicator (lock screen + desktop file)
- (Interrupted) Dell updates scheduled task discussion → pivoted to new architecture
- **New architecture**: xetup.exe acts as state machine across reboots — saves state, copies itself to stable path, ensures adminx9, sets autologon, registers X9-Resume task; PS scripts just exit code 9 = reboot needed; user confirmed design and said "jo" (implement)

2. Key Technical Concepts:

- **Walk GUI** (Win32, no OpenGL) — three-phase: form → run → done
- **Step progress strip** — color-coded labels: · pending gray, ► running blue, ✓ ok green, ✗ error red, – skipped gray
- **COLORREF format** — 0x00BBGGRR: #223B47 = COLORREF 4668194 (71×65536 + 59×256 + 34)
- **ABGR format** — Windows accent color storage: #223B47 = 0xFF473B22 = 4283612962
- **AccentColorMenu** vs **DWM\AccentColor** — taskbar reads Explorer\Accent\AccentColorMenu, title bars read DWM\AccentColor
- **BackInfo AutoBackground=1** unreliable during deployment (live session doesn't reflect registry changes yet)
- **File Explorer.lnk** missing from Default profile on clean Win11 → TaskbarLayoutModification.xml pin silently skipped
- **Web bind mount** — `./web:/usr/share/nginx/html:ro` → git pull on host = immediate deploy, no container restart
- **heal.sh** — git pull + nginx reload on web changes + deploy.json for version tracking
- **State machine architecture** — xetup.exe saves xetup-state.json, copies self to stable path C:\Windows\Setup\Scripts\xetup.exe, ensures adminx9, sets autologon (AutoLogonCount=20 safety cap), registers X9-Resume scheduled task (AtLogOn adminx9, RunLevel Highest)
- **Exit code 9** — PS scripts exit with 9 to signal "reboot needed" to xetup (instead of own scheduled tasks)
- **Resume mode** — xetup detects state file → skips form → runs pending steps → shows "Pokračuji po restartu..."
- **Pre-reboot logic**: if not running as adminx9 → create adminx9 inline, copy self, autologon, register task; if already adminx9 → skip setup, just update state + reboot

3. Files and Code Sections:

- **`internal/gui/gui.go`**
  - Added `"strings"` import (was missing, caused compile error for `strings.ToLower(res.Status)`)
  - Step progress strip fully implemented with `stepIndicator` struct, `updateIndicator()`, horizontal composite
  - NOT YET: resume mode, pre-reboot flow (pending implementation)

- **`internal/runner/runner.go`**
  - Fixed step ordering: `pcIdentity` Num="09" ScriptName="09-pc-identity.ps1", `network` Num="10" ScriptName="10-network.ps1", both before dellUpdate(11) and windowsUpdate(12)
  - NOT YET: exit code 9 → REBOOT status (pending)

- **`assets/Backinfo/BackInfo.ini`**
  - Changed `AutoBackground = 1` → `AutoBackground = 0`
  - Changed `BackgroundColor = 2097152` (near-black) → `BackgroundColor = 4668194` (#223B47 in COLORREF)

- **`scripts/05-personalization.ps1`**
  - Added `Explorer\Accent\AccentColorMenu` and `Explorer\Accent\StartColorMenu` = `$AccentColorABGR` (0xFF473B22) in `Apply-ThemeSettings`
  - Previously only set DWM\AccentColor (title bars), not taskbar color key

- **`scripts/04-default-profile.ps1`**
  - Before writing LayoutModification.xml, explicitly creates `File Explorer.lnk` in `C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\System Tools\`
  - For admin profile: also creates `Windows PowerShell.lnk` in `...\Windows PowerShell\`
  - Uses `WScript.Shell.CreateShortcut()` pointing to `$env:WINDIR\explorer.exe`

- **`scripts/12-windows-update.ps1`**
  - Added autologon setup (AutoAdminLogon=1, DefaultUserName=adminx9, DefaultPassword="", AutoLogonCount=10)
  - Added completion indicators: `! WU HOTOVO yyyy-MM-dd HH:mm.txt` on `C:\Users\Public\Desktop\`
  - Added lock workstation via one-shot X9-WUDoneLock task (runs as adminx9 interactive, self-deletes after 5 min)
  - Cleanup removes AutoAdminLogon, DefaultPassword, AutoLogonCount when no more updates

- **`scripts/11-dell-update.ps1`**
  - Read but NOT YET MODIFIED — interrupted before implementing scheduled task (architecture changed)

- **`web/spec/index.html`**
  - Sidebar: 00-12 in order, arch renamed "Go GUI"
  - Step 03: Edge NTP rows (NewTabPageQuickLinksEnabled=0, NewTabPageBackgroundEnabled=0, NewTabPageAllowedBackgroundTypes=3)
  - Step 04: taskbar row updated (explicit Explorer+Edge, PinListPlacement=Replace)
  - Step 05: Wallpaper="" fix row
  - Steps 09/10/11: correct IDs, moved from "Planovane" to main section
  - Step 12: new Windows Update card
  - Arch: Walk GUI, badge-ok, correct dependencies (lxn/walk)
  - Footer: `web: <span id="deploy-sha">...</span>` with JS loading /data/deploy.json
  - JS STEP_SCRIPT updated: step-09/10/11/12 (was step-pc/step-net/step-dell)
  - Version: 0.4

- **`web/index.html`**
  - Added paragraph after install-box: "Pokud ve stazeni brani antivirus, pouzijte curl – staci bezny prikazovy radek, neni potreba administrator."

- **`heal.sh`**
  - Extended with git fetch + reset --hard origin/main when behind
  - Writes `web/data/deploy.json` with `{"sha":"...","ts":"..."}`
  - nginx reload if web/ changed

- **`cmd/xetup/main.go`** — read, not yet modified (pending state file check)
- **`internal/config/config.go`** — read, not modified

4. Errors and Fixes:

- **Missing `strings` import in gui.go**: `strings.ToLower(res.Status)` used but `"strings"` not imported. Fixed by adding to import block. Build then succeeded.
- **Step order bug**: `network` (Num:"09") and `pcIdentity` (Num:"10") had swapped script names AND were placed after steps 11+12 in AllSteps() slice. Fixed by correcting both the Nums, ScriptNames, and position in slice.
- **BackInfo black background**: `AutoBackground=1` reads live desktop color; during deployment Windows hasn't propagated registry color change to live session → reads black. Plus `BackgroundColor=2097152` = RGB(0,0,32) = near-black fallback. Fixed with `AutoBackground=0` + `BackgroundColor=4668194`.
- **Taskbar wrong color RGB(130,232,253)**: Script set DWM\AccentColor (title bar colorization) but not `Explorer\Accent\AccentColorMenu` (actual taskbar color source). Windows used default "Steel" blue. Fixed by adding AccentColorMenu + StartColorMenu.
- **Explorer missing from taskbar**: `%APPDATA%\...\System Tools\File Explorer.lnk` doesn't exist in Default profile on clean Win11. XML pin silently skipped. Fixed by explicitly creating the .lnk before writing XML.
- **git push auth "Credentials are incorrect"**: Used `sudo docker exec -u git xetup-forgejo forgejo admin user generate-access-token --username x9 --token-name "push-$(date +%s)" --raw --scopes "write:repository"` for each push.

5. Problem Solving:

- **Architecture pivot**: Instead of each PS script managing its own scheduled task (complex, fragmented), xetup.exe becomes the single orchestrator that handles all restarts. PS scripts simply signal "reboot needed" via exit code 9. This eliminates: X9-WUDoneLock complexity, cross-task dependencies between Dell and WU tasks, autologon management scattered across scripts.
- **adminx9 not existing at reboot time**: Solved by having xetup create adminx9 inline (if not already running as adminx9) before setting up autologon and scheduling itself.
- **Web auto-deploy**: Since web/ is a bind mount, git pull on host = immediate content update. heal.sh runs every 5 min as systemd timer.
- **WU completion indicator**: Two-signal approach: desktop text file (visible after login) + workstation lock (visible immediately = login screen as "done" signal).

6. All User Messages:

- "je ten nový xetup vypublikovaný na webu???"
- "proč jsou ty kroky v přeházeném pořadí? 6 7 8 11 12 9 10?"
- "a teď pořádněji podle reality aktualizuj https://xetup.x9.cz/spec/"
- "a udělej na to taky nějaký automatický job: ale udělej nějaké verzování nebo podle timestampů. Pokud se https://xetup.x9.cz/spec/ aktualizoval dříve, než byl nasazen nový xetup.exe... tak aktualizovat"
- "je tam asi nějaký bug (kolem bginfo?), který nastavuje barvu pozadí na černou a barvu taskbaru na RGB 130 232 253"
- "ještě mi na tu landing page napiš pod ten download link 'Pokud ve stažení brání antivirus, použijte curl'. Curl stačí spustit v normálním příkazovém řádku, není potřeba administrator."
- "a pořád na taskbaru chybí explorer :-)"
- "a dodělals ten autologon pro adminx9 (zrušit pak zároveň se schtaskem)"
- "hm... ale jak budu vědět, že už je to hotový? :-) Nikde není žádný indikátor ..."
- "a co když ty dell updaty budou chtít restart? Máme na to taky nějaké řešení? Jako např. xetup s příslušným parametrem v 'po spuštění' nebo tak něco?"
- "[Request interrupted] ještě nic nedělej, pojďme diskutovat. Co když si prostě xetup bude zapisovat, kde je a po spuštění se znovu spustí, načte si stav a pokračuje? Hm? Pak se to může restartovat, jak chce a pokud bude tohle a autologon, tak to pofrčí ...."
- "nooo ... jenomže co když je xetup spuštěný pod jiným uživatelem? Co když adminx9 neexistuje? Jen diskutuji ... V podstatě by to asi mělo být tak, že pokud už není spuštěn pod adminx9, tak si toho uživatele vytvoří, zaregistruje si autologon pro adminx9, do profilu adminx9 si vytvoří ten schtask ... a jede. A druhá otázka: to gui automaticky ukládá konfigurační json, když se dá spustit, že?"
- "jo, to už musí technik vědět .. spustit to z nějaké opakovatelné cesty při přechodu mezi profily. To by si ostatně xetup po startu mohl zkontrolovat a upozornit, žejo. Takže máme vše zodpovězeno?"
- "jo aha, on se stejně někam nakopíruje - no tak to nemusí ani upozorňovat ... Takže pokud se už ale pustí pod adminx9 (což se bude reálně dít), tak co?"
- "jo"

7. Pending Tasks:

- **Implement state machine architecture** (confirmed by user with "jo"):
  - `internal/state/state.go` + `state_other.go` — state file at `C:\Windows\Setup\Scripts\xetup-state.json`
  - `internal/prereboot/prereboot_windows.go` + `prereboot_other.go` — ensure adminx9, copy self, autologon (AutoLogonCount=20), register X9-Resume task (AtLogOn adminx9, RunLevel Highest)
  - `internal/runner/runner.go` — add `NeedsReboot bool` to Result, detect exit code 9
  - `internal/gui/gui.go` — resume mode (detect state file, skip form, run pending steps with "Pokračuji po restartu..."), pre-reboot flow (call prereboot.Prepare, save state, reboot)
  - `cmd/xetup/main.go` — check for `--resume` flag and/or state file existence
  - `scripts/11-dell-update.ps1` — exit 9 when DCU exit code = 1 (reboot needed), remove own scheduled task logic
  - `scripts/12-windows-update.ps1` — exit 9 when updates installed, remove all task/autologon/lock-screen code (xetup handles everything now)

8. Current Work:

Immediately before this summary, the user confirmed the state machine architecture design with "jo" (yes, implement it). The discussion had just concluded:

Pre-reboot logic:
```
if (current user != adminx9):
    ensure adminx9 exists (inline creation, no password, hide from login screen)
    copy xetup.exe → C:\Windows\Setup\Scripts\xetup.exe
    enable autologon adminx9
    register scheduled task "X9-Resume: xetup.exe --resume at adminx9 logon"

// always:
save xetup-state.json (config + pending steps + completed steps)
reboot
```

Resume mode (running as adminx9 after reboot):
- State file exists → skip form, run pending steps directly
- If another reboot needed → already adminx9, just update state + reboot (no setup needed)
- When all done → disable autologon, remove X9-Resume task, delete state file, show summary + lock screen

The assistant had read `cmd/xetup/main.go` and `internal/config/config.go` in preparation for implementation.

9. Optional Next Step:

Implement the state machine architecture as confirmed by the user ("jo"). Start with:

1. Create `internal/state/state.go` (Windows) + `internal/state/state_other.go` (stub)
2. Create `internal/prereboot/prereboot_windows.go` + `prereboot_other.go`
3. Modify `internal/runner/runner.go` — add exit code 9 detection → `Result.NeedsReboot = true`
4. Modify `internal/gui/gui.go` — resume mode + pre-reboot flow
5. Modify `cmd/xetup/main.go` — check state file / `--resume` flag
6. Simplify `scripts/11-dell-update.ps1` and `scripts/12-windows-update.ps1`

Direct quote from user confirming: "jo" (in response to "Jdeme implementovat?")

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/dev/.claude/projects/-opt-xetup/b64c8c21-dc81-41a5-a66f-f0f6667e34d7.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


<#
.SYNOPSIS
    Installs all available Windows Updates via PSWindowsUpdate module.

.DESCRIPTION
    First pass: installs all currently available updates without rebooting.
    Then registers a scheduled task "X9-WindowsUpdate" that runs on every
    logon (as SYSTEM) until no more updates are found - handles the typical
    2-3 reboot cycles required on a fresh Windows installation.

    Operator workflow:
      1. xetup completes all steps
      2. Operator reboots manually
      3. On each subsequent logon the scheduled task runs another update pass
      4. Task removes itself automatically when system is fully up to date

.ITEMS
    nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
    spustit-prvni-kolo-windows-update: First update pass without reboot - installs all currently available updates.
    registrovat-scheduled-task-pro-dalsi-kola: Registers X9-WindowsUpdate scheduled task that runs on logon, handles post-reboot update rounds, and self-deletes when no more updates are found.
#>
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"
    $null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
    Write-Output $line
}

Write-Log "=== Step 12 - Windows Update ===" -Level STEP

# -----------------------------------------------------------------------
# 1. NuGet provider + PSWindowsUpdate module
# -----------------------------------------------------------------------
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
try {
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
    $existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
    if ($existing) {
        Write-Log "  PSWindowsUpdate $($existing.Version) already installed" -Level INFO
    } else {
        Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
        Write-Log "  PSWindowsUpdate installed" -Level OK
    }
    Import-Module PSWindowsUpdate -Force
} catch {
    Write-Log "  Module setup failed: $_" -Level ERROR
    exit 1
}

# -----------------------------------------------------------------------
# 2. First update pass (no reboot)
# -----------------------------------------------------------------------
Write-Log "Running first Windows Update pass..." -Level INFO
try {
    $result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
    $installed = @($result | Where-Object { $_ -match 'KB\d+|Downloaded|Installed' })
    if ($installed.Count -gt 0) {
        $result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log "  $_" -Level INFO }
        Write-Log "  First pass complete - reboot required for remaining rounds" -Level OK
    } else {
        Write-Log "  System already up to date" -Level OK
    }
} catch {
    Write-Log "  First pass failed: $_" -Level ERROR
}

# -----------------------------------------------------------------------
# 3. Enable autologon for adminx9 (temporary - disabled when updates complete)
# -----------------------------------------------------------------------
Write-Log "Enabling temporary autologon for adminx9..." -Level INFO

$winlogonPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
try {
    Set-ItemProperty -Path $winlogonPath -Name "AutoAdminLogon"  -Value "1"  -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultUserName" -Value "adminx9" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultPassword" -Value "" -Type String -Force
    Set-ItemProperty -Path $winlogonPath -Name "DefaultDomainName" -Value "." -Type String -Force
    # Safety cap: max 10 automatic logons in case the task fails to clean up
    Set-ItemProperty -Path $winlogonPath -Name "AutoLogonCount"  -Value 10 -Type DWord -Force
    Write-Log "  Autologon enabled (adminx9, max 10 rounds)" -Level OK
} catch {
    Write-Log "  Failed to enable autologon: $_" -Level WARN
    Write-Log "  Windows Update rounds will require manual login after each reboot" -Level WARN
}

# -----------------------------------------------------------------------
# 4. Scheduled task for post-reboot update rounds (self-deleting)
# -----------------------------------------------------------------------
Write-Log "Registering post-reboot update task..." -Level INFO

$taskName = "X9-WindowsUpdate"

# PowerShell block that runs on each logon until no more updates found.
# When done: disables autologon and removes itself.
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
    Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
    # No more updates - disable autologon and clean up
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -Path $wl -Name "AutoAdminLogon" -Value "0" -Type String -Force
    Remove-ItemProperty -Path $wl -Name "DefaultPassword"   -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path $wl -Name "AutoLogonCount"    -ErrorAction SilentlyContinue

    # Leave a visible marker on the shared Desktop so the operator knows it's done
    $ts      = Get-Date -Format "yyyy-MM-dd HH:mm"
    $doneMsg = "Windows Update dokoncen: $ts`r`nStroj je plne aktualizovan a pripraven k predani klientovi."
    [System.IO.File]::WriteAllText(
        "C:\Users\Public\Desktop\! WU HOTOVO $ts.txt",
        $doneMsg,
        [System.Text.Encoding]::UTF8
    )

    # Lock the workstation - login screen = clear visual signal for the operator.
    # Runs as adminx9 (interactive session) via a one-shot task, self-deletes after 5 min.
    $lockAction    = New-ScheduledTaskAction -Execute "rundll32.exe" -Argument "user32.dll,LockWorkStation"
    $lockTrigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(5)
    $lockPrincipal = New-ScheduledTaskPrincipal -UserId "adminx9" -LogonType Interactive -RunLevel Limited
    $lockSettings  = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5)
    Register-ScheduledTask -TaskName "X9-WUDoneLock" -Action $lockAction -Trigger $lockTrigger `
        -Principal $lockPrincipal -Settings $lockSettings -Force -ErrorAction SilentlyContinue | Out-Null

    Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@

try {
    $action  = New-ScheduledTaskAction -Execute "powershell.exe" `
                 -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
    $trigger = New-ScheduledTaskTrigger -AtLogOn
    $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
                  -MultipleInstances IgnoreNew
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

    # Remove existing task first (idempotent)
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

    Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
      -Settings $settings -Principal $principal -Force | Out-Null

    Write-Log "  Task '$taskName' registered - runs on each logon until fully updated" -Level OK
} catch {
    Write-Log "  Failed to register scheduled task: $_" -Level WARN
    Write-Log "  Manual Windows Update rounds will be needed after reboot" -Level WARN
}

Write-Log "Step 12 - Windows Update complete" -Level OK


<!DOCTYPE html>

<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">

    xetupby X9.cz

    Specifikace
    Git
    Issues

MSP deployment tool — X9.cz

Automaticky nastavene Windows za 20 minut

    Nahrazuje 3 hodiny rucniho nastavovani jednim skriptem.
    Win10 + Win11, OEM i cisty install, funguje offline.

    Zobrazit specifikaci
    Git repozitar

    curl -Lo xetup.exe https://xetup.x9.cz/dl
    <button id="copy-btn" onclick="copyCmd()">Kopirovat</button>

    Pokud ve stazeni brani antivirus, pouzijte curl – staci bezny prikazovy radek, neni potreba administrator.

      ⬇ xetup.exe

    ·
    nacitam...
    ·

      vsechny verze

      ⚙

~20 stroju / mesic

Ruzni klienti, Win10 i Win11, vcetne nepodporovaneho HW.

      🔒

Offline provoz

Scripty + assets jsou soucasti balicku. Site jen pro winget a Atera.

      📄

Per-client config

config.json vedle .exe pro opakovatelne nasazeni u stejneho klienta.

      🚀

Go TUI launcher

xetup.exe — jednotny binarni spoustec s TUI formularom a live logem.

  © 2026 X9.cz s.r.o.
   ·
  Forgejo
   ·
  Specifikace