Commit Graph

67 Commits

Author SHA1 Message Date
750bb8cfc9 Settings: Database section with connection params, test, and replay button
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>
2026-05-03 08:25:32 +02:00
6f8a320d69 DB migrations: bundled SQL applied automatically after install
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>
2026-05-03 08:21:53 +02:00
48d601176d admin/licenses : per-machine view with individual remove
- 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>
2026-05-03 07:57:01 +02:00
7fb02e4945 v0.9.0 — DL UX: cancel/restart confirmations, auto-revalidate license, fix progress refresh
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>
2026-05-02 19:16:14 +02:00
07d58a8854 Licensing: 100-day offline cache with anti-rollback + cached server status
- 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>
2026-05-02 19:15:49 +02:00
9cea07d6be Downloads: parallel multi-segment, sparse pre-alloc, faster SHA-256, URL auto-refresh
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>
2026-05-02 19:15:27 +02:00
b4f205cbe5 i18n: full pass + culture-aware dates/sizes + Software Update License wording
- 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>
2026-05-02 19:14:57 +02:00
962f5a8ce0 Server: faster releases (cache hashes), per-version skip, longer signed-URL TTL
- 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>
2026-05-02 19:14:32 +02:00
0d4f126e3a i18n: localize all MessageBox dialogs
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>
2026-05-02 13:56:52 +02:00
b9ce591d22 ComboBox fixes: clickable center + display the option name, not the record
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>
2026-05-02 13:44:00 +02:00
5d0284f1db Theme: dark ComboBox to match the rest of the launcher chrome
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>
2026-05-02 13:10:14 +02:00
01a11d5616 i18n: 5 languages (FR/EN/ZH/TH/AR), auto-detect from Windows + Settings picker
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>
2026-05-02 13:02:50 +02:00
9c1b34b041 Compact rows: full-height accent strip with rounded left corners 2026-05-02 12:50:27 +02:00
ddfebb8611 Featured card: round the left edge of the accent strip to match card corners 2026-05-02 12:48:13 +02:00
2e320d2ab5 Brand: install folder uses uppercase PROSERVE v{version}
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>
2026-05-02 12:46:26 +02:00
7a12153343 LaunchVersion: restore launcher window when Proserve exits
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>
2026-05-02 12:41:54 +02:00
8988d130ac Single-instance launcher + tolerant build pipeline
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>
2026-05-02 12:31:11 +02:00
f5f1fe4d36 docs: PowerPoint deck for internal admin + end-user guide, embedded in installer
Two presentations generated from pptxgenjs scripts in docs/, both using
the launcher's dark palette (black bg, #161B23 cards, #3B82F6 blue
accent for internal, #16A34A green accent for the user guide):

- PS_Launcher-Documentation-Interne.pptx (17 slides)
  Cover, sommaire, architecture client/serveur/MySQL, setup OVH initial,
  config.php, génération secrets, backoffice (Dashboard / Licenses /
  Versions / Launcher / Audit), workflow release Proserve, workflow
  release launcher, security (Ed25519 / HMAC / DPAPI), file locations
  + logs, troubleshooting, closing.

- PS_Launcher-Guide-Utilisateur.pptx (14 slides)
  Cover, présentation du launcher, prérequis Windows, installation
  setup.exe (avec gestion SmartScreen), activation license PRSRV-,
  bibliothèque (featured + autres), lancer une version, installer une
  nouvelle version, suivi du DL avec resume, menu ⋯ par version,
  paramètres ⚙, auto-update du launcher, FAQ (license expirée /
  offline / changement PC / SHA-256 KO), support.

