Files
vendoo/scripts/updater/Vendoo.Updater.UI.ps1
T
Masterluke77andGitHub 40071f718e Vendoo 1.43.0 – Production Update
Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
2026-07-10 13:16:21 +02:00

334 lines
28 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[CmdletBinding()]
param([switch]$SkipProductionDeploy)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Xaml
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$TargetVersion = '1.43.0'
$UpdaterVersion = '3.3.0'
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker.ps1'
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
$SessionRoot = Join-Path $LocalRoot 'sessions'
New-Item -ItemType Directory -Path $RuntimeRoot,$SessionRoot -Force | Out-Null
$createdNew = $false
$mutex = [System.Threading.Mutex]::new($true, 'Local\VendooProductionUpdater', [ref]$createdNew)
if (-not $createdNew) {
[System.Windows.MessageBox]::Show('Der Vendoo-Updater läuft bereits. Bitte das vorhandene Fenster verwenden.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Information) | Out-Null
return
}
$xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Vendoo Production Updater" Width="1080" Height="720" MinWidth="960" MinHeight="650"
WindowStartupLocation="CenterScreen" Background="#0F1115" Foreground="#EEF1F5"
FontFamily="Segoe UI" ResizeMode="CanResize">
<Window.Resources>
<SolidColorBrush x:Key="Panel" Color="#171A20"/>
<SolidColorBrush x:Key="Panel2" Color="#1D2129"/>
<SolidColorBrush x:Key="Border" Color="#2B313C"/>
<SolidColorBrush x:Key="Muted" Color="#9AA4B2"/>
<SolidColorBrush x:Key="Accent" Color="#4F8CFF"/>
<Style TargetType="Button">
<Setter Property="Foreground" Value="#EEF1F5"/><Setter Property="Background" Value="#252B35"/>
<Setter Property="BorderBrush" Value="#343C49"/><Setter Property="Padding" Value="16,9"/>
<Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="7" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border><ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"><Setter Property="Background" Value="#303744"/></Trigger>
<Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0.42"/></Trigger>
</ControlTemplate.Triggers>
</ControlTemplate></Setter.Value></Setter>
</Style>
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#2E6FE8"/><Setter Property="BorderBrush" Value="#4F8CFF"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions><RowDefinition Height="76"/><RowDefinition Height="*"/><RowDefinition Height="68"/></Grid.RowDefinitions>
<Border Grid.Row="0" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,0,0,1">
<Grid Margin="28,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<TextBlock FontSize="23" FontWeight="SemiBold" Text="Vendoo Production Updater"/>
<TextBlock Foreground="{StaticResource Muted}" FontSize="12" Margin="0,4,0,0" Text="Sicherer Release-, GitHub- und Coolify-Ablauf"/>
</StackPanel>
<Border Grid.Column="1" Background="#202631" BorderBrush="#343C49" BorderThickness="1" CornerRadius="8" Padding="14,8" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal"><TextBlock Text="ZIEL "/><TextBlock FontWeight="Bold" Text="1.43.0"/></StackPanel>
</Border>
</Grid>
</Border>
<Grid Grid.Row="1" Margin="24,22,24,16">
<Grid.ColumnDefinitions><ColumnDefinition Width="300"/><ColumnDefinition Width="18"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="18">
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<StackPanel><TextBlock FontSize="15" FontWeight="SemiBold" Text="Update-Ablauf"/><TextBlock Foreground="{StaticResource Muted}" FontSize="11" Margin="0,4,0,14" Text="Jeder Schritt wird einzeln geprüft."/></StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"><StackPanel x:Name="StepsPanel"/></ScrollViewer>
<Border Grid.Row="2" Background="#12151A" BorderBrush="#272D37" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,14,0,0">
<StackPanel><TextBlock Foreground="{StaticResource Muted}" FontSize="10" Text="SICHERHEIT"/>
<TextBlock FontSize="11" Margin="0,5,0,0" TextWrapping="Wrap" Text="Kein Datenbank-Reset. Keine Uploads, Secrets oder /app/data-Dateien werden ersetzt."/>
</StackPanel>
</Border>
</Grid>
</Border>
<Grid Grid.Column="2"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="22">
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel><TextBlock x:Name="PhaseText" FontSize="20" FontWeight="SemiBold" Text="Updater wird vorbereitet"/>
<TextBlock x:Name="DetailText" Foreground="{StaticResource Muted}" Margin="0,7,20,0" TextWrapping="Wrap" Text="Die sichere Update-Umgebung wird initialisiert."/>
</StackPanel>
<Border x:Name="StatusBadge" Grid.Column="1" Background="#22314A" BorderBrush="#36598A" BorderThickness="1" CornerRadius="7" Padding="12,7" VerticalAlignment="Top">
<TextBlock x:Name="StatusText" FontWeight="Bold" FontSize="11" Text="START"/>
</Border>
</Grid>
<Grid Grid.Row="1" Margin="0,24,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="64"/></Grid.ColumnDefinitions>
<ProgressBar x:Name="ProgressBar" Height="10" Minimum="0" Maximum="100" Value="0" Foreground="{StaticResource Accent}" Background="#262C35" BorderThickness="0"/>
<TextBlock x:Name="PercentText" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" FontWeight="SemiBold" Text="0 %"/>
</Grid>
<Grid Grid.Row="2" Margin="0,13,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBlock x:Name="ActivityText" Foreground="{StaticResource Muted}" FontSize="11" Text="Laufzeit 00:00"/>
<TextBlock x:Name="HeartbeatText" Grid.Column="1" Foreground="{StaticResource Muted}" FontSize="11" Text="Status wird aufgebaut"/>
</Grid>
<Border x:Name="InputPanel" Grid.Row="3" Visibility="Collapsed" Background="#171E2A" BorderBrush="#35547E" BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,18,0,0">
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<TextBlock x:Name="InputTitle" FontWeight="SemiBold" Text="Eingabe erforderlich"/>
<TextBlock x:Name="InputPrompt" Grid.Row="1" Foreground="{StaticResource Muted}" Margin="0,6,0,10" TextWrapping="Wrap"/>
<Grid Grid.Row="2"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<PasswordBox x:Name="SecretInput" Height="34" Padding="8" Background="#0F1217" Foreground="#EEF1F5" BorderBrush="#3A4657"/>
<Button x:Name="SubmitInputButton" Grid.Column="1" Style="{StaticResource PrimaryButton}" Margin="10,0,0,0" Content="Sicher übernehmen"/>
</Grid>
</Grid>
</Border>
</Grid>
</Border>
<Border Grid.Row="1" x:Name="ErrorPanel" Visibility="Collapsed" Background="#24181B" BorderBrush="#7A3640" BorderThickness="1" CornerRadius="10" Padding="18" Margin="0,14,0,0">
<StackPanel><TextBlock x:Name="ErrorTitle" FontSize="15" FontWeight="SemiBold" Foreground="#FFB6BE" Text="Update gestoppt"/>
<TextBlock x:Name="ErrorText" Margin="0,7,0,0" TextWrapping="Wrap"/>
<TextBlock x:Name="RemediationText" Foreground="#D7A9AF" Margin="0,8,0,0" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<Border Grid.Row="2" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Margin="0,14,0,0">
<Grid><Grid.RowDefinitions><RowDefinition Height="42"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<Grid Margin="16,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="Technisches Protokoll"/>
<TextBlock x:Name="LogPathText" Grid.Column="1" VerticalAlignment="Center" Foreground="{StaticResource Muted}" FontSize="10" Text="wird vorbereitet"/>
</Grid>
<TextBox x:Name="LogBox" Grid.Row="1" Margin="12,0,12,12" IsReadOnly="True" AcceptsReturn="True" TextWrapping="NoWrap" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="#0D0F13" Foreground="#CDD4DE" BorderBrush="#292F38" FontFamily="Consolas" FontSize="11" Padding="10"/>
</Grid>
</Border>
</Grid>
</Grid>
<Border Grid.Row="2" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,1,0,0">
<Grid Margin="24,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="OpenLogButton" Content="Protokoll öffnen" IsEnabled="False"/>
<Button x:Name="OpenRuntimeButton" Content="Diagnoseordner" Margin="10,0,0,0" IsEnabled="False"/>
<Button x:Name="CopyErrorButton" Content="Fehler kopieren" Margin="10,0,0,0" Visibility="Collapsed"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="CancelButton" Content="Update abbrechen"/>
<Button x:Name="RetryButton" Content="Erneut versuchen" Margin="10,0,0,0" Visibility="Collapsed"/>
<Button x:Name="CloseButton" Style="{StaticResource PrimaryButton}" Content="Schließen" Margin="10,0,0,0" IsEnabled="False"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Window>
'@
$reader = [System.Xml.XmlNodeReader]::new([xml]$xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)
$names = @('StepsPanel','PhaseText','DetailText','StatusBadge','StatusText','ProgressBar','PercentText','ActivityText','HeartbeatText','InputPanel','InputTitle','InputPrompt','SecretInput','SubmitInputButton','ErrorPanel','ErrorTitle','ErrorText','RemediationText','LogBox','LogPathText','OpenLogButton','OpenRuntimeButton','CopyErrorButton','CancelButton','RetryButton','CloseButton')
foreach ($name in $names) { Set-Variable -Name $name -Value $window.FindName($name) -Scope Script }
$steps = @(
@{id='package'; title='Paket prüfen'}, @{id='tools'; title='System prüfen'}, @{id='github'; title='GitHub verbinden'},
@{id='production'; title='Produktion vorbereiten'}, @{id='workspace'; title='Arbeitskopie erstellen'}, @{id='tests'; title='Vollständig testen'},
@{id='pullrequest'; title='Pull Request erstellen'}, @{id='merge'; title='Sicher mergen'}, @{id='deploy'; title='Coolify deployen'}, @{id='verify'; title='Produktion bestätigen'}
)
$script:StepControls = @{}
foreach ($step in $steps) {
$border = New-Object System.Windows.Controls.Border
$border.CornerRadius = [System.Windows.CornerRadius]::new(6); $border.Padding = [System.Windows.Thickness]::new(9,8,9,8); $border.Margin = [System.Windows.Thickness]::new(0,0,0,4); $border.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#171A20')
$grid = New-Object System.Windows.Controls.Grid
$c1=New-Object System.Windows.Controls.ColumnDefinition; $c1.Width=[System.Windows.GridLength]::new(28); $grid.ColumnDefinitions.Add($c1)
$c2=New-Object System.Windows.Controls.ColumnDefinition; $c2.Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star); $grid.ColumnDefinitions.Add($c2)
$icon=New-Object System.Windows.Controls.TextBlock; $icon.Text='○'; $icon.Foreground='#6E7886'; $icon.FontSize=16; $icon.VerticalAlignment='Center'
$label=New-Object System.Windows.Controls.TextBlock; $label.Text=$step.title; $label.Foreground='#AAB3BF'; $label.VerticalAlignment='Center'; [System.Windows.Controls.Grid]::SetColumn($label,1)
$grid.Children.Add($icon)|Out-Null; $grid.Children.Add($label)|Out-Null; $border.Child=$grid; $StepsPanel.Children.Add($border)|Out-Null
$script:StepControls[$step.id]=@{Border=$border;Icon=$icon;Label=$label}
}
$script:Worker=$null; $script:Timer=$null; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:LastStatusAt=Get-Date; $script:StartedAt=Get-Date
$script:StatusFile=''; $script:EventFile=''; $script:InputFile=''; $script:CancelFile=''; $script:SessionFile=Join-Path $SessionRoot "$TargetVersion.json"; $script:LogFile=''; $script:StdoutFile=''; $script:StderrFile=''; $script:EventOffset=0; $script:LastRequestId=''; $script:LastError=''
function Set-Badge([string]$state) {
switch ($state) {
'success' { $StatusText.Text='ERFOLGREICH'; $StatusBadge.Background='#173322'; $StatusBadge.BorderBrush='#2C7A4B'; $StatusText.Foreground='#8FE0AC' }
'failed' { $StatusText.Text='GESTOPPT'; $StatusBadge.Background='#351B20'; $StatusBadge.BorderBrush='#8B3B48'; $StatusText.Foreground='#FFB6BE' }
'input' { $StatusText.Text='EINGABE'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
'waiting' { $StatusText.Text='WARTE'; $StatusBadge.Background='#1B2D3B'; $StatusBadge.BorderBrush='#35627D'; $StatusText.Foreground='#91CBE9' }
'cancelling' { $StatusText.Text='ABBRUCH'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
default { $StatusText.Text='LÄUFT'; $StatusBadge.Background='#1C2D49'; $StatusBadge.BorderBrush='#36598A'; $StatusText.Foreground='#9FC0FF' }
}
}
function Update-Steps($status) {
if (-not $status.steps) { return }
foreach ($s in @($status.steps)) {
if (-not $script:StepControls.ContainsKey([string]$s.id)) { continue }
$c=$script:StepControls[[string]$s.id]
switch ([string]$s.state) {
'done' { $c.Icon.Text='●'; $c.Icon.Foreground='#62D28B'; $c.Label.Foreground='#EEF1F5'; $c.Border.Background='#18251E' }
'active' { $c.Icon.Text='●'; $c.Icon.Foreground='#6EA1FF'; $c.Label.Foreground='#FFFFFF'; $c.Label.FontWeight='SemiBold'; $c.Border.Background='#1A2435' }
'failed' { $c.Icon.Text='×'; $c.Icon.Foreground='#FF7A88'; $c.Label.Foreground='#FFD2D7'; $c.Border.Background='#2A191D' }
'skipped' { $c.Icon.Text=''; $c.Icon.Foreground='#77808D'; $c.Label.Foreground='#77808D'; $c.Border.Background='#171A20' }
default { $c.Icon.Text='○'; $c.Icon.Foreground='#6E7886'; $c.Label.Foreground='#AAB3BF'; $c.Label.FontWeight='Normal'; $c.Border.Background='#171A20' }
}
}
}
function Write-InitialStatus {
$steps=@(
@{id='package';title='Paket prüfen';state='pending'},@{id='tools';title='System prüfen';state='pending'},@{id='github';title='GitHub verbinden';state='pending'},
@{id='production';title='Produktion vorbereiten';state='pending'},@{id='workspace';title='Arbeitskopie erstellen';state='pending'},@{id='tests';title='Vollständig testen';state='pending'},
@{id='pullrequest';title='Pull Request erstellen';state='pending'},@{id='merge';title='Sicher mergen';state='pending'},@{id='deploy';title='Coolify deployen';state='pending'},@{id='verify';title='Produktion bestätigen';state='pending'}
)
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=0;phase='Updater wird vorbereitet';detail='Die überwachte Update-Umgebung wird initialisiert.';state='running';error='';remediation='';updatedAt=(Get-Date).ToString('o');logFile=$script:LogFile;sessionFile=$script:SessionFile;steps=$steps;request=$null}|ConvertTo-Json -Depth 8
[IO.File]::WriteAllText($script:StatusFile,$payload,(New-Object Text.UTF8Encoding($false)))
}
function Get-WorkerExitDiagnostics {
$parts=New-Object Collections.Generic.List[string]
try {
$stderr=[string]$script:Worker.StandardError.ReadToEnd()
if($stderr){$stderr|Set-Content -LiteralPath $script:StderrFile -Encoding UTF8;$parts.Add("PowerShell-Fehlerkanal:`n$stderr")}
} catch {}
try {
$stdout=[string]$script:Worker.StandardOutput.ReadToEnd()
if($stdout){$stdout|Set-Content -LiteralPath $script:StdoutFile -Encoding UTF8;$parts.Add("PowerShell-Ausgabe:`n$stdout")}
} catch {}
if(Test-Path -LiteralPath $script:LogFile){
try{
$tail=@(Get-Content -LiteralPath $script:LogFile -Encoding UTF8 -Tail 35) -join [Environment]::NewLine
if($tail){$parts.Add("Letzte Protokollzeilen:`n$tail")}
}catch{}
}
if($parts.Count -eq 0){$parts.Add('Der Prozess lieferte keinen Fehlertext. Die Bootstrap-Dateien im Laufzeitordner wurden dennoch erhalten.')}
return ($parts -join ([Environment]::NewLine+[Environment]::NewLine))
}
function Start-Worker {
$stamp=Get-Date -Format 'yyyyMMdd-HHmmss-fff'; $run=Join-Path $RuntimeRoot $stamp; New-Item -ItemType Directory -Path $run -Force|Out-Null
$script:StatusFile=Join-Path $run 'status.json'; $script:EventFile=Join-Path $run 'events.ndjson'; $script:InputFile=Join-Path $run 'input.json'; $script:CancelFile=Join-Path $run 'cancel.flag'
$script:LogFile=Join-Path $run 'updater.log'; $script:StdoutFile=Join-Path $run 'worker.stdout.log'; $script:StderrFile=Join-Path $run 'worker.stderr.log'
$script:EventOffset=0; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:StartedAt=Get-Date; $script:LastStatusAt=Get-Date; $script:LastRequestId=''; $script:LastError=''
$ErrorPanel.Visibility='Collapsed'; $InputPanel.Visibility='Collapsed'; $CopyErrorButton.Visibility='Collapsed'; $RetryButton.Visibility='Collapsed'; $CloseButton.IsEnabled=$false; $CancelButton.IsEnabled=$true; $OpenLogButton.IsEnabled=$true; $LogBox.Clear()
$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile)
[IO.File]::WriteAllText($script:LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') [UI] Laufzeitordner angelegt: $run$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
$OpenRuntimeButton.IsEnabled=$true
foreach($entry in $script:StepControls.Values){$entry.Icon.Text='○';$entry.Icon.Foreground='#6E7886';$entry.Label.Foreground='#AAB3BF';$entry.Label.FontWeight='Normal';$entry.Border.Background='#171A20'}
Set-Badge 'running'; $PhaseText.Text='Updater wird vorbereitet'; $DetailText.Text='Die überwachte Update-Umgebung wird initialisiert.'; $ProgressBar.Value=0; $PercentText.Text='0 %'
Write-InitialStatus
$args=@('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"{0}"' -f $WorkerHostScript),'-CoreScript',('"{0}"' -f $WorkerScript),'-StatusFile',('"{0}"' -f $script:StatusFile),'-EventFile',('"{0}"' -f $script:EventFile),'-InputFile',('"{0}"' -f $script:InputFile),'-CancelFile',('"{0}"' -f $script:CancelFile),'-SessionFile',('"{0}"' -f $script:SessionFile),'-LogFile',('"{0}"' -f $script:LogFile))
if($SkipProductionDeploy){$args+='-SkipProductionDeploy'}
$psi=New-Object Diagnostics.ProcessStartInfo
$psi.FileName='powershell.exe'; $psi.Arguments=($args -join ' '); $psi.UseShellExecute=$false; $psi.CreateNoWindow=$true; $psi.WorkingDirectory=$PackageRoot
$psi.RedirectStandardOutput=$true; $psi.RedirectStandardError=$true
$script:Worker=New-Object Diagnostics.Process; $script:Worker.StartInfo=$psi
if(-not $script:Worker.Start()){throw 'Der überwachte Update-Prozess konnte nicht gestartet werden.'}
}
function Read-NewEvents {
if(-not (Test-Path -LiteralPath $script:EventFile)){return}
$stream=[IO.File]::Open($script:EventFile,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite)
try {
if($script:EventOffset -gt $stream.Length){$script:EventOffset=0}
$stream.Seek($script:EventOffset,[IO.SeekOrigin]::Begin)|Out-Null
$reader=New-Object IO.StreamReader($stream,[Text.Encoding]::UTF8,$true,4096,$true)
while(-not $reader.EndOfStream){
$line=$reader.ReadLine(); if(-not $line){continue}
try{$e=$line|ConvertFrom-Json; $display=('{0} {1,-7} {2}' -f ([DateTime]::Parse($e.at).ToString('HH:mm:ss')),[string]$e.level,[string]$e.message)}catch{$display=$line}
$LogBox.AppendText($display+[Environment]::NewLine); $LogBox.ScrollToEnd()
}
$script:EventOffset=$stream.Position; $reader.Dispose()
} finally {$stream.Dispose()}
}
function Apply-Status($s) {
$script:LastStatusAt=Get-Date
$PhaseText.Text=[string]$s.phase; $DetailText.Text=[string]$s.detail
$p=[Math]::Max(0,[Math]::Min(100,[int]$s.percent)); $ProgressBar.Value=$p; $PercentText.Text="$p %"
Set-Badge ([string]$s.state); Update-Steps $s
if($s.logFile){$script:LogFile=[string]$s.logFile;$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile);$OpenLogButton.IsEnabled=$true}
if([string]$s.state -eq 'input' -and $s.request){
$requestId=[string]$s.request.id
if($requestId -ne $script:LastRequestId){$script:LastRequestId=$requestId;$SecretInput.Clear();$InputTitle.Text=[string]$s.request.title;$InputPrompt.Text=[string]$s.request.prompt;$InputPanel.Visibility='Visible';$SecretInput.Focus()}
} else {$InputPanel.Visibility='Collapsed'}
if([string]$s.state -in @('success','failed','cancelled')){
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true
if([string]$s.state -eq 'failed'){
$script:LastError=[string]$s.error; $ErrorTitle.Text='Update sicher gestoppt';$ErrorText.Text=[string]$s.error;$RemediationText.Text=[string]$s.remediation;$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
} elseif([string]$s.state -eq 'cancelled'){
$script:LastError='Das Update wurde durch den Benutzer abgebrochen.';$ErrorTitle.Text='Update abgebrochen';$ErrorText.Text=$script:LastError;$RemediationText.Text='Ein erneuter Start setzt den kontrollierten Ablauf fort.';$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
}
}
}
$SubmitInputButton.Add_Click({
$value=$SecretInput.Password.Trim(); if(-not $value){[System.Windows.MessageBox]::Show('Bitte einen Wert eintragen.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Warning)|Out-Null;return}
@{requestId=$script:LastRequestId;value=$value;submittedAt=(Get-Date).ToString('o')}|ConvertTo-Json|Set-Content -LiteralPath $script:InputFile -Encoding UTF8
$SecretInput.Clear();$InputPanel.Visibility='Collapsed';$DetailText.Text='Eingabe wurde sicher übernommen und wird geprüft.'
})
$CancelButton.Add_Click({if($script:Worker -and -not $script:Worker.HasExited){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;Set-Badge 'cancelling';$DetailText.Text='Der aktuelle Prozess wird kontrolliert beendet. Bitte kurz geöffnet lassen.';$CancelButton.IsEnabled=$false}})
$RetryButton.Add_Click({ try { Start-Worker } catch { $script:LastError=$_.Exception.Message; $ErrorText.Text=$script:LastError; $ErrorPanel.Visibility='Visible'; Set-Badge 'failed' } })
$CloseButton.Add_Click({$script:ClosingAllowed=$true;$window.Close()})
$OpenLogButton.Add_Click({if($script:LogFile -and (Test-Path -LiteralPath $script:LogFile)){Start-Process notepad.exe -ArgumentList ('"{0}"' -f $script:LogFile)}elseif($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
$OpenRuntimeButton.Add_Click({if($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
$CopyErrorButton.Add_Click({if($script:LastError){[System.Windows.Clipboard]::SetText($script:LastError+[Environment]::NewLine+[Environment]::NewLine+'Protokoll: '+$script:LogFile)}})
$window.Add_Closing({param($sender,$e) if(-not $script:ClosingAllowed){if($script:Worker -and -not $script:Worker.HasExited){$choice=[System.Windows.MessageBox]::Show('Das Update läuft noch. Soll es kontrolliert abgebrochen werden?','Vendoo Updater',[System.Windows.MessageBoxButton]::YesNo,[System.Windows.MessageBoxImage]::Warning);if($choice -eq [System.Windows.MessageBoxResult]::Yes){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;$e.Cancel=$true;$CancelButton.IsEnabled=$false;$DetailText.Text='Abbruch wird ausgeführt …'}else{$e.Cancel=$true}}else{$e.Cancel=$true}}})
$script:Timer=New-Object System.Windows.Threading.DispatcherTimer
$script:Timer.Interval=[TimeSpan]::FromMilliseconds(350)
$script:Timer.Add_Tick({
try {
Read-NewEvents
if(Test-Path -LiteralPath $script:StatusFile){try{$s=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $s}catch{}}
$elapsed=(Get-Date)-$script:StartedAt;$idle=[int]((Get-Date)-$script:LastStatusAt).TotalSeconds
$ActivityText.Text=('Laufzeit {0:00}:{1:00}' -f [int]$elapsed.TotalMinutes,$elapsed.Seconds);$HeartbeatText.Text=if($idle -lt 5){'Status aktuell'}else{"Letzte Meldung vor $idle Sek."}
if($script:Worker -and $script:Worker.HasExited -and -not $script:Terminal){
if(Test-Path -LiteralPath $script:StatusFile){
try{$final=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $final}catch{}
}
if(-not $script:Terminal){
$diagnostics=Get-WorkerExitDiagnostics
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true;$RetryButton.Visibility='Visible';$CopyErrorButton.Visibility='Visible';Set-Badge 'failed'
$PhaseText.Text='Updater-Prozess unerwartet beendet';$DetailText.Text='Die vollständige Startdiagnose wurde aufgezeichnet. Das Fenster bleibt geöffnet.'
$script:LastError="Der überwachte Worker-Host wurde mit Exit-Code $($script:Worker.ExitCode) beendet.`n`n$diagnostics"
$ErrorText.Text=$script:LastError;$RemediationText.Text='Protokoll öffnen zeigt die vollständige Ausnahme mit Datei und Zeile. Danach kann derselbe sichere Lauf erneut gestartet werden.';$ErrorPanel.Visibility='Visible'
}
}
} catch {
$script:LastError=$_.Exception.Message
}
})
try { Start-Worker; $script:Timer.Start(); $window.ShowDialog()|Out-Null }
finally { try{$script:Timer.Stop()}catch{};if($script:Worker -and -not $script:Worker.HasExited){try{New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null}catch{}};try{$mutex.ReleaseMutex()}catch{};$mutex.Dispose() }