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>