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}}" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs
index 8550a5b..4164f92 100644
--- a/src/PSLauncher.Core/Localization/Strings.cs
+++ b/src/PSLauncher.Core/Localization/Strings.cs
@@ -511,6 +511,36 @@ public static class Strings
"نشر أداة التقارير تلقائياً عند تثبيت نسخة جديدة"
);
public static string SettingsReportRedeploy => T("📂 Re-déployer le Report maintenant", "📂 Re-deploy Report now", "📂 立即重新部署报告", "📂 ปรับใช้รายงานใหม่เดี๋ยวนี้", "📂 إعادة نشر التقرير الآن");
+
+ // ---- Backups Report tool ----
+ public static string SettingsReportBackups => T("Backups disponibles", "Available backups", "可用备份", "สำรองข้อมูลที่มี", "النسخ الاحتياطية المتاحة");
+ public static string SettingsReportMaxBackups => T("Conserver N backups", "Keep N backups", "保留 N 个备份", "เก็บสำรอง N ชุด", "الاحتفاظ بـ N نسخة احتياطية");
+ public static string SettingsReportRevert => T("↶ Revert", "↶ Revert", "↶ 还原", "↶ ย้อนกลับ", "↶ استرجاع");
+ public static string SettingsReportNoBackups => T(
+ "Aucun backup pour l'instant. Le premier sera créé au prochain deploy.",
+ "No backups yet. The first one will be created on the next deploy.",
+ "暂无备份。下次部署时将创建第一个。",
+ "ยังไม่มีสำรองข้อมูล จะสร้างครั้งแรกในการปรับใช้ครั้งถัดไป",
+ "لا توجد نسخ احتياطية بعد. سيتم إنشاء أول نسخة عند النشر التالي."
+ );
+ public static string SettingsReportBackupSize(string size) => T(
+ $"taille : {size}", $"size: {size}", $"大小:{size}", $"ขนาด: {size}", $"الحجم: {size}"
+ );
+ public static string MsgBoxRevertReport => T("Revert Outil Report", "Revert Report tool", "还原报告工具", "ย้อนกลับเครื่องมือรายงาน", "استرجاع أداة التقارير");
+ public static string MsgRevertReportConfirm(string date) => T(
+ $"Revenir à la version du {date} ?\n\nLa version actuellement déployée sera elle-même sauvegardée comme nouveau backup, donc tu pourras y revenir si besoin.",
+ $"Revert to the version from {date}?\n\nThe currently deployed version will itself be saved as a new backup, so you can switch back if needed.",
+ $"还原到 {date} 的版本?\n\n当前已部署的版本将作为新备份保存,因此您可以在需要时切换回去。",
+ $"ย้อนกลับไปยังเวอร์ชันของ {date}?\n\nเวอร์ชันที่ปรับใช้อยู่ในปัจจุบันจะถูกบันทึกเป็นสำรองใหม่ ดังนั้นคุณสามารถสลับกลับได้หากต้องการ",
+ $"العودة إلى نسخة {date}؟\n\nسيتم حفظ النسخة المنشورة حالياً كنسخة احتياطية جديدة، لذا يمكنك التبديل إليها إذا لزم الأمر."
+ );
+ public static string MsgRevertReportSuccess(string date) => T(
+ $"✓ Reverted vers la version du {date}",
+ $"✓ Reverted to version from {date}",
+ $"✓ 已还原到 {date} 的版本",
+ $"✓ ย้อนกลับเป็นเวอร์ชันของ {date} แล้ว",
+ $"✓ تم الاسترجاع إلى نسخة {date}"
+ );
public static string MsgMigrationFailed(string detail) => T(
$"Une migration de la base de données a échoué :\n\n{detail}\n\nLa version a été extraite mais la base n'a PAS été modifiée (rollback automatique).\nVérifie que XAMPP est démarré et que les paramètres de connexion sont corrects (Settings → Base de données), puis clique « Rejouer les migrations » dans Settings.",
$"A database migration failed:\n\n{detail}\n\nThe version was extracted but the database was NOT modified (automatic rollback).\nMake sure XAMPP is running and the connection settings are correct (Settings → Database), then click « Replay migrations » in Settings.",
diff --git a/src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs b/src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs
index ae14098..5b16812 100644
--- a/src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs
+++ b/src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs
@@ -3,6 +3,7 @@ namespace PSLauncher.Core.ReportTool;
///
/// Déploie l'outil Report (page web hébergée dans XAMPP) depuis le sous-dossier
/// _report/ bundled dans chaque ZIP PROSERVE vers le htdocs local.
+/// Conserve N backups horodatés permettant un revert manuel via la UI Settings.
///
public interface IReportToolDeployer
{
@@ -11,18 +12,28 @@ public interface IReportToolDeployer
/// {HtdocsRoot}/{FolderName}/ via un rename atomique :
///
/// - copie récursive vers {target}.new/
- /// - rename de l'existant vers {target}.old/ (s'il existait)
+ /// - rename de l'existant vers {target}.backup-{yyyyMMdd-HHmmss}/
/// - rename .new → final
- /// - delete de .old en best-effort
+ /// - prune des vieux backups au-delà de MaxBackups
///
- /// Si _report/ absent du ZIP : skip silencieux. Si htdocs introuvable :
- /// . Si erreur disque :
- /// + message d'erreur.
///
Task DeployAsync(
string installFolder,
IProgress? progress,
CancellationToken ct);
+
+ ///
+ /// Liste les backups horodatés disponibles dans {HtdocsRoot}, du plus
+ /// récent au plus ancien. Vide si aucun backup ou si htdocs introuvable.
+ ///
+ Task> ListBackupsAsync(CancellationToken ct);
+
+ ///
+ /// Revient à un backup horodaté précis. Swap atomique : la version actuelle
+ /// est elle-même renommée en backup-{now} (donc le revert est lui-même
+ /// reversible), puis le backup choisi est renommé en version active.
+ ///
+ Task RevertAsync(string backupFolderName, CancellationToken ct);
}
public enum DeployStatus
@@ -33,8 +44,10 @@ public enum DeployStatus
XamppNotFound,
/// Erreur I/O pendant la copie ou le rename.
Failed,
- /// Déploiement réussi.
+ /// Déploiement (ou revert) réussi.
Deployed,
+ /// Revert demandé sur un backup qui n'existe pas / plus.
+ BackupNotFound,
}
public sealed record DeployResult(
@@ -45,3 +58,11 @@ public sealed record DeployResult(
string? ErrorMessage = null);
public sealed record DeployProgress(int FilesDone, int FilesTotal, string CurrentFilename);
+
+///
+/// Métadonnées d'un backup horodaté présent dans {HtdocsRoot}.
+///
+public sealed record BackupInfo(
+ string FolderName, // ProserveReport.backup-20260503-143022
+ DateTime TimestampUtc, // parsé depuis le suffixe du nom de dossier
+ long SizeBytes);
diff --git a/src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs b/src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs
index 4ad631b..d097bb9 100644
--- a/src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs
+++ b/src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs
@@ -1,17 +1,26 @@
+using System.Globalization;
using Microsoft.Extensions.Logging;
using PSLauncher.Models;
namespace PSLauncher.Core.ReportTool;
///
-/// Implémentation simple : copy récursif + atomic rename. L'atomicité du rename
-/// garantit qu'Apache (qui sert les fichiers en parallèle) ne tombera jamais sur
-/// un état mi-déployé. Au pire il sert l'ancienne version pendant 1ms le temps
-/// du rename, puis sert la nouvelle.
+/// Implémentation : copy récursif + atomic rename + backups horodatés.
+/// L'atomicité du rename garantit qu'Apache (qui sert les fichiers en parallèle)
+/// ne tombera jamais sur un état mi-déployé. Au pire il sert l'ancienne version
+/// pendant 1ms le temps du rename, puis sert la nouvelle.
+///
+/// Backups : à chaque deploy, l'ancien dossier est conservé sous le nom
+/// {FolderName}.backup-yyyyMMdd-HHmmss. On garde les MaxBackups
+/// plus récents et on supprime les autres en best-effort. Permet le revert
+/// manuel via Settings → Avancés → Outil Report.
///
public sealed class ReportToolDeployer : IReportToolDeployer
{
private const string SourceSubdir = "_report";
+ private const string BackupSuffix = ".backup-";
+ // Format trié-par-nom = trié-par-date (lexicographique == chronologique)
+ private const string TimestampFormat = "yyyyMMdd-HHmmss";
private readonly Func _configProvider;
private readonly ILogger _logger;
@@ -47,15 +56,14 @@ public sealed class ReportToolDeployer : IReportToolDeployer
}
var stagingTarget = target + ".new";
- var oldBackup = target + ".old";
+ var backupTarget = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture);
try
{
- // Nettoyage d'un éventuel résidu de tentative précédente
+ // Nettoyage d'un éventuel résidu de tentative précédente (le .new orphelin)
if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true);
- if (Directory.Exists(oldBackup)) Directory.Delete(oldBackup, recursive: true);
- // Énumère pour avoir un total + reporter la progression
+ // Copy avec progress
var allFiles = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories).ToList();
int total = allFiles.Count;
long bytesTotal = 0;
@@ -79,29 +87,147 @@ public sealed class ReportToolDeployer : IReportToolDeployer
}
}, ct).ConfigureAwait(false);
- // Atomic swap
- if (Directory.Exists(target)) Directory.Move(target, oldBackup);
+ // Atomic swap : current → backup-{ts}, .new → current
+ if (Directory.Exists(target))
+ {
+ Directory.Move(target, backupTarget);
+ }
Directory.Move(stagingTarget, target);
- // Cleanup ancienne version (best-effort, fichiers peuvent être verrouillés
- // par Apache qui les sert encore — pas grave, sera nettoyé au prochain deploy)
- if (Directory.Exists(oldBackup))
- {
- try { Directory.Delete(oldBackup, recursive: true); }
- catch (Exception ex) { _logger.LogDebug(ex, "Could not clean .old backup, will retry next deploy"); }
- }
+ // Prune des vieux backups (best-effort, ne bloque pas le retour)
+ PruneOldBackups(cfg);
- _logger.LogInformation("Report tool deployed to {Target} ({Files} files, {Bytes} bytes)",
- target, total, bytesTotal);
+ _logger.LogInformation("Report tool deployed to {Target} ({Files} files, {Bytes} bytes), backup at {Backup}",
+ target, total, bytesTotal, backupTarget);
return new DeployResult(DeployStatus.Deployed, total, bytesTotal, target);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deploy report tool to {Target}", target);
- // Best-effort cleanup pour ne pas laisser de staging à moitié écrit
try { if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true); }
catch { /* ignore */ }
return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
}
}
+
+ public Task> ListBackupsAsync(CancellationToken ct)
+ {
+ var cfg = _configProvider();
+ var prefix = cfg.FolderName + BackupSuffix;
+
+ if (!Directory.Exists(cfg.HtdocsRoot))
+ return Task.FromResult>(Array.Empty());
+
+ var list = new List();
+ foreach (var dir in Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly))
+ {
+ ct.ThrowIfCancellationRequested();
+ var name = Path.GetFileName(dir);
+ var tsPart = name.Substring(prefix.Length);
+ if (!DateTime.TryParseExact(tsPart, TimestampFormat, CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ts))
+ {
+ continue; // dossier non conforme, on ignore
+ }
+ long size = 0;
+ try
+ {
+ foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
+ size += new FileInfo(f).Length;
+ }
+ catch { /* size best-effort */ }
+ list.Add(new BackupInfo(name, ts, size));
+ }
+ // Plus récent en premier
+ list.Sort((a, b) => b.TimestampUtc.CompareTo(a.TimestampUtc));
+ return Task.FromResult>(list);
+ }
+
+ public async Task RevertAsync(string backupFolderName, CancellationToken ct)
+ {
+ var cfg = _configProvider();
+ var backupPath = Path.Combine(cfg.HtdocsRoot, backupFolderName);
+ var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName);
+
+ if (!Directory.Exists(backupPath))
+ {
+ return new DeployResult(DeployStatus.BackupNotFound, 0, 0, target,
+ $"Le backup {backupFolderName} n'existe plus.");
+ }
+
+ // Le revert lui-même crée un backup de l'état actuel, pour qu'on puisse
+ // revert le revert si besoin.
+ var newBackupForCurrent = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture);
+
+ try
+ {
+ await Task.Run(() =>
+ {
+ ct.ThrowIfCancellationRequested();
+ if (Directory.Exists(target))
+ Directory.Move(target, newBackupForCurrent);
+ Directory.Move(backupPath, target);
+ }, ct).ConfigureAwait(false);
+
+ // Compte les fichiers du backup restauré pour le retour
+ int files = 0;
+ long bytes = 0;
+ try
+ {
+ foreach (var f in Directory.EnumerateFiles(target, "*", SearchOption.AllDirectories))
+ {
+ files++;
+ bytes += new FileInfo(f).Length;
+ }
+ }
+ catch { /* best-effort */ }
+
+ PruneOldBackups(cfg);
+ _logger.LogInformation("Reverted to backup {Backup} ({Files} files), previous current saved as {New}",
+ backupFolderName, files, newBackupForCurrent);
+ return new DeployResult(DeployStatus.Deployed, files, bytes, target);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Revert to {Backup} failed", backupFolderName);
+ return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
+ }
+ }
+
+ ///
+ /// Garde les MaxBackups plus récents, supprime les autres en best-effort.
+ /// Si MaxBackups = 0, supprime tous les backups (mode "no history").
+ ///
+ private void PruneOldBackups(ReportToolConfig cfg)
+ {
+ try
+ {
+ var prefix = cfg.FolderName + BackupSuffix;
+ var dirs = Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly)
+ .Select(d => new { Path = d, Name = Path.GetFileName(d) })
+ // Tri descending par nom = descending par date (yyyyMMdd-HHmmss est lexicographique)
+ .OrderByDescending(d => d.Name, StringComparer.Ordinal)
+ .ToList();
+
+ int keep = Math.Max(0, cfg.MaxBackups);
+ for (int i = keep; i < dirs.Count; i++)
+ {
+ try
+ {
+ Directory.Delete(dirs[i].Path, recursive: true);
+ _logger.LogDebug("Pruned old report backup {Dir}", dirs[i].Name);
+ }
+ catch (Exception ex)
+ {
+ // Apache peut tenir un handle sur un fichier (logs ouverts, etc.).
+ // On retentera au prochain deploy. Pas bloquant.
+ _logger.LogDebug(ex, "Could not prune backup {Dir} (will retry next deploy)", dirs[i].Name);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Backup pruning skipped");
+ }
+ }
}
diff --git a/src/PSLauncher.Models/LocalConfig.cs b/src/PSLauncher.Models/LocalConfig.cs
index d5b67c2..db5264f 100644
--- a/src/PSLauncher.Models/LocalConfig.cs
+++ b/src/PSLauncher.Models/LocalConfig.cs
@@ -107,6 +107,15 @@ public sealed class ReportToolConfig
public string HtdocsRoot { get; set; } = @"C:\xampp\htdocs";
public string FolderName { get; set; } = "ProserveReport";
public bool AutoDeploy { get; set; } = true;
+
+ ///
+ /// Nombre de backups horodatés à conserver dans htdocs après chaque deploy.
+ /// Format des dossiers : {FolderName}.backup-yyyyMMdd-HHmmss.
+ /// Permet le revert via Settings si le nouveau deploy a un bug visible.
+ /// 3 par défaut = couvre les dernières releases sans grossir trop le disque.
+ /// 0 = pas de backup (retourne au comportement v0.12.0).
+ ///
+ public int MaxBackups { get; set; } = 3;
}
public sealed class LicenseConfig
diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj
index cabb3c1..3192f7f 100644
--- a/src/PSLauncher.Updater/PSLauncher.Updater.csproj
+++ b/src/PSLauncher.Updater/PSLauncher.Updater.csproj
@@ -8,7 +8,7 @@
latest
PSLauncher.Updater
PSLauncher.Updater
- 0.12.0
+ 0.13.0
..\PSLauncher.App\Resources\favicon.ico
ASTERION VR
© 2026 ASTERION VR — All rights reserved