From 562b4e5ed0e245bee97b783a7d007a131259a2b2 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sun, 3 May 2026 09:15:09 +0200 Subject: [PATCH] =?UTF-8?q?v0.13.0=20=E2=80=94=20Report=20tool=20:=20times?= =?UTF-8?q?tamped=20backups=20+=20manual=20revert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous deploy mechanism kept the old version under {target}.old just long enough to swap, then deleted it — no way back if the new report had a runtime bug only visible at use time. Now each deploy keeps the previous active version under {FolderName}.backup-yyyyMMdd-HHmmss/. The N most recent are kept (MaxBackups, default 3) and older ones are pruned in best-effort. UI in Settings → Avancés → Outil Report : - "Conserver N backups" input (0 = no history, back to v0.12 behavior) - List of backups with date + size + cryptic folder name + per-row "↶ Revert" - Confirmation dialog: "Revert to {date}? The currently deployed version will itself be saved as a new backup, so you can switch back." Implementation - IReportToolDeployer: + ListBackupsAsync, + RevertAsync, + BackupInfo, + DeployStatus.BackupNotFound. - ReportToolDeployer: deploy renames {target} → backup-{ts}; PruneOldBackups trims to MaxBackups; RevertAsync atomically swaps current ↔ chosen backup, the previous current becoming itself a fresh backup. - ReportBackupViewModel + DataTemplate for the list rows. - Strings: backup labels, "↶ Revert", confirmation message. Caveats documented in the design discussion: - Backups cover ONLY the report tool files. DB migrations are not reverted (irreversible). If a migration broke the schema, that's a separate fix. - No HTTP-HEAD post-deploy auto-revert: too fragile for false positives. Revert stays a deliberate manual action. Versions bumped to 0.13.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- src/PSLauncher.App/PSLauncher.App.csproj | 6 +- .../ViewModels/SettingsViewModel.cs | 80 +++++++++ src/PSLauncher.App/Views/SettingsDialog.xaml | 62 +++++++ src/PSLauncher.Core/Localization/Strings.cs | 30 ++++ .../ReportTool/IReportToolDeployer.cs | 33 +++- .../ReportTool/ReportToolDeployer.cs | 166 +++++++++++++++--- src/PSLauncher.Models/LocalConfig.cs | 9 + .../PSLauncher.Updater.csproj | 2 +- 9 files changed, 359 insertions(+), 31 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 4e9d4d9..f07da43 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PSLauncher" -#define MyAppVersion "0.12.0" +#define MyAppVersion "0.13.0" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PSLauncher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 6fb5fc6..9528405 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -15,9 +15,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.12.0 - 0.12.0.0 - 0.12.0.0 + 0.13.0 + 0.13.0.0 + 0.13.0.0 true diff --git a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs index d14c23b..eec2f37 100644 --- a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs +++ b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs @@ -63,8 +63,12 @@ public sealed partial class SettingsViewModel : ObservableObject [ObservableProperty] private string _reportHtdocsRoot; [ObservableProperty] private string _reportFolderName; [ObservableProperty] private bool _reportAutoDeploy; + [ObservableProperty] private int _reportMaxBackups; [ObservableProperty] private string? _reportDeployStatus; [ObservableProperty] private bool _isDeployingReport; + public System.Collections.ObjectModel.ObservableCollection ReportBackups { get; } + = new(); + public bool HasReportBackups => ReportBackups.Count > 0; public IReadOnlyList AvailableLanguages { get; } = PSLauncher.Core.Localization.Strings.Available @@ -164,6 +168,9 @@ public sealed partial class SettingsViewModel : ObservableObject _reportHtdocsRoot = config.ReportTool.HtdocsRoot; _reportFolderName = config.ReportTool.FolderName; _reportAutoDeploy = config.ReportTool.AutoDeploy; + _reportMaxBackups = config.ReportTool.MaxBackups; + // Charge la liste de backups en arrière-plan (UI immédiate puis remplie quand prête) + _ = LoadReportBackupsAsync(); LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; MachineId = licenseService.GetMachineId(); @@ -287,6 +294,7 @@ public sealed partial class SettingsViewModel : ObservableObject _config.ReportTool.HtdocsRoot = ReportHtdocsRoot.Trim(); _config.ReportTool.FolderName = ReportFolderName.Trim(); _config.ReportTool.AutoDeploy = ReportAutoDeploy; + _config.ReportTool.MaxBackups = Math.Max(0, ReportMaxBackups); _configStore.Save(_config); _logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged); @@ -494,4 +502,76 @@ public sealed partial class SettingsViewModel : ObservableObject IsReplayingMigrations = false; } } + + // ===================== REPORT TOOL BACKUPS ===================== + + private async Task LoadReportBackupsAsync() + { + try + { + var list = await _reportDeployer.ListBackupsAsync(CancellationToken.None); + ReportBackups.Clear(); + foreach (var b in list) + ReportBackups.Add(new ReportBackupViewModel(b, RevertReportToBackupAsync)); + OnPropertyChanged(nameof(HasReportBackups)); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Could not list report backups"); + } + } + + /// Appelé par chaque ligne du tableau de backups via son own command. + private async Task RevertReportToBackupAsync(BackupInfo backup) + { + var localDate = backup.TimestampUtc.ToLocalTime(); + var dateLabel = Strings.FormatLongDate(localDate); + var confirm = MessageBox.Show( + Strings.MsgRevertReportConfirm(dateLabel), + Strings.MsgBoxRevertReport, + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (confirm != MessageBoxResult.Yes) return; + + IsDeployingReport = true; + ReportDeployStatus = $"Revert vers {dateLabel}…"; + try + { + var result = await _reportDeployer.RevertAsync(backup.FolderName, CancellationToken.None); + ReportDeployStatus = result.Status == DeployStatus.Deployed + ? Strings.MsgRevertReportSuccess(dateLabel) + : $"✗ {result.ErrorMessage}"; + await LoadReportBackupsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Revert report failed"); + ReportDeployStatus = $"✗ {ex.Message}"; + } + finally + { + IsDeployingReport = false; + } + } +} + +/// +/// Ligne du tableau "Backups disponibles" dans Settings → Avancés → Outil Report. +/// Chaque instance porte sa propre commande Revert paramétrée pour ce backup. +/// +public sealed partial class ReportBackupViewModel : ObservableObject +{ + public BackupInfo Info { get; } + public string DateDisplay => Strings.FormatLongDate(Info.TimestampUtc.ToLocalTime()); + public string SizeDisplay => Strings.SettingsReportBackupSize(Strings.FormatSize(Info.SizeBytes)); + public string FolderName => Info.FolderName; + + private readonly Func _revertHandler; + public ReportBackupViewModel(BackupInfo info, Func revertHandler) + { + Info = info; + _revertHandler = revertHandler; + } + + [RelayCommand] + private Task Revert() => _revertHandler(Info); } diff --git a/src/PSLauncher.App/Views/SettingsDialog.xaml b/src/PSLauncher.App/Views/SettingsDialog.xaml index 6c5967c..3193531 100644 --- a/src/PSLauncher.App/Views/SettingsDialog.xaml +++ b/src/PSLauncher.App/Views/SettingsDialog.xaml @@ -444,6 +444,22 @@ Foreground="{StaticResource Brush.Text.Primary}" Content="{x:Static loc:Strings.SettingsReportAutoDeploy}" /> + + + + + + + + + + @@ -460,6 +476,52 @@ Command="{Binding RedeployReportCommand}" IsEnabled="{Binding IsDeployingReport, Converter={StaticResource InverseBoolToBool}}" /> + + + + + + + + + + + + + + + + + + + +