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>
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>
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>
- 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>
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>
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 check-updates button was anchored inside the footer band, which
felt heavy and obscured the background bitmap. Move it to a floating
position: bottom-left of the body, 32px from the left edge, 24px above
where the footer would render. The button now sits directly over the
ASTERION VR background art, giving the chrome a lighter look.
The footer goes back to its original behavior — visible only when busy
or when there's a status message — instead of being permanently shown
just to host the check button.
Implementation: wrap the body's ScrollViewer in a single-cell Grid so
a sibling Button with HorizontalAlignment=Left, VerticalAlignment=
Bottom can overlay it without interfering with the version cards or
the scroll behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top bar (3-column layout)
-------------------------
- Col 1: PROSERVE Launcher wordmark.
- Col 2: license badge centered, the badge IS the click target now (a
Border with an InputBindings MouseBinding LeftClick to OpenLicense).
No more separate "🔑 Activer / changer" button cluttering the right.
- Col 3: ⚙ Paramètres + window chrome (min/max/close).
- "📁 Dossier" button removed from the top bar — install root is still
reachable from Settings.
Footer (always visible)
-----------------------
- Row 1: "🔄 Vérifier les MAJ" pinned bottom-left, always shown. The
optional "Annuler" button stays bottom-right while a download runs.
- Row 2: status text + progress bar, only shown when busy or after a
status message — the previous "footer entirely hidden when idle"
hid the check button too.
License flow split in two
-------------------------
Click on the license badge:
- License is active (valid / expired / revoked) → LicenseDetailsDialog
opens. Header pill in matching status color (green/amber/red), shows
owner, validity, issued date, machine ID with copy-to-clipboard. Two
buttons: "🗑 Désactiver la license" (with confirmation) and Close.
- No license OR after deactivation → falls through to the existing
OnboardingDialog for re-keying.
Settings rework
---------------
LICENSE section is now first in SettingsDialog with the same
green/amber/red colored chrome as the top bar — at a glance the user
sees the same status everywhere. Machine ID copy moved into this card.
Sign-manifest no longer needs exec()
------------------------------------
The "🔁 Sync" button in admin/versions.php previously shelled out to
`php tools/sign-manifest.php` via exec(). OVH mutualisé often disables
exec(), causing silent no-ops and the symptom the user just hit: the
ZIP changed (new size 469,657,770 vs manifest's stale 469,831,428) and
the launcher rejected it as size mismatch.
Refactor:
- New PSLauncher\Tools\SignManifest class with ->run() that does the
hashing, latest-bump and Ed25519 signing in-process.
- tools/sign-manifest.php is now a 6-line wrapper for the class.
- admin/versions.php's 'sync' action calls the class directly via
require_once + new — works on any host, no exec dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Window
------
- Default size 1280x720 (16:9 widescreen — matches the screenshot framing
the user requested), MinWidth=980 MinHeight=600.
- WindowStyle=None + ResizeMode=CanResize: drop the OS title bar but
keep edge resize.
- WindowChrome: CaptionHeight=56 makes the top bar Border the
drag-handle for moving the window. ResizeBorderThickness=6 gives
6px of grab area on each edge, GlassFrameThickness=0 disables Aero.
Custom chrome buttons
---------------------
Three new style keys in Theme.xaml:
- WindowControlButton: 46x32 transparent button, hovers at #FFFFFF22
(semi-translucent white) — used for minimize and maximize/restore.
- WindowCloseButton: same shape, hovers at #E81123 (Win11 red).
Three handlers wired up: OnMinimizeClick, OnMaxRestoreClick, OnCloseClick.
Caption click-through
---------------------
WindowChrome reserves the caption area for window dragging by default —
buttons inside it would otherwise feel "dead". Each interactive control
in the top bar (Vérifier les MAJ / Dossier / License pill / Activer /
Paramètres / min / max / close) gets
shell:WindowChrome.IsHitTestVisibleInChrome="True" so clicks reach the
button instead of starting a drag. The text/logo region remains
draggable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The implicit Window style in Theme.xaml WAS being applied, but defensive
coding wins: when MainWindow.Background was unset, WPF's intermediate
composition surface for elements with Opacity<1 (the Image) blended
against whatever the parent presented. In some Windows render paths
that's effectively the system white. Setting Window.Background="Black"
AND Grid.Background="Black" pins the surface under the image to pure
black explicitly, no system theme involvement.
Also drop the previous "darken overlay" Rectangle — once the surface
under the image is genuinely black, the image at 0.25 opacity is
already where we want it, no second dimming layer needed.
Launcher subtitle: FontWeight Light → Bold so it competes equally with
PROSERVE in the Pirulen wordmark.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The background image was too dominant at 50% opacity over a black
window — bright spots of the engraved logo flooded the body and made
the chrome look milky. Reduce image opacity to 0.18 and add a 35%
black rectangle on top, dimming the image to roughly 12% effective
brightness while keeping the texture readable.
"Launcher" wordmark next to PROSERVE was barely visible (14pt, secondary
grey). Now 20pt, FontWeight Light, primary white — sits naturally
beside PROSERVE without competing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Window background
-----------------
Brush.Bg.Window is now pure #000000 (was a slightly blue-tinted dark).
The previous tint added value to the bitmap overlay, washing out the
image's bright spots. With pure black behind, the 50% overlay sits
cleanly on top.
Cards / sidebar / footer keep a faint blue tint (#161B23, #0E1218,
#050709) so the chrome reads as cool/dark while the body shows the
hardware texture clearly through the gaps.
Background image
----------------
Resources/Background.png (the engraved ASTERION VR wordmark on dark
metal) is now an embedded WPF Resource on the App project. MainWindow
draws it as the first child of the root grid, RowSpan=3, Stretch=
UniformToFill, Opacity=0.5, IsHitTestVisible=False so it never steals
clicks. It shows behind the body's empty regions; the cards' solid
backgrounds cover it where readability matters.
Pirulen wordmark
----------------
Resources/pirulen.otf (the user-supplied OpenType font) is embedded as
a Resource and exposed via Theme.xaml's Font.Brand FontFamily resource
using the standard pack URI form (#Pirulen). The top bar replaces the
plain "PROSERVE Launcher" with a horizontal stack: PROSERVE in 22pt
Pirulen + a smaller "Launcher" label in secondary text. The brand
typeface is reusable elsewhere via {StaticResource Font.Brand}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The license info was rendered as plain secondary-grey text, easy to
miss. Now it's a rounded pill (CornerRadius=14) with traffic-light
colors driven by the new MainViewModel.LicenseSeverity property:
- Ok → green pill (Brush.Status.Installed) with ✓ icon
- Warning → amber pill (Brush.Status.Busy) with ⏰ icon, fires when
entitlement is < 30 days away from expiry
- Error → red pill (#EF4444 / #2A1414 bg) with 🔒/⚠/🚫 icon
depending on whether license is missing, expired, revoked
Two new VM properties accompany LicenseSummary:
- LicenseIcon: emoji glyph for the pill
- LicenseSeverity: enum-like string Ok/Warning/Error driving DataTriggers
Both are kept in sync via NotifyLicenseChanged(), called wherever
_license used to trigger only OnPropertyChanged(LicenseSummary).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings (⚙ button in top bar)
------------------------------
SettingsDialog opens via OpenSettings command in MainViewModel. Sections:
- Serveur: server URL field + "Tester" button that GETs /api/health
and reports status inline.
- Installation: installRoot path with Browse button
(Microsoft.Win32.OpenFolderDialog, .NET 8 native).
- License: shows status / owner / exp / machine ID (read-only,
copy-to-clipboard button), "Désactiver" wipes the cached license.
- Cache: shows download cache path + current size, opens or empties it.
- Logs & Application: launcher version + logs path with "Open" button.
Apply persists to LocalConfig via IConfigStore. After save, MainViewModel
RebuildList()s so changes to ServerBaseUrl or InstallRoot take effect
without restart.
Persistent Serilog logs
-----------------------
Logs now live in %LocalAppData%/PSLauncher/logs/app-YYYYMMDD.log, rolling
daily, 10 days kept. Output template includes source context and stack
traces. Generic Host wired up via .UseSerilog() so all
ILogger<T>-injected types share the sink. Unhandled AppDomain and
Dispatcher exceptions are routed to Serilog before propagation.
Windows toasts
--------------
IToastService + ToastService backed by Microsoft.Toolkit.Uwp.Notifications
(ToastContentBuilder.Show()). Required bumping the App TFM from
net8.0-windows to net8.0-windows10.0.17763.0 (with explicit
WindowsSdkPackageVersion=10.0.17763.41 to satisfy CommunityToolkit.Mvvm
8.3.2's MVVMTKCFG0003 check). Triggered on:
- successful install completion: "Proserve v{X} est prête à être lancée"
- install/download error: short error excerpt
Misc
----
- InverseBoolConverter for "disable button while busy" patterns.
- Added Markdig.Wpf import to ReleaseNotesViewerDialog (was implicit
before, now required explicitly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX bonus: clicking "Reprendre" on an interrupted download skips the
release-notes confirmation dialog (the user already approved when they
first clicked Installer).
Server side
-----------
- migrations/001_init.sql: licenses, license_machines, rate_limit,
audit_log on InnoDB/utf8mb4. Foreign keys, unique on license_key, slot
uniqueness per (license_id, machine_id).
- api/lib/Db.php: thin PDO singleton (exception mode, prepared, no emul).
- api/lib/Crypto.php: Ed25519 sign/verify via libsodium (sodium_crypto_*),
HMAC-SHA-256 helper for v0.6, canonicalJson() that strips `signature`
before serializing — must match exactly the encoding done client-side
before verify.
- api/routes/ValidateLicense.php: POST /license/validate. Looks up the
key, walks the machine slot logic (insert or update last_seen),
enforces max_machines, returns a payload signed Ed25519 + status of
valid/expired/revoked/machine_limit_exceeded/invalid. Audit logs every
outcome. Rate-limit 10/min/IP via the rate_limit table.
- tools/generate-keypair.php: prints a fresh sodium keypair so the
operator drops the hex into config.php and the public_key_hex into the
launcher resource.
- tools/issue-license.php: PRSRV-XXXX-XXXX-XXXX-XXXX generator (32-char
unambiguous alphabet), inserts the license, prints the key once.
- tools/sign-manifest.php: now also signs the manifest itself with
Ed25519 after computing the per-zip sha256s.
- config.example.php: schema rewritten with sections db / hmac / ed25519
/ jwt / rate-limit. config.php remains gitignored.
Client side
-----------
- Models/License.cs: LicenseValidationRequest + LicenseValidationResponse
with CanDownload(VersionManifest) — entitlement_until vs version's
minLicenseDate. The status valid|expired|revoked|machine_limit_exceeded
flow is preserved end-to-end.
- Core/Licensing/LicenseService.cs:
* machineId = SHA-256 of HKLM/Software/Microsoft/Cryptography/MachineGuid
+ UserName (stable, no PII leak)
* online ValidateAsync calls /license/validate with launcher version
* embedded server-pubkey.txt drives Ed25519 verification of the
response (skipped gracefully if pubkey not yet provisioned)
* SaveCached / GetCached use DPAPI CurrentUser scope on the license
key; the cleartext key never touches disk
* GetCached has a 7-day offline grace window after the last successful
validation, so going offline doesn't lock the user out
- Core/Resources/server-pubkey.txt: EmbeddedResource. Default content is
a comment, which the service treats as "no pubkey configured" and
bypasses verification. Operator pastes the real hex post-deploy and
rebuilds.
- Core/PSLauncher.Core.csproj: Polly, NSec.Cryptography (Ed25519),
System.Security.Cryptography.ProtectedData (DPAPI).
- App/Views/OnboardingDialog.xaml(.cs): first-launch / "🔑 Activer"
modal. Calls LicenseService, displays status messages with red
foreground on errors and green-tinted secondary text otherwise.
- ViewModels/VersionRowViewModel.cs: new LicenseAllowsDownload property.
Install button label switches to "🔒 License insuffisante" when the
user's entitlement_until precedes the version's minLicenseDate;
CanInstall is false in that case so the click is a no-op too.
- ViewModels/MainViewModel.cs: loads the cached license at startup (no
network call), surfaces it as LicenseSummary in the top bar, exposes
ActivateLicenseCommand to (re)open the onboarding dialog. RebuildList
applies the per-version license filter so older installed versions
remain launchable but newer-than-license ones can't be downloaded.
- Views/MainWindow.xaml: top bar gains a "🔑 Activer / changer" button
next to the license summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the robustness layer needed for real 14 GB builds. A 99%-complete
download that gets interrupted no longer means re-downloading 14 GB.
DownloadManager
---------------
- Range: bytes={resumeFrom}- on every (re)attempt; reads resumeFrom from
the actual size of the .partial file so retries always resume from real
on-disk state, not from a remembered counter.
- If-Range: ETag (or Last-Modified fallback). When the server returns 200
instead of 206 we know the resource changed under us, so we discard
.partial and start fresh.
- Polly resilience pipeline: 6 retries, exponential 1-32s with jitter,
on HttpRequestException / IOException / TimeoutException / 5xx / 408 /
429. Each retry re-evaluates resumeFrom from disk, so the server is
asked only for what's actually missing.
- state.json persisted every 5s OR every 100 MiB, whichever comes first,
via atomic write-then-rename. Holds url, total, downloaded, sha256,
etag, last-modified, and the .partial path.
- Disk-space check happens once at fresh-start (1.05x expected size); a
resume doesn't redo it.
- On final success: SHA-256 of the assembled .partial verified, then
atomic rename to .zip and state.json deleted.
DownloadStateStore
------------------
- New IDownloadStateStore in PSLauncher.Core/Downloads.
- Stores under %LocalAppData%/PSLauncher/downloads/.
- Save / Load / Discard / ScanResumable. Tolerates malformed state files
by ignoring them.
UI hint
-------
VersionRowViewModel now has ResumableBytes; when > 0, the install button
label switches to "↻ Reprendre (X%)" computed from
ResumableBytes / Remote.Download.SizeBytes. MainViewModel.RebuildList
queries IDownloadManager.GetResumableState(version) for each remote-only
row and populates ResumableBytes. Both the featured hero card and the
compact rows bind to InstallButtonLabel.
API change
----------
IDownloadManager gains GetResumableState(version) and
DiscardResumableState(version) so callers (and the UI) can reason about
in-progress downloads without poking at the filesystem directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Featured version
----------------
The highest installed version (or highest remote if none installed) now
gets a large hero card at the top of the window: 32px Proserve title,
oversized colored status pill, and a much bigger primary action button
(LANCER / INSTALLER 56px padding). All other versions move below into a
"AUTRES VERSIONS" section with a horizontal divider, displayed as the
existing compact rows.
MainViewModel exposes FeaturedVersion + OtherVersions instead of one
flat Versions collection.
Vivid status colors
-------------------
Card backgrounds were too close (dark gray vs slightly bluer dark gray).
New scheme:
- Installed: card stays dark gray, but a 4px green strip on the left and
a vivid green "● Installée" pill make it unmistakable.
- Available remote-only: distinct dark blue card + vivid blue strip + blue
"○ Disponible" pill.
- Busy: amber strip + amber pill, amber progress bar.
Brushes added to Theme.xaml: Brush.Status.Installed (#16A34A),
Brush.Status.Available (#3B82F6), Brush.Status.Busy (#F59E0B), each with
a matching very-dark-tint *Bg variant for card backgrounds.
Markdown theming
----------------
Release notes dialogs were unreadable (white background, near-white text).
Markdig.Wpf produces a FlowDocument with default white bg / black fg
regardless of the host control. Added MarkdownTheming.BuildThemedDocument
which renders the Markdown then walks all blocks/inlines to apply the
launcher's dark palette (Brush.Bg.Card transparent, Brush.Text.Primary
foreground, Brush.Accent links). Both UpdateAvailableDialog and
ReleaseNotesViewerDialog use this helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the sidebar + hero + single big-button layout with a flat list of
per-version cards. Each card carries its own action button:
- Installed: ▶ Lancer (green, primary)
- Available on server only: ⬇ Installer (blue, accent) + lighter blue card
- Busy: inline mini progress bar with %, card tinted green
Each card also exposes a "..." menu (left-click opens it) with:
- Voir les release notes (works for installed and remote-only versions)
- Ouvrir le dossier (installed only)
- Supprimer cette version (installed only, with confirmation dialog)
VersionRowViewModel owns its state (InstalledIdle / AvailableIdle /
Downloading / Installing / Uninstalling) and its commands; MainViewModel
wires per-row handlers after instantiation so the row VM stays UI-only
and the services live one layer up.
ReleaseNotesViewerDialog: separate dialog reused by the menu — same
Markdown rendering as UpdateAvailableDialog but no download CTA.
Theme: AccentButton + IconButton + dark ContextMenu/MenuItem styles.
Behavior changes:
- The single global SelectedVersion + AvailableUpdate are gone; each row
is independently actionable.
- The list now merges installed + remote: a version present on the
server but not locally appears as a "remote-only" row, and vice-versa.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client (C# / .NET 8 / WPF, MVVM via CommunityToolkit.Mvvm):
- PSLauncher.App: WPF UI dark theme (Epic-style sidebar + hero + big play button)
with UpdateAvailableDialog rendering Markdown release notes via Markdig.Wpf.
- PSLauncher.Core: services for installation registry (scans Proserve v{X.Y.Z}/),
process launcher, manifest fetch, SHA-256 integrity, HTTP download with
progress, ZIP install via .tmp + atomic rename, update orchestrator.
- PSLauncher.Models: RemoteManifest, InstalledVersion, LocalConfig DTOs.
Server (PHP 8 for OVH mutualisé, deployed under www/PS_Launcher/):
- Front controller + routes /manifest and /releasenotes/{version}.
- Static signed-manifest workflow with tools/sign-manifest.php CLI to
recompute SHA-256 and sizeBytes after each ZIP upload.
- .htaccess: HTTPS redirect, rewrite, security headers.
- config.example.php template; real config.php is gitignored.
Cohabiting versions: each release lives in its own Proserve v{version}/ folder
under installRoot. Old versions are never deleted automatically.
Roadmap: v0.3 = HTTP Range resume + Polly retry + state.json,
v0.4 = MySQL license + Ed25519 signatures + DPAPI, v0.5 = settings/UX polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>