Both decks use cards with colored left-accent strips, numbered step
flows, code blocks (Consolas on #0A0E14), and consistent footer
branding "© 2026 ASTERION VR".

installer/PSLauncher.iss now copies the user guide into {app}\docs\
and creates a "Guide utilisateur" Start Menu shortcut next to the
launcher. The internal doc stays in the repo, never shipped to clients.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:11:35 +02:00
2dc9c98d1e Bump version 0.7.0 / 0.5.0 → 0.8.0 (synced across launcher, updater, installer)
After the auto-update test loop the App was at 0.7.0 and Updater /
Inno Setup were still at 0.5.0. Sync everything to 0.8.0 so a single
version covers the recent batch of branding + UX changes:

- Default installRoot is C:\ASTERION_VR
- ASTERION favicon (window icons + .exe icon + Setup icon)
- ASTERION wordmark left of PROSERVE in the top bar
- Centered, readable "© 2026 ASTERION VR — Tous droits réservés" pill
- Window control hover in blue (was unreadable yellow)
- Background image anchored bottom-right so the brand mark doesn't
  get cropped on resize or hidden by the download footer
- Launcher window minimizes when a Proserve version is launched

Next time the operator pushes a release, "Définir" 0.8.0 in the
backoffice → upload PSLauncher-0.8.0.exe → click the blue Sync.
Existing 0.7.0 launchers will then auto-update to 0.8.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:01:55 +02:00
89f039e355 UI: ASTERION logo left of PROSERVE wordmark + minimize on launch
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>
2026-05-02 11:59:03 +02:00
661c464b7a Background: anchor logo to bottom-right, never overlap the footer
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>
2026-05-02 11:54:20 +02:00
c27066ffe4 Theme: window control buttons hover in blue, not yellow-white
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>
2026-05-02 11:50:59 +02:00
3c31d1ea8a Copyright: center the floating pill at the bottom of the body 2026-05-02 11:49:55 +02:00
8ebde15535 Copyright: readable pill with proper "© 2026 ASTERION VR — All rights reserved"
The previous copyright was a faded TextBlock at Opacity 0.7 with the
secondary grey color, sitting directly on top of the variable-
luminance background image. Hard to read on the bright spots of the
ASTERION VR engraving.

Wrap it in a Border with #A0000000 background (~63% opacity black)
and CornerRadius=10 — same pill style as the license badge — so the
text reads cleanly regardless of what's behind. Foreground is now
plain white instead of the secondary grey.

Text changed from the terse "© ASTERION VR" to the proper convention:
- UI: "© 2026 ASTERION VR — Tous droits réservés" (matches the
  French-language UI in MainWindow + Settings)
- Assembly metadata (Properties → Détails on the .exe): English form
  "© 2026 ASTERION VR — All rights reserved"

Same change applied to PSLauncher.Updater.csproj.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:48:25 +02:00
dfad967eae branding: ASTERION VR favicon + copyright everywhere
Icon
----
src/favicon64.jpg converted to favicon.ico (single-resolution 64x64)
via PowerShell + System.Drawing.Icon.FromHandle. The .ico is now an
EmbeddedResource of PSLauncher.App and referenced as:
- <ApplicationIcon> on PSLauncher.App.csproj → .exe icon in
  Explorer / taskbar / Alt-Tab
- Window.Icon on every WPF window: MainWindow, SettingsDialog,
  OnboardingDialog, LicenseDetailsDialog, UpdateAvailableDialog,
  ReleaseNotesViewerDialog, LauncherUpdateDialog
- <ApplicationIcon> on PSLauncher.Updater.csproj → updater also
  carries the brand icon
- SetupIconFile in installer/PSLauncher.iss → the Inno Setup .exe
  installer shows the icon too

Copyright
---------
- <Company>, <Product>, <Copyright> assembly attributes set in
  both csprojs → properties dialog on the .exe shows "© ASTERION VR".
- MainWindow body has a discreet floating "© ASTERION VR" text in the
  bottom-right, mirroring the "Vérifier les MAJ" button on the left.
  Opacity 0.7 + secondary color so it doesn't compete with the cards.
- SettingsDialog "Logs & Application" section gains a
  "© ASTERION VR — Tous droits réservés" line under the version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:44:28 +02:00
3484c23683 ConfigStore: default installRoot to C:\ASTERION_VR for client installs
Previous default fell back to %UserProfile%\ASTERION_VR which is
fine for dev but unconventional for end users; the standard
deployment path is plainly C:\ASTERION_VR. New first-launch behavior:

1. If a sibling ASTERION_VR exists next to the exe (portable / dev
   layout), prefer it — keeps the development workflow intact.
2. Otherwise, default to C:\ASTERION_VR. The folder is created on
   the first install (Directory.CreateDirectory in ZipInstaller
   handles non-existent parents).

Existing users with an explicit installRoot in their config.json are
unaffected — only fresh configs (or empty installRoot) hit this path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:38:07 +02:00
e7d4b7a04c Add per-module build scripts at repo root
build-launcher.bat: publishes PSLauncher.App only (Release single-file).
build-updater.bat:  publishes PSLauncher.Updater only.
build-all.bat:      both, sequentially, fast-fail on the first error.

All three pause at the end so a double-click from Explorer shows the
result before the window closes. The existing build-installer.bat
(launcher + updater + Inno Setup .exe) stays as the full release
pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:34:23 +02:00
9cb0502338 LicenseService: signed URL via ?key= instead of Authorization header
Apache FastCGI on OVH mutualisé strips Authorization headers before
PHP sees them. The mod_rewrite [E=HTTP_AUTHORIZATION:%1] workaround
in .htaccess does not survive on this hosting profile (probably
because rewrite runs after the FastCGI handler accepts the request).
The endpoint already accepts ?key= as a fallback path, so use that
from the client. HTTPS still protects the key on the wire and the
launcher never logs URLs containing the key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:29:08 +02:00
466755ddc2 DownloadUrl: tolerate Apache FastCGI stripping the Authorization header
OVH mutualisé serves PHP via FastCGI which strips the Authorization
header by default for security. The user's curl test of GET
/api/download-url/{ver} returned 401 even with -H "Authorization:
Bearer <key>" because $_SERVER['HTTP_AUTHORIZATION'] was empty server-side.

Fix on two layers:

1. .htaccess at root re-injects the header into the Apache env so PHP
   can read it via the standard $_SERVER variable. The mod_rewrite
   one-liner `RewriteRule ^ - [E=HTTP_AUTHORIZATION:%1]` is the de-
   facto FastCGI workaround.

2. DownloadUrl.php now reads from any of: $_SERVER['HTTP_AUTHORIZATION'],
   $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] (passthrough variant),
   apache_request_headers(), getallheaders(). Belt and braces — works
   regardless of host config. Falls back to ?key= as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:26:38 +02:00
