Deux bugs introduits par v0.27.x dans api/index.php :
1. La route /api/manifest ne require pas lib/Crypto.php → erreur 500
« Class PSLauncher\Crypto not found » dès qu'on tente la re-signature
du manifest filtré (depuis v0.27.0). Conséquence : le client voit
tout cassé, pas seulement les versions filtrées.
2. La regex de route /api/releasenotes/X.Y.Z n'accepte que les versions
au format SemVer. Avec v0.27.1, les release notes sont addressables
par id stable (v + 8 hex chars), donc le manifest pointe vers
/api/releasenotes/va3f2c891 qui matchait pas la regex → 404.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
À 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- *.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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>