From 6f9cb5646fbb29298d4102633db433fb63800293 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Fri, 8 May 2026 10:45:14 -0400 Subject: [PATCH] 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) --- installer/webhook-server.iss | 12 ++++-- src/WebhookServer.Core/Ipc/AdminProtocol.cs | 6 +++ src/WebhookServer.Core/Storage/ConfigStore.cs | 18 ++++++++- .../Services/AdminPipeClient.cs | 4 +- .../ViewModels/ConfigCheckpointsViewModel.cs | 13 ++++++- .../Views/ConfigCheckpointsDialog.xaml | 6 ++- .../Views/TakeCheckpointDialog.xaml | 34 +++++++++++++++++ .../Views/TakeCheckpointDialog.xaml.cs | 27 +++++++++++++ src/WebhookServer.Service/AdminPipeServer.cs | 38 ++++++++++++++++--- .../CheckpointScheduler.cs | 2 +- 10 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 src/WebhookServer.Gui/Views/TakeCheckpointDialog.xaml create mode 100644 src/WebhookServer.Gui/Views/TakeCheckpointDialog.xaml.cs diff --git a/installer/webhook-server.iss b/installer/webhook-server.iss index 8f14e2b..562546b 100644 --- a/installer/webhook-server.iss +++ b/installer/webhook-server.iss @@ -70,10 +70,14 @@ Filename: "powershell.exe"; \ Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\install-service.ps1"" -BinaryPath ""{app}\{#ServiceExeName}"""; \ StatusMsg: "Installing Windows Service..."; \ Flags: runhidden -; No post-install GUI launch: the GUI is requireAdministrator and the launch -; from the installer wizard ends up un-elevated for the post-install user, so -; it would just fail to connect to the admin pipe. The Start Menu shortcut -; handles the elevation correctly via the embedded manifest. +; Post-install GUI launch. The GUI's app.manifest is requireAdministrator, +; so launching with shellexec (ShellExecute) honors the manifest and triggers +; a clean UAC prompt. Using plain CreateProcess via the default Run path +; would skip the manifest and result in an un-elevated GUI that cannot connect +; to the admin pipe. +Filename: "{app}\{#AppExeName}"; \ + Description: "Launch {#AppName}"; \ + Flags: postinstall nowait shellexec skipifsilent [UninstallRun] Filename: "powershell.exe"; \ diff --git a/src/WebhookServer.Core/Ipc/AdminProtocol.cs b/src/WebhookServer.Core/Ipc/AdminProtocol.cs index 6977ccb..449eb58 100644 --- a/src/WebhookServer.Core/Ipc/AdminProtocol.cs +++ b/src/WebhookServer.Core/Ipc/AdminProtocol.cs @@ -34,6 +34,7 @@ public sealed class BackupEntry public string FileName { get; set; } = ""; public DateTimeOffset SavedAt { get; set; } public long SizeBytes { get; set; } + public string? Description { get; set; } } public sealed class RestoreBackupArgs @@ -41,6 +42,11 @@ public sealed class RestoreBackupArgs public string FileName { get; set; } = ""; } +public sealed class CreateCheckpointArgs +{ + public string? Description { get; set; } +} + public sealed class AdminRequest { [JsonPropertyName("op")] public string Op { get; set; } = ""; diff --git a/src/WebhookServer.Core/Storage/ConfigStore.cs b/src/WebhookServer.Core/Storage/ConfigStore.cs index 2f6daa3..e8d5417 100644 --- a/src/WebhookServer.Core/Storage/ConfigStore.cs +++ b/src/WebhookServer.Core/Storage/ConfigStore.cs @@ -48,7 +48,14 @@ public sealed class ConfigStore Directory.CreateDirectory(backupsDir); var stamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"); var backupPath = System.IO.Path.Combine(backupsDir, $"config-{stamp}.json"); - File.Copy(Path, backupPath, overwrite: false); + if (!File.Exists(backupPath)) + { + File.Copy(Path, backupPath, overwrite: false); + var sidecar = new { description = "Before save", reason = "before-save" }; + File.WriteAllText( + System.IO.Path.ChangeExtension(backupPath, ".meta.json"), + JsonSerializer.Serialize(sidecar, ConfigJson.Compact)); + } PruneBackups(backupsDir, retain: 30); } catch @@ -71,11 +78,18 @@ public sealed class ConfigStore private static void PruneBackups(string backupsDir, int retain) { var stale = new DirectoryInfo(backupsDir).GetFiles("config-*.json") + .Where(f => !f.Name.EndsWith(".meta.json", StringComparison.OrdinalIgnoreCase)) .OrderByDescending(f => f.Name) .Skip(retain); foreach (var f in stale) { - try { f.Delete(); } catch { } + try + { + f.Delete(); + var sidecar = System.IO.Path.ChangeExtension(f.FullName, ".meta.json"); + if (File.Exists(sidecar)) File.Delete(sidecar); + } + catch { } } } diff --git a/src/WebhookServer.Gui/Services/AdminPipeClient.cs b/src/WebhookServer.Gui/Services/AdminPipeClient.cs index 300d3f0..3611aaa 100644 --- a/src/WebhookServer.Gui/Services/AdminPipeClient.cs +++ b/src/WebhookServer.Gui/Services/AdminPipeClient.cs @@ -101,6 +101,6 @@ public sealed class AdminPipeClient public Task ImportConfigAsync(ServerConfig config, CancellationToken ct = default) => InvokeAsync(AdminOps.ImportConfig, config, ct); - public Task CreateCheckpointAsync(CancellationToken ct = default) => - InvokeAsync(AdminOps.CreateCheckpoint, null, ct); + public Task CreateCheckpointAsync(string? description, CancellationToken ct = default) => + InvokeAsync(AdminOps.CreateCheckpoint, new CreateCheckpointArgs { Description = description }, ct); } diff --git a/src/WebhookServer.Gui/ViewModels/ConfigCheckpointsViewModel.cs b/src/WebhookServer.Gui/ViewModels/ConfigCheckpointsViewModel.cs index 72e6797..d0fa3fc 100644 --- a/src/WebhookServer.Gui/ViewModels/ConfigCheckpointsViewModel.cs +++ b/src/WebhookServer.Gui/ViewModels/ConfigCheckpointsViewModel.cs @@ -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) { diff --git a/src/WebhookServer.Gui/Views/ConfigCheckpointsDialog.xaml b/src/WebhookServer.Gui/Views/ConfigCheckpointsDialog.xaml index a4f70c8..45ce611 100644 --- a/src/WebhookServer.Gui/Views/ConfigCheckpointsDialog.xaml +++ b/src/WebhookServer.Gui/Views/ConfigCheckpointsDialog.xaml @@ -39,11 +39,13 @@ HeadersVisibility="Column" GridLinesVisibility="Horizontal"> - + - diff --git a/src/WebhookServer.Gui/Views/TakeCheckpointDialog.xaml b/src/WebhookServer.Gui/Views/TakeCheckpointDialog.xaml new file mode 100644 index 0000000..0591b88 --- /dev/null +++ b/src/WebhookServer.Gui/Views/TakeCheckpointDialog.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + +