From d845edb90f403f319b4f8eb8c8573e105f8531a0 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 7 Jul 2026 17:16:58 +0200 Subject: [PATCH] =?UTF-8?q?v1.0.11=20=E2=80=94=20/manifest=20:=20opt-in=20?= =?UTF-8?q?multiChannel=20pour=20ne=20plus=20d=C3=A9dupliquer=20par=20vers?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug rapporté après v1.0.10 : client sur license channel=full avec les deux entries v1.5.4.32 dans le backoffice (Entry #1 tagged firefighter+full, Entry #2 tagged full), le launcher n'affichait toujours qu'une seule ligne. Root cause côté serveur, PAS côté client cette fois. Manifest.php faisait un « group by version + pick most specific » dans filterVersions() — comportement historique introduit pour ne pas faire crasher les vieux clients qui déduisaient par ToDictionary(v.Version). Résultat : Entry #2 était filtrée avant même d'atteindre le launcher. Ce filtrage était nécessaire à l'époque (vieux clients) mais bloque tous les fixes multi-channels client (v1.0.8-1.0.10) qui avaient rendu le client capable d'afficher plusieurs entries au même numéro. Fix : opt-in via query param sur /manifest?multiChannel=1 Server (Manifest.php) : • filterVersions() prend un `bool $multiChannel = false`. • Si true → skip group-by, retourne toutes les entries visibles. • Si false (défaut, vieux clients) → comportement historique préservé. • Query param `multiChannel` lu depuis $_GET, transmis à filterVersions. Client (ManifestService.FetchFromOvhAsync) : • Ajoute `?multiChannel=1` inconditionnellement. Un serveur ancien ignore silencieusement le param (pas de header d'échec). • Combiné avec &channel=X quand la license a un channel. Rétro-compat : • Vieux client (v1.0.9-) + serveur nouveau : n'envoie pas multiChannel=1, serveur dédupe comme avant, launcher ne crash pas. • Client nouveau (v1.0.11+) + serveur ancien : le param est ignoré, même comportement qu'avant (dédup côté serveur, une seule row visible). • Client nouveau + serveur nouveau (config voulue) : les deux entries remontent, le row-key-par-Id de v1.0.10 fait le reste. Bump : 1.0.10 → 1.0.11 (fix ciblé serveur+client). Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- server/api/routes/Manifest.php | 25 ++++++++++++++++--- src/PSLauncher.App/PSLauncher.App.csproj | 6 ++--- .../Manifests/ManifestService.cs | 13 +++++++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 6f9e4b4..0906c55 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "1.0.10" +#define MyAppVersion "1.0.11" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/server/api/routes/Manifest.php b/server/api/routes/Manifest.php index c57f6e6..bd2e311 100644 --- a/server/api/routes/Manifest.php +++ b/server/api/routes/Manifest.php @@ -40,9 +40,17 @@ final class Manifest $clientChannel = null; } + // Multi-channel opt-in : les clients v1.0.10+ savent afficher plusieurs + // entries au même numéro de version (channels distincts). Ils passent + // ?multiChannel=1 pour signaler la capabilité. Les clients v1.0.9- (et + // avant refactor row-by-Id) crasheraient sur ToDictionary(v.Version) → + // le serveur reste en mode dédup pour eux (comportement historique). + $multiChannel = !empty($_GET['multiChannel']); + $manifest['versions'] = self::filterVersions( $manifest['versions'] ?? [], - $clientChannel + $clientChannel, + $multiChannel ); // Re-signature à la volée. La clé privée Ed25519 est dans config.php @@ -85,7 +93,7 @@ final class Manifest * @param array> $versions * @return list> */ - private static function filterVersions(array $versions, ?string $clientChannel): array + private static function filterVersions(array $versions, ?string $clientChannel, bool $multiChannel = false): array { // Étape 1 : filter visible par ce client $visible = []; @@ -102,7 +110,18 @@ final class Manifest } } - // Étape 2 : group by version, pick most specific per group + // Client v1.0.10+ : bypass la dédup, retourne toutes les entries visibles. + // Le client sait maintenant afficher plusieurs rows au même numéro (keyage + // par entryId côté RebuildList) et bloque les collisions d'install côté + // guard. Le badge channel n'apparaît que si plusieurs entries partagent + // le numéro (v1.0.9+) → UX propre. + if ($multiChannel) { + return array_map(fn($item) => $item['entry'], $visible); + } + + // Étape 2 (clients v1.0.9-) : group by version, pick most specific per + // group. Comportement historique pour ne pas crasher les vieux clients + // qui font ToDictionary(v.Version) et exploseraient sur des duplicates. $byVersion = []; foreach ($visible as $item) { $key = (string)($item['entry']['version'] ?? '?'); diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index b6f553e..6288bb5 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -18,9 +18,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 1.0.10 - 1.0.10.0 - 1.0.10.0 + 1.0.11 + 1.0.11.0 + 1.0.11.0 true diff --git a/src/PSLauncher.Core/Manifests/ManifestService.cs b/src/PSLauncher.Core/Manifests/ManifestService.cs index 22e87f0..2569d9d 100644 --- a/src/PSLauncher.Core/Manifests/ManifestService.cs +++ b/src/PSLauncher.Core/Manifests/ManifestService.cs @@ -146,9 +146,16 @@ public sealed class ManifestService : IManifestService // Whitelist côté client aussi pour éviter des chars exotiques qui casseraient l'URL. var channel = _channelProvider(); var baseUrl = TrimSlash(_serverBaseUrlProvider()) + "/manifest"; - var url = !string.IsNullOrWhiteSpace(channel) && System.Text.RegularExpressions.Regex.IsMatch(channel!, "^[a-z0-9_-]{1,64}$") - ? $"{baseUrl}?channel={Uri.EscapeDataString(channel!)}" - : baseUrl; + // Query params : ?channel=X (filtrage par channel license) + &multiChannel=1 + // (opt-in au comportement v1.0.10+ : le serveur envoie plusieurs entries au + // même numéro de version sans dédup, le client sait afficher chacune sur + // sa row). Toujours envoyé — les serveurs anciens l'ignorent silencieusement. + var qs = new System.Text.StringBuilder("?multiChannel=1"); + if (!string.IsNullOrWhiteSpace(channel) && System.Text.RegularExpressions.Regex.IsMatch(channel!, "^[a-z0-9_-]{1,64}$")) + { + qs.Append("&channel=").Append(Uri.EscapeDataString(channel!)); + } + var url = baseUrl + qs.ToString(); using var ovhCts = CancellationTokenSource.CreateLinkedTokenSource(ct); ovhCts.CancelAfter(OvhFetchTimeoutMs); using var req = new HttpRequestMessage(HttpMethod.Get, url);