Commit Graph

122 Commits

Author SHA1 Message Date
72f99e0c56 v0.27.1 — Versions : permettre numéro identique sur channels différents
Cas d'usage rapporté : « je peux avoir une version 1.5.3 pour les
pompiers et une version 1.5.3 pour la police ». Le check anti-doublon
sur le numéro de version bloquait la création — c'était un faux check
puisque la duplication SUR DES CHANNELS DIFFÉRENTS est légitime.

Changements :

1. ID stable interne : chaque entry de version reçoit un `id` immutable
   généré à la création (`v` + 8 hex aléatoires). Ce id devient l'identifiant
   primaire dans les forms et URLs admin. Le numéro de version reste un
   libellé human-readable, possiblement dupliqué.

2. Migration douce : loadManifest() ajoute un id aux entrées qui n'en
   ont pas, et auto-save pour persister. Stable aux reloads suivants.
   Les manifests legacy fonctionnent sans aucune action manuelle.

3. Check anti-doublon déplacé sur le NOM DE ZIP au lieu du numéro de
   version. Deux entries v1.5.3 sont autorisées si elles pointent vers
   des ZIPs différents (proserve-1.5.3-police.zip + proserve-1.5.3-pompier.zip).
   Une vraie ambiguïté sur disque (deux entries → même ZIP) est rejetée.

4. Toutes les actions row-level (edit_meta, edit_notes, set_beta,
   set_channels, toggle_available, delete, set_skip_hash, sync_one)
   identifient l'entry par `id` au lieu de `version`. Forms HTML mis
   à jour en conséquence.

5. Release notes : addressables par id stable. Stockées en
   releasenotes/{id}.md. Compat ascendante : si un fichier {id}.md
   n'existe pas, l'admin lit le legacy {version}.md (pour les notes
   créées avant v0.27.1). L'endpoint API releasenotes/{key} accepte
   les deux formats.

6. Suppression d'une entry : nettoie le fichier {id}.md correspondant.

7. SignManifest::run filtre toujours par version string : si plusieurs
   entries partagent un numéro de version, toutes sont re-hashées via
   « 🔁 Hash » sur une row. Comportement correct (chaque entry a sa
   propre URL/ZIP, donc son hash spécifique), juste moins efficient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:02:00 +02:00
49a3b855af v0.27.0 — Channels comme TAGS sur versions (refactor du modèle)
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>
2026-05-05 08:51:10 +02:00
0c69bb1c85 Fix : badge 'absent' faux positif sur ZIPs en sous-dossier de channel
Le commit 7ff5e56 avait changé $zips pour stocker des chemins relatifs
(asterion-vr/proserve-1.5.3.zip) afin de lister les ZIPs en sous-dossier,
mais le check par-row utilisait toujours basename(URL) → comparait
"proserve-1.5.3.zip" contre ["asterion-vr/proserve-1.5.3.zip"], donc
in_array() retournait toujours false → badge "absent" alors que le ZIP
était bien là.

Fix : on compute zipRel depuis l'URL de la même façon que $zips (extrait
tout après /builds/), pour que les deux côtés du in_array() utilisent le
même format. Le tooltip "attendu :" affiche aussi le chemin relatif
complet, c'est plus clair pour le SFTP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:37:35 +02:00
7638a7a25c Admin versions : nom du ZIP libre + renommage post-création
À l'ajout d'une version, nouveau champ "Nom du fichier ZIP (optionnel)" :
par défaut proserve-{version}.zip mais l'admin peut surcharger pour
distinguer des builds homonymes entre channels :
  builds/asterion-vr/proserve-1.4.6-asterion.zip
  builds/client-foo/proserve-1.4.6-foo.zip

Sur les versions existantes, la dialog "Méta" gagne un champ "Renommer
le ZIP" qui :
- ne touche que le filename, garde le préfixe builds/{channel}/
- invalide le sha256 (REPLACE_AFTER_BUILD) + sizeBytes pour forcer le
  recalcul au prochain Sync — le ZIP physique change, le manifest
  refléterait sinon l'ancien hash et le client ferait fail la vérif.

Normalisation côté serveur via ps_normalize_zip_filename() :
- basename() pour empêcher tout path traversal (../)
- whitelist [a-zA-Z0-9_.-] (laisse les majuscules, points, tirets)
- auto-suffix .zip si manquant
- fallback sur proserve-{version}.zip si l'input devient vide après nettoyage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:19:24 +02:00
7ff5e56102 Fix : SignManifest et listing ZIPs supportent les sous-dossiers de channel
v0.26.0 générait des URLs builds/{channel}/proserve-X.Y.Z.zip dans le
manifest pour les channels non-default, mais SignManifest faisait juste
basename(url) → cherchait builds/proserve-X.Y.Z.zip à plat → ZIP introuvable
au moment du Sync.

Fixes :

1. SignManifest::resolveZipPath() : extrait tout le chemin après /builds/
   dans l'URL et le mappe sur {$buildsDir}/. Strip ../ pour anti-traversal
   même si le manifest est de toute façon signé Ed25519.

2. SignManifest fallback fuzzy étendu à 1 niveau de sous-dossier (glob */*.zip)
   pour le cas "admin a renommé le ZIP".

3. admin/versions.php $zips inclut maintenant builds/ + builds/*/ et stocke
   les chemins relatifs (asterion-vr/proserve-X.Y.Z.zip) au lieu du basename.
   Le check "référencé ?" matche aussi sur le chemin relatif.

4. Workflow text en haut de la page indique le bon chemin SFTP selon le
   channel actif (avec rappel "crée le sous-dossier si absent").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:14:02 +02:00
95c70903e7 Fix : 'Aller' silencieux sur création de channel + dropdown vide
Deux bugs UX rapportés sur v0.26.0 admin/versions.php :

1. Cliquer 'Aller' avec un nom contenant majuscules / espaces / accents
   ne faisait RIEN. Cause : pattern HTML5 strict (`[a-z0-9_-]{1,64}`) qui
   bloque la submission du form GET sans message visible. Selon le browser
   le tooltip de validation est barely visible, donc le user a l'impression
   que le bouton est cassé.

   Fix : retire le pattern strict, normalise côté serveur :
   « ASTERION VR » → « asterion-vr », « éàç!? » → « » (et on dit pourquoi).
   Flash success qui annonce la normalisation, ou flash error si le nom
   est inexploitable après nettoyage.

2. Après création d'un channel, le dropdown affichait encore '(default)'
   parce que listExistingChannels() filtre sur les versions-*.json
   existants et l'admin n'a pas encore ajouté de version donc le fichier
   n'existe pas. Du coup l'admin pensait que la création n'avait pas
   marché alors que la session était bien sur le nouveau channel.

   Fix : injection forcée du channel actif dans le tableau des options du
   dropdown avec label « X (vide — pas encore de versions) ».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:09:59 +02:00
2b95472393 Fix : 500 sur admin/versions.php (PHP imbriqué)
Mon edit v0.26.0 avait laissé un <?php à l'intérieur d'un bloc PHP déjà
ouvert. Erreur de syntaxe → 500. Refactor pour fermer le bloc PHP
proprement après Layout::header() avant de basculer sur le HTML.

Vérifié avec php -l (XAMPP) : « No syntax errors detected ».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:06:12 +02:00
9499c6ae58 Migration 002 : version portable MariaDB pré-10.0.2 (OVH mutualisé)
ALTER TABLE ... IF NOT EXISTS n'est dispo qu'à partir de MariaDB 10.0.2.
OVH mutualisé tourne sur du plus ancien → erreur 1064 syntaxe.

Remplacement par une PROCEDURE temporaire qui consulte INFORMATION_SCHEMA
avant chaque ALTER. Marche sur toutes versions MariaDB / MySQL 5.5+ et
reste idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:55:58 +02:00
c371a79f93 v0.26.0 — Channels per-license + tag BÊTA sur versions
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>
2026-05-05 07:38:00 +02:00
3d691c3e38 v0.25.10 — LAN P2P offline-friendly : manifest fallback + perf
Fonctionne entièrement offline si un PC du LAN a fetché OVH récemment :
les autres PCs récupèrent le manifest + les ZIPs depuis lui sans Internet.
Discovery des peers quasi-instantanée au cold start, fail-fast partout sur
les fetch OVH pour ne pas bloquer la UI.

== Manifest fallback offline ==

- IManifestCache + ManifestCache (Core/Manifests/) : cache disque du dernier
  manifest réussi (JSON brut byte-for-byte pour préserver la signature Ed25519).
  Path : %LocalAppData%\PSLauncher\manifest-cache.json
- IPeerManifestFetcher + PeerManifestFetcher (Core/Lan/) : récupère le
  manifest brut depuis un peer LAN via GET /v1/manifest. Probe parallèle
  avec timeout court par peer.
- LanCacheServer : nouvelle route GET /v1/manifest qui sert le cache disque
  byte-for-byte aux clients sans Internet.
- ManifestService.FetchAsync : online check d'abord (ping ICMP 8.8.8.8 +
  fallback TCP connect au serveur OVH si firewall bloque ICMP, timeout
  800ms × 2). Si online → OVH only (canonique, voit toutes les versions).
  Si offline → peer LAN puis cache disque local.
