v0.28.10 — Mode auto + settings lock par license + fix post-install state

Mode auto (auto-launch d'une version désignée au démarrage et après exit) :
 - Master toggle + version désignée via bouton AUTO (orange) sur chaque row
 - Countdown 5 s configurable avec popup d'annulation (StartAutoLaunchCountdown)
 - Args CLI passés à PROSERVE : opérateur tape juste le nom (autoconnect, login),
   le launcher ajoute -/=/quotes. Format produit identique à un .bat.
   Fix : passage de ArgumentList à Arguments string pour éviter le quotage auto
   .NET sur les args contenant '=' (cassait FParse::Value).
 - Garde-fou anti double-lancement : refuse de lancer si _runningProserve vivant
   OU IProcessLauncher.IsRunning() détecte une instance externe. Silent pour
   les triggers auto, popup pour les clics manuels.
 - Option « Attendre santé système » : countdown différé tant que les health
   checks ne sont pas tous OK (phase 1 du dialog avec liste des pending).

Settings lock par license (remplace l'ancien lock global manifest) :
 - Colonne settings_lock_password_hash sur table licenses (migration 003)
 - Backoffice : bouton 🔒 par license pour set/clear hash SHA-256
 - Payload /license/validate inclut settingsLockPasswordHash (v3 canonique)
 - 3-tier fallback signature : v3 → v2 → legacy pour compat cache offline
 - SettingsLockService : in-memory unlock state, gate l'expander Avancés
 - SettingsLockDialog : prompt mdp, persiste déverrouillé jusqu'au restart

Fix post-install : la row restait en visuel « Installing » jusqu'au prochain
Check Updates. Reset explicite row.State=InstalledIdle + _activeRow=null
AVANT le RebuildList post-install pour purger l'instance orpheline.

Migrations :
 - 002_channel_betas.sql réécrit en ALTER simples (DELIMITER cassait
   migrate.php qui split sur ';\n')
 - migrate.php tolère « Duplicate column/key name » comme idempotent

Strings (5 langues, ~25 nouvelles) :
 - Renomme « Relance automatique » → « Lancement automatique » (dialog
   utilisé pour les 3 entry points : clic, startup, post-exit)
 - Tooltips auto-mode, settings lock prompts, health wait phase, etc.

Bumps : 0.27.4 → 0.28.10 (csproj + .iss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:56:55 +02:00
parent 9b17dfa84c
commit efb53f079e
31 changed files with 1994 additions and 77 deletions

View File

@@ -76,6 +76,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = "Sortie du script (scope: launcher{$forceLabel}) :\n" . implode("\n", $result['log']);
if (!$result['ok']) $messageType = 'error';
}
// (Le settings lock est maintenant par-license, géré dans licenses.php — voir migration 003.)
} catch (Exception $e) {
$message = $e->getMessage();
$messageType = 'error';

View File

@@ -145,6 +145,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->prepare('UPDATE licenses SET max_machines = ? WHERE id = ?')->execute([$newMax, $id]);
$message = "License #{$id} : limite passée à {$newMax} machine(s).";
}
elseif ($action === 'set_settings_lock_password') {
$id = (int)($_POST['id'] ?? 0);
$pwd = (string)($_POST['settings_lock_password'] ?? '');
if ($id <= 0) {
throw new Exception('License id invalide.');
}
if ($pwd === '') {
// Vide = retire le verrouillage pour cette license
$db->prepare('UPDATE licenses SET settings_lock_password_hash = NULL WHERE id = ?')->execute([$id]);
$message = "License #{$id} : verrouillage des paramètres avancés retiré.";
} else {
if (strlen($pwd) < 4) {
throw new Exception('Mot de passe trop court (minimum 4 caractères).');
}
// SHA-256 hex lowercase, format compatible avec SettingsLockService côté launcher.
$hash = hash('sha256', $pwd);
$db->prepare('UPDATE licenses SET settings_lock_password_hash = ? WHERE id = ?')->execute([$hash, $id]);
$message = "License #{$id} : mot de passe des paramètres avancés mis à jour.";
}
}
elseif ($action === 'reset_machines') {
$id = (int)($_POST['id'] ?? 0);
$db->prepare('DELETE FROM license_machines WHERE license_id = ?')->execute([$id]);
@@ -363,6 +383,24 @@ Layout::header('Licenses', 'licenses');
<button class="btn btn-primary" type="submit">OK</button>
</form>
</details>
<?php $hasLock = !empty($l['settings_lock_password_hash']); ?>
<details style="display: inline-block; margin: 0 4px;">
<summary class="btn btn-secondary"
title="Mot de passe qui verrouille la section Settings → Avancés du launcher pour cette license. Laisser vide pour retirer le verrouillage.">
<?= $hasLock ? '🔒 Lock' : 'Lock' ?>
</summary>
<form method="post" style="margin-top: 8px; display: flex; gap: 4px; align-items: center;">
<?= Layout::csrfField() ?>
<input type="hidden" name="action" value="set_settings_lock_password">
<input type="hidden" name="id" value="<?= $l['id'] ?>">
<input type="password" name="settings_lock_password"
placeholder="<?= $hasLock ? 'Nouveau mot de passe (vide = retirer)' : 'Mot de passe (≥4)' ?>"
minlength="0" maxlength="100"
style="width: 200px;"
autocomplete="new-password">
<button class="btn btn-primary" type="submit">OK</button>
</form>
</details>
<?php if ($l['machines_count'] > 0): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Libérer TOUTES les machines de cette license ?\n\nUtilise plutôt « Voir machines » ci-dessous pour libérer un slot précis.')">
<?= Layout::csrfField() ?>

View File

@@ -30,7 +30,7 @@ final class ValidateLicense
$stmt = $db->prepare(
'SELECT id, license_key, owner_name, issued_at, download_entitlement_until,
max_machines, channel, can_see_betas, revoked_at
max_machines, channel, can_see_betas, settings_lock_password_hash, revoked_at
FROM licenses WHERE license_key = ? LIMIT 1'
);
$stmt->execute([$licenseKey]);
@@ -92,6 +92,12 @@ final class ValidateLicense
? (string)$lic['channel']
: null;
$canSeeBetas = (bool)($lic['can_see_betas'] ?? 0);
// SettingsLockPasswordHash : présent depuis migration 003, NULL = pas de
// verrouillage. On renvoie tel quel (null ou hex 64). Le launcher applique
// côté ISettingsLockService.SetPasswordHash après validation signée.
$settingsLockHash = isset($lic['settings_lock_password_hash']) && $lic['settings_lock_password_hash'] !== ''
? (string)$lic['settings_lock_password_hash']
: null;
$payload = [
'status' => $expired ? 'expired' : 'valid',
@@ -102,6 +108,7 @@ final class ValidateLicense
'maxMachines' => (int)$lic['max_machines'],
'channel' => $channel,
'canSeeBetas' => $canSeeBetas,
'settingsLockPasswordHash' => $settingsLockHash,
'serverTime' => $serverTime,
];

View File

@@ -5,45 +5,14 @@
--
-- À jouer après 001_init.sql.
--
-- IDEMPOTENCE : OVH mutualisé peut tourner sur MariaDB pré-10.0.2 où
-- ALTER TABLE ... IF NOT EXISTS n'existe pas. On contourne avec une PROCEDURE
-- temporaire qui consulte INFORMATION_SCHEMA avant chaque ALTER. Comme ça la
-- migration peut être re-jouée sans erreur sur n'importe quelle version
-- MariaDB / MySQL 5.5+.
-- IDEMPOTENCE : les ALTER ci-dessous throw une exception sur OVH si la colonne /
-- l'index existe déjà. migrate.php attrape ces erreurs spécifiques ("Duplicate
-- column", "Duplicate key name") et continue. Tu peux rejouer le script sans
-- risque sur n'importe quelle DB (vide, partiellement migrée, complètement).
DROP PROCEDURE IF EXISTS ps_launcher_migrate_002;
DELIMITER $$
CREATE PROCEDURE ps_launcher_migrate_002()
BEGIN
-- channel
IF NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'licenses' AND COLUMN_NAME = 'channel'
) THEN
ALTER TABLE licenses ADD COLUMN channel VARCHAR(64) NULL AFTER max_machines;
END IF;
-- can_see_betas
IF NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'licenses' AND COLUMN_NAME = 'can_see_betas'
) THEN
ALTER TABLE licenses ADD COLUMN can_see_betas TINYINT(1) NOT NULL DEFAULT 0 AFTER channel;
END IF;
-- index sur channel (une simple recherche bénéficie peu mais c'est pas cher)
IF NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'licenses' AND INDEX_NAME = 'idx_channel'
) THEN
ALTER TABLE licenses ADD INDEX idx_channel (channel);
END IF;
END $$
DELIMITER ;
CALL ps_launcher_migrate_002();
DROP PROCEDURE ps_launcher_migrate_002;
ALTER TABLE licenses ADD COLUMN channel VARCHAR(64) NULL AFTER max_machines;
ALTER TABLE licenses ADD COLUMN can_see_betas TINYINT(1) NOT NULL DEFAULT 0 AFTER channel;
ALTER TABLE licenses ADD INDEX idx_channel (channel);
-- Note : pas de migration de data. Les licenses existantes gardent
-- channel = NULL (= manifest default) et can_see_betas = 0 (= pas d'accès aux betas).

View File

@@ -0,0 +1,15 @@
-- PS_Launcher schema v3
-- Ajoute settings_lock_password_hash sur la table licenses : permet à l'admin
-- de définir un mot de passe par-license qui verrouille la section "Avancés"
-- des Settings côté launcher. Le hash SHA-256 (hex lowercase) est renvoyé au
-- launcher dans la réponse signée de /license/validate, donc l'intégrité
-- bénéficie de la signature Ed25519 existante.
--
-- À jouer après 001_init.sql et 002_channel_betas.sql.
-- migrate.php attrape "Duplicate column" comme idempotent.
ALTER TABLE licenses ADD COLUMN settings_lock_password_hash CHAR(64) NULL AFTER can_see_betas;
-- Note : les licenses existantes ont settings_lock_password_hash = NULL → pas
-- de verrouillage côté launcher → comportement identique à avant. L'admin
-- attribue manuellement via le bouton 🔒 Lock dans licenses.php.

View File

@@ -76,7 +76,20 @@ foreach ($files as $f) {
$first40 = substr(preg_replace('/\s+/', ' ', $stmt), 0, 60);
out("" . $first40 . "");
} catch (Exception $e) {
out(" ✘ ERREUR : " . $e->getMessage());
$msg = $e->getMessage();
// Tolérance idempotence : on n'aboie pas sur les ALTER qui re-touchent
// une colonne / un index existant. Permet de rejouer migrate.php sans
// risque sur une DB déjà partiellement / complètement migrée.
$idempotent = str_contains($msg, 'Duplicate column')
|| str_contains($msg, 'Duplicate key name')
|| str_contains($msg, "Can't DROP") // ALTER DROP COLUMN inexistante
|| str_contains($msg, 'check that column/key exists');
if ($idempotent) {
$first40 = substr(preg_replace('/\s+/', ' ', $stmt), 0, 60);
out(" ↺ (déjà appliqué) " . $first40 . "");
continue;
}
out(" ✘ ERREUR : " . $msg);
out(" Sur : " . substr($stmt, 0, 80) . "");
exit(3);
}