v0.12.0 — Report tool auto-deploy from PROSERVE ZIP
Each PROSERVE ZIP can ship a _report/ subfolder alongside _migrations/.
After install + DB migration, the launcher copies _report/ to the local
XAMPP htdocs (default C:\xampp\htdocs\ProserveReport\) so the Reports
tab in the sidebar always serves the matching version.
Service (PSLauncher.Core/ReportTool)
- IReportToolDeployer + ReportToolDeployer.
- Atomic deploy: copy to {target}.new/ → rename current to .old/ →
rename .new → final → cleanup .old. Apache never sees a half-state.
- Skip silencieux si _report/ absent du ZIP (PROSERVE release qui ne
touche pas au Report).
- DeployStatus enum (SkippedNoSourceFolder, XamppNotFound, Failed,
Deployed) renvoyé pour gestion UI claire.
Config (LocalConfig)
- ReportToolConfig (HtdocsRoot, FolderName, AutoDeploy). Defaults
matchent l'install standard ASTERION (C:\xampp\htdocs\ProserveReport).
Install pipeline (MainViewModel)
- Étape 6 après extraction + migrations. Progress reporté via le footer
Library : « 📂 Déploiement Report : 47/120 — assets/main.js ».
XAMPP introuvable → dialog avec lien vers Settings → Avancés.
Settings UI (SettingsDialog → Avancés)
- Nouveau bloc « OUTIL REPORT (XAMPP htdocs) » : 2 champs path + checkbox
AutoDeploy + bouton « 📂 Re-déployer maintenant » qui rejoue le deploy
sur la dernière version installée. Utile après changement de chemin
htdocs ou si l'auto-deploy avait foiré (XAMPP éteint).
Versions bumped to 0.12.0 (App + Updater + installer .iss).
Côté release : tu ajoutes _report/ dans le source de PROSERVE, tu zippes,
tu uploades. Les clients récupèrent ZIP + migrations + Report en une
seule install.
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 "PSLauncher"
|
#define MyAppShortName "PSLauncher"
|
||||||
#define MyAppVersion "0.11.0"
|
#define MyAppVersion "0.12.0"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PSLauncher.exe"
|
#define MyAppExeName "PSLauncher.exe"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using PSLauncher.Core.Localization;
|
|||||||
using PSLauncher.Core.Manifests;
|
using PSLauncher.Core.Manifests;
|
||||||
using PSLauncher.Core.Migrations;
|
using PSLauncher.Core.Migrations;
|
||||||
using PSLauncher.Core.Process;
|
using PSLauncher.Core.Process;
|
||||||
|
using PSLauncher.Core.ReportTool;
|
||||||
using PSLauncher.Core.Updates;
|
using PSLauncher.Core.Updates;
|
||||||
using PSLauncher.Models;
|
using PSLauncher.Models;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -174,6 +175,13 @@ public partial class App : Application
|
|||||||
sp.GetRequiredService<LocalConfig>().Database.EncryptedPassword),
|
sp.GetRequiredService<LocalConfig>().Database.EncryptedPassword),
|
||||||
sp.GetRequiredService<ILogger<DatabaseMigrationService>>()));
|
sp.GetRequiredService<ILogger<DatabaseMigrationService>>()));
|
||||||
|
|
||||||
|
// Déploiement de l'outil Report (page web XAMPP) à l'install d'une
|
||||||
|
// nouvelle version : copie atomique de _report/ → C:\xampp\htdocs\ProserveReport\
|
||||||
|
services.AddSingleton<IReportToolDeployer>(sp =>
|
||||||
|
new ReportToolDeployer(
|
||||||
|
configProvider: () => sp.GetRequiredService<LocalConfig>().ReportTool,
|
||||||
|
sp.GetRequiredService<ILogger<ReportToolDeployer>>()));
|
||||||
|
|
||||||
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.11.0</Version>
|
<Version>0.12.0</Version>
|
||||||
<AssemblyVersion>0.11.0.0</AssemblyVersion>
|
<AssemblyVersion>0.12.0.0</AssemblyVersion>
|
||||||
<FileVersion>0.11.0.0</FileVersion>
|
<FileVersion>0.12.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>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using PSLauncher.Core.Localization;
|
|||||||
using PSLauncher.Core.Manifests;
|
using PSLauncher.Core.Manifests;
|
||||||
using PSLauncher.Core.Migrations;
|
using PSLauncher.Core.Migrations;
|
||||||
using PSLauncher.Core.Process;
|
using PSLauncher.Core.Process;
|
||||||
|
using PSLauncher.Core.ReportTool;
|
||||||
using PSLauncher.Core.Updates;
|
using PSLauncher.Core.Updates;
|
||||||
using PSLauncher.Models;
|
using PSLauncher.Models;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
private readonly ILauncherSelfUpdater _selfUpdater;
|
private readonly ILauncherSelfUpdater _selfUpdater;
|
||||||
private readonly IToastService _toastService;
|
private readonly IToastService _toastService;
|
||||||
private readonly IDatabaseMigrationService _migrationService;
|
private readonly IDatabaseMigrationService _migrationService;
|
||||||
|
private readonly IReportToolDeployer _reportDeployer;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<MainViewModel> _logger;
|
private readonly ILogger<MainViewModel> _logger;
|
||||||
|
|
||||||
@@ -223,6 +225,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
ILauncherSelfUpdater selfUpdater,
|
ILauncherSelfUpdater selfUpdater,
|
||||||
IToastService toastService,
|
IToastService toastService,
|
||||||
IDatabaseMigrationService migrationService,
|
IDatabaseMigrationService migrationService,
|
||||||
|
IReportToolDeployer reportDeployer,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILogger<MainViewModel> logger)
|
ILogger<MainViewModel> logger)
|
||||||
{
|
{
|
||||||
@@ -238,6 +241,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
_selfUpdater = selfUpdater;
|
_selfUpdater = selfUpdater;
|
||||||
_toastService = toastService;
|
_toastService = toastService;
|
||||||
_migrationService = migrationService;
|
_migrationService = migrationService;
|
||||||
|
_reportDeployer = reportDeployer;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@@ -745,6 +749,50 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6) Déploiement de l'outil Report (page web XAMPP).
|
||||||
|
// Cherche _report/ dans le ZIP extrait et copie vers htdocs en
|
||||||
|
// atomic rename. Skip silencieux si _report/ absent du ZIP.
|
||||||
|
if (_config.ReportTool.AutoDeploy)
|
||||||
|
{
|
||||||
|
StatusMessage = Strings.StatusDeployingReport(row.Version);
|
||||||
|
ProgressDetail = null;
|
||||||
|
ProgressPercent = 0;
|
||||||
|
var rdProgress = 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.ProgressReportDeploy(dp.FilesDone, dp.FilesTotal, dp.CurrentFilename);
|
||||||
|
ProgressDetail = label;
|
||||||
|
row.ProgressDetail = label;
|
||||||
|
});
|
||||||
|
var rdResult = await _reportDeployer.DeployAsync(target, rdProgress, ct);
|
||||||
|
switch (rdResult.Status)
|
||||||
|
{
|
||||||
|
case DeployStatus.Deployed:
|
||||||
|
_logger.LogInformation("Report tool deployed ({N} files) for v{Version}", rdResult.FilesDeployed, row.Version);
|
||||||
|
break;
|
||||||
|
case DeployStatus.SkippedNoSourceFolder:
|
||||||
|
_logger.LogInformation("No _report/ in v{Version} ZIP, report tool unchanged", row.Version);
|
||||||
|
break;
|
||||||
|
case DeployStatus.XamppNotFound:
|
||||||
|
MessageBox.Show(
|
||||||
|
Strings.MsgReportXamppNotFound(_config.ReportTool.HtdocsRoot),
|
||||||
|
Strings.MsgBoxReportDeployFailed,
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
break;
|
||||||
|
case DeployStatus.Failed:
|
||||||
|
MessageBox.Show(
|
||||||
|
Strings.MsgReportDeployFailed(rdResult.ErrorMessage ?? "?"),
|
||||||
|
Strings.MsgBoxReportDeployFailed,
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StatusMessage = Strings.StatusInstalledSuccess(row.Version);
|
StatusMessage = Strings.StatusInstalledSuccess(row.Version);
|
||||||
ProgressDetail = null;
|
ProgressDetail = null;
|
||||||
ProgressPercent = 0;
|
ProgressPercent = 0;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using PSLauncher.Core.Installations;
|
|||||||
using PSLauncher.Core.Licensing;
|
using PSLauncher.Core.Licensing;
|
||||||
using PSLauncher.Core.Localization;
|
using PSLauncher.Core.Localization;
|
||||||
using PSLauncher.Core.Migrations;
|
using PSLauncher.Core.Migrations;
|
||||||
|
using PSLauncher.Core.ReportTool;
|
||||||
using PSLauncher.Models;
|
using PSLauncher.Models;
|
||||||
|
|
||||||
namespace PSLauncher.App.ViewModels;
|
namespace PSLauncher.App.ViewModels;
|
||||||
@@ -30,6 +31,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly ILicenseService _licenseService;
|
private readonly ILicenseService _licenseService;
|
||||||
private readonly IDownloadStateStore _downloadStore;
|
private readonly IDownloadStateStore _downloadStore;
|
||||||
private readonly IDatabaseMigrationService _migrationService;
|
private readonly IDatabaseMigrationService _migrationService;
|
||||||
|
private readonly IReportToolDeployer _reportDeployer;
|
||||||
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;
|
||||||
@@ -57,6 +59,13 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string? _migrationsReplayStatus;
|
[ObservableProperty] private string? _migrationsReplayStatus;
|
||||||
[ObservableProperty] private bool _isReplayingMigrations;
|
[ObservableProperty] private bool _isReplayingMigrations;
|
||||||
|
|
||||||
|
// ----- Report Tool deploy settings -----
|
||||||
|
[ObservableProperty] private string _reportHtdocsRoot;
|
||||||
|
[ObservableProperty] private string _reportFolderName;
|
||||||
|
[ObservableProperty] private bool _reportAutoDeploy;
|
||||||
|
[ObservableProperty] private string? _reportDeployStatus;
|
||||||
|
[ObservableProperty] private bool _isDeployingReport;
|
||||||
|
|
||||||
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))
|
||||||
@@ -124,6 +133,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
ILicenseService licenseService,
|
ILicenseService licenseService,
|
||||||
IDownloadStateStore downloadStore,
|
IDownloadStateStore downloadStore,
|
||||||
IDatabaseMigrationService migrationService,
|
IDatabaseMigrationService migrationService,
|
||||||
|
IReportToolDeployer reportDeployer,
|
||||||
IInstallationRegistry registry,
|
IInstallationRegistry registry,
|
||||||
HttpClient http,
|
HttpClient http,
|
||||||
ILogger<SettingsViewModel> logger)
|
ILogger<SettingsViewModel> logger)
|
||||||
@@ -133,6 +143,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
_downloadStore = downloadStore;
|
_downloadStore = downloadStore;
|
||||||
_migrationService = migrationService;
|
_migrationService = migrationService;
|
||||||
|
_reportDeployer = reportDeployer;
|
||||||
_registry = registry;
|
_registry = registry;
|
||||||
_http = http;
|
_http = http;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -150,6 +161,10 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_dbName = config.Database.Database;
|
_dbName = config.Database.Database;
|
||||||
_dbAutoApplyMigrations = config.Database.AutoApplyMigrations;
|
_dbAutoApplyMigrations = config.Database.AutoApplyMigrations;
|
||||||
|
|
||||||
|
_reportHtdocsRoot = config.ReportTool.HtdocsRoot;
|
||||||
|
_reportFolderName = config.ReportTool.FolderName;
|
||||||
|
_reportAutoDeploy = config.ReportTool.AutoDeploy;
|
||||||
|
|
||||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||||
MachineId = licenseService.GetMachineId();
|
MachineId = licenseService.GetMachineId();
|
||||||
LogsDirectory = App.LogsDirectory;
|
LogsDirectory = App.LogsDirectory;
|
||||||
@@ -269,6 +284,10 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_config.Database.Database = DbName.Trim();
|
_config.Database.Database = DbName.Trim();
|
||||||
_config.Database.AutoApplyMigrations = DbAutoApplyMigrations;
|
_config.Database.AutoApplyMigrations = DbAutoApplyMigrations;
|
||||||
|
|
||||||
|
_config.ReportTool.HtdocsRoot = ReportHtdocsRoot.Trim();
|
||||||
|
_config.ReportTool.FolderName = ReportFolderName.Trim();
|
||||||
|
_config.ReportTool.AutoDeploy = ReportAutoDeploy;
|
||||||
|
|
||||||
_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);
|
||||||
|
|
||||||
@@ -334,6 +353,53 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
|
|
||||||
private static string FormatSize(long bytes) => Strings.FormatSize(bytes);
|
private static string FormatSize(long bytes) => Strings.FormatSize(bytes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-déploie l'outil Report depuis la dernière version PROSERVE installée
|
||||||
|
/// vers htdocs. Utile si le déploiement post-install avait échoué (XAMPP
|
||||||
|
/// déplacé, htdocs absent), ou pour resync après changement du chemin htdocs
|
||||||
|
/// dans Settings.
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RedeployReportAsync()
|
||||||
|
{
|
||||||
|
var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
|
||||||
|
if (installed is null)
|
||||||
|
{
|
||||||
|
ReportDeployStatus = "Aucune version installée — installe d'abord une version qui contient _report/.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IsDeployingReport = true;
|
||||||
|
ReportDeployStatus = $"Déploiement depuis v{installed.Version}…";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Persiste les éventuels changements de chemin avant le deploy
|
||||||
|
Save();
|
||||||
|
var progress = new Progress<DeployProgress>(dp =>
|
||||||
|
{
|
||||||
|
if (dp.FilesTotal > 0)
|
||||||
|
ReportDeployStatus = $"{dp.FilesDone}/{dp.FilesTotal} : {dp.CurrentFilename}";
|
||||||
|
});
|
||||||
|
var result = await _reportDeployer.DeployAsync(installed.FolderPath, progress, CancellationToken.None);
|
||||||
|
ReportDeployStatus = 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 _report/ — rien à déployer",
|
||||||
|
DeployStatus.XamppNotFound => $"✗ {result.ErrorMessage}",
|
||||||
|
DeployStatus.Failed => $"✗ {result.ErrorMessage}",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Redeploy report failed");
|
||||||
|
ReportDeployStatus = $"✗ {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsDeployingReport = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== DB MIGRATIONS =====================
|
// ===================== DB MIGRATIONS =====================
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -413,6 +413,56 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Outil Report (XAMPP htdocs deploy) -->
|
||||||
|
<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.SettingsReportTool}"
|
||||||
|
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 ReportHtdocsRoot, 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 ReportFolderName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{Binding ReportAutoDeploy}"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsReportAutoDeploy}" />
|
||||||
|
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding ReportDeployStatus}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsReportRedeploy}"
|
||||||
|
Command="{Binding RedeployReportCommand}"
|
||||||
|
IsEnabled="{Binding IsDeployingReport, Converter={StaticResource InverseBoolToBool}}" />
|
||||||
|
</Grid>
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -468,6 +468,49 @@ public static class Strings
|
|||||||
$"🗄 الترقية {done}/{total}: {filename}"
|
$"🗄 الترقية {done}/{total}: {filename}"
|
||||||
);
|
);
|
||||||
public static string MsgBoxMigrationFailed => T("Migration DB échouée", "DB migration failed", "数据库迁移失败", "การโยกย้าย DB ล้มเหลว", "فشل ترقية قاعدة البيانات");
|
public static string MsgBoxMigrationFailed => T("Migration DB échouée", "DB migration failed", "数据库迁移失败", "การโยกย้าย DB ล้มเหลว", "فشل ترقية قاعدة البيانات");
|
||||||
|
|
||||||
|
// ==================== REPORT TOOL DEPLOY ====================
|
||||||
|
public static string StatusDeployingReport(string version) => T(
|
||||||
|
$"Déploiement de l'outil Report v{version}…",
|
||||||
|
$"Deploying Report tool for v{version}…",
|
||||||
|
$"正在部署 v{version} 的报告工具…",
|
||||||
|
$"กำลังปรับใช้เครื่องมือรายงาน v{version}…",
|
||||||
|
$"جارٍ نشر أداة التقارير v{version}…"
|
||||||
|
);
|
||||||
|
public static string ProgressReportDeploy(int done, int total, string filename) => T(
|
||||||
|
$"📂 Déploiement Report : {done}/{total} — {filename}",
|
||||||
|
$"📂 Deploying Report: {done}/{total} — {filename}",
|
||||||
|
$"📂 部署报告:{done}/{total} — {filename}",
|
||||||
|
$"📂 ปรับใช้รายงาน: {done}/{total} — {filename}",
|
||||||
|
$"📂 نشر التقرير: {done}/{total} — {filename}"
|
||||||
|
);
|
||||||
|
public static string MsgBoxReportDeployFailed => T("Déploiement Report échoué", "Report deploy failed", "报告部署失败", "การปรับใช้รายงานล้มเหลว", "فشل نشر التقرير");
|
||||||
|
public static string MsgReportXamppNotFound(string path) => T(
|
||||||
|
$"L'outil Report n'a pas pu être déployé : le dossier XAMPP {path} est introuvable.\n\nVérifie que XAMPP est installé à cet emplacement (par défaut C:\\xampp) ou ajuste le chemin dans Paramètres → Avancés → Outil Report.",
|
||||||
|
$"The Report tool could not be deployed: XAMPP folder {path} not found.\n\nMake sure XAMPP is installed there (default C:\\xampp) or adjust the path in Settings → Advanced → Report Tool.",
|
||||||
|
$"无法部署报告工具:找不到 XAMPP 文件夹 {path}。\n\n请确保 XAMPP 安装在该位置(默认 C:\\xampp),或在 设置 → 高级 → 报告工具 中调整路径。",
|
||||||
|
$"ไม่สามารถปรับใช้เครื่องมือรายงานได้: ไม่พบโฟลเดอร์ XAMPP {path}\n\nตรวจสอบว่า XAMPP ติดตั้งที่ตำแหน่งนั้น (ค่าเริ่มต้น C:\\xampp) หรือปรับเส้นทางใน ตั้งค่า → ขั้นสูง → เครื่องมือรายงาน",
|
||||||
|
$"تعذر نشر أداة التقارير: مجلد XAMPP {path} غير موجود.\n\nتأكد من تثبيت XAMPP في هذا الموقع (الافتراضي C:\\xampp) أو اضبط المسار في الإعدادات → متقدم → أداة التقارير."
|
||||||
|
);
|
||||||
|
public static string MsgReportDeployFailed(string detail) => T(
|
||||||
|
$"Le déploiement de l'outil Report a échoué :\n\n{detail}\n\nLa version PROSERVE est installée mais l'onglet Statistiques pourrait être désynchronisé. Tu peux retenter via Paramètres → Avancés → Re-déployer le Report.",
|
||||||
|
$"Report tool deploy failed:\n\n{detail}\n\nThe PROSERVE version is installed but the Reports tab might be out of sync. You can retry via Settings → Advanced → Re-deploy Report.",
|
||||||
|
$"报告工具部署失败:\n\n{detail}\n\nPROSERVE 版本已安装,但报告选项卡可能不同步。您可以通过 设置 → 高级 → 重新部署报告 重试。",
|
||||||
|
$"การปรับใช้เครื่องมือรายงานล้มเหลว:\n\n{detail}\n\nเวอร์ชัน PROSERVE ติดตั้งแล้วแต่แท็บรายงานอาจไม่ซิงค์ คุณสามารถลองใหม่ผ่าน ตั้งค่า → ขั้นสูง → ปรับใช้รายงานใหม่",
|
||||||
|
$"فشل نشر أداة التقارير:\n\n{detail}\n\nنسخة PROSERVE مثبتة لكن علامة التبويب التقارير قد لا تكون متزامنة. يمكنك إعادة المحاولة عبر الإعدادات → متقدم → إعادة نشر التقرير."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static string SettingsReportTool => T("OUTIL REPORT (XAMPP htdocs)", "REPORT TOOL (XAMPP htdocs)", "报告工具 (XAMPP htdocs)", "เครื่องมือรายงาน (XAMPP htdocs)", "أداة التقارير (XAMPP htdocs)");
|
||||||
|
public static string SettingsReportHtdocs => T("Racine XAMPP htdocs", "XAMPP htdocs root", "XAMPP htdocs 根目录", "รากของ XAMPP htdocs", "جذر XAMPP htdocs");
|
||||||
|
public static string SettingsReportFolder => T("Nom du dossier de déploiement", "Deploy folder name", "部署文件夹名称", "ชื่อโฟลเดอร์ปรับใช้", "اسم مجلد النشر");
|
||||||
|
public static string SettingsReportAutoDeploy => T(
|
||||||
|
"Déployer automatiquement l'outil Report à l'install d'une nouvelle version",
|
||||||
|
"Automatically deploy the Report tool when installing a new version",
|
||||||
|
"安装新版本时自动部署报告工具",
|
||||||
|
"ปรับใช้เครื่องมือรายงานโดยอัตโนมัติเมื่อติดตั้งเวอร์ชันใหม่",
|
||||||
|
"نشر أداة التقارير تلقائياً عند تثبيت نسخة جديدة"
|
||||||
|
);
|
||||||
|
public static string SettingsReportRedeploy => T("📂 Re-déployer le Report maintenant", "📂 Re-deploy Report now", "📂 立即重新部署报告", "📂 ปรับใช้รายงานใหม่เดี๋ยวนี้", "📂 إعادة نشر التقرير الآن");
|
||||||
public static string MsgMigrationFailed(string detail) => T(
|
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.",
|
$"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.",
|
$"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.",
|
||||||
|
|||||||
47
src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs
Normal file
47
src/PSLauncher.Core/ReportTool/IReportToolDeployer.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace PSLauncher.Core.ReportTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Déploie l'outil Report (page web hébergée dans XAMPP) depuis le sous-dossier
|
||||||
|
/// <c>_report/</c> bundled dans chaque ZIP PROSERVE vers le htdocs local.
|
||||||
|
/// </summary>
|
||||||
|
public interface IReportToolDeployer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Si <c>{installFolder}/_report/</c> existe, le copie vers
|
||||||
|
/// <c>{HtdocsRoot}/{FolderName}/</c> via un rename atomique :
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>copie récursive vers <c>{target}.new/</c></item>
|
||||||
|
/// <item>rename de l'existant vers <c>{target}.old/</c> (s'il existait)</item>
|
||||||
|
/// <item>rename <c>.new</c> → final</item>
|
||||||
|
/// <item>delete de <c>.old</c> en best-effort</item>
|
||||||
|
/// </list>
|
||||||
|
/// Si _report/ absent du ZIP : skip silencieux. Si htdocs introuvable :
|
||||||
|
/// <see cref="DeployStatus.XamppNotFound"/>. Si erreur disque :
|
||||||
|
/// <see cref="DeployStatus.Failed"/> + message d'erreur.
|
||||||
|
/// </summary>
|
||||||
|
Task<DeployResult> DeployAsync(
|
||||||
|
string installFolder,
|
||||||
|
IProgress<DeployProgress>? progress,
|
||||||
|
CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DeployStatus
|
||||||
|
{
|
||||||
|
/// <summary>Le ZIP n'avait pas de sous-dossier <c>_report/</c>, rien à faire.</summary>
|
||||||
|
SkippedNoSourceFolder,
|
||||||
|
/// <summary>HtdocsRoot configuré n'existe pas (XAMPP pas installé là).</summary>
|
||||||
|
XamppNotFound,
|
||||||
|
/// <summary>Erreur I/O pendant la copie ou le rename.</summary>
|
||||||
|
Failed,
|
||||||
|
/// <summary>Déploiement réussi.</summary>
|
||||||
|
Deployed,
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DeployResult(
|
||||||
|
DeployStatus Status,
|
||||||
|
int FilesDeployed,
|
||||||
|
long BytesDeployed,
|
||||||
|
string TargetPath,
|
||||||
|
string? ErrorMessage = null);
|
||||||
|
|
||||||
|
public sealed record DeployProgress(int FilesDone, int FilesTotal, string CurrentFilename);
|
||||||
107
src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs
Normal file
107
src/PSLauncher.Core/ReportTool/ReportToolDeployer.cs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PSLauncher.Models;
|
||||||
|
|
||||||
|
namespace PSLauncher.Core.ReportTool;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReportToolDeployer : IReportToolDeployer
|
||||||
|
{
|
||||||
|
private const string SourceSubdir = "_report";
|
||||||
|
|
||||||
|
private readonly Func<ReportToolConfig> _configProvider;
|
||||||
|
private readonly ILogger<ReportToolDeployer> _logger;
|
||||||
|
|
||||||
|
public ReportToolDeployer(
|
||||||
|
Func<ReportToolConfig> configProvider,
|
||||||
|
ILogger<ReportToolDeployer> 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 report 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 oldBackup = target + ".old";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Nettoyage d'un éventuel résidu de tentative précédente
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Atomic swap
|
||||||
|
if (Directory.Exists(target)) Directory.Move(target, oldBackup);
|
||||||
|
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"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Report tool deployed to {Target} ({Files} files, {Bytes} bytes)",
|
||||||
|
target, total, bytesTotal);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,14 @@ public sealed class LocalConfig
|
|||||||
|
|
||||||
public LicenseConfig License { get; set; } = new();
|
public LicenseConfig License { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration du déploiement de l'outil Report (page web locale embarquée
|
||||||
|
/// dans l'onglet "Statistiques") vers la racine XAMPP htdocs.
|
||||||
|
/// Le launcher copie le dossier <c>_report/</c> bundled dans chaque ZIP
|
||||||
|
/// PROSERVE vers <c>{HtdocsRoot}\{FolderName}</c> à chaque install.
|
||||||
|
/// </summary>
|
||||||
|
public ReportToolConfig ReportTool { 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
|
||||||
@@ -87,6 +95,20 @@ public sealed class DatabaseConfig
|
|||||||
public bool AutoApplyMigrations { get; set; } = true;
|
public bool AutoApplyMigrations { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paramètres de déploiement de l'outil Report local.
|
||||||
|
/// Le launcher cherche un sous-dossier <c>_report/</c> dans chaque ZIP PROSERVE
|
||||||
|
/// installé et le copie vers <c>{HtdocsRoot}\{FolderName}</c> via un rename
|
||||||
|
/// atomique (deploy en .new puis swap). L'URL de l'onglet Statistiques pointe
|
||||||
|
/// vers <c>http://localhost/{FolderName}/</c> ; les deux doivent matcher.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReportToolConfig
|
||||||
|
{
|
||||||
|
public string HtdocsRoot { get; set; } = @"C:\xampp\htdocs";
|
||||||
|
public string FolderName { get; set; } = "ProserveReport";
|
||||||
|
public bool AutoDeploy { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
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>PSLauncher.Updater</AssemblyName>
|
<AssemblyName>PSLauncher.Updater</AssemblyName>
|
||||||
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
||||||
<Version>0.11.0</Version>
|
<Version>0.12.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