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>
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>
- New action `remove_machine` releasing a single (license_id, machine_id) slot.
- Pre-fetches all machines in one query and groups by license_id (no N+1).
- Machines column "X / Y" is now clickable: opens an inline expandable row
showing each machine on that license — truncated SHA-256 ID with full hash
in tooltip, machine_label, first_seen, last_seen with a "stale" warning
badge for slots not seen in >30 days, plus a per-row "Libérer" button.
- Existing "Libérer machines" button kept but renamed "Libérer toutes" with
a beefier confirmation that hints at the per-row alternative.
Replaces the all-or-nothing reset workflow with surgical control: when one
user changed PCs you can free their old slot without touching their colleagues'.
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>
- gate.php : ETag now derived from size+mtime instead of md5_file($zip).
Previously the gate hashed the entire 14 GB ZIP on every HEAD/GET call
(including each of 8 parallel segments) → 30s-5min preparation lag for
clients before the first byte. Now <1ms per request.
- SignManifest : disk-backed cache for SHA-256 keyed by (path,size,mtime).
Re-signing 5×14 GB versions used to take ~25 min, now ~1s when nothing
changed. New "Force re-hash" toggle in admin to ignore the cache.
- versions.php : per-row "🔁 Hash" button to sign a single version, plus
a "⚙ Hash" dropdown to toggle hashAlgorithm:none for builds where the
user accepts skipping client-side verification (manifest stays signed
Ed25519, only the per-ZIP SHA-256 verification is bypassed).
- DownloadUrl.php : signed-URL TTL bumped 1h → 6h to cover slow ADSL users
who need >1h to finish a 14 GB download.
- .gitignore : track server/builds/.htaccess + gate.php (still ignore the
actual ZIP/exe binaries).
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>