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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Changement majeur du modèle de channels en réponse au feedback :
« c'est trop compliqué. Il faudrait juste pouvoir tout laisser dans
builds, et taguer une version dans un canal ou un autre voir plusieurs
canaux. ». Le précédent design (1 fichier manifest par channel +
sous-dossier builds/{channel}/) imposait une duplication de structure
et empêchait une version d'être visible sur plusieurs channels à la fois.
NOUVEAU MODÈLE :
- 1 seul fichier manifest/versions.json
- 1 seul dossier builds/ flat (les ZIPs ont des noms libres pour éviter
les collisions homonymes — proserve-1.5.3.zip, proserve-1.5.3-police.zip…)
- Chaque entry de version a un champ `channels: ["default", "police"]`
qui dit qui peut la voir
- Sémantique additive : un client sur channel "police" voit les versions
taggées "default" + celles taggées "police". "default" = public.
- Une version peut être taggée sur plusieurs channels en une fois.
NOUVEAU REGISTRE channels.json :
Le registre des channels (avec name + label + description) vit dans
manifest/channels.json. Le channel "default" est implicite et ne peut
pas être supprimé. Nouvelle page admin Channels (CRUD) pour gérer la
liste, avec garde-fou anti-suppression : compte les versions taggées
et les licenses attribuées avant d'autoriser la suppression.
SERVEUR :
- api/routes/Manifest.php : reçoit ?channel=X depuis la license,
filtre versions où channels[] contient X ou "default", re-signe avec
la clé privée Ed25519 à la volée. Coût ~1ms par requête.
- admin/versions.php : refactor — plus de session "channel actif", plus
de switcher en haut, plus de prefix builds/{channel}/. Add form a
des checkboxes channels[]. Nouveau bouton "Channels" par-row pour
retager.
- admin/licenses.php : dropdown channel alimenté depuis channels.json
au lieu de scanner les fichiers manifest.
- tools/SignManifest.php : revert du constructeur channel-aware,
toujours sur versions.json + builds/ flat.
CLIENT :
- VersionManifest.Channels (List<string>) ajouté pour debug, mais le
filtrage est server-side donc le client n'a rien à faire de ce champ
côté UX.
- L'envoi du ?channel=X depuis la license signée fonctionne déjà
depuis v0.26.0, pas de changement nécessaire.
MIGRATION :
Les fichiers manifest/versions-{X}.json créés en v0.26.0 deviennent
inutiles. Si tu en avais (test "police" par exemple), copie les
versions concernées dans versions.json et tague-les avec les bons
channels via la nouvelle UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DEUX features liées :
1. CHANNELS : chaque license peut être attribuée à un manifest distinct
(« channel »). Permet de servir des versions différentes selon le
client. Le serveur lit ?channel=X et sert manifest/versions-{X}.json
avec fallback transparent sur versions.json. NULL = default.
2. BÊTA : nouveau flag isBeta + betaNotes par version dans le manifest.
Visible uniquement par les licenses avec can_see_betas=1. Affichage
d'une pill orange « BÊTA » sur la row + tooltip avec les notes pour
les testeurs. Les installations locales déjà présentes restent
visibles même si l'accès BÊTA est retiré ensuite (on n'efface pas
le disque du client).
DB :
002_channel_betas.sql ajoute channel + can_see_betas sur licenses.
Idempotent (ALTER TABLE IF NOT EXISTS), zero data migration.
Serveur PHP :
- ValidateLicense.php signe channel + canSeeBetas dans la réponse
(ordre des clés CRITIQUE pour matcher le canonical client).
- Manifest.php : whitelist regex anti-traversal sur ?channel=, fallback
silencieux sur versions.json si channel inconnu (évite leak de la
liste de channels par probing).
- SignManifest.php prend un channel optionnel → l'admin peut signer
chaque manifest indépendamment.
- admin/licenses.php : dropdown channel + checkbox bêta sur create,
bouton détails repliable par-row pour edit.
- admin/versions.php : channel switcher en tête, badge BÊTA sur chaque
row, dialog repliable « Bêta » avec checkbox + notes des testeurs.
Client C# :
- License.Channel + License.CanSeeBetas (dans le canonical signé).
- VersionManifest.IsBeta + BetaNotes.
- ManifestService prend un channelProvider via DI, lu depuis license
cachée à chaque fetch (lazy, pas de circular dep).
- MainViewModel.RebuildList filtre les versions IsBeta si !CanSeeBetas
(mais conserve les installées locales — on ne retire pas l'accès
rétroactivement à ce qui est déjà sur disque).
- VersionRowViewModel : props IsBeta / BetaNotes / BetaTooltip.
- MainWindow.xaml : pill orange à côté du n° version pour le featured
et les rows compactes, tooltip dynamique avec les notes testeurs.
Backward compat signature :
Anciennes licenses cachées (signées sans channel/canSeeBetas) sont
toujours validées via un fallback canonical legacy dans VerifySignature.
Sans ce fallback, le passage à v0.26 invaliderait toutes les caches
hors-ligne et bloquerait les users en mobilité.
Migration côté admin : jouer 002_channel_betas.sql sur la base, déployer
les fichiers PHP, créer manifest/versions-{channel}.json pour les
nouveaux channels (l'admin versions.php propose un input « Créer/utiliser
un nouveau channel »). Les licenses existantes restent en channel=NULL
= default = comportement actuel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client (C# / .NET 8 / WPF, MVVM via CommunityToolkit.Mvvm):
- PSLauncher.App: WPF UI dark theme (Epic-style sidebar + hero + big play button)
with UpdateAvailableDialog rendering Markdown release notes via Markdig.Wpf.
- PSLauncher.Core: services for installation registry (scans Proserve v{X.Y.Z}/),
process launcher, manifest fetch, SHA-256 integrity, HTTP download with
progress, ZIP install via .tmp + atomic rename, update orchestrator.
- PSLauncher.Models: RemoteManifest, InstalledVersion, LocalConfig DTOs.
Server (PHP 8 for OVH mutualisé, deployed under www/PS_Launcher/):
- Front controller + routes /manifest and /releasenotes/{version}.
- Static signed-manifest workflow with tools/sign-manifest.php CLI to
recompute SHA-256 and sizeBytes after each ZIP upload.
- .htaccess: HTTPS redirect, rewrite, security headers.
- config.example.php template; real config.php is gitignored.
Cohabiting versions: each release lives in its own Proserve v{version}/ folder
under installRoot. Old versions are never deleted automatically.
Roadmap: v0.3 = HTTP Range resume + Polly retry + state.json,
v0.4 = MySQL license + Ed25519 signatures + DPAPI, v0.5 = settings/UX polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>