UI overhaul: minimal top bar, license-first settings, footer-pinned check

Top bar (3-column layout)
-------------------------
- Col 1: PROSERVE Launcher wordmark.
- Col 2: license badge centered, the badge IS the click target now (a
  Border with an InputBindings MouseBinding LeftClick to OpenLicense).
  No more separate "🔑 Activer / changer" button cluttering the right.
- Col 3: ⚙ Paramètres + window chrome (min/max/close).
- "📁 Dossier" button removed from the top bar — install root is still
  reachable from Settings.

Footer (always visible)
-----------------------
- Row 1: "🔄 Vérifier les MAJ" pinned bottom-left, always shown. The
  optional "Annuler" button stays bottom-right while a download runs.
- Row 2: status text + progress bar, only shown when busy or after a
  status message — the previous "footer entirely hidden when idle"
  hid the check button too.

License flow split in two
-------------------------
Click on the license badge:
- License is active (valid / expired / revoked) → LicenseDetailsDialog
  opens. Header pill in matching status color (green/amber/red), shows
  owner, validity, issued date, machine ID with copy-to-clipboard. Two
  buttons: "🗑 Désactiver la license" (with confirmation) and Close.
- No license OR after deactivation → falls through to the existing
  OnboardingDialog for re-keying.

Settings rework
---------------
LICENSE section is now first in SettingsDialog with the same
green/amber/red colored chrome as the top bar — at a glance the user
sees the same status everywhere. Machine ID copy moved into this card.

Sign-manifest no longer needs exec()
------------------------------------
The "🔁 Sync" button in admin/versions.php previously shelled out to
`php tools/sign-manifest.php` via exec(). OVH mutualisé often disables
exec(), causing silent no-ops and the symptom the user just hit: the
ZIP changed (new size 469,657,770 vs manifest's stale 469,831,428) and
the launcher rejected it as size mismatch.

Refactor:
- New PSLauncher\Tools\SignManifest class with ->run() that does the
  hashing, latest-bump and Ed25519 signing in-process.
- tools/sign-manifest.php is now a 6-line wrapper for the class.
- admin/versions.php's 'sync' action calls the class directly via
  require_once + new — works on any host, no exec dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 09:24:01 +02:00
parent 74f48419e6
commit b10a3fbabf
9 changed files with 609 additions and 246 deletions

View File

@@ -315,12 +315,41 @@ public sealed partial class MainViewModel : ObservableObject
}
}
/// <summary>
/// Clic sur le badge license dans la top bar.
/// - License active (valid/expired/revoked) → ouvre LicenseDetailsDialog (détails + désactiver)
/// - Sinon → ouvre OnboardingDialog (saisie de clé)
/// Si l'utilisateur désactive depuis les détails, on enchaîne sur l'onboarding pour
/// faciliter le re-keying éventuel — sinon on revient simplement à l'état "Aucune license".
/// </summary>
[RelayCommand]
private async Task ActivateLicenseAsync()
private async Task OpenLicenseAsync()
{
var dialog = new Views.OnboardingDialog(_licenseService) { Owner = Application.Current.MainWindow };
dialog.ShowDialog();
if (dialog.LicenseActivated)
var current = _licenseService.GetCached();
if (current is not null)
{
var details = new Views.LicenseDetailsDialog(_licenseService, current)
{
Owner = Application.Current.MainWindow
};
details.ShowDialog();
if (!details.DeactivateRequested)
{
// Pas de changement
await Task.CompletedTask;
return;
}
// Désactivation : on a déjà vidé le cache, on rafraîchit la UI puis on reboucle
_license = null;
NotifyLicenseChanged();
RebuildList();
}
// Aucune license → propose l'onboarding
var onboard = new Views.OnboardingDialog(_licenseService) { Owner = Application.Current.MainWindow };
onboard.ShowDialog();
if (onboard.LicenseActivated)
{
_license = _licenseService.GetCached();
NotifyLicenseChanged();

View File

@@ -38,11 +38,53 @@ public sealed partial class SettingsViewModel : ObservableObject
get
{
var l = _licenseService.GetCached();
if (l is null) return "Aucune license activée";
return $"{l.Status} • {l.OwnerName} • exp. {l.DownloadEntitlementUntil:dd/MM/yyyy}";
if (l is null) return "Aucune license activée — clique pour saisir une clé";
if (l.Status == "valid")
return $"{l.OwnerName} • valable jusqu'au {l.DownloadEntitlementUntil:dd/MM/yyyy}";
if (l.Status == "expired")
return $"{l.OwnerName} • expirée le {l.DownloadEntitlementUntil:dd/MM/yyyy}";
return $"{l.OwnerName} • {l.Status}";
}
}
public string LicenseIcon
{
get
{
var l = _licenseService.GetCached();
if (l is null) return "🔒";
return l.Status switch
{
"valid" when IsExpiringSoon(l) => "⏰",
"valid" => "✓",
"expired" => "⚠",
"revoked" => "🚫",
_ => "❓",
};
}
}
public string LicenseSeverity
{
get
{
var l = _licenseService.GetCached();
if (l is null) return "Error";
return l.Status switch
{
"valid" when IsExpiringSoon(l) => "Warning",
"valid" => "Ok",
_ => "Error",
};
}
}
private static bool IsExpiringSoon(LicenseValidationResponse l)
{
if (l.DownloadEntitlementUntil is not { } until) return false;
return (until - DateTime.UtcNow).TotalDays < 30;
}
public SettingsViewModel(
IConfigStore configStore,
LocalConfig config,