v0.13.0 — Report tool : timestamped backups + manual revert
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) <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.12.0</Version>
|
||||
<AssemblyVersion>0.12.0.0</AssemblyVersion>
|
||||
<FileVersion>0.12.0.0</FileVersion>
|
||||
<Version>0.13.0</Version>
|
||||
<AssemblyVersion>0.13.0.0</AssemblyVersion>
|
||||
<FileVersion>0.13.0.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -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<ReportBackupViewModel> ReportBackups { get; }
|
||||
= new();
|
||||
public bool HasReportBackups => ReportBackups.Count > 0;
|
||||
|
||||
public IReadOnlyList<LanguageOption> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Appelé par chaque ligne du tableau de backups via son own command.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ligne du tableau "Backups disponibles" dans Settings → Avancés → Outil Report.
|
||||
/// Chaque instance porte sa propre commande Revert paramétrée pour ce backup.
|
||||
/// </summary>
|
||||
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<BackupInfo, Task> _revertHandler;
|
||||
public ReportBackupViewModel(BackupInfo info, Func<BackupInfo, Task> revertHandler)
|
||||
{
|
||||
Info = info;
|
||||
_revertHandler = revertHandler;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task Revert() => _revertHandler(Info);
|
||||
}
|
||||
|
||||
@@ -444,6 +444,22 @@
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
Content="{x:Static loc:Strings.SettingsReportAutoDeploy}" />
|
||||
|
||||
<!-- Conserver N backups -->
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="80" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{x:Static loc:Strings.SettingsReportMaxBackups}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="12" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding ReportMaxBackups, UpdateSourceTrigger=PropertyChanged}"
|
||||
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8" />
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -460,6 +476,52 @@
|
||||
Command="{Binding RedeployReportCommand}"
|
||||
IsEnabled="{Binding IsDeployingReport, Converter={StaticResource InverseBoolToBool}}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Liste des backups disponibles avec bouton Revert par ligne -->
|
||||
<TextBlock Text="{x:Static loc:Strings.SettingsReportBackups}"
|
||||
FontWeight="Bold" FontSize="11"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,16,0,6" />
|
||||
<TextBlock Text="{x:Static loc:Strings.SettingsReportNoBackups}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="12" FontStyle="Italic"
|
||||
Visibility="{Binding HasReportBackups, Converter={StaticResource InverseBoolToVisibility}}" />
|
||||
<ItemsControl ItemsSource="{Binding ReportBackups}"
|
||||
Visibility="{Binding HasReportBackups, Converter={StaticResource BoolToVisibility}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#1A1A20"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||
CornerRadius="4" Padding="10,8" Margin="0,0,0,4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding DateDisplay}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding SizeDisplay}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="11" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding FolderName}"
|
||||
FontFamily="Consolas" FontSize="10"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Content="{x:Static loc:Strings.SettingsReportRevert}"
|
||||
Command="{Binding RevertCommand}"
|
||||
Padding="12,6" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user