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:
@@ -14,6 +14,7 @@ using PSLauncher.Core.Installations;
|
||||
using PSLauncher.Core.Licensing;
|
||||
using PSLauncher.Core.Localization;
|
||||
using PSLauncher.Core.Manifests;
|
||||
using PSLauncher.Core.DocTool;
|
||||
using PSLauncher.Core.Migrations;
|
||||
using PSLauncher.Core.Process;
|
||||
using PSLauncher.Core.ReportTool;
|
||||
@@ -43,6 +44,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private readonly IToastService _toastService;
|
||||
private readonly IDatabaseMigrationService _migrationService;
|
||||
private readonly IReportToolDeployer _reportDeployer;
|
||||
private readonly IDocToolDeployer _docDeployer;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<MainViewModel> _logger;
|
||||
|
||||
@@ -226,6 +228,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
IToastService toastService,
|
||||
IDatabaseMigrationService migrationService,
|
||||
IReportToolDeployer reportDeployer,
|
||||
IDocToolDeployer docDeployer,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<MainViewModel> logger)
|
||||
{
|
||||
@@ -242,6 +245,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_toastService = toastService;
|
||||
_migrationService = migrationService;
|
||||
_reportDeployer = reportDeployer;
|
||||
_docDeployer = docDeployer;
|
||||
_serviceProvider = serviceProvider;
|
||||
_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);
|
||||
ProgressDetail = null;
|
||||
ProgressPercent = 0;
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSLauncher.App.Views;
|
||||
using PSLauncher.Core.Configuration;
|
||||
using PSLauncher.Core.DocTool;
|
||||
using PSLauncher.Core.Downloads;
|
||||
using PSLauncher.Core.Installations;
|
||||
using PSLauncher.Core.Licensing;
|
||||
@@ -33,6 +34,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
private readonly IDownloadStateStore _downloadStore;
|
||||
private readonly IDatabaseMigrationService _migrationService;
|
||||
private readonly IReportToolDeployer _reportDeployer;
|
||||
private readonly IDocToolDeployer _docDeployer;
|
||||
private readonly IInstallationRegistry _registry;
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<SettingsViewModel> _logger;
|
||||
@@ -71,6 +73,17 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
= new();
|
||||
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; } =
|
||||
PSLauncher.Core.Localization.Strings.Available
|
||||
.Select(t => new LanguageOption(t.Code, t.Name))
|
||||
@@ -139,6 +152,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
IDownloadStateStore downloadStore,
|
||||
IDatabaseMigrationService migrationService,
|
||||
IReportToolDeployer reportDeployer,
|
||||
IDocToolDeployer docDeployer,
|
||||
IInstallationRegistry registry,
|
||||
HttpClient http,
|
||||
ILogger<SettingsViewModel> logger)
|
||||
@@ -149,6 +163,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_downloadStore = downloadStore;
|
||||
_migrationService = migrationService;
|
||||
_reportDeployer = reportDeployer;
|
||||
_docDeployer = docDeployer;
|
||||
_registry = registry;
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
@@ -170,8 +185,15 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_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)
|
||||
|
||||
_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();
|
||||
_ = LoadDocBackupsAsync();
|
||||
|
||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||
MachineId = licenseService.GetMachineId();
|
||||
@@ -297,6 +319,11 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_config.ReportTool.AutoDeploy = ReportAutoDeploy;
|
||||
_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);
|
||||
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
||||
|
||||
@@ -553,6 +580,96 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
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>
|
||||
@@ -576,3 +693,22 @@ public sealed partial class ReportBackupViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user