From 67678fe173f31e86a30ad5517492c1494f2fd69e Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 5 May 2026 09:30:34 +0200 Subject: [PATCH] =?UTF-8?q?Manifest=20:=20d=C3=A9dup=20par=20version,=20l'?= =?UTF-8?q?entry=20channel-sp=C3=A9cifique=20gagne=20sur=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cas reporté : un client firefighter recevait DEUX entries v1.5.3 (default + firefighter) parce que la sémantique additive est correcte pour la visibilité, mais ne dédupe pas. Le launcher faisait alors un ToDictionary(v => v.Version) qui collisionne sur "1.5.3" et garde silencieusement le premier (la default), donc le mauvais ZIP s'affichait dans la UI. Fix : Manifest.php groupe par version après le filtre, et pour chaque groupe à plusieurs entries pick l'entry la plus spécifique au client : 1. priorité à celle taggée avec le channel propre du client (firefighter) 2. sinon l'entry default sert de fallback Sémantique pour un user firefighter : - v1.5.3 (default) + v1.5.3 (firefighter) → renvoie uniquement firefighter - v1.4.0 (default uniquement) → renvoie default (visible) - v1.5.3 (firefighter uniquement) → renvoie firefighter Fix purement server-side, pas de modif client requise. Le launcher recevra naturellement un seul ZIP par version, le bon. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/api/routes/Manifest.php | 57 +++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/server/api/routes/Manifest.php b/server/api/routes/Manifest.php index 9cfb5be..c57f6e6 100644 --- a/server/api/routes/Manifest.php +++ b/server/api/routes/Manifest.php @@ -68,25 +68,72 @@ final class Manifest } /** + * Filtre + dédupe par numéro de version. + * + * Étape 1 — filter : on garde les versions visibles par ce client. Un + * client sur channel X voit les entries taggées 'default' OU 'X'. + * + * Étape 2 — dedupe : pour chaque numéro de version qui apparaît plusieurs + * fois (cas v1.5.3 default + v1.5.3 firefighter), on ne renvoie qu'UNE + * seule entry — la plus spécifique au client. Priorité : l'entry taggée + * avec le channel propre du client gagne ; à défaut, l'entry default + * sert de fallback. Sans cette dédup côté serveur, le launcher recevait + * deux entries v1.5.3 et son `ToDictionary(v => v.Version)` collisionnait + * silencieusement, ce qui faisait apparaître dans l'UI le mauvais ZIP + * pour un user firefighter. + * * @param array> $versions * @return list> */ private static function filterVersions(array $versions, ?string $clientChannel): array { - $filtered = []; + // Étape 1 : filter visible par ce client + $visible = []; foreach ($versions as $v) { - // Si pas de channels sur l'entry → implicitement "default" $channels = (isset($v['channels']) && is_array($v['channels']) && !empty($v['channels'])) ? $v['channels'] : ['default']; - $isPublic = in_array('default', $channels, true); + $isPublic = in_array('default', $channels, true); $matchesUser = $clientChannel !== null && in_array($clientChannel, $channels, true); if ($isPublic || $matchesUser) { - $filtered[] = $v; + $visible[] = ['entry' => $v, 'channels' => $channels]; } } - return array_values($filtered); + + // Étape 2 : group by version, pick most specific per group + $byVersion = []; + foreach ($visible as $item) { + $key = (string)($item['entry']['version'] ?? '?'); + $byVersion[$key][] = $item; + } + + $result = []; + foreach ($byVersion as $items) { + if (count($items) === 1) { + $result[] = $items[0]['entry']; + continue; + } + // Plusieurs entries pour ce numéro : on cherche d'abord une qui + // matche le channel spécifique du client (firefighter, police…), + // sinon on retombe sur l'entry default. + $specific = null; + $defaultEntry = null; + foreach ($items as $item) { + if ($clientChannel !== null + && $specific === null + && in_array($clientChannel, $item['channels'], true)) { + $specific = $item; + } + if ($defaultEntry === null && in_array('default', $item['channels'], true)) { + $defaultEntry = $item; + } + } + $picked = $specific ?? $defaultEntry ?? $items[0]; + $result[] = $picked['entry']; + } + + return array_values($result); } }