v0.22.0 — Health checks editor : icon picker + per-check refresh
Settings → Avancés → Bandeau de santé : liste éditable des checks (Add / Edit / Delete) avec dialog modal pour chaque entry. Picker d'emoji parmi 28 icônes curatées (VR / streaming / réseau / système). Édition du nom, type (Process / Ping), cible, intervalle de refresh en ms (per-check), et seuils Ping (warn / error / timeout) sous une section Avancé. La config est persistée dans %LocalAppData%\PSLauncher\config.json donc elle survit aux auto-updates du launcher. Modifiable aussi à la main dans le json pour les power-users. Le bandeau hot-reload après Save : cancel des boucles de polling existantes, reconstruction des HealthIndicators depuis la config fraîche, puis relance d'une boucle dédiée par check (chacune cadencée sur son propre RefreshIntervalMs — Process check rapide peut tourner à 1 s, Ping reste à 5-10 s). 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 MyAppShortName "PS_Launcher"
|
||||
#define MyAppVersion "0.21.0"
|
||||
#define MyAppVersion "0.22.0"
|
||||
#define MyAppPublisher "ASTERION VR"
|
||||
#define MyAppURL "https://asterionvr.com"
|
||||
#define MyAppExeName "PS_Launcher.exe"
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.21.0</Version>
|
||||
<AssemblyVersion>0.21.0.0</AssemblyVersion>
|
||||
<FileVersion>0.21.0.0</FileVersion>
|
||||
<Version>0.22.0</Version>
|
||||
<AssemblyVersion>0.22.0.0</AssemblyVersion>
|
||||
<FileVersion>0.22.0.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -74,6 +74,14 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
public ObservableCollection<HealthIndicatorViewModel> HealthIndicators { get; } = new();
|
||||
public bool HasHealthIndicators => HealthIndicators.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Cancelle les boucles de polling per-check actuellement en vol. Recréée à
|
||||
/// chaque appel à <see cref="StartHealthLoop"/> pour permettre un redémarrage
|
||||
/// propre après édition des checks dans Settings (hot-reload sans relancer
|
||||
/// l'app).
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _healthLoopCts;
|
||||
|
||||
/// <summary>
|
||||
/// Le bandeau santé n'a de sens que sur la page Library — Reports et
|
||||
/// Documentation sont des WebView pleine-page où afficher des pills
|
||||
@@ -319,41 +327,53 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lance une tâche en arrière-plan qui exécute tous les checks en parallèle,
|
||||
/// applique les résultats sur les VM via le dispatcher UI, puis attend
|
||||
/// RefreshIntervalSeconds avant de recommencer. La tâche tourne tant que
|
||||
/// l'app vit ; pas de cancel explicite (l'arrêt suit la fermeture du process).
|
||||
/// Lance une boucle indépendante par check, chacune cadencée sur son propre
|
||||
/// <see cref="HealthCheckEntry.RefreshIntervalMs"/>. Un Process check rapide
|
||||
/// (~1 ms) peut tourner toutes les 1-2 s sans impact CPU, alors qu'un Ping
|
||||
/// (qui coûte un round-trip réseau) tourne typiquement à 5-10 s. Premier
|
||||
/// passage immédiat puis re-check selon l'intervalle.
|
||||
///
|
||||
/// Cancelle la boucle précédente si elle existe (cas du hot-reload depuis
|
||||
/// Settings → édition des checks).
|
||||
/// </summary>
|
||||
private void StartHealthLoop()
|
||||
{
|
||||
// Stop l'ancienne boucle si on est en hot-reload (édition Settings)
|
||||
try { _healthLoopCts?.Cancel(); } catch { /* déjà disposée */ }
|
||||
_healthLoopCts?.Dispose();
|
||||
_healthLoopCts = null;
|
||||
|
||||
if (HealthIndicators.Count == 0) return;
|
||||
var interval = TimeSpan.FromSeconds(Math.Max(2, _config.HealthChecks.RefreshIntervalSeconds));
|
||||
_ = Task.Run(async () =>
|
||||
|
||||
_healthLoopCts = new CancellationTokenSource();
|
||||
var ct = _healthLoopCts.Token;
|
||||
|
||||
foreach (var vm in HealthIndicators.ToList())
|
||||
{
|
||||
while (true)
|
||||
var localVm = vm;
|
||||
// Floor de 500 ms pour éviter qu'un user mette 0/1 ms par erreur dans le json
|
||||
// et sature un coeur CPU à pleins gaz.
|
||||
var intervalMs = Math.Max(500, localVm.Entry.RefreshIntervalMs);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var snapshot = HealthIndicators.ToList();
|
||||
var tasks = snapshot.Select(async vm =>
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _healthService.RunCheckAsync(vm.Entry, CancellationToken.None);
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => vm.Apply(result));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Health check {Name} threw", vm.Entry.Name);
|
||||
}
|
||||
});
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
var result = await _healthService.RunCheckAsync(localVm.Entry, ct).ConfigureAwait(false);
|
||||
if (ct.IsCancellationRequested) break;
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => localVm.Apply(result));
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Health check {Name} threw", localVm.Entry.Name);
|
||||
}
|
||||
try { await Task.Delay(intervalMs, ct).ConfigureAwait(false); }
|
||||
catch { break; }
|
||||
}
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Health loop tick"); }
|
||||
try { await Task.Delay(interval).ConfigureAwait(false); }
|
||||
catch { break; }
|
||||
}
|
||||
});
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -546,6 +566,15 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_license = _licenseService.GetCached();
|
||||
NotifyLicenseChanged();
|
||||
RebuildList();
|
||||
|
||||
// Hot-reload du bandeau de santé : la liste des checks (et leurs intervals)
|
||||
// a pu être éditée. On reconstruit les indicateurs et on relance la boucle
|
||||
// per-check sans avoir à fermer/rouvrir le launcher.
|
||||
InitHealthIndicators();
|
||||
StartHealthLoop();
|
||||
OnPropertyChanged(nameof(ReportUri));
|
||||
OnPropertyChanged(nameof(DocumentationUri));
|
||||
OnPropertyChanged(nameof(HasDocumentationUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,17 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
= new();
|
||||
public bool HasDocBackups => DocBackups.Count > 0;
|
||||
|
||||
// ----- Health checks (bandeau de santé système) -----
|
||||
/// <summary>
|
||||
/// Liste éditable des checks affichés dans le bandeau de santé. Chaque ligne
|
||||
/// pointe vers une copie modifiable de l'entrée (l'original dans
|
||||
/// <c>_config.HealthChecks.Checks</c> n'est écrasé qu'à <see cref="Save"/>),
|
||||
/// pour que Cancel ne laisse pas de modifs partielles dans la config.
|
||||
/// </summary>
|
||||
public System.Collections.ObjectModel.ObservableCollection<HealthCheckRowViewModel> HealthChecks { get; }
|
||||
= new();
|
||||
public bool HasHealthChecks => HealthChecks.Count > 0;
|
||||
|
||||
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
||||
PSLauncher.Core.Localization.Strings.Available
|
||||
.Select(t => new LanguageOption(t.Code, t.Name))
|
||||
@@ -193,6 +204,11 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_docAutoDeploy = config.DocTool.AutoDeploy;
|
||||
_docMaxBackups = config.DocTool.MaxBackups;
|
||||
|
||||
// Snapshot éditable des health checks. On clone chaque entry pour ne pas
|
||||
// muter _config.HealthChecks.Checks tant que l'utilisateur n'a pas cliqué Save.
|
||||
foreach (var entry in config.HealthChecks.Checks)
|
||||
HealthChecks.Add(new HealthCheckRowViewModel(CloneEntry(entry), DeleteHealthCheck));
|
||||
|
||||
// Charge les listes de backups en arrière-plan (UI immédiate puis remplies quand prêtes)
|
||||
_ = LoadReportBackupsAsync();
|
||||
_ = LoadDocBackupsAsync();
|
||||
@@ -327,6 +343,11 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
_config.DocTool.AutoDeploy = DocAutoDeploy;
|
||||
_config.DocTool.MaxBackups = Math.Max(0, DocMaxBackups);
|
||||
|
||||
// Health checks : on remplace intégralement la liste persistée par
|
||||
// les copies éditées. Les originaux restent en mémoire de toute façon
|
||||
// (les VM les ont clonés), donc Cancel a déjà préservé l'état initial.
|
||||
_config.HealthChecks.Checks = HealthChecks.Select(r => r.Entry).ToList();
|
||||
|
||||
_configStore.Save(_config);
|
||||
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
||||
|
||||
@@ -633,6 +654,53 @@ public sealed partial class SettingsViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== HEALTH CHECKS =====================
|
||||
|
||||
[RelayCommand]
|
||||
private void AddHealthCheck()
|
||||
{
|
||||
var entry = new HealthCheckEntry
|
||||
{
|
||||
Name = "",
|
||||
Icon = "🔌",
|
||||
Kind = "Process",
|
||||
Target = "",
|
||||
RefreshIntervalMs = 5000,
|
||||
};
|
||||
var dlg = new HealthCheckEditorDialog(entry, isNew: true)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
if (dlg.ShowDialog() == true && dlg.Saved)
|
||||
{
|
||||
HealthChecks.Add(new HealthCheckRowViewModel(entry, DeleteHealthCheck));
|
||||
OnPropertyChanged(nameof(HasHealthChecks));
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteHealthCheck(HealthCheckRowViewModel row)
|
||||
{
|
||||
var confirm = ThemedMessageBox.Show(
|
||||
Strings.SettingsHealthDeleteConfirm(string.IsNullOrEmpty(row.Name) ? row.Kind : row.Name),
|
||||
Strings.MsgBoxConfirm,
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
HealthChecks.Remove(row);
|
||||
OnPropertyChanged(nameof(HasHealthChecks));
|
||||
}
|
||||
|
||||
private static HealthCheckEntry CloneEntry(HealthCheckEntry src) => new()
|
||||
{
|
||||
Name = src.Name,
|
||||
Icon = src.Icon,
|
||||
Kind = src.Kind,
|
||||
Target = src.Target,
|
||||
RefreshIntervalMs = src.RefreshIntervalMs,
|
||||
PingWarnMs = src.PingWarnMs,
|
||||
PingErrorMs = src.PingErrorMs,
|
||||
PingTimeoutMs = src.PingTimeoutMs,
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RedeployDocAsync()
|
||||
{
|
||||
@@ -697,6 +765,66 @@ public sealed partial class ReportBackupViewModel : ObservableObject
|
||||
private Task Revert() => _revertHandler(Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ligne du tableau "Bandeau de santé système" dans Settings → Avancés. Pointe
|
||||
/// vers une copie éditable de l'entrée (clonée à l'ouverture des Settings) ;
|
||||
/// la persistence vers <c>config.HealthChecks.Checks</c> n'a lieu qu'au Save.
|
||||
/// </summary>
|
||||
public sealed partial class HealthCheckRowViewModel : ObservableObject
|
||||
{
|
||||
public HealthCheckEntry Entry { get; private set; }
|
||||
|
||||
/// <summary>Re-publié à chaque édition pour rafraîchir le label dans la liste.</summary>
|
||||
[ObservableProperty]
|
||||
private string _displayName;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayIcon;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayDetail;
|
||||
|
||||
public string Name => Entry.Name;
|
||||
public string Kind => Entry.Kind;
|
||||
|
||||
private readonly Action<HealthCheckRowViewModel> _onDelete;
|
||||
|
||||
public HealthCheckRowViewModel(HealthCheckEntry entry, Action<HealthCheckRowViewModel> onDelete)
|
||||
{
|
||||
Entry = entry;
|
||||
_onDelete = onDelete;
|
||||
_displayName = string.IsNullOrEmpty(entry.Name) ? "(sans nom)" : entry.Name;
|
||||
_displayIcon = string.IsNullOrEmpty(entry.Icon) ? "🔌" : entry.Icon;
|
||||
_displayDetail = ComputeDetail(entry);
|
||||
}
|
||||
|
||||
private static string ComputeDetail(HealthCheckEntry e)
|
||||
{
|
||||
var kind = e.Kind?.Equals("ping", StringComparison.OrdinalIgnoreCase) == true ? "Ping" : "Process";
|
||||
var target = string.IsNullOrEmpty(e.Target) ? "—" : e.Target;
|
||||
return $"{kind} · {target} · {e.RefreshIntervalMs} ms";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Edit()
|
||||
{
|
||||
var dlg = new PSLauncher.App.Views.HealthCheckEditorDialog(Entry)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
if (dlg.ShowDialog() == true && dlg.Saved)
|
||||
{
|
||||
// Entry a été muté in-place par le dialog. Refresh des bindings d'affichage.
|
||||
DisplayName = string.IsNullOrEmpty(Entry.Name) ? "(sans nom)" : Entry.Name;
|
||||
DisplayIcon = string.IsNullOrEmpty(Entry.Icon) ? "🔌" : Entry.Icon;
|
||||
DisplayDetail = ComputeDetail(Entry);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Delete() => _onDelete(this);
|
||||
}
|
||||
|
||||
/// <summary>Variante doc — même structure que <see cref="ReportBackupViewModel"/>.</summary>
|
||||
public sealed partial class DocBackupViewModel : ObservableObject
|
||||
{
|
||||
|
||||
246
src/PSLauncher.App/Views/HealthCheckEditorDialog.xaml
Normal file
246
src/PSLauncher.App/Views/HealthCheckEditorDialog.xaml
Normal file
@@ -0,0 +1,246 @@
|
||||
<Window x:Class="PSLauncher.App.Views.HealthCheckEditorDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
|
||||
Title="{x:Static loc:Strings.HealthEditorTitle}"
|
||||
Icon="pack://application:,,,/Resources/favicon.ico"
|
||||
Width="540" Height="640"
|
||||
MinWidth="480" MinHeight="540"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{StaticResource Brush.Bg.Window}"
|
||||
ResizeMode="CanResize">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Border Grid.Row="0" Padding="24,20,24,12">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="{x:Static loc:Strings.HealthEditorTitle}"
|
||||
FontSize="20" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Brush.Text.Primary}" />
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="24,0,24,12">
|
||||
<StackPanel>
|
||||
|
||||
<!-- Nom -->
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorName}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,4" />
|
||||
<TextBox x:Name="NameBox"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8" />
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorNameHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,4,0,12" />
|
||||
|
||||
<!-- Type de check -->
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorKind}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,4" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<RadioButton x:Name="KindProcess"
|
||||
GroupName="Kind"
|
||||
Content="{x:Static loc:Strings.HealthEditorKindProcess}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
Margin="0,0,20,0"
|
||||
Checked="OnKindChanged" />
|
||||
<RadioButton x:Name="KindPing"
|
||||
GroupName="Kind"
|
||||
Content="{x:Static loc:Strings.HealthEditorKindPing}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
Checked="OnKindChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Cible (process name ou IP/host) -->
|
||||
<TextBlock x:Name="TargetLabel"
|
||||
Text="{x:Static loc:Strings.HealthEditorTargetProcess}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,4" />
|
||||
<TextBox x:Name="TargetBox"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
FontFamily="Consolas" />
|
||||
<TextBlock x:Name="TargetHint"
|
||||
Text="{x:Static loc:Strings.HealthEditorTargetProcessHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,4,0,12" TextWrapping="Wrap" />
|
||||
|
||||
<!-- Refresh interval -->
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorRefresh}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,4" />
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="RefreshMsBox"
|
||||
Grid.Column="0"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8" />
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center"
|
||||
Margin="8,0,0,0"
|
||||
Text="ms"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}" />
|
||||
</Grid>
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorRefreshHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,12" TextWrapping="Wrap" />
|
||||
|
||||
<!-- Picto picker (grille d'emojis) -->
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorIcon}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,0,0,4" />
|
||||
<Border Background="#1A1A20"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="6">
|
||||
<ItemsControl x:Name="IconPicker">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Click="OnIconClicked"
|
||||
Tag="{Binding Emoji}"
|
||||
Content="{Binding Emoji}"
|
||||
FontSize="22"
|
||||
Width="40" Height="40"
|
||||
Margin="2"
|
||||
BorderThickness="2"
|
||||
Cursor="Hand">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#0F1117" />
|
||||
<Setter Property="BorderBrush" Value="#2A2F3A" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#1F2530" />
|
||||
<Setter Property="BorderBrush" Value="#3B82F6" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter Property="Background" Value="#1B2A40" />
|
||||
<Setter Property="BorderBrush" Value="#3B82F6" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorIconHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,4,0,12" TextWrapping="Wrap" />
|
||||
|
||||
<!-- Section Avancé : seuils Ping. Visible uniquement si Kind=Ping. -->
|
||||
<Expander x:Name="PingAdvanced"
|
||||
Header="{x:Static loc:Strings.HealthEditorPingAdvanced}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Margin="0,4,0,0" IsExpanded="False"
|
||||
Visibility="Collapsed">
|
||||
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16" Margin="0,8,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorPingWarn}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="12" Margin="0,0,0,4" />
|
||||
<TextBox x:Name="PingWarnBox"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
Width="120" HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorPingError}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="12" Margin="0,12,0,4" />
|
||||
<TextBox x:Name="PingErrorBox"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
Width="120" HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock Text="{x:Static loc:Strings.HealthEditorPingTimeout}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="12" Margin="0,12,0,4" />
|
||||
<TextBox x:Name="PingTimeoutBox"
|
||||
Background="#1A1A20"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
BorderBrush="{StaticResource Brush.Border}"
|
||||
BorderThickness="1" Padding="8"
|
||||
Width="120" HorizontalAlignment="Left" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Expander>
|
||||
|
||||
<TextBlock x:Name="ErrorText"
|
||||
Foreground="#F87171"
|
||||
FontSize="12"
|
||||
Margin="0,12,0,0"
|
||||
Visibility="Collapsed"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border Grid.Row="2" Background="{StaticResource Brush.Bg.Footer}"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,1,0,0"
|
||||
Padding="20,12">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="{x:Static loc:Strings.ActionCancel}"
|
||||
IsCancel="True"
|
||||
Margin="0,0,12,0"
|
||||
Click="OnCancel" />
|
||||
<Button Style="{StaticResource AccentButton}"
|
||||
Content="{x:Static loc:Strings.ActionSave}"
|
||||
Padding="32,8"
|
||||
IsDefault="True"
|
||||
Click="OnSave" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
169
src/PSLauncher.App/Views/HealthCheckEditorDialog.xaml.cs
Normal file
169
src/PSLauncher.App/Views/HealthCheckEditorDialog.xaml.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using PSLauncher.Core.Localization;
|
||||
using PSLauncher.Models;
|
||||
|
||||
namespace PSLauncher.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Modal d'édition d'une entrée du bandeau de santé. Présenté soit pour création
|
||||
/// (constructeur sans argument), soit pour modification (constructeur avec entry
|
||||
/// existante). À la sauvegarde, applique les valeurs sur l'objet d'origine pour
|
||||
/// que la liste parente n'ait rien à reconstruire.
|
||||
///
|
||||
/// Choix UI : icon picker en grille avec 28 emojis curatés couvrant les usages
|
||||
/// VR/streaming/réseau (les checks typiques de PROSERVE). On évite la saisie
|
||||
/// libre d'un emoji pour ne pas se retrouver avec des caractères qui ne rendent
|
||||
/// pas bien dans la pill du bandeau.
|
||||
/// </summary>
|
||||
public partial class HealthCheckEditorDialog : Window
|
||||
{
|
||||
/// <summary>Emojis proposés dans le picker. Listés du plus probable au moins probable.</summary>
|
||||
private static readonly string[] CuratedIcons =
|
||||
{
|
||||
"🎮", "🥽", "📡", "🌐", "📶", "🔌", "💻", "🖥",
|
||||
"📷", "🎧", "🛡", "🔥", "⚙", "🚀", "🔧", "📦",
|
||||
"🎯", "🏃", "🚨", "🔋", "💾", "🔍", "📊", "🖱",
|
||||
"⌨", "🎚", "🟢", "🔵",
|
||||
};
|
||||
|
||||
public HealthCheckEntry Entry { get; }
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
private readonly ObservableCollection<IconOption> _icons = new();
|
||||
|
||||
public HealthCheckEditorDialog() : this(new HealthCheckEntry
|
||||
{
|
||||
Name = "",
|
||||
Icon = "🔌",
|
||||
Kind = "Process",
|
||||
Target = "",
|
||||
RefreshIntervalMs = 5000,
|
||||
}, isNew: true)
|
||||
{
|
||||
}
|
||||
|
||||
public HealthCheckEditorDialog(HealthCheckEntry entry, bool isNew = false)
|
||||
{
|
||||
Entry = entry;
|
||||
InitializeComponent();
|
||||
|
||||
TitleText.Text = isNew ? Strings.HealthEditorTitleNew : Strings.HealthEditorTitle;
|
||||
|
||||
// Charge les valeurs courantes
|
||||
NameBox.Text = entry.Name ?? "";
|
||||
TargetBox.Text = entry.Target ?? "";
|
||||
RefreshMsBox.Text = entry.RefreshIntervalMs.ToString();
|
||||
|
||||
var isPing = entry.Kind?.Equals("ping", System.StringComparison.OrdinalIgnoreCase) == true;
|
||||
KindProcess.IsChecked = !isPing;
|
||||
KindPing.IsChecked = isPing;
|
||||
UpdateKindUi(isPing);
|
||||
|
||||
PingWarnBox.Text = (entry.PingWarnMs ?? 50).ToString();
|
||||
PingErrorBox.Text = (entry.PingErrorMs ?? 200).ToString();
|
||||
PingTimeoutBox.Text = (entry.PingTimeoutMs ?? 1000).ToString();
|
||||
|
||||
// Construit le picker, marque l'icône courante comme sélectionnée
|
||||
var current = string.IsNullOrEmpty(entry.Icon) ? "🔌" : entry.Icon;
|
||||
var iconList = CuratedIcons.ToList();
|
||||
if (!iconList.Contains(current)) iconList.Insert(0, current); // garde une icône custom si déjà en config
|
||||
foreach (var ic in iconList)
|
||||
_icons.Add(new IconOption(ic) { IsSelected = ic == current });
|
||||
IconPicker.ItemsSource = _icons;
|
||||
}
|
||||
|
||||
private void UpdateKindUi(bool isPing)
|
||||
{
|
||||
if (isPing)
|
||||
{
|
||||
TargetLabel.Text = Strings.HealthEditorTargetPing;
|
||||
TargetHint.Text = Strings.HealthEditorTargetPingHint;
|
||||
PingAdvanced.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetLabel.Text = Strings.HealthEditorTargetProcess;
|
||||
TargetHint.Text = Strings.HealthEditorTargetProcessHint;
|
||||
PingAdvanced.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKindChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (TargetLabel is null) return; // pendant l'init avant que tout soit chargé
|
||||
UpdateKindUi(KindPing.IsChecked == true);
|
||||
}
|
||||
|
||||
private void OnIconClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button btn || btn.Tag is not string emoji) return;
|
||||
foreach (var ic in _icons) ic.IsSelected = ic.Emoji == emoji;
|
||||
}
|
||||
|
||||
private void OnSave(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var name = NameBox.Text?.Trim() ?? "";
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
ShowError(Strings.HealthEditorErrorName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(RefreshMsBox.Text?.Trim(), out var refreshMs) || refreshMs < 200)
|
||||
{
|
||||
ShowError(Strings.HealthEditorErrorRefresh);
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedIcon = _icons.FirstOrDefault(i => i.IsSelected)?.Emoji ?? Entry.Icon;
|
||||
|
||||
Entry.Name = name;
|
||||
Entry.Icon = string.IsNullOrEmpty(selectedIcon) ? "🔌" : selectedIcon;
|
||||
Entry.Kind = KindPing.IsChecked == true ? "Ping" : "Process";
|
||||
Entry.Target = TargetBox.Text?.Trim() ?? "";
|
||||
Entry.RefreshIntervalMs = refreshMs;
|
||||
|
||||
if (Entry.Kind == "Ping")
|
||||
{
|
||||
Entry.PingWarnMs = TryParseNullableInt(PingWarnBox.Text);
|
||||
Entry.PingErrorMs = TryParseNullableInt(PingErrorBox.Text);
|
||||
Entry.PingTimeoutMs = TryParseNullableInt(PingTimeoutBox.Text);
|
||||
}
|
||||
|
||||
Saved = true;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnCancel(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ShowError(string msg)
|
||||
{
|
||||
ErrorText.Text = msg;
|
||||
ErrorText.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private static int? TryParseNullableInt(string? s)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s)) return null;
|
||||
return int.TryParse(s.Trim(), out var v) ? v : null;
|
||||
}
|
||||
|
||||
/// <summary>Élément du picker d'icônes — emoji + état sélectionné pour le data trigger.</summary>
|
||||
public sealed partial class IconOption : ObservableObject
|
||||
{
|
||||
public string Emoji { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isSelected;
|
||||
|
||||
public IconOption(string emoji) { Emoji = emoji; }
|
||||
}
|
||||
}
|
||||
@@ -648,6 +648,87 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Bandeau de santé système : liste éditable des checks -->
|
||||
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{x:Static loc:Strings.SettingsHealthChecks}"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Content="{x:Static loc:Strings.SettingsHealthAdd}"
|
||||
Command="{Binding AddHealthCheckCommand}"
|
||||
Padding="12,6" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="{x:Static loc:Strings.SettingsHealthChecksHint}"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,8,0,8" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Text="{x:Static loc:Strings.SettingsHealthEmpty}"
|
||||
FontSize="12" FontStyle="Italic"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
Margin="0,4,0,0" TextWrapping="Wrap"
|
||||
Visibility="{Binding HasHealthChecks, Converter={StaticResource InverseBoolToVisibility}}" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding HealthChecks}"
|
||||
Visibility="{Binding HasHealthChecks, Converter={StaticResource BoolToVisibility}}"
|
||||
Margin="0,4,0,0">
|
||||
<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="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding DisplayIcon}"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="2,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Foreground="{StaticResource Brush.Text.Primary}"
|
||||
FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding DisplayDetail}"
|
||||
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||
FontSize="11"
|
||||
FontFamily="Consolas" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Content="{x:Static loc:Strings.SettingsHealthEdit}"
|
||||
Command="{Binding EditCommand}"
|
||||
Margin="0,0,6,0"
|
||||
Padding="12,6" />
|
||||
<Button Grid.Column="3"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Content="{x:Static loc:Strings.SettingsHealthDelete}"
|
||||
Command="{Binding DeleteCommand}"
|
||||
Padding="12,6" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Logs (path + open button) -->
|
||||
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||
|
||||
@@ -946,6 +946,109 @@ public static class Strings
|
||||
$"تاريخ الإصدار: {date} • {size}"
|
||||
);
|
||||
|
||||
// ==================== HEALTH BANNER ====================
|
||||
public static string SettingsHealthChecks => T(
|
||||
"BANDEAU DE SANTÉ SYSTÈME",
|
||||
"SYSTEM HEALTH BANNER",
|
||||
"系统健康状态栏",
|
||||
"แถบสถานะระบบ",
|
||||
"شريط حالة النظام"
|
||||
);
|
||||
public static string SettingsHealthChecksHint => T(
|
||||
"Pings et processus surveillés sous la top bar (Library uniquement). Édité ici, sauvegardé dans config.json — survit à un auto-update.",
|
||||
"Pings and processes monitored below the top bar (Library only). Edited here, saved to config.json — survives auto-updates.",
|
||||
"在顶部栏下监控的 Ping 和进程(仅库视图)。在此编辑,保存到 config.json — 在自动更新后保留。",
|
||||
"Ping และโปรเซสที่ติดตามใต้แถบบนสุด (เฉพาะ Library) แก้ไขที่นี่ บันทึกใน config.json — คงอยู่หลังการอัปเดตอัตโนมัติ",
|
||||
"Ping والعمليات المراقَبة أسفل الشريط العلوي (المكتبة فقط). يُحرَّر هنا، ويُحفَظ في config.json — يبقى بعد التحديث التلقائي."
|
||||
);
|
||||
public static string SettingsHealthAdd => T("➕ Ajouter", "➕ Add", "➕ 添加", "➕ เพิ่ม", "➕ إضافة");
|
||||
public static string SettingsHealthEdit => T("✎ Éditer", "✎ Edit", "✎ 编辑", "✎ แก้ไข", "✎ تحرير");
|
||||
public static string SettingsHealthDelete => T("🗑 Supprimer", "🗑 Delete", "🗑 删除", "🗑 ลบ", "🗑 حذف");
|
||||
public static string SettingsHealthEmpty => T(
|
||||
"Aucun check configuré. Le bandeau ne s'affichera pas tant qu'aucune entrée n'aura été ajoutée.",
|
||||
"No checks configured. The banner stays hidden until at least one entry is added.",
|
||||
"未配置任何检查。在添加至少一项之前,状态栏将保持隐藏。",
|
||||
"ยังไม่มีการตรวจสอบที่ตั้งค่าไว้ แถบจะถูกซ่อนจนกว่าจะมีการเพิ่มอย่างน้อยหนึ่งรายการ",
|
||||
"لم يُهيَّأ أي فحص. سيبقى الشريط مخفياً حتى تتم إضافة إدخال واحد على الأقل."
|
||||
);
|
||||
public static string SettingsHealthDeleteConfirm(string name) => T(
|
||||
$"Supprimer le check « {name} » ?",
|
||||
$"Delete check « {name} »?",
|
||||
$"删除检查 \"{name}\"?",
|
||||
$"ลบการตรวจสอบ « {name} »?",
|
||||
$"حذف الفحص « {name} »؟"
|
||||
);
|
||||
|
||||
public static string HealthEditorTitle => T("Éditer le check", "Edit check", "编辑检查", "แก้ไขการตรวจสอบ", "تحرير الفحص");
|
||||
public static string HealthEditorTitleNew => T("Nouveau check", "New check", "新建检查", "การตรวจสอบใหม่", "فحص جديد");
|
||||
public static string HealthEditorName => T("NOM", "NAME", "名称", "ชื่อ", "الاسم");
|
||||
public static string HealthEditorNameHint => T(
|
||||
"Libellé court affiché dans la pill du bandeau, ex. « SteamVR ».",
|
||||
"Short label shown in the banner pill, e.g. \"SteamVR\".",
|
||||
"状态栏胶囊中显示的简短标签,例如 \"SteamVR\"。",
|
||||
"ชื่อย่อที่แสดงในป้ายของแถบ เช่น \"SteamVR\"",
|
||||
"تسمية قصيرة تظهر في الشريط، مثل \"SteamVR\"."
|
||||
);
|
||||
public static string HealthEditorKind => T("TYPE DE VÉRIFICATION", "CHECK TYPE", "检查类型", "ประเภทการตรวจสอบ", "نوع الفحص");
|
||||
public static string HealthEditorKindProcess => T("Processus (Windows)", "Process (Windows)", "进程 (Windows)", "โปรเซส (Windows)", "عملية (Windows)");
|
||||
public static string HealthEditorKindPing => T("Ping (réseau ICMP)", "Ping (ICMP network)", "Ping (ICMP 网络)", "Ping (เครือข่าย ICMP)", "Ping (شبكة ICMP)");
|
||||
public static string HealthEditorTargetProcess => T("CIBLE — NOM DE PROCESSUS", "TARGET — PROCESS NAME", "目标 — 进程名", "เป้าหมาย — ชื่อโปรเซส", "الهدف — اسم العملية");
|
||||
public static string HealthEditorTargetPing => T("CIBLE — IP OU HOSTNAME", "TARGET — IP OR HOSTNAME", "目标 — IP 或主机名", "เป้าหมาย — IP หรือชื่อโฮสต์", "الهدف — IP أو اسم المضيف");
|
||||
public static string HealthEditorTargetProcessHint => T(
|
||||
"Nom de l'exécutable sans .exe, ex. « vrserver » ou « HtcConnectionUtility ». Insensible à la casse.",
|
||||
"Executable name without .exe, e.g. \"vrserver\" or \"HtcConnectionUtility\". Case-insensitive.",
|
||||
"可执行文件名(不含 .exe),例如 \"vrserver\" 或 \"HtcConnectionUtility\"。不区分大小写。",
|
||||
"ชื่อไฟล์ปฏิบัติการโดยไม่มี .exe เช่น \"vrserver\" หรือ \"HtcConnectionUtility\" ไม่คำนึงถึงตัวพิมพ์",
|
||||
"اسم الملف التنفيذي بدون .exe، مثل \"vrserver\" أو \"HtcConnectionUtility\". غير حساس لحالة الأحرف."
|
||||
);
|
||||
public static string HealthEditorTargetPingHint => T(
|
||||
"Adresse IP (ex. 192.168.1.42) ou hostname résolvable. Vide = check ignoré (statut Inconnu).",
|
||||
"IP address (e.g. 192.168.1.42) or resolvable hostname. Empty = check skipped (Unknown status).",
|
||||
"IP 地址(例如 192.168.1.42)或可解析的主机名。空 = 跳过检查(未知状态)。",
|
||||
"ที่อยู่ IP (เช่น 192.168.1.42) หรือชื่อโฮสต์ที่แก้ได้ ว่างเปล่า = ข้ามการตรวจสอบ (สถานะไม่ทราบ)",
|
||||
"عنوان IP (مثل 192.168.1.42) أو اسم مضيف قابل للحل. فارغ = تخطّي الفحص (الحالة غير معروفة)."
|
||||
);
|
||||
public static string HealthEditorRefresh => T("INTERVALLE DE RAFRAÎCHISSEMENT", "REFRESH INTERVAL", "刷新间隔", "ช่วงเวลารีเฟรช", "فاصل التحديث");
|
||||
public static string HealthEditorRefreshHint => T(
|
||||
"Délai entre 2 vérifications, en millisecondes. Process ~1 ms (1000 OK) ; Ping = round-trip réseau (5000-10000 conseillé). Min 200 ms.",
|
||||
"Delay between 2 checks, in milliseconds. Process ~1 ms (1000 OK); Ping = network round-trip (5000-10000 recommended). Min 200 ms.",
|
||||
"两次检查之间的延迟(毫秒)。进程约 1 毫秒(1000 即可);Ping = 网络往返(建议 5000-10000)。最小 200 毫秒。",
|
||||
"ระยะเวลาระหว่าง 2 การตรวจสอบเป็นมิลลิวินาที โปรเซส ~1 ms (1000 OK); Ping = round-trip เครือข่าย (แนะนำ 5000-10000). ขั้นต่ำ 200 ms.",
|
||||
"التأخير بين فحصين بالمللي ثانية. عملية ~1 ms (1000 جيدة)؛ Ping = ذهاب وعودة الشبكة (يوصى 5000-10000). الحد الأدنى 200 ms."
|
||||
);
|
||||
public static string HealthEditorIcon => T("PICTOGRAMME", "ICON", "图标", "ไอคอน", "أيقونة");
|
||||
public static string HealthEditorIconHint => T(
|
||||
"Affiché à gauche du nom dans la pill. Choisis-en un dans la liste.",
|
||||
"Displayed to the left of the name in the pill. Pick one from the list.",
|
||||
"显示在胶囊中名称的左侧。从列表中选择一个。",
|
||||
"แสดงทางซ้ายของชื่อในป้าย เลือกหนึ่งจากรายการ",
|
||||
"تُعرض على يسار الاسم في الشارة. اختر واحدةً من القائمة."
|
||||
);
|
||||
public static string HealthEditorPingAdvanced => T(
|
||||
"Avancé — seuils Ping",
|
||||
"Advanced — Ping thresholds",
|
||||
"高级 — Ping 阈值",
|
||||
"ขั้นสูง — เกณฑ์ Ping",
|
||||
"متقدم — حدود Ping"
|
||||
);
|
||||
public static string HealthEditorPingWarn => T("Warning ms — RTT au-dessus = orange", "Warning ms — RTT above = amber", "警告 ms — 高于此值 = 橙色", "Warning ms — RTT เกินค่านี้ = ส้ม", "تحذير ms — RTT فوق ذلك = برتقالي");
|
||||
public static string HealthEditorPingError => T("Error ms — RTT au-dessus = rouge", "Error ms — RTT above = red", "错误 ms — 高于此值 = 红色", "Error ms — RTT เกินค่านี้ = แดง", "خطأ ms — RTT فوق ذلك = أحمر");
|
||||
public static string HealthEditorPingTimeout => T("Timeout ms — abandon de la requête", "Timeout ms — abort the request", "超时 ms — 中止请求", "Timeout ms — ยกเลิกคำขอ", "مهلة ms — إلغاء الطلب");
|
||||
public static string HealthEditorErrorName => T(
|
||||
"Le nom est requis.",
|
||||
"Name is required.",
|
||||
"需要名称。",
|
||||
"ต้องระบุชื่อ",
|
||||
"الاسم مطلوب."
|
||||
);
|
||||
public static string HealthEditorErrorRefresh => T(
|
||||
"Intervalle invalide. Saisis un entier ≥ 200 (millisecondes).",
|
||||
"Invalid interval. Enter an integer ≥ 200 (milliseconds).",
|
||||
"间隔无效。请输入 ≥ 200 的整数(毫秒)。",
|
||||
"ช่วงเวลาไม่ถูกต้อง ใส่จำนวนเต็ม ≥ 200 (มิลลิวินาที)",
|
||||
"فاصل غير صالح. أدخل عدداً صحيحاً ≥ 200 (مللي ثانية)."
|
||||
);
|
||||
|
||||
// ==================== COPYRIGHT ====================
|
||||
public static string Copyright => T(
|
||||
"© 2026 ASTERION VR — Tous droits réservés",
|
||||
|
||||
@@ -208,6 +208,14 @@ public sealed class HealthCheckEntry
|
||||
/// </summary>
|
||||
public string Target { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Période entre 2 vérifications de CE check, en millisecondes. Indépendant
|
||||
/// pour chaque entry car un Process check est rapide (~1ms) et peut tourner
|
||||
/// toutes les secondes, alors qu'un Ping coûte une requête réseau et peut
|
||||
/// tourner toutes les 5-10 s. Default 5000 ms.
|
||||
/// </summary>
|
||||
public int RefreshIntervalMs { get; set; } = 5000;
|
||||
|
||||
/// <summary>Pour Ping : RTT au-dessus duquel le statut passe en Warning (Orange). Default 50 ms.</summary>
|
||||
public int? PingWarnMs { get; set; } = 50;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user