6da628d687 gitignore: also exclude .bak backup files at repo root 2026-05-02 11:21:48 +02:00
03483d02e1 launcher.php: fix PHP 8 fatal in saveManifest
`usort($manifest['versions'] ?? [], ...)` worked in PHP 7 but is a
fatal error in PHP 8 — usort requires its first arg by reference and
the null-coalesce operator produces an rvalue, not a variable. The
existing versions.php didn't trigger it because it sorts
$manifest['versions'] directly. Fixed launcher.php to guard with an
isset/is_array check and only sort when there's actually a versions
array.

Triggered by clicking "Définir" in the new Launcher page → 500 page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:21:29 +02:00
c24e67f560 Bump app version 0.5.0 -> 0.6.0 for the self-update test loop
The auto-update test was looping: the manifest declared launcher
v0.6.0 but the binary uploaded as PSLauncher-0.6.0.exe was still
internally v0.5.0 (same file, just renamed). After swap, the new
launcher's AssemblyVersion still reported 0.5.0, the manifest still
advertised 0.6.0, and the prompt fired on every restart.

Bump <Version> / <AssemblyVersion> / <FileVersion> to 0.6.0 so the
freshly published PSLauncher.exe genuinely identifies as 0.6.0. After
the swap, IsNewerThanCurrent will return false and the prompt will
stop reappearing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:06:51 +02:00
5411f607b6 Updater: file logging + relaunch fallback to direct Process.Start
The updater was a black box on failure (no console window, errors went
to a discarded stderr). When the user reported "launcher closes but
doesn't relaunch" there was nothing to inspect.

Add a log sink at %LocalAppData%/PSLauncher/logs/updater.log with
simple 256 KiB rotation. Every step (args parsed, PID wait, target
unlock, backup, copy, zone-strip, relaunch) writes a timestamped
line, plus the FATAL stack trace on uncaught exceptions.

