Contexte : après le fix v1.0.5 (/download-url disambigué par filename), un
opérateur peut avoir un manifest avec deux entries partageant un numéro de
version sur des channels différents (ex : proserve-firefighter-1.5.4.32 vs
proserve-full-1.5.4.32). Deux problèmes restants :
1. Le client n'affichait qu'UNE row : `remote.ToDictionary(v.Version)` dans
RebuildList crashait sur duplicate key.
2. À l'install, les deux entries résolvaient au même dossier via le default
`installFolderTemplate = "PROSERVE v{version}"` → l'install le plus récent
écrasait silencieusement le précédent (ZipInstaller rename en .bak-{ts}
puis delete en background).
Solution end-to-end :
── Client ─────────────────────────────────────────────────────────────
• RebuildList refactor : index par folder name (résolu via GetInstallFolder-
Name()) au lieu de par version. Deux entries au même numéro deviennent
visibles dès qu'elles ont des templates distincts. Warning log si deux
entries résolvent au même folder.
• VersionRowViewModel : nouveau RowKey (basename du folder ou fallback
Version), ChannelBadge (premier channel non-default). Sites de lookup
(DL-in-flight preservation, 404 retry) migrés sur RowKey.
• MainWindow.xaml : badge bleu channel affiché à côté du badge BÊTA, dans
la row compact ET dans FeaturedVersion.
• Install guard : refuse une install si le dossier cible contient déjà un
.proserve-meta.json avec un entryId différent. Le meta stocke maintenant
l'entryId à chaque WriteInstallMetadataAsync. Message clair localisé
(FR/EN/CN/TH/AR/ES/DE) qui pointe l'opérateur vers le backoffice.
• VersionManifest client model : nouveau champ optionnel `Id` (mappé sur
le champ serveur existant), utilisé pour identifier l'entrée source.
• Registry regex broadened : accepte `PROSERVE(-<channel>)? v...` en plus
du `PROSERVE v...` legacy. Les folders custom par channel sont scannés.
── Serveur admin (versions.php) ──────────────────────────────────────
• Nouveau champ éditable `install_folder_template` dans le formulaire
d'ajout ET dans edit_meta. Validation regex (contient {version}, charset
whitelisted).
• Default intelligent à la création : si un seul channel non-default est
coché, pré-remplit avec "PROSERVE-<channel> v{version}". Sinon garde
"PROSERVE v{version}" (legacy).
• Validation croisée : refuse la save si deux entries résolvent au même
dossier, avec un message clair qui suggère un template alternatif.
── Rétro-compat ──────────────────────────────────────────────────────
• Vieux installs (sans entryId dans meta) : install guard fail-open, se
laisse écraser à la ré-install et retrofit l'entryId.
• Vieux manifests (sans `id` sur les entries) : `Id` est null côté client,
l'install guard reste passif, comportement identique à v1.0.7.
• Vieux serveurs (sans `install_folder_template` éditable) : le manifest
reste avec le default généré par generate_entry_id, aucune breaking
change. Le badge channel s'affiche quand même si `channels` est renseigné.
• Setups mono-channel (99 % des cas) : aucun changement visible, sort et
matching identiques.
Bump : 1.0.7 → 1.0.8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PS_Launcher — Côté serveur
Contenu de ce dossier à uploader sous www/PS_Launcher/ sur le mutualisé OVH.
Arborescence finale après upload
www/
└── PS_Launcher/
├── .htaccess
├── api/ ← API consommée par le launcher
│ ├── config.php ← À CRÉER (copie de config.example.php)
│ ├── index.php
│ ├── lib/{Response,Db,Crypto}.php
│ └── routes/{Manifest,Releasenotes,ValidateLicense}.php
├── admin/ ← Backoffice web (auth par mot de passe)
│ ├── .htaccess
│ ├── index.php (dashboard)
│ ├── login.php / logout.php
│ ├── licenses.php / versions.php / audit.php
│ ├── lib/{Auth,Layout}.php
│ └── assets/style.css
├── manifest/versions.json
├── releasenotes/1.4.X.md
├── builds/ ← ZIPs uploadés en SFTP
├── migrations/001_init.sql ← Schéma MySQL initial
└── tools/
├── generate-keypair.php
├── issue-license.php
└── sign-manifest.php
Setup initial (à faire une fois)
1. Crée la base MySQL
Manager OVH → Hébergements → Bases de données → Créer. Note le DSN, user, password.
2. Joue le schéma
PhpMyAdmin (manager OVH) ou en SSH :
mysql -h <host> -u <user> -p <db> < www/PS_Launcher/migrations/001_init.sql
3. Configure le serveur
- Copie
api/config.example.php→api/config.php - Remplis la section
db,base_url - Génère les clés Ed25519 :
Recopie
cd ~/www/PS_Launcher php tools/generate-keypair.phpprivate_key_hexetpublic_key_hexdansapi/config.phpsectioned25519. Recopie aussipublic_key_hexdanssrc/PSLauncher.Core/Resources/server-pubkey.txtcôté projet C# et recompile le launcher.
4. Configure le mot de passe admin
php -r "echo password_hash('TonMotDePasseFort', PASSWORD_DEFAULT);"
Recopie le hash dans api/config.php → admin_password_hash.
5. Test la chaîne
https://tondomaine.com/PS_Launcher/api/health → JSON status: ok
https://tondomaine.com/PS_Launcher/admin/login.php → page de connexion admin
Workflow de release (via le backoffice)
- Connecte-toi sur
https://tondomaine.com/PS_Launcher/admin/. - Onglet Versions → formulaire « Ajouter une version » : numéro, date min de license, release notes Markdown.
- Upload le ZIP correspondant via SFTP dans
www/PS_Launcher/builds/proserve-{version}.zip. - Bouton 🔁 Sync (sign-manifest) : calcule SHA-256, met à jour
latest, signe le manifest avec Ed25519. - Au prochain « Vérifier les MAJ » côté launcher, la nouvelle version apparaît.
Workflow de release (alternative manuelle SSH)
# 1. Édite manifest/versions.json (ou utilise le backoffice)
# 2. Upload le ZIP en SFTP dans builds/
# 3. Resigne :
cd ~/www/PS_Launcher
php tools/sign-manifest.php
Émettre une license
Via le backoffice (recommandé) : onglet Licenses → formulaire « Émettre ». La clé apparaît une seule fois — copie-la pour le client.
Via SSH :
php tools/issue-license.php "ACME Corp" 2027-12-31 1
Convention du contenu du ZIP
Le client extrait le ZIP dans installRoot/Proserve v{version}/. Le ZIP peut soit contenir
un dossier racine Proserve v{version}/, soit le contenu directement — le launcher détecte
automatiquement le préfixe commun et le strippe.
Test API depuis ton poste
curl.exe https://tondomaine.com/PS_Launcher/api/health
curl.exe https://tondomaine.com/PS_Launcher/api/manifest
curl.exe https://tondomaine.com/PS_Launcher/api/releasenotes/1.4.6
Sécurité
api/config.phpest gitignored. Ne jamais le commit.- Toutes les URLs
/api/*forcent HTTPS via.htaccess. - Les routes /api/* renvoient toujours du JSON (pas de page Apache 500 HTML).
- L'admin est protégé par session + CSRF + mot de passe bcrypt.
- Les licenses sont stockées via UNIQUE en clair (pour la lookup constant-time côté serveur), mais ne sont jamais loguées en clair côté audit.
- Côté client : la clé license est chiffrée DPAPI scope CurrentUser dans
%LocalAppData%. - Les réponses serveur (manifest, validation license) sont signées Ed25519 — le launcher embarque la clé publique et refuse toute réponse mal signée.