- ManifestSource enum exposé via IManifestService.LastSource → call sites
  peuvent prendre des décisions éclairées (skip release notes, badge UI).

== Discovery active des peers (cold start instantané) ==

- LanCacheServer : nouvelle route GET /v1/info qui retourne {host, port,
  versions} (même payload que les beacons UDP).
- LanDiscoveryService : bootstrap au démarrage qui probe en parallèle tous
  les peers connus (cache disque) + manuels (config) via /v1/info, timeout
  1s par peer. Les peers qui répondent sont injectés dans _peers avec
  lastSeen=now → apparaissent immédiatement dans ListDiscovered() et donc
  dans la sidebar + PeerSourceResolver. Plus besoin d'attendre les 15s du
  prochain beacon UDP.
- ILanDiscoveryService.ListKnown() exposé pour que les fetchers puissent
  utiliser le cache disque comme seed.
- LocalConfig.LanCache.ManualPeerUrls : default ["http://10.0.4.100:47623"]
  (IP fixe du serveur ASTERION dans toutes les salles VR).

== Timeouts courts sur tous les fetch OVH (fail-fast offline) ==

- ManifestService : 3s pour le manifest fetch (vs 100s par défaut)
- LicenseService.ValidateAsync : 3s (résolvait le délai de ~15s pendant
  CheckForUpdates en offline — HttpClient global a Timeout=Infinite,
  sans CTS court ça hangait jusqu'à l'abandon SYN TCP par l'OS)
- LicenseService.GetSignedDownloadUrlAsync : 5s
- ManifestService.FetchReleaseNotesAsync : 5s

== UI ==

- StatusMessage post-Check Updates suffixé avec la source du manifest :
  "🌐 OVH (en ligne)" / "📡 Peer LAN (hors ligne)" / "💾 Cache local (hors ligne)"
- InstallVersionAsync : skip le fetch release notes si manifest source
  != OVH → dialog s'ouvre instantanément avec "indisponible" au lieu
  d'attendre 5s de timeout.

== Latences attendues ==

| Scénario | Avant | Après |
|---|---|---|
| Vérifier MAJ online | ~5s | ~500ms |
| Vérifier MAJ offline | ~15-20s | ~5s |
| Cold start discovery (1 peer connu) | jusqu'à 15s | ~200ms |
| Click Install offline | ~5s release notes timeout | instantané |

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:51:50 +02:00
65ff8541b7 v0.25.4 — Cache LAN P2P : un PC sert les ZIPs aux autres du LAN
Feature : sur un site multi-PCs (salle VR), un PC peut servir les ZIPs PROSERVE
qu'il a déjà téléchargés aux autres PCs du LAN au lieu de les forcer à re-DL
14 Go depuis OVH. Auto-discovery par UDP broadcast, fallback transparent OVH
si aucun peer ne répond, intégrité préservée par SHA-256 du manifest signé Ed25519.

Architecture (src/PSLauncher.Core/Lan/) :
- ZipCacheStore : cache local des ZIPs téléchargés (sidecar SHA-256 pour servir
  sans re-hash). Pruning auto au top-N par mtime, protège les versions encore
  installées. Remplace le `File.Delete(zipPath)` post-install historique.
- LanCacheServer : serveur HTTP local sur TcpListener (PAS HttpListener — il
  exigeait une URL ACL admin qui aurait obligé à coder un netsh dans l'installer).
  Routes GET /v1/has/{ver} (probe) + GET /v1/zip/{ver} (Range support manuel,
  ~80 LOC de parsing HTTP). Filtre RFC1918/link-local sur RemoteEndPoint avant
  toute IO, regex stricte sur version (anti-path-traversal), SemaphoreSlim(4)
  pour rate-limiter à 4 DL concurrents.
- LanDiscoveryService : émet beacon UDP (255.255.255.255:47624, JSON avec host+
  port+versions) toutes les 15 s en mode serveur ; écoute en mode client/serveur
  avec TTL 60 s sur les peers. Pure .NET, zero NuGet (pas de mDNS deps).
- PeerSourceResolver : avant chaque DL, probe les peers (auto-découverts +
  manuels) avec timeout 1 s × max 5 peers. Vérifie size + SHA-256 contre le
  manifest avant de retourner une URL peer. Si null → fallback OVH transparent.

Modifs flow d'install (MainViewModel) :
- Avant GetSignedDownloadUrlAsync, tente PeerSourceResolver. Si peer trouvé,
  utilise son URL directement (le DownloadManager multi-segment marche tel quel).
- RefreshUrlAsync handler : pour les peers, retourne la même URL (pas d'expiration
  HMAC). Pour OVH, refresh du HMAC inchangé.
- Post-install : remplace File.Delete(zipPath) par ZipCacheStore.Promote + Prune.

Sidebar info (visible en permanence dans le coin bas-gauche) :
- Lignes "Serveur ON/KO/—", "Client ON/—", "Peers N" — refresh auto toutes les
  10 s via DispatcherTimer. Diagnostic instantané sans naviguer dans Settings.

Footer pendant le DL : préfixe différencié pour rendre la source évidente —
"📡 LAN 10.0.0.5 • v… : … Mo/s" vs "⬇ v… : … Mo/s" (OVH).

Settings → Cache LAN P2P : checkboxes ServerEnabled/ClientEnabled, ports HTTP/UDP,
MaxCachedVersions, statut serveur live, liste peers découverts, champ multi-line
peers manuels (override/fallback de l'auto-discovery). Toggle des flags →
prompt "Restart launcher requis" car les hosted services lisent leur config au
boot uniquement.

Fix critique au passage : App.OnStartup appelait .Build() mais JAMAIS .StartAsync()
sur l'IHost — du coup AUCUN HostedService n'a jamais tourné depuis l'introduction
du pattern. Ajout de StartAsync (fire-and-forget avec log d'erreur) et StopAsync
propre dans OnExit (timeout 5 s pour libérer les sockets sans TIME_WAIT bloquant).

Installer (PSLauncher.iss) : règles firewall TCP 47623 + UDP 47624 préposées
via netsh advfirewall, profile=private,domain (PAS public — safety net même
si l'utilisateur connecte le PC à un Wi-Fi public). Inutile si firewall OFF
(cas du déploiement actuel) mais pose les rails pour les déploiements futurs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:00:10 +02:00
7de0c01b97 v0.24.9 — Deploy _api/, fix WebView2 Program Files, DL robustness
Deploy auto de l'API PHP de stats (parallèle Report/Doc) :
- ApiToolDeployer : copy _api/ → C:\xampp\htdocs\proserve\ avec atomic
  rename + backups horodatés + revert + pruning. Strict clone du pattern
  DocToolDeployer.
- ApiToolConfig dans LocalConfig (default folder "proserve" en minuscule
  pour matcher l'install standard PROSERVE côté client).
- Settings → Avancés : section dédiée (HtdocsRoot, FolderName, AutoDeploy,
  MaxBackups, Re-déployer, liste backups + revert) + ApiBackupViewModel.
- DI registration dans App.xaml.cs, injection MainViewModel, appel
  post-install après le bloc Doc.
- 8 nouvelles strings i18n (5 langues).

Fix WebView2 vide en Program Files :
- Set WEBVIEW2_USER_DATA_FOLDER vers %LocalAppData%\PSLauncher\WebView2\
  dans App.OnStartup. Sans ça, WebView2 essaie d'écrire son cache à côté
  du .exe (read-only en Program Files) → init silencieusement KO →
  onglets Report/Doc vides.

Refresh auto WebView2 sur navigation d'onglet :
- Hook IsVisibleChanged sur ReportWebView et DocsWebView. À chaque
  ré-affichage, CoreWebView2.Reload() pour voir les données serveur à
  jour. Le binding Source garde la nav initiale au cold start.

Fix UI : Cancel décalant le footer pendant Check updates :
- Nouveau flag IsDownloadActive distinct de IsBusy. Visibility du bouton
  Cancel passe de IsBusy → IsDownloadActive : couvre uniquement les DL,
  plus aucune apparition pendant Check updates / Install / Uninstall /
  Self-update qui décalait tout le layout 1 s.

DL robustness (multi-PC) :
- SafeInstallAsync : error boundary sur l'install handler. Plus aucune
  exception silencieuse — log + popup d'erreur visible (résout les cas
  "le 2e PC ne démarre pas, sans message").
- DownloadManager : vérif explicite que CHAQUE segment a Completed=true
  && DownloadedBytes >= Length AVANT le SHA-256. Le check actualSize
  était inutile (fichier sparse-préalloué).
- MaxRetryAttempts Polly 6 → 10 pour absorber les outages PHP-FPM
  request_terminate_timeout sous charge multi-PC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:53:13 +02:00
9de8e95910 v0.24.5 — Install Program Files, multi-PC DL reliability, polish UI
Installeur :
- Bascule en Program Files (autopf) avec PrivilegesRequired=admin.
  L'auto-update reste fonctionnel : LauncherSelfUpdater détecte le dossier
  protégé et lance PS_Launcher.Updater.exe via Verb=runas (UAC à chaque MAJ).
- build-installer.ps1 : détection ISCC.exe robuste (Program Files + LocalAppData
  pour install winget user-mode + PATH), messages d'erreur explicites avec URL
  et commande winget, suppression accents (compat PS 5.1 sans BOM).

Téléchargements multi-PC :
- DownloadManager : détection des connexions fermées prématurément (PHP-FPM
  request_terminate_timeout). Sans ça, segment marqué Completed=false sans
  exception → fichier sparse final avec trous → SHA-256 KO. Lance maintenant
  HttpResumableException(transient) pour que Polly retry au bon offset.
- ParallelDownloadSegments : 16 → 6 par défaut pour permettre plusieurs PCs
  simultanés sans saturer le pool PHP-FPM OVH (~30 workers).

UI :
- Fenêtre maximisée par défaut au démarrage (WindowState=Maximized).
- Fix maximize qui cachait le footer / barre de DL (WM_GETMINMAXINFO clamp
  sur work area, remplace le margin hack 7px imprécis).
- Sidebar : bloc info en bas avec PS_Launcher vX.Y.Z + IPv4 locale alignés.
- Copyright remonté plus près du footer (margin 28 → 8).
- Settings → Health checks : boutons ▲/▼ pour réordonner, CanExecute auto.
- HealthCheck refresh défaut : 5000 → 2000 ms.

Bug fix UI :
- MainViewModel.RebuildList preserve l'état du row actif pendant un DL.
  Sinon ouvrir Settings/License pendant un DL recréait les rows from scratch
  → UI affichait "Reprendre" + "Annuler" alors que le DL tournait. Les
  progress callbacks pointent maintenant sur _activeRow (résolution dynamique)
  au lieu de capturer le row local.

Defaults config :
- ServerBaseUrl : example.com → asterionvr.com (out-of-the-box).
- Vive Business Streaming check : HtcConnectionUtility → rrserver.

Backoffice (server/admin/licenses.php) :
- Action set_max_machines : bouton "Slots" pour ajuster max_machines à chaud
  sur une licence existante. Refus de descendre sous le nombre de machines
  déjà actives.

Build :
- AllowUnsafeBlocks=true sur PSLauncher.App.csproj (compat WinRT generator
  récent qui émet du code unsafe dans WinRTGenericInstantiation.g.cs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:04:36 +02:00
be7ccfced5 v0.23.6 — Fix : footer (barre de DL) invisible en plein écran
Bug WPF classique avec WindowStyle=None + WindowChrome : quand la fenêtre
passe en Maximized, elle déborde physiquement de la taille de
ResizeBorderThickness sur chaque bord (~7 px ici). Conséquence : la Row 3
(footer) passe sous la barre des tâches Windows, et la barre de
téléchargement devient invisible alors que tout le reste continue de
fonctionner.

Fix : on applique un Margin=7 sur le Grid root quand WindowState=Maximized
via un DataTrigger sur le RelativeSource Window. Le trigger se déclenche
automatiquement à chaque maximize/restore — pas de code-behind nécessaire.

Test : maximize + minimize + restore plusieurs fois → le footer reste
toujours visible et la fenêtre revient à sa taille normale sans glitch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:06:54 +02:00
bebb701d70 v0.23.5 — Retire le check VR du default config
Vive Streaming ment à SteamVR : son driver prétend qu'un HMD est présent
même quand la session Wi-Fi avec le casque physique est tombée. Donc
tous les signaux qui passent par SteamVR (VR_IsHmdPresent, vtable
IVRSystem, logs vrserver) sont contaminés et ne reflètent pas l'état
réel du casque. Le check qu'on a ajouté en v0.23.4 induisait l'utilisateur
en erreur ("vert" alors que le casque est éteint).

Pour l'instant on retire le check du default. Bookmark dans le code des
pistes Phase 2 qui ne passent PAS par SteamVR : connexions TCP/UDP de
HtcConnectionUtility via GetExtendedTcpTable, endpoint local de Vive
Console, compteurs IO, ARP avec OUIs HTC, logs Vive directs.

Le Kind « VrDevice » reste dispo dans l'éditeur pour les setups SteamVR
purs (Index/Vive Wand sur Lighthouse) où le signal est honnête, mais
n'est plus poussé par défaut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:45:55 +02:00
37a4cf424e v0.23.4 — Default config : check VR par défaut en VrDevice (out-of-the-box)
Avant : check « Casque VR » par défaut en Ping avec Target vide (à
remplir par l'utilisateur avec l'IP du casque). Pour Vive Streaming
ça nécessite de connaître l'IP du casque côté Wi-Fi, info pas toujours
exposée par Vive Business Streaming → check désactivé chez 90% des users.

Après : check VrDevice avec Target « hmd ». Marche sans aucune config :
détecte SteamVR + HMD via les flat C OpenVR (VR_IsRuntimeInstalled +
VR_IsHmdPresent + vrserver process check). Pas de SHM/IPC donc pas de
risque AccessViolation comme avec la vtable.

Avantage en plus : répond beaucoup mieux à la question qui compte
(« PROSERVE peut-il se lancer ? ») qu'un ping sur l'IP du casque, qui
indiquait juste la connectivité réseau.

Les utilisateurs déjà installés en v0.23.x gardent leur config existante
(le launcher ne réécrase pas un config.json déjà présent) ; ils peuvent
remplacer leur Ping par un VrDevice via Settings → Bandeau de santé.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:32:06 +02:00
7cb2b01403 v0.23.3 — Mode safe : OpenVR sans vtable (Phase 1 stripped down)
Init OpenVR + binding de la vtable IVRSystem_022 fonctionne (confirmé par
les logs : "OpenVR state: Ready, vtable IVRSystem_022 bindée"), mais le
PREMIER appel vtable crashe en AccessViolation natif. Pas une race au
boot — ça plante même 8s après que vrserver est apparu, et probablement
à n'importe quel moment de la session SteamVR.

Diagnostic : les fonctions de la vtable (IsTrackedDeviceConnected,
GetBoolTrackedDeviceProperty, etc.) accèdent à des shared memories avec
vrserver qui peuvent être (re)mappées dynamiquement quand un device se
connecte/déconnecte. L'AccessViolation natif passe au travers du CLR
via Marshal.GetDelegateForFunctionPointer — non rattrapable par
catch (Exception) ni catch (AccessViolationException).

Fix : on retire TOUS les appels vtable. OpenVrService.Query utilise
maintenant uniquement l'API flat C qui ne fait pas de SHM/IPC :

  - VR_IsRuntimeInstalled() — lookup statique dans la DLL
  - VR_IsHmdPresent() — consulte un mutex/event nommé, pas de SHM
  - GetProcessesByName("vrserver") — process check Windows

Résultat : check VR robuste mais réduit à "SteamVR actif + HMD présent
oui/non". Plus de batterie, plus d'info per-device, le device picker du
dialog devient cosmétique (tous les aliases retournent la même réponse).

La batterie + l'info per-device reviendra en Phase 1.5 via une approche
sub-process : un PSLauncher.VrProbe.exe séparé qui fait les vtable calls
et communique par stdout JSON. Si VrProbe crashe en AccessViolation, le
launcher voit juste "exit code != 0" et report "VR query failed" sans
être affecté lui-même.

Le code de la vtable (delegates, BindVTableDelegates, Init/Shutdown) reste
en place dans OpenVrService.cs comme référence pour Phase 1.5 — n'est
juste plus appelé depuis Query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:16:16 +02:00
07ead6011f v0.23.2 — Fix AccessViolation pendant le démarrage de SteamVR
Cause : race condition. Quand l'utilisateur lance SteamVR alors que le
launcher tourne déjà, vrserver.exe apparaît dans la liste des process
avant que ses drivers et ses shared memories soient prêts à servir des
requêtes vtable. La séquence problématique :

  1. tick health loop → IsVrServerRunning() = true (vrserver vient juste
     d'apparaître dans la process list)
  2. VR_InitInternal2 → succeed (init est tolérant, ne lit pas la SHM)
  3. VR_GetGenericInterface → vtable pointer valide
  4. premier appel vtable (IsTrackedDeviceConnected, slot 21) → lit dans
     une shared memory pas encore allouée par vrserver → AccessViolation
  5. SEH passe au travers du CLR via Marshal.GetDelegateForFunctionPointer
     → process killed sans que catch (Exception) ne se déclenche.

Stack confirmée par l'Event Viewer Windows :
  System.AccessViolationException
    at OpenVrService.Query(System.String)
    at SystemHealthService.CheckVrDevice(...)
    at health loop background task

Fix : cooldown de 8 s après la première détection de vrserver. On note
le timestamp UTC de la première apparition, et on refuse l'init tant que
moins de 8 s n'ont passé. Pendant le warmup, la pill reste sur "SteamVR
non lancé" (HmdAbsent), puis bascule sur Ready au tick suivant. Si
vrserver disparaît, on reset le timestamp + on tear-down la session
proprement, prêts pour un prochain cycle de démarrage.

8 s = compromis : sur SSD le ramp-up des drivers SteamVR prend ~3-5 s,
sur HDD ça peut monter à 7 s. La marge couvre les deux.

Bonus diag : flush Serilog avant que le process meure dans le hook
AppDomain.UnhandledException. Sans ça, la trace de la fatale n'arrivait
jamais sur disque (exactement ce qu'on a vu dans les logs : silence
total avant le redémarrage suivant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:08:32 +02:00
b94c0235b6 OpenVR : log la décision d'état au niveau INFO
Single ligne par transition de state (RuntimeMissing → HmdAbsent → Ready).
Si jamais ça re-crash dans cette zone, on aura la dernière trace avant le
silence de Serilog ; ex. « OpenVR state: HmdAbsent (vrserver.exe pas lancé) ».
Pas de spam : on log uniquement à la transition (un check tourne toutes les
5 s, sans dédup ce serait 700 messages/heure).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:42:26 +02:00
c997859750 v0.23.1 — Fix crash OpenVR quand SteamVR non lancé
Deux bugs corrigés :

1. Marshalling bool incorrect sur VR_IsHmdPresent / VR_IsRuntimeInstalled.
   OpenVR renvoie un bool C++ (1 octet) ; sans MarshalAs(I1) le marshaller
   .NET lit 4 octets (Win32 BOOL) et obtient des valeurs random. Conséquence :
   VR_IsHmdPresent passait parfois à "true" même si aucun casque n'était
   détecté, et on tentait alors une init OpenVR sans backend → crash.

2. Garde process avant init. Même avec le bool marshalling correct,
   VR_IsHmdPresent peut renvoyer true si un HMD est branché en USB sans
   que SteamVR soit lancé. VR_InitInternal2 en mode Background tente alors
   de charger les drivers (lighthouse, oculus_link…) qui peuvent crash en
   SEH si vrserver n'a jamais été démarré sur cette session. On ajoute
   une vérification de la présence de vrserver.exe avant de tenter l'init,
   avec cache 2 s pour ne pas faire un GetProcessesByName à chaque tick.

Résultat : si SteamVR est installé mais pas lancé → pill "SteamVR pas lancé"
proprement, sans tenter d'init et donc sans risque de crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:38:35 +02:00
4d0e287227 v0.23.0 — SteamVR / OpenVR : check VrDevice avec batterie + tracking
Nouveau Kind « VrDevice » dans le bandeau de santé. Pour chaque appareil
SteamVR (HMD, manettes, trackers, base stations) on lit via OpenVR :
batterie %, état de charge, niveau d'activité (actif / veille / inactif)
et serial number. Severity du pill calculée sur le batterie : < 15% rouge,
< 30% orange, sinon vert ; charging force le vert.

Implémentation : P/Invoke direct dans openvr_api.dll (BSD-3) — pas de
NuGet, pas de bundling de la DLL. La runtime est résolue dynamiquement
via DllImportResolver depuis :
  1. %LocalAppData%\openvr\openvrpaths.vrpath (canon écrit par SteamVR),
  2. fallback C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\.

Si SteamVR n'est pas installé → pill « SteamVR non installé » (Unknown,
gris). Si SteamVR pas lancé → pill « SteamVR non lancé / aucun HMD »
(Error, rouge). Init en mode VRApplication_Background, donc on lit l'état
sans devenir scene application ni monopoliser le compositor.

Éditeur : nouveau radio « Appareil SteamVR » à côté de Process / Ping ;
quand il est sélectionné, le champ Target devient une ComboBox d'alias
(HMD, Manette gauche/droite, Tracker 1-3, Base station 1-2) au lieu du
TextBox libre. Le mapping alias → TrackedDeviceIndex se fait à chaque
query (controllers via GetTrackedDeviceIndexForControllerRole, trackers
et base stations par énumération avec compteur de classe).

Robustesse : init lazy une fois pour toute la vie du process (init/shutdown
répétés coûtent 100-500 ms). Vtable IVRSystem_022 résolue par index slot
dans une struct contiguë de IntPtr (pas de struct marshaling, donc immune
aux changements de slots qu'on n'utilise pas). Si SteamVR crash en cours
de session, le service détecte au prochain tick et marque la session
broken pour retenter au tick suivant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:29:57 +02:00
211af9dd85 v0.22.0 — Health checks editor : icon picker + per-check refresh
Settings → Avancés → Bandeau de santé : liste éditable des checks (Add /
Edit / Delete) avec dialog modal pour chaque entry. Picker d'emoji parmi
28 icônes curatées (VR / streaming / réseau / système). Édition du nom,
type (Process / Ping), cible, intervalle de refresh en ms (per-check),
et seuils Ping (warn / error / timeout) sous une section Avancé.

La config est persistée dans %LocalAppData%\PSLauncher\config.json donc
elle survit aux auto-updates du launcher. Modifiable aussi à la main
dans le json pour les power-users.

Le bandeau hot-reload après Save : cancel des boucles de polling
existantes, reconstruction des HealthIndicators depuis la config fraîche,
puis relance d'une boucle dédiée par check (chacune cadencée sur son
propre RefreshIntervalMs — Process check rapide peut tourner à 1 s,
Ping reste à 5-10 s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:09:39 +02:00
741e30b7ae Layout : sidebar spans rows 1+2+3, health banner cantonné Col 1
Restructure visuelle pour que la sidebar reste le « chapeau de tout »
côté gauche, comme la top bar l'est en haut :

  ┌──────────────────────────────────────────┐
  │           Top bar (ColSpan 2)             │  Row 0
  ├─────────┬────────────────────────────────┤
  │         │  Health banner (Col 1)         │  Row 1
  │ Sidebar ├────────────────────────────────┤
  │ (Col 0  │  Content (Library / Rep / Doc) │  Row 2
  │  RowSpan│                                │
  │   = 3)  ├────────────────────────────────┤
  │         │  Footer (Col 1)                │  Row 3
  └─────────┴────────────────────────────────┘

Auparavant le health banner traversait toute la largeur (ColSpan=2),
donc la sidebar ne commençait qu'EN-DESSOUS — décalée verticalement
par rapport au logo PROSERVE de la top bar. Maintenant le banner est
cantonné à la colonne content (Col 1), la sidebar (Col 0, RowSpan=3)
court sur toute la hauteur sous la top bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:57:25 +02:00
93f54d094a Theme : dark global ToolTip style
WPF's default ToolTip is a white-on-black system look that clashes
with the dark theme — visible whenever the user hovers a button with
ToolTip="…", a health banner pill, the « ⋯ » menu hint, etc.

Global TargetType=ToolTip style :
  Background  : Brush.Bg.Card
  Foreground  : Brush.Text.Primary
  Border      : Brush.Border, 1 px, 6 px corner radius
  Padding     : 12 × 8 (more breathing room than default)
  MaxWidth    : 360 — so the longer health-banner tooltips wrap to a
                second/third line instead of running off the right edge
  HasDropShadow : true (the only WPF-native cue that says « this is a
                  tooltip floating above » since we removed the OS skin)
  Inner TextBlock style cascades TextWrapping=Wrap on string-content
  tooltips so multi-line messages format nicely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:52:38 +02:00
d61c1d85fa Health banner Library-only (mirror of footer scoping)
Same logic as the footer : the health pills make sense only when the
user is on the Library page. On Reports / Documentation, the WebView
takes the full content area and the system-status row would just steal
vertical space without adding value (and arguably distracts from the
embedded site).

New ShowHealthBanner = HasHealthIndicators && IsLibrary, with
NotifyPropertyChangedFor on CurrentPage so the row collapses/reveals
on tab switch like the footer already does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:50:44 +02:00
60358a6cf5 v0.21.0 — System health banner with colored indicators + tooltips
New row between top bar and content showing pill-shaped status indicators
for the system dependencies that PROSERVE needs (SteamVR, Vive Business
Streaming process, VR headset reachable on the network, etc.). Each pill
is colored : 🟢 OK, 🟠 limitation détectée (e.g. ping élevé), 🔴 KO,
gris = non configuré. Hover → tooltip with full check kind, target,
last RTT/process count and timestamp.

Architecture
- LocalConfig.HealthChecksConfig : list of HealthCheckEntry (Kind=Ping
  or Process, Target=IP/host or process name, ping thresholds in ms,
  refresh interval in s).
- ISystemHealthService + SystemHealthService : ping via
  System.Net.NetworkInformation.Ping (no admin required), process
  lookup via System.Diagnostics.Process.GetProcessesByName. Returns
  HealthResult { Severity, Detail }.
- HealthIndicatorViewModel : observable wrapper per entry with
  Severity-driven Brushes (background pill, border, icon colour) and
  composed Tooltip text.
- MainViewModel.InitHealthIndicators + StartHealthLoop : populates
  ObservableCollection<HealthIndicatorViewModel> from config and runs
  parallel checks every RefreshIntervalSeconds (default 10s).
- MainWindow.xaml : new Row 1 (between top bar and body) hosting an
  ItemsControl bound to HealthIndicators. Row collapses cleanly if the
  list is empty (HasHealthIndicators=false).

Defaults shipped (all editable in %LocalAppData%\PSLauncher\config.json)
- 🎮 SteamVR (Process : vrserver)
- 📡 Vive Business Streaming (Process : HtcConnectionUtility)
- 🥽 Casque VR (Ping : empty, user fills in the headset IP)

The Settings UI editor for managing the list is intentionally deferred
to a future iteration — config.json edit is enough to start using it.

Versions bumped to 0.21.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:44:15 +02:00
30eceaea2c v0.20.0 — Cancel-from-red-button waits for segments + new Verifying row state
Two related UX bugs fixed.

1) Red "Annuler" button on a row : DL appeared to keep going

The button calls RestartFromZeroAsync which used to fire
_activeDownloadCts.Cancel() then sleep 200 ms then delete the .partial.
With 16 parallel segments mid-write, 200 ms is way too short — the
segments were still flushing buffers when the file got deleted, then
recreated it from their own write streams, making it look like the
download "continued".

Now we keep a reference to the in-flight install Task
(_activeInstallTask) and properly await it (with a 10 s safety timeout)
before discarding state. This guarantees all segment FileStreams are
closed and the .partial deletion is the final word.

2) Progress bar said "Downloading…" during SHA-256 verification

The footer message switched to "🔍 Vérification SHA-256 v…" but the
row's badge stayed on "⬇ Downloading…" because row.State stayed at
Downloading throughout DownloadAsync (which internally chains DL +
verify). New VersionRowState.Verifying inserted between Downloading
and Installing, applied on the first hashProgress callback. Badge now
reads "🔍 Vérification…" / "🔍 Verifying…" during the hash phase.

VersionRowViewModel.IsBusy now also includes Verifying so commands
that gate on busy stay disabled during verification.

Versions bumped to 0.20.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:34:18 +02:00
3c8438f3fe v0.19.0 — Bump default parallel segments 8→16 to shorten end-of-DL tail
Issue : on a 14 GB download with 8 segments, when 7 finish there's only
1 connection left handling its assigned 1.75 GB → apparent throughput
collapses to 1/8th of the peak for the final stretch (~5 min visible
slowdown at the end).

Quick fix : double the default segment count.
- 8 segments  : last has 1.75 GB to finish alone
- 16 segments : last has 875 MB → tail ~halved

Tuning surfaced in Settings → Avancés → Serveur as
« Connexions parallèles pour les téléchargements (1-32, défaut 16) ».
Power users on faster servers can push to 24-32. OVH mutualisé tolerates
up to 16-24 ; above that, soft rate-limiting kicks in.

Aligned :
- LocalConfig default : 8 → 16
- DownloadManager clamp : Math.Clamp(_, 1, 16) → 1, 32
- HttpClient handler MaxConnectionsPerServer : 16 → 32 (matches new ceiling)

Versions bumped to 0.19.0.

Existing config.json files will keep their previous value (8) untouched ;
delete config.json to pick up the new default, or just bump it in
Settings → Avancés.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:28:35 +02:00
0bd4c8a4be Theme : pull palette back towards black (v0.18 was too navy)
User feedback : the blue shift was too much. Reining in the saturation:
  Window  : #0A1220 → #050A14
  Sidebar : #0E1422 → #0B1018
  Card    : #161D2C → #131826
  Footer  : #060A14 → #04070D
  BlueTint overlay : 8% opacity → 4%

The result is a near-black palette with just a hint of blue depth, rather
than a clearly navy one. Stays in the dark-theme zone while keeping a
discreet ASTERION VR vibe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:17:28 +02:00
8366ca2669 v0.18.0 — Bluer dark palette to match ASTERION VR identity
Shifts the launcher background from pure black to deep navy and adds a
subtle blue tint over the body bitmap so the overall feel is closer to
the brand's blue/cyan accent rather than neutral black-on-grey.

Theme.xaml palette tweaks
- Brush.Bg.Window  : #000000 → #0A1220 (deep navy, ~95% black)
- Brush.Bg.Sidebar : #0E1218 → #0E1422
- Brush.Bg.Card    : #161B23 → #161D2C
- Brush.Bg.Footer  : #050709 → #060A14
- New Brush.Bg.BlueTint : #3050A0 @ 8% opacity, used as overlay

MainWindow.xaml
- Outer Grid Background : hardcoded "Black" → Brush.Bg.Window
  (so future tweaks of the brush propagate)
- New Rectangle on top of the body Background.png with the blue tint
  brush, IsHitTestVisible=False so it doesn't block clicks.

Versions bumped to 0.18.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:30:24 +02:00
e608a5d29d v0.17.0 — Doc tool auto-deploy + revert (mirror of Report)
Adds the same deploy + backup/revert mechanism as Report, but for the
local Documentation web tool :
  PROSERVE-source/_doc/  →  C:\xampp\htdocs\ProserveDoc\
  http://localhost/ProserveDoc/

Default DocumentationUrl in the launcher set to http://localhost/ProserveDoc/
so the Documentation sidebar tab works out of the box on a fresh install
without any manual config.

Implementation
- LocalConfig.DocToolConfig (HtdocsRoot/FolderName/AutoDeploy/MaxBackups,
  defaults to ProserveDoc folder, 3 backups kept).
- IDocToolDeployer + DocToolDeployer : near-duplicate of the Report
  deployer with SourceSubdir = "_doc". Reuses ReportTool's BackupInfo /
  DeployStatus / DeployResult / DeployProgress types so we don't fork
  the data model.
- MainViewModel : new install step 7 right after the Report deploy step,
  mirrors its progress reporting + error dialogs (silent on
  XamppNotFound since the Report step already warned for the same root).
- SettingsViewModel : DocBackupViewModel + Doc properties + RedeployDoc
  + Revert commands. Loads the doc backups list in background like
  the Report ones.
- SettingsDialog.xaml : new « OUTIL DOCUMENTATION » card under the
  « OUTIL REPORT » one in Avancés, with the same fields and backup table.
- Strings.cs : doc-specific status / progress / error labels in 5 langs ;
  reuses some labels from Report where the wording is identical.

Versions bumped to 0.17.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:18:13 +02:00
c50abeb8d0 infos pass 2026-05-03 10:58:51 +02:00
fd1e9e9e6b gitignore : catch-all WebView2 cache and renamed launcher exes
- *.exe.WebView2/ : couvre les variantes de noms (PSLauncher.exe.WebView2
  legacy + PS_Launcher.exe.WebView2 actuel + tout build de test futur)
- /PS_Launcher*.exe : un seul glob pour PS_Launcher.exe, PS_Launcher.Updater.exe,
  PS_Launcher-X.Y.Z.exe, PS_Launcher_X.Y.Z.exe (séparateur tiret OU underscore)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:51:50 +02:00
886245cc61 v0.16.0 — ThemedMessageBox replacing native MessageBox everywhere
The Win32 MessageBox uses the system white-on-gray look that clashes
with the launcher's dark theme (jarring transition every time we
confirm a cancel, show an error, etc.). Replaced with a custom WPF
dialog that:

- Drops in : same Show(message, title, button, icon, default) API as
  System.Windows.MessageBox, returns the same MessageBoxResult.
- Looks native to the launcher : dark Brush.Bg.Window background,
  Brush.Border outline, Brush.Text.Primary content, AccentButton for
  the default action and SecondaryButton for the others.
- Iconography by emoji + colour code :  red (Error), ⚠ amber (Warning),
   neutral (Question), ℹ blue (Information). Mapped from MessageBoxImage.
- Buttons localised via Strings.ActionOk / ActionYes / ActionNo /
  ActionCancel — works in fr/en/zh/th/ar like the rest of the UI.
- Owner = currently active window (falls back to MainWindow then
  CenterScreen) so it positions correctly even from a child dialog.
- Esc resolves to Cancel/No matching MessageBox semantics.

The 21 MessageBox.Show call sites across MainViewModel, SettingsViewModel,
LicenseDetailsDialog and MainWindow now use ThemedMessageBox.Show with
no signature change — full grep replace + tiny `using` adjustments.

Versions bumped to 0.16.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:46:53 +02:00
42dd0701f5 Footer : drop the leftover Margin=200 hack
The 200px left margin was a workaround when the footer was at outer
Row=2 with no column constraint and had to dodge the sidebar visually.
Now that Row=2 + Col=1 cleanly cantons the footer in the content
column (commit ab40b62), the margin pushes everything 200px further
right and creates a black gap at the bottom-left of the content area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:43:01 +02:00
ab40b62430 Layout : sidebar spans full window height, footer scoped to content column
Restructured the outer Grid to a proper 2×3 layout instead of stacked
rows-with-margins :

  Outer Grid       Col 0 (200)   |   Col 1 (*)
  Row 0 (Auto)   ───── Top bar (ColSpan 2) ─────
  Row 1 (*)        Sidebar      |  Content area
                   (RowSpan 2)  |  (Library / Report / Doc)
  Row 2 (Auto)                  |  Footer (only on Library)

Previously the sidebar lived inside an inner body Grid limited to Row 1,
and the footer was at outer Row 2 with a hand-crafted 200px left margin
to fake the sidebar carve-out — which left a visible footer-background
strip across the bottom and broke the visual continuity of the sidebar.

Now :
- Sidebar Grid.Row=1 + RowSpan=2 → reaches the bottom of the window
  regardless of footer visibility
- Footer Grid.Row=2 + Grid.Column=1 → cleanly cantonné dans la colonne
  content, ne déborde plus
- Top bar Grid.Row=0 + Grid.ColumnSpan=2 → toujours full width
- Background image limitée à Row=1 + Col=1 → ne déborde ni sous la
  sidebar ni sous le footer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:38:07 +02:00
24d1841844 v0.15.0 — release notes Markdown : readable inline code
Bumps to consolidate the dark-theme fix for inline `code` in the
release notes viewer (commit 6d11b43) into a versioned client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:33:23 +02:00
6d11b43d17 Markdown : restyle inline code for the dark theme
Markdig.Wpf's default inline-code style applies a light Background +
"fancy" Foreground that becomes a near-illegible mustard-on-charcoal
mess in our dark theme.

Detect inline code by its FontFamily (Consolas / Courier / Typewriter,
applied by Markdig's default style) and rebrand:
  Background = #1A1F2A  (dark, blends with card)
  Foreground = #CDD6E4  (soft off-white, easy contrast)
  FontFamily = Cascadia Code, Consolas, monospace

Affects both the UpdateAvailableDialog and the ReleaseNotesViewerDialog
since they share BuildThemedDocument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:25:37 +02:00
89c78c9b37 1.5.3 release notes: switch to English
The project is multi-language with English support across the launcher
UI; release notes follow suit. Same content, English wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:22:41 +02:00
27fa793351 PROSERVE v1.5.3 release notes
Markdown rendered by the launcher (Markdig) in the "Mise à jour
disponible" dialog. Sections : Nouveautés, Améliorations, Corrections,
Migration, Notes — same template as 1.4.6.

Covers : MM CQB scenario, weapon persistence per session, stat icon
refresh, VR-controller-only quick calibration, instructor UI fix.

Also documents the 3 SQL migrations the launcher will run automatically
+ the report tool backup/revert mechanism, so support can point users
to the right Settings panel if anything goes sideways post-install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:19:46 +02:00
d09deaddb7 build-proserve-zip.bat : zippe une release PROSERVE prête à uploader
Usage : build-proserve-zip.bat 1.5.3
  → lit version\proserve-1.5.3\
  → produit version\proserve-1.5.3.zip (binaires UE5 + _migrations + _report)
  → affiche taille + SHA-256

Auto-détecte 7-Zip s'il est installé (multi-thread, 3-5x plus rapide sur
14 Go). Fallback sur PowerShell Compress-Archive (mono-thread) sinon.

Sans argument : liste les versions disponibles dans version\.
Avec checks : warn si _migrations/ ou _report/ ou PROSERVE_UE_5_5.exe
manquent (continue après 5s).

Sortie pédagogique à la fin : prochaines étapes (SFTP upload + actions
backoffice à effectuer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:12:58 +02:00
c706d09e72 Drop legacy PSLauncher.exe backward compat (not in prod yet)
Simplifies the rename done in ce3979d : PS_Launcher.exe is the only
name. Removes the dual-path lookup in LauncherSelfUpdater, the
duplicated taskkill blocks in the .bat scripts, the legacy patterns
in .gitignore, and the explanatory comments about the migration.

Cleaner code, single source of truth for the binary name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:08:23 +02:00
ce3979d131 v0.14.0 — rename PSLauncher.exe → PS_Launcher.exe (with backward compat)
Aligns the executable name with the repo / product naming. Backward-
compatible: existing installs that have PSLauncher.exe on disk continue
to work — the self-updater overwrites the file at the running .exe path
without renaming, so the historical filename persists on those machines.

Changes
- AssemblyName : PSLauncher → PS_Launcher (both App and Updater csproj)
- Inno Setup : MyAppExeName, MyUpdaterExeName, OutputBaseFilename all
  now use PS_Launcher prefix
- LauncherSelfUpdater : looks for PS_Launcher.Updater.exe first, falls
  back to legacy PSLauncher.Updater.exe so old installs keep updating
- Build scripts (build-launcher / build-updater / build-installer /
  build-all) : taskkill both legacy AND new names; output paths printed
  with new names
- .gitignore : added PS_Launcher.exe / PS_Launcher.Updater.exe /
  PS_Launcher-*.exe patterns alongside the legacy ones; also ignored
  the WebView2 user-data folder and Office ~$ lock files
- Server admin/launcher.php : URL pattern now generates
  PS_Launcher-{ver}.exe ; SignManifest's existing tolerant glob
  *{ver}*.exe still matches both names
- Versions bumped to 0.14.0 (App + Updater + installer .iss)

Migration story for clients
- Brand-new install via PS_Launcher-Setup-0.14.0.exe → PS_Launcher.exe
  on disk
- Existing install (PSLauncher.exe) auto-updates to v0.14 → file stays
  named PSLauncher.exe but contains v0.14 code; self-updater fallback
  ensures future updates keep working

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:55:41 +02:00
64fbda0191 Footer : align under content area, not under the left sidebar
The footer was at outer Grid.Row=2 with no column constraint, so it
spanned the full window width — visually overflowing under the 200px
left sidebar. Added a 200px left margin so the footer starts where
the content area starts. Comment makes the magic-number coupling
explicit so it stays in sync if the sidebar width changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:43:02 +02:00
1acb735641 PROSERVE v1.5.3 migrations : idempotent SQLs (FK syntax fix)
Schema source-of-truth for the proserveapi DB, versioned outside the
release ZIPs (the actual binaries live in version/ but are gitignored).

0001_Init.sql — full bootstrap of proserveapi (12 tables + view +
reference data + FKs). Reworked from a phpMyAdmin dump to be safely
re-runnable on:
  - fresh install (everything created)
  - existing customer DB pre-populated manually before launcher rollout
  - re-import via phpMyAdmin or mysql CLI
Idempotency tools used:
  - CREATE TABLE IF NOT EXISTS
  - INSERT IGNORE INTO ...  for reference rows
  - ADD PRIMARY KEY / KEY / UNIQUE KEY IF NOT EXISTS
  - ADD FOREIGN KEY IF NOT EXISTS `name` (...)  REFERENCES ...
    (note: MariaDB does NOT accept `ADD CONSTRAINT IF NOT EXISTS name
     FOREIGN KEY ...` — the IF NOT EXISTS goes after FOREIGN KEY, not
     after CONSTRAINT)
  - CREATE OR REPLACE VIEW
  - Removed the dump's `START TRANSACTION` / `COMMIT` (the launcher
    wraps each script in its own tx; embedded BEGIN/COMMIT would break
    the rollback semantics)
  - Removed `DEFINER=root@localhost` (uses CURRENT_USER, more portable)

0002_SessionType_ps_1.4.5.sql — adds id=7 'LongRange' + renames id=6
to 'Rescue'. Now uses INSERT IGNORE for the new row (skips if 0001
already inserted it on a fresh install) and the UPDATE is naturally
idempotent.

0003_UserSize_ps_1.5.0.sql — adds users.size column. Now uses
ADD COLUMN IF NOT EXISTS (the column is also in the recent 0001
baseline, so on a fresh install this will skip; on an upgrade from a
pre-0003 DB it actually adds it).

Validated locally on MariaDB 10.4 (XAMPP) with two consecutive runs of
each file: exit code 0 on every run, no errors.

.gitignore tightened so version/ only tracks _migrations/*.sql — the
~14 GB UE5 binaries and the _report/ build artifacts stay out of git.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:35:14 +02:00
562b4e5ed0 v0.13.0 — Report tool : timestamped backups + manual revert
The previous deploy mechanism kept the old version under {target}.old
just long enough to swap, then deleted it — no way back if the new
report had a runtime bug only visible at use time.

Now each deploy keeps the previous active version under
{FolderName}.backup-yyyyMMdd-HHmmss/. The N most recent are kept
(MaxBackups, default 3) and older ones are pruned in best-effort.

UI in Settings → Avancés → Outil Report :
- "Conserver N backups" input (0 = no history, back to v0.12 behavior)
- List of backups with date + size + cryptic folder name + per-row "↶ Revert"
- Confirmation dialog: "Revert to {date}? The currently deployed
  version will itself be saved as a new backup, so you can switch back."

Implementation
- IReportToolDeployer: + ListBackupsAsync, + RevertAsync, + BackupInfo,
  + DeployStatus.BackupNotFound.
- ReportToolDeployer: deploy renames {target} → backup-{ts}; PruneOldBackups
  trims to MaxBackups; RevertAsync atomically swaps current ↔ chosen
  backup, the previous current becoming itself a fresh backup.
- ReportBackupViewModel + DataTemplate for the list rows.
- Strings: backup labels, "↶ Revert", confirmation message.

Caveats documented in the design discussion:
- Backups cover ONLY the report tool files. DB migrations are not reverted
  (irreversible). If a migration broke the schema, that's a separate fix.
- No HTTP-HEAD post-deploy auto-revert: too fragile for false positives.
  Revert stays a deliberate manual action.

Versions bumped to 0.13.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:15:09 +02:00
67f422539d v0.12.0 — Report tool auto-deploy from PROSERVE ZIP
Each PROSERVE ZIP can ship a _report/ subfolder alongside _migrations/.
After install + DB migration, the launcher copies _report/ to the local
XAMPP htdocs (default C:\xampp\htdocs\ProserveReport\) so the Reports
tab in the sidebar always serves the matching version.

Service (PSLauncher.Core/ReportTool)
- IReportToolDeployer + ReportToolDeployer.
- Atomic deploy: copy to {target}.new/ → rename current to .old/ →
  rename .new → final → cleanup .old. Apache never sees a half-state.
- Skip silencieux si _report/ absent du ZIP (PROSERVE release qui ne
  touche pas au Report).
- DeployStatus enum (SkippedNoSourceFolder, XamppNotFound, Failed,
  Deployed) renvoyé pour gestion UI claire.

Config (LocalConfig)
- ReportToolConfig (HtdocsRoot, FolderName, AutoDeploy). Defaults
  matchent l'install standard ASTERION (C:\xampp\htdocs\ProserveReport).

Install pipeline (MainViewModel)
- Étape 6 après extraction + migrations. Progress reporté via le footer
  Library : « 📂 Déploiement Report : 47/120 — assets/main.js ».
  XAMPP introuvable → dialog avec lien vers Settings → Avancés.

Settings UI (SettingsDialog → Avancés)
- Nouveau bloc « OUTIL REPORT (XAMPP htdocs) » : 2 champs path + checkbox
  AutoDeploy + bouton « 📂 Re-déployer maintenant » qui rejoue le deploy
  sur la dernière version installée. Utile après changement de chemin
  htdocs ou si l'auto-deploy avait foiré (XAMPP éteint).

Versions bumped to 0.12.0 (App + Updater + installer .iss).

Côté release : tu ajoutes _report/ dans le source de PROSERVE, tu zippes,
tu uploades. Les clients récupèrent ZIP + migrations + Report en une
seule install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:05:40 +02:00
a2e67d5405 Footer is Library-only: hide on Report/Documentation tabs
The download/install progress footer made no visual sense on the
WebView pages — it would float above the embedded site. It now only
appears when CurrentPage = Library. The DL itself keeps running in
the background while the user browses Reports or Docs; switching
back to Library restores the live footer.

Implementation: FooterVisibility now ANDs with IsLibrary, and
CurrentPage notifies FooterVisibility/FooterText so the footer
collapses/reveals on tab switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:00:26 +02:00
7a09f73c44 Sidebar: move from right to left
Left-sidebar layout matches the convention of Steam, Discord, Spotify,
VSCode, etc. — nav anchored to the leading edge. Also feels less
intrusive: in a Library-centric default view the eye lands on the hero
card first, sidebar fades into the chrome on the left.

Just swaps the two ColumnDefinitions, the Grid.Column attributes and
the Border's BorderThickness (left edge → right edge of the sidebar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:56:07 +02:00
6d251e9eac v0.11.0 — right sidebar nav with Reports / Documentation tabs
User-visible feature change (sidebar appears on every screen) deserves
a minor bump rather than a patch. WebView2 dependency adds ~5 MB to the
single-file exe (the Edge runtime itself is system-installed, not
shipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:53:14 +02:00