Also strip the file's :Zone.Identifier ADS after copy. Internet-
downloaded exes carry this Mark-of-the-Web stream and Windows can
silently refuse to launch them via explorer.exe with no visible
error.

Relaunch becomes two-tier: try `explorer.exe "<target>"` for
de-elevation; if that fails or nothing spawns, fall back to direct
Process.Start with UseShellExecute=true. Better to inherit admin
than not relaunch at all.

ParseArgs strips surrounding quotes from --target/--source values
since the launcher now wraps paths in quotes (necessary for
UseShellExecute=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:04:42 +02:00
fed5be5916 gitignore: stop tracking built artifacts (PSLauncher-X.Y.Z.exe, installer output)
Last commit accidentally captured two large binaries that landed in the
working tree during the self-update test prep:
- PSLauncher-0.6.0.exe at repo root
- installer/output/PSLauncher-Setup-0.5.0.exe

Neither belongs in version control. Add /PSLauncher-*.exe and
/installer/output/ to .gitignore and `git rm --cached` the existing
entries so the next push doesn't reupload them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:57:56 +02:00
f2a1de9aac Self-update: UAC-aware, de-elevated relaunch, user-mode installer
Three coordinated changes so the auto-update flow works on every
existing install location.

LauncherSelfUpdater.cs
----------------------
- Probe the target directory with a write/delete of a temp file. If
  it fails (e.g. install in Program Files without admin), set
  UseShellExecute=true + Verb=runas on the Updater process so UAC
  prompts the user once for elevation. If the directory is writable
  (user-mode install or portable layout), skip elevation entirely.
- Switched from ProcessStartInfo.ArgumentList to a quoted Arguments
  string because Verb=runas requires UseShellExecute=true, which
  ignores ArgumentList. Paths get explicit quotes to survive spaces.

Updater Program.cs
------------------
After the file swap, the relaunch was a direct Process.Start(target).
With UAC elevation that propagates admin to the new launcher, then to
its child PROSERVE_UE_5_5.exe — undesirable. Replace with
`explorer.exe "<target>"`: explorer is always running in the user's
normal token, opens the exe via shell, the new process inherits the
non-elevated session. Standard de-elevation trick.

installer/PSLauncher.iss
------------------------
Switch from system-wide install (Program Files, requires admin) to
per-user (DefaultDirName={localappdata}\Programs\..., PrivilegesRequired=
lowest). Auto-update then never needs UAC at all on fresh installs.
Existing installs in Program Files keep working thanks to the runas
fallback above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:57:32 +02:00
56069f606d backoffice: dedicated Launcher page, scope-aware Sync
The launcher and the game versions are different release cadences
managed by the same operator. Mixing them on one page muddied the
workflow. Split them into two top-level nav entries.

SignManifest::run(string $scope)
--------------------------------
- 'all' (default, used by CLI tools/sign-manifest.php) — unchanged.
- 'versions' — only re-hashes the Proserve ZIPs and bumps `latest`.
- 'launcher' — only re-hashes the launcher EXE.
The Ed25519 sign step always runs at the end so the manifest stays
verifiable. Selectively hashing avoids unrelated noise (e.g. mass-hash
14 GB ZIPs when all you wanted was to update the launcher exe).

admin/launcher.php (new page)
-----------------------------
Self-contained page with the launcher state, Set/Remove forms, the
blue "🔁 Hasher le launcher + signer" button, and a list of the .exe
files present in builds/launcher/. Workflow doc inline.

admin/versions.php
------------------
Cleaned up: launcher card and its set_launcher / remove_launcher /
sync_launcher actions removed. The remaining global Sync button is
relabeled and now triggers scope='versions' (only Proserve ZIPs).

Layout::navHtml gains a "Launcher" item between Versions and Audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:53:14 +02:00
55ce5318ae backoffice: drop the redundant launcher Sync button
Reverting the previous addition. The blue "🔁 Recalculer hash + signer"
button on the launcher card called the same 'sync' action as the
global one in the manifest table — same code path, same effect. Two
identical buttons created confusion. The workflow text now points
operators to the existing global Sync button below, and only the
launcher-specific actions (Définir / Retirer) live on the launcher
card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:47:58 +02:00
36305c71e5 backoffice: add a blue "Sync" button on the launcher card itself
Operator setting up auto-update was forced to scroll down to the
"Manifest actuel" card and find the global Sync button after each step
of the launcher workflow. Add a dedicated "🔁 Recalculer hash + signer"
primary-blue button right inside the launcher card. It posts the same
action='sync' so behavior is identical — it's just discoverable from
where the operator is looking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:45:13 +02:00
5f1057d118 backoffice: separate launcher Set / Remove forms (nested form bug)
The previous markup had a <form action=remove_launcher> nested inside a
<form action=set_launcher>. HTML doesn't allow nested forms — browsers
flatten the structure unpredictably, and clicking the inner "Définir"
submit could end up posting the outer (or vice versa), causing the
section to be removed when the user actually wanted to set it.

Two sibling forms now, with a 12px gap between them. Same UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:42:31 +02:00
197b07966c backoffice: manage launcher auto-update from the Versions page
Two things change so the operator never has to SSH for a launcher
release.

1. SignManifest::run() now also re-hashes the manifest's `launcher`
   section. It looks for builds/launcher/<basename of launcher.url>;
   falls back to a tolerant glob *<version>*.exe if the exact name
   isn't found. Updates sizeBytes + sha256 in place. The Ed25519 sign
   step at the end already covered this branch — it was just blank
   data before.

2. admin/versions.php has a new "Auto-update du launcher" card above
   the manifest table. Shows the announced version + minRequired, the
   exe presence badge and the hash status, and a small form to set or
   update the launcher entry (version + minRequired only — URL is
   derived from base_url + version automatically). A "Retirer la
   section" button disables the auto-update by deleting the launcher
   key from versions.json. Lists the .exe files present in
   builds/launcher/ for visibility.

Workflow now: edit the version in the form → SFTP-upload
PSLauncher-X.Y.Z.exe to builds/launcher/ → click "🔁 Sync" once →
manifest is hashed and signed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:39:56 +02:00
6bc5381562 installer: use x64 (Inno Setup 6.0+) instead of x64compatible (6.3+)
The value x64compatible was added in Inno Setup 6.3 — earlier 6.x
versions reject it with "Value of [Setup] section directive
ArchitecturesAllowed is invalid". Using plain x64 keeps compatibility
with the whole 6.x line. Modern users on 6.3+ are still happy since
x64 still resolves correctly there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:54:05 +02:00
3da0696c23 Add build-installer.bat at repo root for one-click installer build
Wraps installer/build-installer.ps1 with -NoProfile and -ExecutionPolicy
Bypass so a double-click works without PowerShell config tweaks. Pauses
at the end so the user sees the ISCC output even when launched from
Explorer. Forwards %* for `-ISCC <path>` overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:53:01 +02:00
b78b0b8fb9 UI: float "Vérifier les MAJ" over the body, hide footer when idle
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>
2026-05-02 09:51:47 +02:00
45b2baf399 build: auto-copy published exes to repo root
After every `dotnet publish -c Release`, the single-file binaries land
in C:\ASTERION\GIT\PS_Launcher\ next to PS_Launcher.sln so they can be
launched directly without digging into bin/Release/.../publish/.

Implemented as an MSBuild AfterTargets="Publish" target on each csproj
(PSLauncher.App, PSLauncher.Updater) using $(MSBuildThisFileDirectory)
to resolve the repo root portably. Condition='Release' so debug builds
don't pollute the root.

.gitignore covers /PSLauncher.exe and /PSLauncher.Updater.exe so the
committed tree stays clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:47:29 +02:00
b7de228bc9 v0.6 + v1.0: HMAC download URLs, launcher self-update, Inno Setup installer
Three deliverables shipped together so the next deployment cycle has a
clean distribution story.

(1) Auto-update of the launcher itself
---------------------------------------
- Models: RemoteManifest gains an optional `launcher` section
  (LauncherInfo: version, minRequired, download {url,size,sha256},
   releaseNotesUrl). Server-side, sign-manifest.php passes it through
  unchanged; admins edit versions.json with the new launcher entry +
  upload PSLauncher-X.Y.Z.exe to /builds/launcher/.
- Core: ILauncherSelfUpdater compares assembly version against
  manifest.launcher.version using the existing SemVer parser, and
  reuses DownloadManager (Range/resume/sha256 already proven on the
  game ZIPs) to download the new exe into
  %LocalAppData%/PSLauncher/selfupdate/.
- New project PSLauncher.Updater (~34 MB self-contained console exe):
  spawned by the main app with --target / --source / --pid / --launch.
  Waits for the main process to exit (or for the file lock to release),
  backs up the current exe to .bak, copies the new file in place, and
  restarts. .bak survives the swap so the user can roll back manually.
- App.csproj now declares Version=0.5.0 — currently shipped baseline.
  PSLauncher.App.csproj sets a fixed AssemblyVersion so reflection-based
  comparison works deterministically.
- MainViewModel.PromptLauncherUpdate: dialog after CheckForUpdates if
  the manifest advertises a newer launcher. Download with progress in
  the existing footer, then Application.Shutdown() so the Updater can
  do its job.

(2) Inno Setup installer
------------------------
installer/PSLauncher.iss + build-installer.ps1 produce a single
PSLauncher-Setup-X.Y.Z.exe (~80 MB) that installs into
Program Files\ASTERION VR\PSLauncher\, drops both PSLauncher.exe and
PSLauncher.Updater.exe side by side (the updater MUST live next to
the target), creates Start Menu + optional Desktop shortcuts, and
registers a clean uninstall entry. The user's %LocalAppData%
(license, logs, cache) is intentionally untouched on uninstall — same
license survives a reinstall.

build-installer.ps1 chains dotnet publish for both projects and ISCC
in one command. README explains the bump-version workflow.

(3) HMAC-signed download URLs
-----------------------------
- New PHP route GET /api/download-url/{version} (Authorization: Bearer
  <licenseKey> or ?key=...). Validates the license, checks
  download_entitlement_until >= minLicenseDate of the version, and
  returns a HMAC-signed URL (path|exp|licId, hash_hmac SHA-256, valid
  1 h) + sha256 + sizeBytes for verification.
- /builds/.htaccess routes every *.zip request to gate.php. gate.php
  validates exp, lic, sig (constant-time hash_equals), then streams
  the file with Range: support so the launcher's resume keeps working.
  Audit log gets a download_url_issued entry per request.
- Client-side wired transparently: LicenseService gains
  GetSignedDownloadUrlAsync(version) that GETs the endpoint with the
  decrypted license key from DPAPI. MainViewModel calls it before
  every download; if the endpoint returns 404/401/network-error, the
  client falls back to the manifest's plain download.url (graceful
  degradation for setups that haven't deployed gate.php yet).

Note on PHP streaming for 14 GB ZIPs: gate.php uses set_time_limit(0)
+ ignore_user_abort(true) + 1 MiB chunked fread with periodic flush.
Works on OVH mutualisé but holds a PHP-FPM slot for the duration. If
parallel downloads scale past a few clients, switch to
mod_xsendfile or migrate /builds/ to Cloudflare R2 with native
S3-presigned URLs and remove the gate entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:38:13 +02:00
b10a3fbabf UI overhaul: minimal top bar, license-first settings, footer-pinned check
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>
2026-05-02 09:24:01 +02:00
74f48419e6 UI: borderless window with custom chrome, default 1280x720
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>
2026-05-02 09:09:14 +02:00
dd4ee9bbaa Force black background under bg image, Launcher subtitle bold
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>
2026-05-02 09:05:14 +02:00
911940753c UI tweaks: subdued background image + readable Launcher subtitle
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>
2026-05-02 09:01:42 +02:00
2c79a5111e UI: brand background image + Pirulen font for PROSERVE wordmark
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>
2026-05-02 08:57:49 +02:00