Commit Graph

10 Commits

Author SHA1 Message Date
500f7d12e6 v0.29.10 — Admin UI refonte (modals + onglets), emails de release, 4-digit versions
Admin backoffice — UI redesign :
 - Pages versions.php + licenses.php : remplace les <details> inline qui
   débordaient horizontalement par UN bouton « ✎ Modifier » par row qui
   ouvre une modal <dialog> avec onglets. 5 tabs versions (Méta, Notes,
   BÊTA, Channels, Avancé) + 6 tabs licenses (Prolonger, Slots, Channel,
   BÊTA, Lock, Contacts, Machines). Délégation JS unique pour les onglets.
 - Bouton 📋 Copier la clé license dans chaque row (Clipboard API + feedback
   visuel ✓ vert 1.5s). Évite le détour par phpMyAdmin pour transmettre la
   clé aux clients.
 - Overlay « hashing en cours » plein écran sur tous les boutons de hash
   (3-5 min sur OVH pour 13 Go ZIP). Spinner CSS + message contextualisé
   par scope (bulk vs single).
 - Date de release passée de datetime-local à date (l'heure n'a pas de
   sens UX), avec défaut = aujourd'hui pour release_at et aujourd'hui-1an
   pour min_license_date (= license standard couvre les releases sur 1 an).

Emails de notification release :
 - Migration 004 : colonne contact_emails TEXT NULL sur licenses (CSV)
 - Onglet « Contacts » sur la modal licenses pour saisir les emails par
   license (parsing tolérant : CSV, ligne par ligne, point-virgule)
 - Bouton « ✉ Notifier » par version : POST notify_release filtre les
   licenses éligibles (channel match + min_license_date + can_see_betas
   pour BÊTA) et envoie un email HTML à chaque contact (dédup global)
 - Template email table-based + bgcolor (compat Outlook/Word engine),
   navy foncé #0F172A, logo Asterion en CID embed (= affichage direct
   sans demande de permission Outlook), bouton download installer
   centré (align="center" + margin auto), release notes en <pre>
 - Mailer.php helper : parse emails, multipart/related avec attachments
   inline, fallback execCommand pour clipboard

4-digit version support (X.Y.Z.B) :
 - SemVer Parse/CompareTo/ToString gèrent 3 ou 4 digits ; Build absent =
   0 implicite (1.5.4 == 1.5.4.0 < 1.5.4.13). HasExplicitBuild préserve
   le format d'origine au round-trip.
 - Regex InstallationRegistry étendue avec (?:\.\d+)? → reconnaît
   « PROSERVE v1.5.4.13 » côte-à-côte avec « PROSERVE v1.5.4 » sur disque
 - Server-side : versions.php, launcher.php, DownloadUrl.php, api/index.php,
   Releasenotes.php — toutes les regex de validation acceptent le 4ᵉ digit
 - Use case : dev/test iterations cohabitant avec leur release stable

Bugs fixes :
 - migrate.php : strip ligne par ligne les commentaires SQL avant le
   check is-empty. Sans ça, le PREMIER chunk d'un fichier migration
   (= header + premier ALTER) commençait par `--` et était silencieusement
   skip → ALTER jamais appliqué. Affectait migrations 003, 004.
 - SignManifest::getOrComputeSha256 ignorait son cache interne quand force
   demandé par le caller, retournant l'ancien hash en 0 ms même après
   re-upload SFTP (avec mtime préservé). Propage maintenant le flag $force.
   Bouton « 🔁 Hash » per-row force maintenant un re-calcul systématique.
 - DownloadManager : 416 (Range Not Satisfiable) ajouté aux URL-refresh
   triggers, avec HEAD probe pour comparer taille serveur vs manifest →
   message d'erreur explicite si ZIP tronqué. Bps display lissé sur une
   fenêtre glissante de 12 samples (3 s) → plus de clignotement quand un
   segment finit / Polly retry. SHA mismatch popup enrichi avec les
   deux SHAs (attendu vs calculé) extraits via regex de l'exception.
 - DownloadUrl.php : signature de l'URL utilisait un template hardcodé
   /builds/proserve-{version}.zip, ignorant tout rename serveur. Lit
   maintenant download.url du manifest et signe le filename réel.

Strings i18n (5 langues) :
 - ~15 nouveaux : SHA mismatch enrichi avec sources, 416 size mismatch,
   stale manifest, manifest refreshed auto-retry, force fresh menu

