Restore installer GUI launch (via shellexec) + checkpoint descriptions

Two follow-ups to the previous Config Checkpoints commit:

1. Bring back the post-install "Launch Webhook Server" checkbox in the
   installer. The previous attempt failed because Inno Setup's
   postinstall flag launches via CreateProcess after Setup exits,
   bypassing the GUI's requireAdministrator manifest. Adding the
   shellexec flag switches to ShellExecute, which DOES honor the
   manifest and triggers a clean UAC prompt - so the post-install
   GUI launch works as expected.

2. Each checkpoint now carries a description, stored in a sidecar
   .meta.json file next to the snapshot. Defaults:
     - Auto-on-save: "Before save"
     - Midnight scheduler: "Nightly auto-checkpoint"
     - Manual: opens a small dialog so the user can type a meaningful
       description (defaults to "Manual checkpoint" if blank)
   The dialog and pruning both clean up sidecars alongside snapshots.
   The Config Checkpoints grid grows a Description column between
   When and Size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:45:14 -04:00
parent b6e642da04
commit 6f9cb5646f
10 changed files with 142 additions and 18 deletions
@@ -101,6 +101,6 @@ public sealed class AdminPipeClient
public Task<AdminResponse> ImportConfigAsync(ServerConfig config, CancellationToken ct = default) =>
InvokeAsync(AdminOps.ImportConfig, config, ct);
public Task<BackupEntry?> CreateCheckpointAsync(CancellationToken ct = default) =>
InvokeAsync<BackupEntry>(AdminOps.CreateCheckpoint, null, ct);
public Task<BackupEntry?> CreateCheckpointAsync(string? description, CancellationToken ct = default) =>
InvokeAsync<BackupEntry>(AdminOps.CreateCheckpoint, new CreateCheckpointArgs { Description = description }, ct);
}
@@ -46,9 +46,20 @@ public sealed partial class ConfigCheckpointsViewModel : ObservableObject
[RelayCommand]
private async Task TakeCheckpointAsync()
{
// Prompt for an optional description on the UI thread.
string? description = null;
var prompted = Application.Current.Dispatcher.Invoke(() =>
{
var dlg = new Views.TakeCheckpointDialog { Owner = Application.Current.MainWindow };
if (dlg.ShowDialog() != true) return false;
description = string.IsNullOrWhiteSpace(dlg.Description) ? null : dlg.Description;
return true;
});
if (!prompted) return;
try
{
var entry = await _client.CreateCheckpointAsync().ConfigureAwait(false);
var entry = await _client.CreateCheckpointAsync(description).ConfigureAwait(false);
await RefreshAsync().ConfigureAwait(false);
if (entry is not null)
{
@@ -39,11 +39,13 @@
HeadersVisibility="Column"
GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridTextColumn Header="When (local)" Width="200"
<DataGridTextColumn Header="When (local)" Width="170"
Binding="{Binding SavedAt, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}', ConverterCulture=en-US}"/>
<DataGridTextColumn Header="Description" Width="*"
Binding="{Binding Description}"/>
<DataGridTextColumn Header="Size" Width="100"
Binding="{Binding SizeBytes, StringFormat='{}{0:n0} bytes'}"/>
<DataGridTextColumn Header="File name" Width="*"
<DataGridTextColumn Header="File name" Width="200"
Binding="{Binding FileName}" FontFamily="Consolas"/>
</DataGrid.Columns>
</DataGrid>
@@ -0,0 +1,34 @@
<Window x:Class="WebhookServer.Gui.Views.TakeCheckpointDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Take checkpoint"
Height="180" Width="440"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
Icon="/webhook-server.ico"
ShowInTaskbar="False">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap"
Text="Description for this checkpoint (optional):"/>
<TextBox x:Name="DescriptionBox" Grid.Row="1" Margin="0,8,0,0" MaxLength="120">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding OkCommand, ElementName=Self, FallbackValue={x:Null}}"/>
</TextBox.InputBindings>
</TextBox>
<TextBlock Grid.Row="2" Foreground="Gray" FontStyle="Italic" FontSize="11" Margin="0,4,0,0"
Text="Examples: 'Before adding new endpoint', 'Pre-AD-policy-change'. Leave blank to use 'Manual checkpoint'."/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="OK" Width="80" IsDefault="True" Click="OnOk" Margin="0,0,8,0"/>
<Button Content="Cancel" Width="80" IsCancel="True" Click="OnCancel"/>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,27 @@
using System.Windows;
namespace WebhookServer.Gui.Views;
public partial class TakeCheckpointDialog : Window
{
public string Description { get; private set; } = "";
public TakeCheckpointDialog()
{
InitializeComponent();
Loaded += (_, _) => DescriptionBox.Focus();
}
private void OnOk(object sender, RoutedEventArgs e)
{
Description = DescriptionBox.Text?.Trim() ?? "";
DialogResult = true;
Close();
}
private void OnCancel(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}