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>
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>
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>
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>
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>
The body of the launcher is now a 2-column layout:
- Left : swappable content area
- Right (200px) : sidebar with 3 nav buttons + active-state highlighting
Three pages, all under the same window chrome / footer:
1. **Library** (default) — the existing main view (featured version,
other versions list, floating "Vérifier les MAJ" button, copyright).
2. **Reports** — embedded WebView2 navigated to a configurable URL.
Default http://localhost/ProserveReport/ which matches the local
stats tool installed on each customer machine alongside XAMPP.
3. **Documentation** — WebView2 with a configurable URL, falls back
to a placeholder explaining where to set it if empty.
Implementation
- LocalConfig gains ReportUrl + DocumentationUrl (string).
- MainViewModel gains LauncherPage enum + CurrentPage state with
Navigate{Library,Report,Documentation}Command. ReportUri /
DocumentationUri parse the strings to Uri for WebView2.Source.
- Theme.xaml: NavButton style (transparent, left-aligned, hover
darken, active state highlighted via Tag bool + accent left-border).
- InverseBoolToVisibilityConverter added (true → Collapsed) for the
documentation placeholder fallback.
- Microsoft.Web.WebView2 NuGet (1.0.2792.45). Runtime is pre-installed
on Win11 and auto-pushed via Windows Update on Win10. If absent,
WebView2 surfaces an error which the user sees inline.
- Settings → Avancés → Serveur extended with the two URL fields.
- Strings.cs: NavLibrary / NavReport / NavDocumentation,
SettingsReportUrl / SettingsDocsUrl, DocsPlaceholder, WebViewLoadError.
Sidebar localized in 5 languages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorders the bottom of the Settings dialog so the "À propos" section
(version + copyright) sits between Langue and the collapsed Advanced
expander. The Expander becomes the very last item, since power-user
plumbing is naturally what the eye should reach last.
DB default name was already set to "proserveapi" in the previous commit
(LocalConfig.cs line 64) — re-confirmed.
Final order:
1. License de mise à jour [expanded]
2. Langue [expanded]
3. À propos [expanded]
4. ▸ Paramètres avancés [collapsed]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DB default name: "proserve" → "proserveapi" (matches the ASTERION install
script that provisions XAMPP on client machines).
- Settings dialog reorganized:
1. License de mise à jour [expanded] ← business-critical
2. Langue [expanded] ← user preference
3. ▸ PARAMÈTRES AVANCÉS [collapsed by default]
contains Server, Installation, Cache, Database, Logs
4. À PROPOS [expanded] ← version + copyright
- The Expander hides plumbing the casual user shouldn't touch (server URL,
install root, MySQL config, cache directory) but keeps it one click away
for power users / support diagnostics.
- About card kept always-visible because version info is what support asks
for first when troubleshooting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps version across the three places it lives:
- PSLauncher.App.csproj : 0.9.0 → 0.10.0
- PSLauncher.Updater.csproj : 0.8.0 → 0.10.0 (catch-up; the updater
hadn't been bumped since 0.8 — version doesn't gate behavior, just
matches the .exe metadata)
- installer/PSLauncher.iss : 0.8.0 → 0.10.0 (installer artifact name
PSLauncher-Setup-0.10.0.exe)
This release adds bundled SQL migrations applied automatically after
each PROSERVE install, so a new build that needs schema changes ships
the .sql alongside its binaries and the launcher rolls them out without
manual intervention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The « Update license expired » header was truncating the « Expired on X.
You can still download versions… » subtitle: no TextWrapping on the
TextBlock and the inner StackPanel had no horizontal width constraint.
Switched the icon-and-text layout from a horizontal StackPanel to a
2-column Grid (Auto + *) so the text column expands to the Border width.
TextWrapping="Wrap" on title + subtitle, icon now top-aligned to follow
the wrap. Window switched to SizeToContent="Height" with MinHeight=480
so it grows when the localized message wraps to 3+ lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New « BASE DE DONNÉES (XAMPP / MySQL) » section in Paramètres covering:
- Host / Port / User / Password / Database — defaults match a fresh XAMPP
install (localhost:3306, root, empty password, "proserve"). Password is
DPAPI-encrypted in config.json (same scheme as the license key).
- « Tester » button — opens a fresh MySqlConnection and runs SELECT 1.
Shows ✓ OK or the connection error inline.
- « Appliquer automatiquement les migrations à l'install » checkbox — opt-in
for the post-install migration step. Default true.
- « 🔁 Rejouer les migrations » button — manually re-runs ApplyMigrationsAsync
on the latest installed version. Useful when the post-install run failed
(XAMPP was off) or after a dev added a new SQL file. Live status « 3/5 :
0042_add_index.sql » + final « ✓ N applied, M skipped » or « ✗ failure ».
- Hint paragraph below explaining the _migrations/ convention.
SettingsViewModel
- Pulls IDatabaseMigrationService and IInstallationRegistry via DI.
- Save() now also persists the DB block, encrypting the password before write.
- TestDbAsync temporarily swaps the in-memory config so the service sees the
values being typed (without persisting until Save).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each PROSERVE ZIP can now ship a _migrations/ subfolder with versioned SQL
scripts. The launcher applies them right after extraction, in the same
install session as the binary copy, so the user never lands on a PROSERVE
build whose schema doesn't match its code.
Service (PSLauncher.Core/Migrations)
- IDatabaseMigrationService + DatabaseMigrationService (MySqlConnector,
BSD-licensed). Each .sql runs in a transaction; on failure the DB
state is rolled back and the install completes (files only) but the
user is warned to fix the connection / replay later.
- Tracking table _launcher_migrations (filename, applied_at, checksum,
duration_ms) — same model as Flyway / Doctrine. Already-applied scripts
are skipped on subsequent installs. Modified scripts trigger a warning
log without blocking.
- Custom SQL splitter that respects strings/comments/backticks so a single
.sql file can contain multiple statements separated by `;`.
- DatabasePasswordProtector: DPAPI CurrentUser scope for the MySQL password
in config.json (same protection as the license key).
Config (PSLauncher.Models/LocalConfig.cs)
- New DatabaseConfig section: Host=localhost, Port=3306, User=root, empty
password, Database=proserve, AutoApplyMigrations=true. Defaults match a
fresh XAMPP install. Override via Settings (next commit).
Install pipeline (MainViewModel)
- After ZipInstaller.InstallAsync and before declaring the install complete,
if AutoApplyMigrations and a _migrations/ folder exists, run
ApplyMigrationsAsync with progress reporting (per-file %, filename in
footer). Failure shows MsgMigrationFailed dialog explaining XAMPP must
be running and pointing to Settings → Database for connection params.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX
- Red "Annuler" button next to "↻ Reprendre" on rows with a partial download.
Confirmation MessageBox before discarding the .partial + state.json.
- "Recommencer depuis zéro" entry in the row's "..." context menu, visible
only when a partial exists.
- Cancel button visibility tied to a new ShowRestartFromZero property
(HasResumableDownload && State == AvailableIdle): hides during active DL
to avoid colliding with the inline cancel.
- ResumableBytes refreshed from state.json after cancel/error so the
"Reprendre (X%)" label reflects the actual percentage reached, not the
stale value from launcher startup.
- MainWindow.Closing prompts for confirmation if a download is in progress
(HasActiveDownload), so an accidental ✕ doesn't waste a 14 GB transfer.
- "🔧 Préparation du téléchargement v…" status set immediately on click and
kept visible during HEAD/SetLength so the user knows something's happening
before the first byte. ProgressDetail wired with NotifyPropertyChangedFor
(FooterText) so download speed shows during DL and not just at install
transition.
Auto-revalidation
- CheckForUpdatesAsync now runs RefreshLicenseFromServerAsync first: every
startup auto-check + every manual "Vérifier les MAJ" click pulls the
latest license state from /api/license/validate. Date changes / revocations
done in the backoffice propagate to clients without waiting for the 100-day
cache to expire. Silent fallback to cached state if offline.
Version bumped to 0.9.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CachedValidityDays bumped 7 → 100 days. Covers the "user goes online once
every 3 months for updates" use case without repeatedly nagging for re-auth.
- LastValidationAt now uses response.ServerTime (signed Ed25519 by the server)
rather than DateTime.UtcNow, so the client's local clock can't be used to
forge a fresh validation date.
- LastSeenUtc monotonic counter, persisted on every launch as max(stored, now).
Combined with a 1h grace window: if the user rolls their PC clock backward
to extend the offline cache, the rollback is detected (now < LastSeen - 1h)
and the cache is invalidated → forced re-validation next time online.
- CachedStatus persists the server-returned status (valid/expired/revoked/
invalid/machine_limit_exceeded) so a revocation done while the user is
offline still shows correctly when they next launch with cached data.
Auto-override to "expired" if entitlement date has passed (handles the
valid → expired transition without needing a server round-trip).
- ILicenseService.GetDecryptedKey() exposed for the new auto-revalidation flow.
This puts the trust boundary in the right place: cached data is informational
offline, the actual download authorization always re-validates against the
server (the gate to download is /api/download-url/, which checks the live
license state on each call). User can't fake offline-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DownloadManager
- Parallel multi-segment download (default 8 connections, configurable up to 16)
with per-segment Range requests. Bypasses OVH/Apache per-connection bandwidth
throttling — typical speed-up ×4 to ×8 vs single connection.
- Reporter task on a dedicated Task with Interlocked aggregate counter (no lock
contention with workers). Reports speed/ETA every 250ms even mid-download,
fixes the "speed only shows at install transition" bug.
- Sparse file pre-allocation via FSCTL_SET_SPARSE before SetLength: no zero-fill,
no SeManageVolumePrivilege, instant on any disk type. Removes 5-30s of
preparation lag on HDD.
- HEAD probe skipped (trust manifest size, signed Ed25519). Falls back to
single-segment if first segment returns 200 instead of 206.
- Resume URL comparison fixed: ignores HMAC querystring (?exp=&sig=) which
changes per request, compares only host+path. Previously every resume started
fresh because the old URL never matched the freshly signed one.
- Auto-refresh signed URL on 403/410 mid-DL: SemaphoreSlim with 5s debounce so
8 simultaneous segment expirations trigger a single /api/download-url/ call.
Slow-connection users (1 Mbps, 30+ hours for 14 GB) keep downloading
transparently across multiple TTL cycles.
- Per-version hashAlgorithm:none in manifest skips client SHA-256 verification
(still relying on Ed25519 manifest signature + HMAC URL).
- DangerButton style (red) added to Theme.xaml for the new cancel-resume action.
IntegrityService
- 16 MiB buffer (was 1 MiB), FileOptions.SequentialScan + Asynchronous,
IncrementalHash (uses SHA-NI hardware extensions on .NET 8), double-buffering
to overlap CPU and I/O. Typical 14 GB hash verification: 60-180s → 15-40s.
HttpClient
- MaxConnectionsPerServer=16, EnableMultipleHttp2Connections, HTTP/2 preferred,
AutomaticDecompression=None (ZIPs are already compressed), 5min pooled
connection lifetime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Strings.cs : ~80 new keys covering status messages, progress detail
(DL/extraction/verify), license summary, license details dialog, onboarding
statuses, settings field labels, restart/cancel/quit confirmations, toasts,
resume choice, copyright. Strings.FormatSize and Strings.FormatDate /
FormatLongDate switch units (o/Ko vs B/KB) and pattern (dd/MM/yyyy vs M/d/yyyy
vs 2026年) by active language.
- License terminology renamed across UI: "License" → "License de mise à jour"
/ "Software Update License" to clarify it gates UPDATES, not Proserve itself.
Expired/revoked dialogs now spell out "you can still download versions
released before this date and launch any installed version".
- "🔒 License insuffisante" → "🔒 License de mise à jour requise pour cette
version" / "Valid update license needed for this version" so users understand
it's a per-version eligibility check, not a global block.
- All hardcoded FR strings in views (SettingsDialog labels, LicenseDetails
fields, Onboarding statuses, dialog titles, copyright, window chrome
tooltips, "Sortie le", etc.) replaced with x:Static loc bindings.
- All FormatSize duplicates (5 places) and date format strings (8 places)
delegate to Strings helpers — single source of truth for localization.
- Settings dialog: License section moved to the top before Language. It's
the most important info and conditions what the user can download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous batch only covered XAML strings. The runtime MessageBox
dialogs (errors, confirmations, info popups) stayed hardcoded in
French, breaking immersion for non-FR users.
Added ~15 keys to Strings.cs for the MessageBox bodies and titles:
- error / launch error / confirm / info / patience / language change /
release notes (titles)
- launch failed / self-update failed / install failed / uninstall
failed / uninstall confirm with version+folder+size / no release
notes / fetch failed / clear cache confirm / deactivate license
(short and detailed) / language restart (bodies)
Some are parametrized (MsgLaunchFailed(detail), MsgUninstallConfirm
(version, folder, size)) so the localized strings interpolate the
runtime values cleanly across all 5 languages.
Replaced every MessageBox.Show in MainViewModel, SettingsViewModel
and LicenseDetailsDialog.xaml.cs to use these keys. The "Échec : "
prefix in the cache-clear error was dropped — the error message alone
is sufficient with Strings.MsgBoxError as the dialog title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues with the language picker after the dark restyling:
1. Click on the center of the ComboBox was a no-op — only the arrow
triggered the dropdown. The previous template had a separate
ToggleButton in column 1 covering only the arrow region. New
template wraps the entire body in a single ToggleButton (with its
own template carrying the border + arrow), and the ContentPresenter
for the selected text floats on top with IsHitTestVisible="False"
so clicks pass through to the toggle.
2. Selected item showed "LanguageOption { Code = auto, Name = ... }"
instead of just the name. C# record types stringify with property
names by default. Override LanguageOption.ToString() to return Name
so the SelectionBox falls back to a clean label even though
DisplayMemberPath="Name" is set on the dropdown items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The native WPF ComboBox style is light grey on white — unreadable on
the dark Settings dialog. Re-template both ComboBox and ComboBoxItem:
- Body: #1A1A20 background (same as other inputs), border with hover
highlight in Brush.Accent.
- Custom toggle button arrow in secondary text color, brightens on
hover.
- Popup: Brush.Bg.Card background with border, 4px radius, 300px max
height, scrollable.
- Items: highlighted hover in Brush.Accent with white foreground
(same convention as MenuItem). Selected item gets a subtle
#2C2C32 strip so the current value is visible after closing.
Affects every ComboBox in the app — only one for now (language
picker in Settings) but stays consistent if more get added later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Localization infrastructure
---------------------------
PSLauncher.Core/Localization/Strings.cs is a static class exposing each
UI string as a static property. T(fr,en,zh,th,ar) helper switches by the
current language code. ~50 keys cover the visible top-level strings:
top bar, body, status badges, action buttons, "..." menu items, license
badge texts, Settings section headers, Onboarding dialog, Update dialog.
Bindings via x:Static in XAML:
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
Content="{x:Static loc:Strings.ActionLaunch}"
Auto-detection
--------------
LocalConfig gains a Language field defaulting to "auto". Strings.Init()
called at App.xaml.cs OnStartup before any UI:
- "auto" (or unknown code) → reads CultureInfo.CurrentUICulture
.TwoLetterISOLanguageName, picks the matching supported language,
falls back to English.
- explicit code → forced.
The chosen culture is then propagated to CurrentCulture / CurrentUICulture
so date/number formats follow.
Settings picker
---------------
SettingsDialog gets a top section "LANGUE" with a ComboBox bound to
SettingsViewModel.AvailableLanguages (Auto / FR / EN / ZH / TH / AR).
On Save, if the language code changed, prompt the user to confirm
restart, spawn `cmd /c timeout 1 & start PSLauncher.exe` and Shutdown
the current process — the new instance picks up the language at
bootstrap.
RTL for Arabic
--------------
Strings.IsRightToLeft is true when lang=="ar". App.xaml.cs sets
window.FlowDirection = RightToLeft on the MainWindow — WPF mirrors the
layout (icons on right, text aligned right).
Translations
------------
Done as best-effort by the assistant. The product wordmark "PROSERVE"
stays untranslated (brand). Logs and debug-level messages remain in
French in code — only user-visible UI is localized. Operator can
refine translations by editing Strings.cs and rebuilding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The product wordmark is rendered uppercase everywhere in the UI
(PROSERVE in Pirulen). Align the install directory naming so it reads
"PROSERVE v1.4.7" instead of "Proserve v1.4.7" — same brand, same case
on disk and in dialog titles.
Changes:
- server/manifest/versions.json: installFolderTemplate updated for both
existing entries.
- server/admin/versions.php: default template for new versions added
via the backoffice form.
- src/PSLauncher.Models/RemoteManifest.cs: default fallback for the
property when missing from JSON.
- src/PSLauncher.App/Views/MainWindow.xaml + dialogs + ViewModel
toasts: UI strings now read "PROSERVE v..." consistent with the brand.
InstallationRegistry's regex was already RegexOptions.IgnoreCase, so
existing user installs in "Proserve v..." folders keep working
(case-insensitive on Windows filesystems anyway). Re-installing an
older version after the change re-creates the folder with the new
case — Windows is case-preserving but case-insensitive, so launching
remains identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After clicking Lancer, the launcher minimizes to the taskbar so Proserve
has the foreground. Add an Exited event handler on the launched Process
that brings the launcher back from the taskbar once the game closes.
EnableRaisingEvents is required (false by default) to receive Exited.
The handler runs on a worker thread so we marshal back to the UI via
Dispatcher.Invoke before touching WindowState. A brief Topmost=true /
false toggle nudges the window to the foreground without permanently
locking it on top.
Wrapped in try/catch — some ShellExecute spawns return a Process with
limited access (no event support), in which case we just log and let
the user click the taskbar icon manually.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-instance
---------------
Two PSLauncher.exe instances ran in parallel during the user's last
release build, locking the repo-root copy and breaking the publish.
Two-layer protection:
1. App.xaml.cs uses a Global\ named Mutex (UUID-based key) created at
OnStartup. If the mutex is already held, the second instance
PostMessages a registered Windows message (PSLAUNCHER_BRING_TO_FRONT)
to HWND_BROADCAST and Shutdown()s immediately.
2. MainWindow hooks WndProc via SourceInitialized + HwndSource.AddHook;
when it sees the broadcast, it restores from minimized, calls
ShowWindow(SW_RESTORE) + Activate() + SetForegroundWindow so the
already-running instance pops to the user's foreground.
Net result: clicking the launcher icon a second time pops the existing
window instead of starting a duplicate process.
Tolerant build pipeline
-----------------------
- Both csproj post-publish copy targets now have
ContinueOnError="WarnAndContinue". A locked PSLauncher.exe at the
repo root no longer fails the entire publish — the binary still
exists in bin\Release\...\publish\ and the user gets a warning
instead of an error.
- All four .bat scripts (build-launcher / build-updater / build-all /
build-installer) now run `taskkill /F /IM PSLauncher.exe /T` and the
same for PSLauncher.Updater.exe before the publish step. This
defensively closes any stray instance left behind by previous tests
so the build pipeline is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brand mark
----------
Resources/logo-asterion-white.png embedded as a WPF Resource. Placed
in the top-bar StackPanel before the PROSERVE Pirulen wordmark, at
36px height, with HighQuality bitmap scaling and SnapsToDevicePixels
to keep the white logo crisp against the dark chrome.
Minimize on launch
------------------
Once a Proserve version is started via _processLauncher.Launch(),
the MainWindow drops to WindowState.Minimized so it doesn't sit on
top of the game. The launcher stays running (so the user can come
back to install / switch / see the badge license), just out of the way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related issues with the background bitmap:
1. The footer (download progress bar) appeared on top of the image,
cropping the bottom strip — including the ASTERION VR mark in the
bottom-right corner of Background.png.
2. UniformToFill scales-and-crops to maintain aspect ratio, but with
the previous setup the crop was centered, so resizing the window
wider/narrower silently pushed the bottom-right logo off-screen.
Fix:
- The bitmap now lives in a Rectangle restricted to Grid.Row="1"
(body only). When the footer appears at Row 2, it sits below the
body without overlapping the image.
- Switch from <Image Stretch="UniformToFill"> to
<Rectangle><Fill><ImageBrush AlignmentX="Right" AlignmentY="Bottom"
Stretch="UniformToFill" /></Fill></Rectangle>. The crop now
happens on the LEFT and TOP edges, keeping the logo's corner pinned
to the bottom-right and visible regardless of window proportions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The min/max button hover was set to #FFFFFF22, intended as "white at
13% alpha". WPF Color literals are AARRGGBB though, not RRGGBBAA, so
that's actually opaque red+green+a-touch-of-blue → yellow-ish white,
unreadable on the dark chrome.
Switch to Brush.Accent (#3B82F6) on hover with white foreground,
matching the close button red on hover (#E81123) — both controls now
have clear, distinct hover states.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>