Bumps : 0.29.7 → 0.29.10 (4-digit support + accumulated UI fixes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:24:50 +02:00
50da755a92 v0.29.7 — SteamVR merge + résilience DL (404 retry, force fresh, SHA purge)
SteamVR + Vive Business Streaming settings merge :
 - Nouveau ISteamVrSettingsDeployer : si _steamvr/steamvr.vrsettings est
   dans le ZIP, deep-merge récursif des blocs racine dans le fichier user
   (clé absente → ajout, deux objets → recurse, sinon → replace ; les
   sous-clés cible non touchées sont préservées — crucial pour "trackers")
 - Auto-localisation via registre Steam (HKCU/HKLM) + fallback Program Files
 - Phase pré-check (CheckMergeNeededAsync) : skip silencieux si la cible
   est déjà à jour (deep equality) → pas de kill SteamVR/VBS inutile, pas
   de popup à l'opérateur. Si changements nécessaires → popup OK/Annuler
   avec nombre de blocs qui changeront + liste des process à fermer.
 - Liste de kill construite depuis les health checks de type "Process"
   (l'opérateur connaît déjà sa stack VR) + guardians (Vive Business
   Streaming en tête car il relance SteamVR auto)
 - Settings UI : section dédiée avec opt-in, override path, liste process
   éditable + affichage du path canonique attendu

Résilience téléchargements :
 - 404 mid-DL traité comme 403/410 (URL refresh trigger) au niveau segment :
   on extrait le nouveau filename via /download-url server-side, retry transparent
 - Auto-retry install une fois après refresh manifest sur 404 : l'opérateur
   ne voit rien si la cause était un manifest local stale
 - Si retry échoue aussi en 404 → message ciblé "manifest serveur stale"
   (problème côté serveur, pas client)
 - Sur SHA-256 mismatch : auto-purge du cache LAN local (.zip + .sha256)
   + message d'erreur dédié avec source du DL (peer ou OVH) + nouveau menu
   "↻ Forcer re-téléchargement" pour purger état + cache manuellement
 - Détection client-side de l'incohérence "signed URL filename != manifest
   URL filename" avant DL : abort immédiat plutôt que 14 Go pour rien
 - IZipCacheStore.InvalidateAsync : nouvelle API pour purger un cache par
   version (utilisée par SHA mismatch handler + Force Fresh menu)

Bug serveur (DownloadUrl.php) :
 - L'endpoint construisait l'URL signée avec un template hardcodé
   `/builds/proserve-{version}.zip`, ignorant complètement
   download.url du manifest. Conséquence : un opérateur qui rename
   son ZIP pour buster le cache CDN OVH (ex. proserve-full-1.5.4.zip)
   voyait toutes ses releases retournent du 404 silencieux côté client.
 - Fix : on lit basename(parse_url(entry.download.url).path), whitelist
   sur le filename, vérif is_file() avant de signer, erreur 500 explicite
   si manifest et filesystem désynchros.

Strings (5 langues) :
 - ~20 nouvelles : SteamVR popups + status, SHA mismatch dialog + source
   labels, force fresh confirm + menu, manifest stale + auto-retry status

Bumps : 0.29.6 (déployé hors-commit pendant la session) → 0.29.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:52:00 +02:00
01793eb32d v0.28.12 — Fix DL >100% : onBytes au checkpoint, pas par WriteAsync
Régression introduite par v0.28.11 (durability checkpoint) : le footer
affichait des pourcentages > 100% en fin de DL, même sans pause manuelle.

Cause : seg.DownloadedBytes était devenu durable (incrémenté au checkpoint
tous les 64 MiB), MAIS onBytes(seg.Index, n) continuait à fire par
WriteAsync. Quand Polly retry un segment mid-stream (fréquent sur OVH
mutualisé qui coupe les requêtes longues via PHP-FPM
request_terminate_timeout) :

  Attempt 1 :
    - onBytes fire pour bytes 0..30 MiB (live)
    - HttpResumableException (PHP-FPM kill)
    - Dispose flush, mais seg.DownloadedBytes encore à 0 (dernier checkpoint)
  Polly retry attempt 2 :
    - segStart = seg.Start + 0
    - Re-download bytes 0..30 MiB (overwrite same content sur disque, OK)
    - onBytes fire À NOUVEAU pour ces 30 MiB ← DOUBLE COMPTAGE

Multiplié par 16 segments × N retries → aggregate dépasse total.

Fix : onBytes fire UNIQUEMENT au checkpoint, avec la valeur inFlight
juste avant la reset. Comme ça les bytes d'une attempt qui a failé ne
sont jamais reportés (la failure se produit AVANT que le checkpoint
soit atteint), et la retry re-télécharge + reporte une seule fois.

Trade-off : UI updates tous les 64 MiB par segment au lieu de chaque
4 MiB. Avec 16 segments en parallèle, ça fait ~10-20 reports/s en pic,
le reporter task échantillonne à 4 Hz de toute façon, invisible côté UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:06:45 +02:00
eae0514058 v0.28.11 — Multi-segment DL : durability checkpoint pour fix SHA fail post-pause
Bug rapporté : ~80% des reprises après pause failaient la vérification
SHA-256 finale alors que la DL semblait s'être passée correctement et
que le fichier était de la bonne taille. Root cause : seg.DownloadedBytes
était incrémenté juste après le retour de WriteAsync, mais cet appel met
seulement les bytes dans le buffer interne du FileStream (4 MiB) — pas
forcément sur disque ni dans le cache OS.

À la pause, le ct est cancelé, le segment throw une OperationCanceledException
au prochain await, et `await using var dst` dispose le FileStream — qui
SENSE flush le buffer vers le disque mais avec useAsync=true + cancellation
en cours, ce flush peut être partiel ou échouer silencieusement. Résultat :
le compteur dit "X bytes écrits" mais le disque en a moins. Au resume,
segStart = seg.Start + X évite donc les bytes perdus, ces régions
restent en zéros sparse (pré-alloc), et le SHA final fail.

Fix : pattern checkpoint. Le compteur seg.DownloadedBytes n'est mis à
jour qu'APRÈS un FlushAsync(CancellationToken.None) réussi. Le flush
est forcé non-cancelable pour éviter qu'une pause l'interrompe en
plein milieu. Granularité : tous les 64 MiB de download par segment.

Invariant garanti après le fix :
  seg.DownloadedBytes <= bytes_réellement_sur_disque

Au pire, en cas de pause, seg.DownloadedBytes est UN PEU en arrière de
ce qui est sur disque (jusqu'à 64 MiB par segment). Au resume, on
re-télécharge ces bytes — ils sont écrasés avec le même contenu, le
SHA final passe. Le cas dangereux ("compteur en avance, trou de zéros
sur disque") est définitivement impossible.

Bonus : initialisation de aggregateBytes depuis sum(seg.DownloadedBytes)
au lieu de state.DownloadedBytes pour éviter un drift cosmétique du
footer après resume (state.DownloadedBytes capturait la live aggregate
incluant l'in-flight, donc pouvait dépasser le total durable).

Trade-off : un peu plus de re-download au resume (jusqu'à 1 GB pour
16 segments × 64 MiB) en échange d'une fiabilité totale. Le user ne
verra pas la différence sur un 14 GB de DL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:25:51 +02:00
7de0c01b97 v0.24.9 — Deploy _api/, fix WebView2 Program Files, DL robustness
Deploy auto de l'API PHP de stats (parallèle Report/Doc) :
- ApiToolDeployer : copy _api/ → C:\xampp\htdocs\proserve\ avec atomic
  rename + backups horodatés + revert + pruning. Strict clone du pattern
  DocToolDeployer.
- ApiToolConfig dans LocalConfig (default folder "proserve" en minuscule
  pour matcher l'install standard PROSERVE côté client).
- Settings → Avancés : section dédiée (HtdocsRoot, FolderName, AutoDeploy,
  MaxBackups, Re-déployer, liste backups + revert) + ApiBackupViewModel.
- DI registration dans App.xaml.cs, injection MainViewModel, appel
  post-install après le bloc Doc.
- 8 nouvelles strings i18n (5 langues).

Fix WebView2 vide en Program Files :
- Set WEBVIEW2_USER_DATA_FOLDER vers %LocalAppData%\PSLauncher\WebView2\
  dans App.OnStartup. Sans ça, WebView2 essaie d'écrire son cache à côté
  du .exe (read-only en Program Files) → init silencieusement KO →
  onglets Report/Doc vides.

Refresh auto WebView2 sur navigation d'onglet :
- Hook IsVisibleChanged sur ReportWebView et DocsWebView. À chaque
  ré-affichage, CoreWebView2.Reload() pour voir les données serveur à
  jour. Le binding Source garde la nav initiale au cold start.

Fix UI : Cancel décalant le footer pendant Check updates :
- Nouveau flag IsDownloadActive distinct de IsBusy. Visibility du bouton
  Cancel passe de IsBusy → IsDownloadActive : couvre uniquement les DL,
  plus aucune apparition pendant Check updates / Install / Uninstall /
  Self-update qui décalait tout le layout 1 s.

DL robustness (multi-PC) :
- SafeInstallAsync : error boundary sur l'install handler. Plus aucune
  exception silencieuse — log + popup d'erreur visible (résout les cas
  "le 2e PC ne démarre pas, sans message").
- DownloadManager : vérif explicite que CHAQUE segment a Completed=true
  && DownloadedBytes >= Length AVANT le SHA-256. Le check actualSize
  était inutile (fichier sparse-préalloué).
- MaxRetryAttempts Polly 6 → 10 pour absorber les outages PHP-FPM
  request_terminate_timeout sous charge multi-PC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:53:13 +02:00
9de8e95910 v0.24.5 — Install Program Files, multi-PC DL reliability, polish UI
Installeur :
- Bascule en Program Files (autopf) avec PrivilegesRequired=admin.
  L'auto-update reste fonctionnel : LauncherSelfUpdater détecte le dossier
  protégé et lance PS_Launcher.Updater.exe via Verb=runas (UAC à chaque MAJ).
- build-installer.ps1 : détection ISCC.exe robuste (Program Files + LocalAppData
  pour install winget user-mode + PATH), messages d'erreur explicites avec URL
  et commande winget, suppression accents (compat PS 5.1 sans BOM).

Téléchargements multi-PC :
- DownloadManager : détection des connexions fermées prématurément (PHP-FPM
  request_terminate_timeout). Sans ça, segment marqué Completed=false sans
  exception → fichier sparse final avec trous → SHA-256 KO. Lance maintenant
  HttpResumableException(transient) pour que Polly retry au bon offset.
- ParallelDownloadSegments : 16 → 6 par défaut pour permettre plusieurs PCs
  simultanés sans saturer le pool PHP-FPM OVH (~30 workers).

UI :
- Fenêtre maximisée par défaut au démarrage (WindowState=Maximized).
- Fix maximize qui cachait le footer / barre de DL (WM_GETMINMAXINFO clamp
  sur work area, remplace le margin hack 7px imprécis).
- Sidebar : bloc info en bas avec PS_Launcher vX.Y.Z + IPv4 locale alignés.
- Copyright remonté plus près du footer (margin 28 → 8).
- Settings → Health checks : boutons ▲/▼ pour réordonner, CanExecute auto.
- HealthCheck refresh défaut : 5000 → 2000 ms.

Bug fix UI :
- MainViewModel.RebuildList preserve l'état du row actif pendant un DL.
  Sinon ouvrir Settings/License pendant un DL recréait les rows from scratch
  → UI affichait "Reprendre" + "Annuler" alors que le DL tournait. Les
  progress callbacks pointent maintenant sur _activeRow (résolution dynamique)
  au lieu de capturer le row local.

Defaults config :
- ServerBaseUrl : example.com → asterionvr.com (out-of-the-box).
- Vive Business Streaming check : HtcConnectionUtility → rrserver.

Backoffice (server/admin/licenses.php) :
- Action set_max_machines : bouton "Slots" pour ajuster max_machines à chaud
  sur une licence existante. Refus de descendre sous le nombre de machines
  déjà actives.

Build :
- AllowUnsafeBlocks=true sur PSLauncher.App.csproj (compat WinRT generator
  récent qui émet du code unsafe dans WinRTGenericInstantiation.g.cs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:04:36 +02:00
3c8438f3fe v0.19.0 — Bump default parallel segments 8→16 to shorten end-of-DL tail
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>
2026-05-03 18:28:35 +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
9bdcdabb9e v0.3: resumable downloads with HTTP Range, Polly retry, persistent state
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>
2026-05-01 09:55:31 +02:00
1c8c6803e8 Initial scaffolding: PS_Launcher v0.2
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>
2026-05-01 08:54:45 +02:00