v0.17.0 — Doc tool auto-deploy + revert (mirror of Report)
Adds the same deploy + backup/revert mechanism as Report, but for the local Documentation web tool : PROSERVE-source/_doc/ → C:\xampp\htdocs\ProserveDoc\ http://localhost/ProserveDoc/ Default DocumentationUrl in the launcher set to http://localhost/ProserveDoc/ so the Documentation sidebar tab works out of the box on a fresh install without any manual config. Implementation - LocalConfig.DocToolConfig (HtdocsRoot/FolderName/AutoDeploy/MaxBackups, defaults to ProserveDoc folder, 3 backups kept). - IDocToolDeployer + DocToolDeployer : near-duplicate of the Report deployer with SourceSubdir = "_doc". Reuses ReportTool's BackupInfo / DeployStatus / DeployResult / DeployProgress types so we don't fork the data model. - MainViewModel : new install step 7 right after the Report deploy step, mirrors its progress reporting + error dialogs (silent on XamppNotFound since the Report step already warned for the same root). - SettingsViewModel : DocBackupViewModel + Doc properties + RedeployDoc + Revert commands. Loads the doc backups list in background like the Report ones. - SettingsDialog.xaml : new « OUTIL DOCUMENTATION » card under the « OUTIL REPORT » one in Avancés, with the same fields and backup table. - Strings.cs : doc-specific status / progress / error labels in 5 langs ; reuses some labels from Report where the wording is identical. Versions bumped to 0.17.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "0.16.0"
|
#define MyAppVersion "0.17.0"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using PSLauncher.Core.Integrity;
|
|||||||
using PSLauncher.Core.Licensing;
|
using PSLauncher.Core.Licensing;
|
||||||
using PSLauncher.Core.Localization;
|
using PSLauncher.Core.Localization;
|
||||||
using PSLauncher.Core.Manifests;
|
using PSLauncher.Core.Manifests;
|
||||||
|
using PSLauncher.Core.DocTool;
|
||||||
using PSLauncher.Core.Migrations;
|
using PSLauncher.Core.Migrations;
|
||||||
using PSLauncher.Core.Process;
|
using PSLauncher.Core.Process;
|
||||||
using PSLauncher.Core.ReportTool;
|
using PSLauncher.Core.ReportTool;
|
||||||
@@ -182,6 +183,13 @@ public partial class App : Application
|
|||||||
configProvider: () => sp.GetRequiredService<LocalConfig>().ReportTool,
|
configProvider: () => sp.GetRequiredService<LocalConfig>().ReportTool,
|
||||||
sp.GetRequiredService<ILogger<ReportToolDeployer>>()));
|
sp.GetRequiredService<ILogger<ReportToolDeployer>>()));
|
||||||
|
|
||||||
|
// Déploiement de l'outil Documentation (parallèle à Report) :
|
||||||
|
// copie atomique de _doc/ → C:\xampp\htdocs\ProserveDoc\
|
||||||
|
services.AddSingleton<IDocToolDeployer>(sp =>
|
||||||
|
new DocToolDeployer(
|
||||||
|
configProvider: () => sp.GetRequiredService<LocalConfig>().DocTool,
|
||||||
|
sp.GetRequiredService<ILogger<DocToolDeployer>>()));
|
||||||
|
|
||||||
services.AddTransient<SettingsViewModel>();
|
services.AddTransient<SettingsViewModel>();
|
||||||
services.AddSingleton<MainViewModel>();
|
services.AddSingleton<MainViewModel>();
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>0.16.0</Version>
|
<Version>0.17.0</Version>
|
||||||
<AssemblyVersion>0.16.0.0</AssemblyVersion>
|
<AssemblyVersion>0.17.0.0</AssemblyVersion>
|
||||||
<FileVersion>0.16.0.0</FileVersion>
|
<FileVersion>0.17.0.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using PSLauncher.Core.Installations;
|
|||||||
using PSLauncher.Core.Licensing;
|
using PSLauncher.Core.Licensing;
|
||||||
using PSLauncher.Core.Localization;
|
using PSLauncher.Core.Localization;
|
||||||
using PSLauncher.Core.Manifests;
|
using PSLauncher.Core.Manifests;
|
||||||
|
using PSLauncher.Core.DocTool;
|
||||||
using PSLauncher.Core.Migrations;
|
using PSLauncher.Core.Migrations;
|
||||||
using PSLauncher.Core.Process;
|
using PSLauncher.Core.Process;
|
||||||
using PSLauncher.Core.ReportTool;
|
using PSLauncher.Core.ReportTool;
|
||||||
@@ -43,6 +44,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
private readonly IToastService _toastService;
|
private readonly IToastService _toastService;
|
||||||
private readonly IDatabaseMigrationService _migrationService;
|
private readonly IDatabaseMigrationService _migrationService;
|
||||||
private readonly IReportToolDeployer _reportDeployer;
|
private readonly IReportToolDeployer _reportDeployer;
|
||||||
|
private readonly IDocToolDeployer _docDeployer;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<MainViewModel> _logger;
|
private readonly ILogger<MainViewModel> _logger;
|
||||||
|
|
||||||
@@ -226,6 +228,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
IToastService toastService,
|
IToastService toastService,
|
||||||
IDatabaseMigrationService migrationService,
|
IDatabaseMigrationService migrationService,
|
||||||
IReportToolDeployer reportDeployer,
|
IReportToolDeployer reportDeployer,
|
||||||
|
IDocToolDeployer docDeployer,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILogger<MainViewModel> logger)
|
ILogger<MainViewModel> logger)
|
||||||
{
|
{
|
||||||
@@ -242,6 +245,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
_toastService = toastService;
|
_toastService = toastService;
|
||||||
_migrationService = migrationService;
|
_migrationService = migrationService;
|
||||||
_reportDeployer = reportDeployer;
|
_reportDeployer = reportDeployer;
|
||||||
|
_docDeployer = docDeployer;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@@ -793,6 +797,49 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7) Déploiement de l'outil Documentation (parallèle à Report).
|
||||||
|
// Cherche _doc/ dans le ZIP extrait et copie vers htdocs en
|
||||||
|
// atomic rename. Skip silencieux si _doc/ absent du ZIP.
|
||||||
|
if (_config.DocTool.AutoDeploy)
|
||||||
|
{
|
||||||
|
StatusMessage = Strings.StatusDeployingDoc(row.Version);
|
||||||
|
ProgressDetail = null;
|
||||||
|
ProgressPercent = 0;
|
||||||
|
var ddProgress = new Progress<DeployProgress>(dp =>
|
||||||
|
{
|
||||||
|
if (dp.FilesTotal > 0)
|
||||||
|
{
|
||||||
|
var pct = (double)dp.FilesDone / dp.FilesTotal * 100.0;
|
||||||
|
ProgressPercent = pct;
|
||||||
|
row.ProgressPercent = pct;
|
||||||
|
}
|
||||||
|
var label = Strings.ProgressDocDeploy(dp.FilesDone, dp.FilesTotal, dp.CurrentFilename);
|
||||||
|
ProgressDetail = label;
|
||||||
|
row.ProgressDetail = label;
|
||||||
|
});
|
||||||
|
var ddResult = await _docDeployer.DeployAsync(target, ddProgress, ct);
|
||||||
|
switch (ddResult.Status)
|
||||||
|
{
|
||||||
|
case DeployStatus.Deployed:
|
||||||
|
_logger.LogInformation("Doc tool deployed ({N} files) for v{Version}", ddResult.FilesDeployed, row.Version);
|
||||||
|
break;
|
||||||
|
case DeployStatus.SkippedNoSourceFolder:
|
||||||
|
_logger.LogInformation("No _doc/ in v{Version} ZIP, doc tool unchanged", row.Version);
|
||||||
|
break;
|
||||||
|
case DeployStatus.XamppNotFound:
|
||||||
|
// Le warning Report a déjà été affiché si nécessaire ; on reste silencieux ici
|
||||||
|
// (même cause = même symptôme, inutile de faire 2 popups identiques).
|
||||||
|
_logger.LogWarning("Doc tool deploy : XAMPP htdocs not found at {Root}", _config.DocTool.HtdocsRoot);
|
||||||
|
break;
|
||||||
|
case DeployStatus.Failed:
|
||||||
|
ThemedMessageBox.Show(
|
||||||
|
Strings.MsgDocDeployFailed(ddResult.ErrorMessage ?? "?"),
|
||||||
|
Strings.MsgBoxDocDeployFailed,
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StatusMessage = Strings.StatusInstalledSuccess(row.Version);
|
StatusMessage = Strings.StatusInstalledSuccess(row.Version);
|
||||||
ProgressDetail = null;
|
ProgressDetail = null;
|
||||||
ProgressPercent = 0;
|
ProgressPercent = 0;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PSLauncher.App.Views;
|
using PSLauncher.App.Views;
|
||||||
using PSLauncher.Core.Configuration;
|
using PSLauncher.Core.Configuration;
|
||||||
|
using PSLauncher.Core.DocTool;
|
||||||
using PSLauncher.Core.Downloads;
|
using PSLauncher.Core.Downloads;
|
||||||
using PSLauncher.Core.Installations;
|
using PSLauncher.Core.Installations;
|
||||||
using PSLauncher.Core.Licensing;
|
using PSLauncher.Core.Licensing;
|
||||||
@@ -33,6 +34,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly IDownloadStateStore _downloadStore;
|
private readonly IDownloadStateStore _downloadStore;
|
||||||
private readonly IDatabaseMigrationService _migrationService;
|
private readonly IDatabaseMigrationService _migrationService;
|
||||||
private readonly IReportToolDeployer _reportDeployer;
|
private readonly IReportToolDeployer _reportDeployer;
|
||||||
|
private readonly IDocToolDeployer _docDeployer;
|
||||||
private readonly IInstallationRegistry _registry;
|
private readonly IInstallationRegistry _registry;
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
private readonly ILogger<SettingsViewModel> _logger;
|
private readonly ILogger<SettingsViewModel> _logger;
|
||||||
@@ -71,6 +73,17 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
= new();
|
= new();
|
||||||
public bool HasReportBackups => ReportBackups.Count > 0;
|
public bool HasReportBackups => ReportBackups.Count > 0;
|
||||||
|
|
||||||
|
// ----- Doc Tool deploy settings (mirror of Report) -----
|
||||||
|
[ObservableProperty] private string _docHtdocsRoot;
|
||||||
|
[ObservableProperty] private string _docFolderName;
|
||||||
|
[ObservableProperty] private bool _docAutoDeploy;
|
||||||
|
[ObservableProperty] private int _docMaxBackups;
|
||||||
|
[ObservableProperty] private string? _docDeployStatus;
|
||||||
|
[ObservableProperty] private bool _isDeployingDoc;
|
||||||
|
public System.Collections.ObjectModel.ObservableCollection<DocBackupViewModel> DocBackups { get; }
|
||||||
|
= new();
|
||||||
|
public bool HasDocBackups => DocBackups.Count > 0;
|
||||||
|
|
||||||
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
||||||
PSLauncher.Core.Localization.Strings.Available
|
PSLauncher.Core.Localization.Strings.Available
|
||||||
.Select(t => new LanguageOption(t.Code, t.Name))
|
.Select(t => new LanguageOption(t.Code, t.Name))
|
||||||
@@ -139,6 +152,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
IDownloadStateStore downloadStore,
|
IDownloadStateStore downloadStore,
|
||||||
IDatabaseMigrationService migrationService,
|
IDatabaseMigrationService migrationService,
|
||||||
IReportToolDeployer reportDeployer,
|
IReportToolDeployer reportDeployer,
|
||||||
|
IDocToolDeployer docDeployer,
|
||||||
IInstallationRegistry registry,
|
IInstallationRegistry registry,
|
||||||
HttpClient http,
|
HttpClient http,
|
||||||
ILogger<SettingsViewModel> logger)
|
ILogger<SettingsViewModel> logger)
|
||||||
@@ -149,6 +163,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_downloadStore = downloadStore;
|
_downloadStore = downloadStore;
|
||||||
_migrationService = migrationService;
|
_migrationService = migrationService;
|
||||||
_reportDeployer = reportDeployer;
|
_reportDeployer = reportDeployer;
|
||||||
|
_docDeployer = docDeployer;
|
||||||
_registry = registry;
|
_registry = registry;
|
||||||
_http = http;
|
_http = http;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -170,8 +185,15 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_reportFolderName = config.ReportTool.FolderName;
|
_reportFolderName = config.ReportTool.FolderName;
|
||||||
_reportAutoDeploy = config.ReportTool.AutoDeploy;
|
_reportAutoDeploy = config.ReportTool.AutoDeploy;
|
||||||
_reportMaxBackups = config.ReportTool.MaxBackups;
|
_reportMaxBackups = config.ReportTool.MaxBackups;
|
||||||
// Charge la liste de backups en arrière-plan (UI immédiate puis remplie quand prête)
|
|
||||||
|
_docHtdocsRoot = config.DocTool.HtdocsRoot;
|
||||||
|
_docFolderName = config.DocTool.FolderName;
|
||||||
|
_docAutoDeploy = config.DocTool.AutoDeploy;
|
||||||
|
_docMaxBackups = config.DocTool.MaxBackups;
|
||||||
|
|
||||||
|
// Charge les listes de backups en arrière-plan (UI immédiate puis remplies quand prêtes)
|
||||||
_ = LoadReportBackupsAsync();
|
_ = LoadReportBackupsAsync();
|
||||||
|
_ = LoadDocBackupsAsync();
|
||||||
|
|
||||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||||
MachineId = licenseService.GetMachineId();
|
MachineId = licenseService.GetMachineId();
|
||||||
@@ -297,6 +319,11 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_config.ReportTool.AutoDeploy = ReportAutoDeploy;
|
_config.ReportTool.AutoDeploy = ReportAutoDeploy;
|
||||||
_config.ReportTool.MaxBackups = Math.Max(0, ReportMaxBackups);
|
_config.ReportTool.MaxBackups = Math.Max(0, ReportMaxBackups);
|
||||||
|
|
||||||
|
_config.DocTool.HtdocsRoot = DocHtdocsRoot.Trim();
|
||||||
|
_config.DocTool.FolderName = DocFolderName.Trim();
|
||||||
|
_config.DocTool.AutoDeploy = DocAutoDeploy;
|
||||||
|
_config.DocTool.MaxBackups = Math.Max(0, DocMaxBackups);
|
||||||
|
|
||||||
_configStore.Save(_config);
|
_configStore.Save(_config);
|
||||||
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
||||||
|
|
||||||
@@ -553,6 +580,96 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
IsDeployingReport = false;
|
IsDeployingReport = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== DOC TOOL BACKUPS (mirror of Report) =====================
|
||||||
|
|
||||||
|
private async Task LoadDocBackupsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await _docDeployer.ListBackupsAsync(CancellationToken.None);
|
||||||
|
DocBackups.Clear();
|
||||||
|
foreach (var b in list)
|
||||||
|
DocBackups.Add(new DocBackupViewModel(b, RevertDocToBackupAsync));
|
||||||
|
OnPropertyChanged(nameof(HasDocBackups));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Could not list doc backups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevertDocToBackupAsync(BackupInfo backup)
|
||||||
|
{
|
||||||
|
var localDate = backup.TimestampUtc.ToLocalTime();
|
||||||
|
var dateLabel = Strings.FormatLongDate(localDate);
|
||||||
|
var confirm = ThemedMessageBox.Show(
|
||||||
|
Strings.MsgRevertReportConfirm(dateLabel),
|
||||||
|
Strings.MsgBoxRevertReport,
|
||||||
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
|
if (confirm != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
IsDeployingDoc = true;
|
||||||
|
DocDeployStatus = $"Revert vers {dateLabel}…";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _docDeployer.RevertAsync(backup.FolderName, CancellationToken.None);
|
||||||
|
DocDeployStatus = result.Status == DeployStatus.Deployed
|
||||||
|
? Strings.MsgRevertReportSuccess(dateLabel)
|
||||||
|
: $"✗ {result.ErrorMessage}";
|
||||||
|
await LoadDocBackupsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Revert doc failed");
|
||||||
|
DocDeployStatus = $"✗ {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsDeployingDoc = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RedeployDocAsync()
|
||||||
|
{
|
||||||
|
var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
|
||||||
|
if (installed is null)
|
||||||
|
{
|
||||||
|
DocDeployStatus = "Aucune version installée — installe d'abord une version qui contient _doc/.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IsDeployingDoc = true;
|
||||||
|
DocDeployStatus = $"Déploiement depuis v{installed.Version}…";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
var progress = new Progress<DeployProgress>(dp =>
|
||||||
|
{
|
||||||
|
if (dp.FilesTotal > 0)
|
||||||
|
DocDeployStatus = $"{dp.FilesDone}/{dp.FilesTotal} : {dp.CurrentFilename}";
|
||||||
|
});
|
||||||
|
var result = await _docDeployer.DeployAsync(installed.FolderPath, progress, CancellationToken.None);
|
||||||
|
DocDeployStatus = result.Status switch
|
||||||
|
{
|
||||||
|
DeployStatus.Deployed => $"✓ {result.FilesDeployed} fichiers déployés vers {result.TargetPath}",
|
||||||
|
DeployStatus.SkippedNoSourceFolder => $"⚠ La version v{installed.Version} ne contient pas de _doc/ — rien à déployer",
|
||||||
|
DeployStatus.XamppNotFound => $"✗ {result.ErrorMessage}",
|
||||||
|
DeployStatus.Failed => $"✗ {result.ErrorMessage}",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
await LoadDocBackupsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Redeploy doc failed");
|
||||||
|
DocDeployStatus = $"✗ {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsDeployingDoc = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -576,3 +693,22 @@ public sealed partial class ReportBackupViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task Revert() => _revertHandler(Info);
|
private Task Revert() => _revertHandler(Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Variante doc — même structure que <see cref="ReportBackupViewModel"/>.</summary>
|
||||||
|
public sealed partial class DocBackupViewModel : 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 DocBackupViewModel(BackupInfo info, Func<BackupInfo, Task> revertHandler)
|
||||||
|
{
|
||||||
|
Info = info;
|
||||||
|
_revertHandler = revertHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task Revert() => _revertHandler(Info);
|
||||||
|
}
|
||||||
|
|||||||
@@ -525,6 +525,116 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Outil Doc (XAMPP htdocs deploy, mirror of Report) -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDocTool}"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsReportHtdocs}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<TextBox Text="{Binding DocHtdocsRoot, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsReportFolder}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,12,0,4" />
|
||||||
|
<TextBox Text="{Binding DocFolderName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{Binding DocAutoDeploy}"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsDocAutoDeploy}" />
|
||||||
|
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="80" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{x:Static loc:Strings.SettingsDocMaxBackups}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" VerticalAlignment="Center" />
|
||||||
|
<TextBox Grid.Column="1"
|
||||||
|
Text="{Binding DocMaxBackups, 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="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding DocDeployStatus}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsDocRedeploy}"
|
||||||
|
Command="{Binding RedeployDocCommand}"
|
||||||
|
IsEnabled="{Binding IsDeployingDoc, Converter={StaticResource InverseBoolToBool}}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDocBackups}"
|
||||||
|
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 HasDocBackups, Converter={StaticResource InverseBoolToVisibility}}" />
|
||||||
|
<ItemsControl ItemsSource="{Binding DocBackups}"
|
||||||
|
Visibility="{Binding HasDocBackups, 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>
|
||||||
|
|
||||||
<!-- Logs (path + open button) -->
|
<!-- Logs (path + open button) -->
|
||||||
<Border Background="{StaticResource Brush.Bg.Card}"
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
|||||||
211
src/PSLauncher.Core/DocTool/DocToolDeployer.cs
Normal file
211
src/PSLauncher.Core/DocTool/DocToolDeployer.cs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PSLauncher.Core.ReportTool;
|
||||||
|
using PSLauncher.Models;
|
||||||
|
|
||||||
|
namespace PSLauncher.Core.DocTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implémentation : copy récursif + atomic rename + backups horodatés.
|
||||||
|
/// Strictement parallèle à <see cref="ReportToolDeployer"/> mais avec
|
||||||
|
/// <c>_doc/</c> comme source au lieu de <c>_report/</c>. Voir le commentaire
|
||||||
|
/// de cette dernière classe pour les détails du mécanisme.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DocToolDeployer : IDocToolDeployer
|
||||||
|
{
|
||||||
|
private const string SourceSubdir = "_doc";
|
||||||
|
private const string BackupSuffix = ".backup-";
|
||||||
|
private const string TimestampFormat = "yyyyMMdd-HHmmss";
|
||||||
|
|
||||||
|
private readonly Func<DocToolConfig> _configProvider;
|
||||||
|
private readonly ILogger<DocToolDeployer> _logger;
|
||||||
|
|
||||||
|
public DocToolDeployer(
|
||||||
|
Func<DocToolConfig> configProvider,
|
||||||
|
ILogger<DocToolDeployer> logger)
|
||||||
|
{
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeployResult> DeployAsync(
|
||||||
|
string installFolder,
|
||||||
|
IProgress<DeployProgress>? progress,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var cfg = _configProvider();
|
||||||
|
var source = Path.Combine(installFolder, SourceSubdir);
|
||||||
|
var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName);
|
||||||
|
|
||||||
|
if (!Directory.Exists(source))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No {Sub}/ directory in {Folder}, skipping doc deploy", SourceSubdir, installFolder);
|
||||||
|
return new DeployResult(DeployStatus.SkippedNoSourceFolder, 0, 0, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(cfg.HtdocsRoot))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("HtdocsRoot {Root} not found — XAMPP not installed at expected location", cfg.HtdocsRoot);
|
||||||
|
return new DeployResult(DeployStatus.XamppNotFound, 0, 0, target,
|
||||||
|
$"Le dossier {cfg.HtdocsRoot} est introuvable. XAMPP est-il installé à cet emplacement ? (Settings → Avancés)");
|
||||||
|
}
|
||||||
|
|
||||||
|
var stagingTarget = target + ".new";
|
||||||
|
var backupTarget = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true);
|
||||||
|
|
||||||
|
var allFiles = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories).ToList();
|
||||||
|
int total = allFiles.Count;
|
||||||
|
long bytesTotal = 0;
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(stagingTarget);
|
||||||
|
int done = 0;
|
||||||
|
foreach (var srcFile in allFiles)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var rel = Path.GetRelativePath(source, srcFile);
|
||||||
|
var dstFile = Path.Combine(stagingTarget, rel);
|
||||||
|
var dstDir = Path.GetDirectoryName(dstFile);
|
||||||
|
if (!string.IsNullOrEmpty(dstDir) && !Directory.Exists(dstDir))
|
||||||
|
Directory.CreateDirectory(dstDir);
|
||||||
|
File.Copy(srcFile, dstFile, overwrite: true);
|
||||||
|
bytesTotal += new FileInfo(srcFile).Length;
|
||||||
|
done++;
|
||||||
|
progress?.Report(new DeployProgress(done, total, rel));
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (Directory.Exists(target))
|
||||||
|
Directory.Move(target, backupTarget);
|
||||||
|
Directory.Move(stagingTarget, target);
|
||||||
|
|
||||||
|
PruneOldBackups(cfg);
|
||||||
|
|
||||||
|
_logger.LogInformation("Doc 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 doc tool to {Target}", target);
|
||||||
|
try { if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true); }
|
||||||
|
catch { /* ignore */ }
|
||||||
|
return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<BackupInfo>> ListBackupsAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var cfg = _configProvider();
|
||||||
|
var prefix = cfg.FolderName + BackupSuffix;
|
||||||
|
|
||||||
|
if (!Directory.Exists(cfg.HtdocsRoot))
|
||||||
|
return Task.FromResult<IReadOnlyList<BackupInfo>>(Array.Empty<BackupInfo>());
|
||||||
|
|
||||||
|
var list = new List<BackupInfo>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
list.Sort((a, b) => b.TimestampUtc.CompareTo(a.TimestampUtc));
|
||||||
|
return Task.FromResult<IReadOnlyList<BackupInfo>>(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeployResult> 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 doc 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, "Doc revert to {Backup} failed", backupFolderName);
|
||||||
|
return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PruneOldBackups(DocToolConfig 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) })
|
||||||
|
.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 doc backup {Dir}", dirs[i].Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Could not prune doc backup {Dir} (will retry next deploy)", dirs[i].Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Doc backup pruning skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/PSLauncher.Core/DocTool/IDocToolDeployer.cs
Normal file
25
src/PSLauncher.Core/DocTool/IDocToolDeployer.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using PSLauncher.Core.ReportTool;
|
||||||
|
|
||||||
|
namespace PSLauncher.Core.DocTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Déploie l'outil Documentation (page web hébergée dans XAMPP) depuis le
|
||||||
|
/// sous-dossier <c>_doc/</c> bundled dans chaque ZIP PROSERVE vers le htdocs
|
||||||
|
/// local. Mêmes garanties que <see cref="IReportToolDeployer"/> : copy + atomic
|
||||||
|
/// rename + backups horodatés + revert manuel.
|
||||||
|
///
|
||||||
|
/// On réutilise les types <see cref="DeployResult"/>, <see cref="DeployStatus"/>,
|
||||||
|
/// <see cref="DeployProgress"/> et <see cref="BackupInfo"/> du namespace ReportTool
|
||||||
|
/// pour ne pas dupliquer la modélisation — le mécanisme est strictement le même.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDocToolDeployer
|
||||||
|
{
|
||||||
|
Task<DeployResult> DeployAsync(
|
||||||
|
string installFolder,
|
||||||
|
IProgress<DeployProgress>? progress,
|
||||||
|
CancellationToken ct);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<BackupInfo>> ListBackupsAsync(CancellationToken ct);
|
||||||
|
|
||||||
|
Task<DeployResult> RevertAsync(string backupFolderName, CancellationToken ct);
|
||||||
|
}
|
||||||
@@ -515,6 +515,42 @@ public static class Strings
|
|||||||
);
|
);
|
||||||
public static string SettingsReportRedeploy => T("📂 Re-déployer le Report maintenant", "📂 Re-deploy Report now", "📂 立即重新部署报告", "📂 ปรับใช้รายงานใหม่เดี๋ยวนี้", "📂 إعادة نشر التقرير الآن");
|
public static string SettingsReportRedeploy => T("📂 Re-déployer le Report maintenant", "📂 Re-deploy Report now", "📂 立即重新部署报告", "📂 ปรับใช้รายงานใหม่เดี๋ยวนี้", "📂 إعادة نشر التقرير الآن");
|
||||||
|
|
||||||
|
// ---- Doc tool (mêmes mécanique que Report, autres libellés) ----
|
||||||
|
public static string StatusDeployingDoc(string version) => T(
|
||||||
|
$"Déploiement de la Documentation v{version}…",
|
||||||
|
$"Deploying Documentation for v{version}…",
|
||||||
|
$"正在部署 v{version} 的文档…",
|
||||||
|
$"กำลังปรับใช้เอกสาร v{version}…",
|
||||||
|
$"جارٍ نشر التوثيق v{version}…"
|
||||||
|
);
|
||||||
|
public static string ProgressDocDeploy(int done, int total, string filename) => T(
|
||||||
|
$"📖 Déploiement Doc : {done}/{total} — {filename}",
|
||||||
|
$"📖 Deploying Doc: {done}/{total} — {filename}",
|
||||||
|
$"📖 部署文档:{done}/{total} — {filename}",
|
||||||
|
$"📖 ปรับใช้เอกสาร: {done}/{total} — {filename}",
|
||||||
|
$"📖 نشر التوثيق: {done}/{total} — {filename}"
|
||||||
|
);
|
||||||
|
public static string MsgBoxDocDeployFailed => T("Déploiement Doc échoué", "Doc deploy failed", "文档部署失败", "การปรับใช้เอกสารล้มเหลว", "فشل نشر التوثيق");
|
||||||
|
public static string MsgDocDeployFailed(string detail) => T(
|
||||||
|
$"Le déploiement de la Documentation a échoué :\n\n{detail}\n\nLa version PROSERVE est installée mais l'onglet Documentation pourrait être désynchronisé. Tu peux retenter via Paramètres → Avancés → Re-déployer la Doc.",
|
||||||
|
$"Documentation deploy failed:\n\n{detail}\n\nThe PROSERVE version is installed but the Documentation tab might be out of sync. You can retry via Settings → Advanced → Re-deploy Doc.",
|
||||||
|
$"文档部署失败:\n\n{detail}\n\nPROSERVE 版本已安装,但文档选项卡可能不同步。您可以通过 设置 → 高级 → 重新部署文档 重试。",
|
||||||
|
$"การปรับใช้เอกสารล้มเหลว:\n\n{detail}\n\nเวอร์ชัน PROSERVE ติดตั้งแล้วแต่แท็บเอกสารอาจไม่ซิงค์ คุณสามารถลองใหม่ผ่าน ตั้งค่า → ขั้นสูง → ปรับใช้เอกสารใหม่",
|
||||||
|
$"فشل نشر التوثيق:\n\n{detail}\n\nنسخة PROSERVE مثبتة لكن علامة التبويب التوثيق قد لا تكون متزامنة. يمكنك إعادة المحاولة عبر الإعدادات → متقدم → إعادة نشر التوثيق."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static string SettingsDocTool => T("OUTIL DOCUMENTATION (XAMPP htdocs)", "DOCUMENTATION TOOL (XAMPP htdocs)", "文档工具 (XAMPP htdocs)", "เครื่องมือเอกสาร (XAMPP htdocs)", "أداة التوثيق (XAMPP htdocs)");
|
||||||
|
public static string SettingsDocAutoDeploy => T(
|
||||||
|
"Déployer automatiquement la Documentation à l'install d'une nouvelle version",
|
||||||
|
"Automatically deploy the Documentation when installing a new version",
|
||||||
|
"安装新版本时自动部署文档",
|
||||||
|
"ปรับใช้เอกสารโดยอัตโนมัติเมื่อติดตั้งเวอร์ชันใหม่",
|
||||||
|
"نشر التوثيق تلقائياً عند تثبيت نسخة جديدة"
|
||||||
|
);
|
||||||
|
public static string SettingsDocRedeploy => T("📖 Re-déployer la Doc maintenant", "📖 Re-deploy Doc now", "📖 立即重新部署文档", "📖 ปรับใช้เอกสารใหม่เดี๋ยวนี้", "📖 إعادة نشر التوثيق الآن");
|
||||||
|
public static string SettingsDocBackups => SettingsReportBackups;
|
||||||
|
public static string SettingsDocMaxBackups => SettingsReportMaxBackups;
|
||||||
|
|
||||||
// ---- Backups Report tool ----
|
// ---- Backups Report tool ----
|
||||||
public static string SettingsReportBackups => T("Backups disponibles", "Available backups", "可用备份", "สำรองข้อมูลที่มี", "النسخ الاحتياطية المتاحة");
|
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 SettingsReportMaxBackups => T("Conserver N backups", "Keep N backups", "保留 N 个备份", "เก็บสำรอง N ชุด", "الاحتفاظ بـ N نسخة احتياطية");
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ public sealed class LocalConfig
|
|||||||
public string ReportUrl { get; set; } = "http://localhost/ProserveReport/";
|
public string ReportUrl { get; set; } = "http://localhost/ProserveReport/";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// URL de la documentation locale (onglet "Documentation"). Vide par défaut :
|
/// URL de la documentation locale (onglet "Documentation"). Pointe par défaut
|
||||||
/// l'utilisateur peut pointer vers une URL locale ou web. Si vide, l'onglet
|
/// vers l'install standard ASTERION dans XAMPP. Surchargeable dans Settings →
|
||||||
/// affiche un placeholder avec les liens vers les fichiers .pptx du dossier
|
/// Avancés. Si vide, l'onglet affiche un placeholder.
|
||||||
/// d'install du launcher.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DocumentationUrl { get; set; } = string.Empty;
|
public string DocumentationUrl { get; set; } = "http://localhost/ProserveDoc/";
|
||||||
|
|
||||||
public LicenseConfig License { get; set; } = new();
|
public LicenseConfig License { get; set; } = new();
|
||||||
|
|
||||||
@@ -53,6 +52,14 @@ public sealed class LocalConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ReportToolConfig ReportTool { get; set; } = new();
|
public ReportToolConfig ReportTool { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration du déploiement de l'outil Documentation (page web locale
|
||||||
|
/// embarquée dans l'onglet "Documentation"). Même mécanisme que ReportTool :
|
||||||
|
/// copie de <c>_doc/</c> bundled dans le ZIP PROSERVE vers
|
||||||
|
/// <c>{HtdocsRoot}\{FolderName}</c> avec backups horodatés et revert.
|
||||||
|
/// </summary>
|
||||||
|
public DocToolConfig DocTool { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connexion à la base MySQL locale (XAMPP) utilisée par PROSERVE pour ses
|
/// Connexion à la base MySQL locale (XAMPP) utilisée par PROSERVE pour ses
|
||||||
/// statistiques. Le launcher s'en sert pour appliquer les migrations bundled
|
/// statistiques. Le launcher s'en sert pour appliquer les migrations bundled
|
||||||
@@ -118,6 +125,19 @@ public sealed class ReportToolConfig
|
|||||||
public int MaxBackups { get; set; } = 3;
|
public int MaxBackups { get; set; } = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mêmes paramètres que <see cref="ReportToolConfig"/> mais pour la
|
||||||
|
/// documentation locale (sous-dossier <c>_doc/</c> du ZIP, déployé vers
|
||||||
|
/// <c>{HtdocsRoot}\ProserveDoc</c> par défaut).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DocToolConfig
|
||||||
|
{
|
||||||
|
public string HtdocsRoot { get; set; } = @"C:\xampp\htdocs";
|
||||||
|
public string FolderName { get; set; } = "ProserveDoc";
|
||||||
|
public bool AutoDeploy { get; set; } = true;
|
||||||
|
public int MaxBackups { get; set; } = 3;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class LicenseConfig
|
public sealed class LicenseConfig
|
||||||
{
|
{
|
||||||
public string? EncryptedKey { get; set; }
|
public string? EncryptedKey { get; set; }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblyName>PS_Launcher.Updater</AssemblyName>
|
<AssemblyName>PS_Launcher.Updater</AssemblyName>
|
||||||
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
||||||
<Version>0.16.0</Version>
|
<Version>0.17.0</Version>
|
||||||
<ApplicationIcon>..\PSLauncher.App\Resources\favicon.ico</ApplicationIcon>
|
<ApplicationIcon>..\PSLauncher.App\Resources\favicon.ico</ApplicationIcon>
|
||||||
<Company>ASTERION VR</Company>
|
<Company>ASTERION VR</Company>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
|
|||||||
Reference in New Issue
Block a user