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

@@ -1,104 +1,16 @@
<?php
/**
* Met à jour les champs `download.sizeBytes` et `download.sha256` de versions.json
* en lisant les ZIPs présents dans /builds/. Le nom local du ZIP est dérivé du
* champ `download.url` (basename de l'URL), donc tu peux nommer ton fichier comme
* tu veux tant que le manifest pointe vers le bon nom.
* Wrapper CLI pour PSLauncher\Tools\SignManifest.
*
* Usage (à exécuter via SSH OVH dans www/PS_Launcher/) :
* php tools/sign-manifest.php
* Usage : cd ~/www/PS_Launcher && php tools/sign-manifest.php
*
* (v0.4) Ajoutera la signature Ed25519 globale du manifest.
* Le backoffice admin appelle directement la classe (pas d'exec).
*/
declare(strict_types=1);
$root = dirname(__DIR__);
$manifestPath = "$root/manifest/versions.json";
$buildsDir = "$root/builds";
require __DIR__ . '/SignManifest.php';
if (!is_file($manifestPath)) {
fwrite(STDERR, "Manifest not found: $manifestPath\n");
exit(1);
}
$manifest = json_decode(file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
$updated = 0;
$hashedVersions = [];
foreach ($manifest['versions'] as &$v) {
$version = $v['version'] ?? '?';
$url = $v['download']['url'] ?? '';
if ($url === '') {
echo " [skip] $version : pas d'URL dans le manifest\n";
continue;
}
$filename = basename(parse_url($url, PHP_URL_PATH) ?: '');
$zip = "$buildsDir/$filename";
if (!is_file($zip)) {
// Fallback : si le fichier n'existe pas exactement avec le nom de l'URL,
// on tente une recherche tolérante par version (espaces / casse / séparateurs).
$candidates = glob("$buildsDir/*{$version}*.zip", GLOB_NOSORT) ?: [];
$candidates = array_values(array_filter($candidates, 'is_file'));
if (count($candidates) === 1) {
$zip = $candidates[0];
echo " [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place\n";
} else {
echo " [skip] $version : ZIP introuvable pour $url\n";
if (count($candidates) > 1) {
echo " Plusieurs candidats : " . implode(', ', array_map('basename', $candidates)) . "\n";
}
continue;
}
}
$size = filesize($zip);
echo " [hash] $version : " . basename($zip) . " ($size octets)...";
$sha = hash_file('sha256', $zip);
echo " sha256={$sha}\n";
$v['download']['sizeBytes'] = $size;
$v['download']['sha256'] = $sha;
$updated++;
$hashedVersions[] = $version;
}
unset($v);
// Met à jour automatiquement le champ `latest` avec la plus haute version
// effectivement uploadée (celle pour laquelle on a calculé un hash).
if (!empty($hashedVersions)) {
usort($hashedVersions, function ($a, $b) {
return version_compare($a, $b);
});
$newLatest = end($hashedVersions);
if (($manifest['latest'] ?? null) !== $newLatest) {
echo " [latest] {$manifest['latest']} -> {$newLatest}\n";
$manifest['latest'] = $newLatest;
}
}
// (v0.4) Signature Ed25519 du manifest
$configPath = dirname(__DIR__) . '/api/config.php';
if (is_file($configPath)) {
require_once dirname(__DIR__) . '/api/lib/Crypto.php';
$config = require $configPath;
$sk = $config['ed25519']['private_key_hex'] ?? '';
if ($sk !== '' && strlen($sk) === 128) {
// Retire signature précédente, encode canonical, signe, ré-injecte
$manifest['signature'] = null;
$payload = \PSLauncher\Crypto::canonicalJson($manifest);
$manifest['signature'] = \PSLauncher\Crypto::signEd25519($payload, $sk);
echo " [sign] manifest signed (Ed25519)\n";
} else {
echo " [warn] ed25519.private_key_hex non configuré, manifest non signé\n";
}
} else {
echo " [warn] config.php absent, manifest non signé\n";
}
file_put_contents(
$manifestPath,
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
);
echo "Manifest mis à jour ({$updated} version(s)) : $manifestPath\n";
$signer = new \PSLauncher\Tools\SignManifest(dirname(__DIR__));
$result = $signer->run();
foreach ($result['log'] as $line) echo $line . "\n";
exit($result['ok'] ? 0 : 1);