Compare commits

...

2 Commits

Author SHA1 Message Date
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
14 changed files with 1895 additions and 4 deletions

View File

@@ -5,6 +5,10 @@ REM Pour l'installeur Inno Setup, utiliser build-installer.bat.
setlocal setlocal
set "REPO=%~dp0" set "REPO=%~dp0"
echo. echo.
echo ==^> Termine les eventuelles instances qui verrouilleraient les .exe
taskkill /F /IM PSLauncher.exe /T >nul 2>&1
taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1
echo.
echo ==^> dotnet publish PSLauncher.App (Release single-file) echo ==^> dotnet publish PSLauncher.App (Release single-file)
echo. echo.
dotnet publish "%REPO%src\PSLauncher.App\PSLauncher.App.csproj" -c Release --nologo dotnet publish "%REPO%src\PSLauncher.App\PSLauncher.App.csproj" -c Release --nologo

View File

@@ -12,6 +12,10 @@ REM powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 -IS
setlocal setlocal
set "SCRIPT_DIR=%~dp0" set "SCRIPT_DIR=%~dp0"
REM Termine les eventuelles instances en cours qui verrouilleraient les .exe
taskkill /F /IM PSLauncher.exe /T >nul 2>&1
taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%installer\build-installer.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%installer\build-installer.ps1" %*
set "EXITCODE=%ERRORLEVEL%" set "EXITCODE=%ERRORLEVEL%"

View File

@@ -5,6 +5,10 @@ REM Sortie : C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe (~78 Mo single-file self
setlocal setlocal
set "REPO=%~dp0" set "REPO=%~dp0"
echo. echo.
echo ==^> Termine les eventuelles instances de PSLauncher qui verrouilleraient les .exe
taskkill /F /IM PSLauncher.exe /T >nul 2>&1
taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1
echo.
echo ==^> dotnet publish PSLauncher.App (Release single-file) echo ==^> dotnet publish PSLauncher.App (Release single-file)
echo. echo.

View File

@@ -6,6 +6,10 @@ REM L'updater accompagne PSLauncher.exe — il doit etre dans le meme dossier.
setlocal setlocal
set "REPO=%~dp0" set "REPO=%~dp0"
echo. echo.
echo ==^> Termine les eventuelles instances qui verrouilleraient les .exe
taskkill /F /IM PSLauncher.exe /T >nul 2>&1
taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1
echo.
echo ==^> dotnet publish PSLauncher.Updater (Release single-file) echo ==^> dotnet publish PSLauncher.Updater (Release single-file)
echo. echo.

Binary file not shown.

Binary file not shown.

26
docs/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Documentation PS_Launcher
Deux documents PowerPoint, l'un pour l'admin/dev (interne), l'autre destiné aux clients finaux.
| Fichier | Public | Contenu |
|---|---|---|
| [PS_Launcher-Documentation-Interne.pptx](PS_Launcher-Documentation-Interne.pptx) | Équipe ASTERION | Architecture, setup OVH, backoffice, workflows release, sécurité, troubleshooting |
| [PS_Launcher-Guide-Utilisateur.pptx](PS_Launcher-Guide-Utilisateur.pptx) | Clients | Installation, activation license, utilisation, FAQ, support |
## Régénérer les documents
Si tu modifies le contenu, regénère les `.pptx` depuis les sources Node :
```powershell
cd docs
$env:NODE_PATH = "$env:APPDATA\npm\node_modules"
node _build_internal.js
node _build_user.js
```
(Pré-requis : Node.js + `npm install -g pptxgenjs`)
## Distribution
- **Doc interne** : reste dans le repo, pas distribuée. Garde-la à jour pour les nouveaux arrivants côté équipe.
- **Guide utilisateur** : embarqué automatiquement par l'installeur Inno Setup dans `{app}\docs\`. Un raccourci « Guide utilisateur » est créé dans le menu Démarrer à l'install.

981
docs/_build_internal.js Normal file
View File

@@ -0,0 +1,981 @@
// Generates PS_Launcher-Documentation-Interne.pptx
const pptxgen = require("pptxgenjs");
const COL = {
bg: "000000",
card: "161B23",
elev: "1E2632",
border: "2A2F3A",
text: "F2F2F2",
muted: "A0A0A8",
accent: "3B82F6",
accentDk: "1F3A66",
green: "16A34A",
greenDk: "0E2A1B",
amber: "F59E0B",
amberDk: "3A2A0E",
red: "EF4444",
redDk: "2A1414",
codeBg: "0A0E14",
};
const F_TITLE = "Calibri";
const F_BODY = "Calibri";
const F_CODE = "Consolas";
const pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "ASTERION VR";
pres.title = "PS_Launcher — Documentation Interne";
pres.company = "ASTERION VR";
const W = 10, H = 5.625;
// ===================== HELPERS =====================
function fillBg(slide) {
slide.background = { color: COL.bg };
}
function addHeader(slide, title, kicker) {
// Top accent bar
slide.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: W, h: 0.06, fill: { color: COL.accent }, line: { type: "none" }
});
if (kicker) {
slide.addText(kicker.toUpperCase(), {
x: 0.5, y: 0.25, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true,
color: COL.accent, charSpacing: 4, margin: 0
});
}
slide.addText(title, {
x: 0.5, y: kicker ? 0.55 : 0.35, w: W - 1, h: 0.7,
fontFace: F_TITLE, fontSize: 32, bold: true,
color: COL.text, margin: 0
});
// Underline-ish thin separator
slide.addShape(pres.shapes.LINE, {
x: 0.5, y: kicker ? 1.3 : 1.15, w: 1.2, h: 0,
line: { color: COL.accent, width: 2 }
});
}
function addFooter(slide, page) {
slide.addText("PS_LAUNCHER • Documentation Interne • ASTERION VR", {
x: 0.5, y: H - 0.35, w: W - 2, h: 0.25,
fontFace: F_BODY, fontSize: 9, color: COL.muted, margin: 0
});
if (page) {
slide.addText(String(page), {
x: W - 1, y: H - 0.35, w: 0.5, h: 0.25,
fontFace: F_BODY, fontSize: 9, color: COL.muted, align: "right", margin: 0
});
}
}
function addCard(slide, x, y, w, h, options = {}) {
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w, h,
fill: { color: options.fill || COL.card },
line: { color: options.border || COL.border, width: 1 },
});
}
function addAccentCard(slide, x, y, w, h, accentColor) {
addCard(slide, x, y, w, h);
// left accent strip
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w: 0.08, h,
fill: { color: accentColor }, line: { type: "none" }
});
}
function addCodeBlock(slide, x, y, w, h, lines) {
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w, h, fill: { color: COL.codeBg }, line: { color: COL.border, width: 1 }
});
const text = (Array.isArray(lines) ? lines : [lines]).map((line, i, arr) => ({
text: line,
options: { breakLine: i < arr.length - 1 }
}));
slide.addText(text, {
x: x + 0.15, y: y + 0.1, w: w - 0.3, h: h - 0.2,
fontFace: F_CODE, fontSize: 11, color: COL.text, margin: 0,
valign: "top"
});
}
function addBullets(slide, x, y, w, h, items, opts = {}) {
const fontSize = opts.fontSize || 14;
const color = opts.color || COL.text;
const arr = items.map((it, i) => ({
text: it,
options: { bullet: { code: "25A0" }, breakLine: i < items.length - 1, color }
}));
slide.addText(arr, {
x, y, w, h, fontFace: F_BODY, fontSize, color, margin: 0,
paraSpaceAfter: 6
});
}
function addPill(slide, x, y, w, h, label, color) {
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x, y, w, h, fill: { color }, line: { type: "none" },
rectRadius: h / 2
});
slide.addText(label, {
x, y, w, h, fontFace: F_BODY, fontSize: 10, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
}
// ===================== SLIDE 1: TITLE =====================
{
const s = pres.addSlide();
fillBg(s);
// Big colored band on the right
s.addShape(pres.shapes.RECTANGLE, {
x: 6.5, y: 0, w: 3.5, h: H, fill: { color: COL.accentDk }, line: { type: "none" }
});
// Vertical accent line
s.addShape(pres.shapes.RECTANGLE, {
x: 6.5, y: 0, w: 0.05, h: H, fill: { color: COL.accent }, line: { type: "none" }
});
// Brand label
s.addText("ASTERION VR", {
x: 0.6, y: 0.5, w: 4, h: 0.4,
fontFace: F_BODY, fontSize: 13, bold: true,
color: COL.accent, charSpacing: 8, margin: 0
});
s.addText("PS_Launcher", {
x: 0.6, y: 1.5, w: 6, h: 1,
fontFace: F_TITLE, fontSize: 56, bold: true,
color: COL.text, margin: 0
});
s.addText("Documentation Interne", {
x: 0.6, y: 2.5, w: 6, h: 0.7,
fontFace: F_TITLE, fontSize: 28,
color: COL.muted, margin: 0
});
s.addText("Déploiement, configuration & exploitation", {
x: 0.6, y: 3.2, w: 6, h: 0.5,
fontFace: F_BODY, fontSize: 16, italic: true,
color: COL.muted, margin: 0
});
// Right column big version pill
s.addText("v0.8", {
x: 6.7, y: 1.5, w: 3.2, h: 1.2,
fontFace: F_TITLE, fontSize: 76, bold: true,
color: COL.text, align: "center", margin: 0
});
s.addText("VERSION", {
x: 6.7, y: 2.6, w: 3.2, h: 0.3,
fontFace: F_BODY, fontSize: 11,
color: COL.muted, align: "center", charSpacing: 6, margin: 0
});
// Date
s.addText("Mai 2026", {
x: 6.7, y: 3.6, w: 3.2, h: 0.4,
fontFace: F_BODY, fontSize: 14,
color: COL.text, align: "center", margin: 0
});
// Bottom band
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
});
s.addText("Confidentiel — usage interne uniquement", {
x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 11, italic: true,
color: COL.muted, margin: 0
});
}
// ===================== SLIDE 2: SOMMAIRE =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Sommaire");
addFooter(s, 2);
const sections = [
["1", "Architecture", "Vue d'ensemble client-serveur"],
["2", "Setup OVH initial", "MySQL, config.php, clés Ed25519"],
["3", "Tour du backoffice", "Dashboard, Licenses, Versions, Launcher, Audit"],
["4", "Workflow de release", "Proserve & launcher"],
["5", "Gestion des licenses", "Émission, prolongation, révocation"],
["6", "Sécurité", "URLs HMAC, signatures Ed25519, DPAPI"],
["7", "Exploitation", "Logs, fichiers, troubleshooting"],
];
let y = 1.6;
sections.forEach(([num, title, desc]) => {
// Number circle
s.addShape(pres.shapes.OVAL, {
x: 0.5, y, w: 0.5, h: 0.5,
fill: { color: COL.accent }, line: { type: "none" }
});
s.addText(num, {
x: 0.5, y, w: 0.5, h: 0.5,
fontFace: F_TITLE, fontSize: 18, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
// Title
s.addText(title, {
x: 1.2, y: y - 0.05, w: 4, h: 0.35,
fontFace: F_TITLE, fontSize: 16, bold: true,
color: COL.text, margin: 0
});
// Description
s.addText(desc, {
x: 1.2, y: y + 0.27, w: 6, h: 0.3,
fontFace: F_BODY, fontSize: 12,
color: COL.muted, margin: 0
});
y += 0.55;
});
}
// ===================== SLIDE 3: ARCHITECTURE =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Architecture", "section 1");
addFooter(s, 3);
// Three boxes : Client - HTTPS - OVH
const boxY = 1.8, boxH = 2.2, boxW = 2.7;
// Client
addAccentCard(s, 0.5, boxY, boxW, boxH, COL.green);
s.addText("CLIENT", {
x: 0.7, y: boxY + 0.1, w: boxW - 0.3, h: 0.3,
fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
});
s.addText("PSLauncher.exe", {
x: 0.7, y: boxY + 0.4, w: boxW - 0.3, h: 0.4,
fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
});
addBullets(s, 0.7, boxY + 0.85, boxW - 0.3, 1.3, [
"WPF / .NET 8",
"License DPAPI locale",
"Cache & logs",
"Updater intégré"
], { fontSize: 11, color: COL.muted });
// Arrow / HTTPS
s.addShape(pres.shapes.LINE, {
x: 3.3, y: boxY + 1.1, w: 0.8, h: 0,
line: { color: COL.accent, width: 2.5 }
});
s.addText("HTTPS", {
x: 3.3, y: boxY + 0.7, w: 0.8, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true, color: COL.accent, align: "center", margin: 0
});
// OVH
addAccentCard(s, 4.15, boxY, boxW, boxH, COL.accent);
s.addText("SERVEUR", {
x: 4.35, y: boxY + 0.1, w: boxW - 0.3, h: 0.3,
fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
});
s.addText("OVH mutualisé", {
x: 4.35, y: boxY + 0.4, w: boxW - 0.3, h: 0.4,
fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
});
addBullets(s, 4.35, boxY + 0.85, boxW - 0.3, 1.3, [
"PHP 8.3 + Apache",
"API REST + backoffice",
"Manifest signé Ed25519",
"Builds ZIPs servis HMAC"
], { fontSize: 11, color: COL.muted });
// Arrow / SQL
s.addShape(pres.shapes.LINE, {
x: 6.95, y: boxY + 1.1, w: 0.8, h: 0,
line: { color: COL.accent, width: 2.5 }
});
s.addText("PDO", {
x: 6.95, y: boxY + 0.7, w: 0.8, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true, color: COL.accent, align: "center", margin: 0
});
// MySQL
addAccentCard(s, 7.8, boxY, boxW - 0.5, boxH, COL.amber);
s.addText("BASE", {
x: 8, y: boxY + 0.1, w: boxW - 0.7, h: 0.3,
fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
});
s.addText("MySQL OVH", {
x: 8, y: boxY + 0.4, w: boxW - 0.7, h: 0.4,
fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
});
addBullets(s, 8, boxY + 0.85, boxW - 0.7, 1.3, [
"licenses",
"license_machines",
"rate_limit",
"audit_log"
], { fontSize: 11, color: COL.muted });
// Footnote
s.addText("Le launcher conserve sa license en cache local (DPAPI scope CurrentUser) et fonctionne offline 7 jours après la dernière validation serveur.", {
x: 0.5, y: 4.4, w: W - 1, h: 0.6,
fontFace: F_BODY, fontSize: 11, italic: true, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 4: SETUP OVH =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Setup OVH initial", "section 2 — première fois uniquement");
addFooter(s, 4);
const steps = [
["1", "Créer la base MySQL", "Manager OVH → Bases de données → Créer.\nNoter host, user, password, nom."],
["2", "Jouer la migration", "PhpMyAdmin OVH → onglet SQL → coller migrations/001_init.sql\n(crée 4 tables : licenses, license_machines, rate_limit, audit_log)"],
["3", "Uploader le code en SFTP", "server/ → www/PS_Launcher/\nRespecter l'arborescence : api/, admin/, manifest/, builds/, tools/, migrations/"],
["4", "Créer api/config.php", "Copier api/config.example.php → api/config.php\nÉditer DSN MySQL, secrets HMAC/JWT, hash bcrypt admin, paire Ed25519"],
];
let y = 1.5;
steps.forEach(([num, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.85);
// Number
s.addShape(pres.shapes.OVAL, {
x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
fill: { color: COL.accent }, line: { type: "none" }
});
s.addText(num, {
x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
fontFace: F_TITLE, fontSize: 16, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
// Title
s.addText(title, {
x: 1.4, y: y + 0.1, w: 7.5, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
// Desc
s.addText(desc, {
x: 1.4, y: y + 0.4, w: 7.5, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
y += 0.95;
});
}
// ===================== SLIDE 5: CONFIG.PHP =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "config.php — secrets serveur", "section 2");
addFooter(s, 5);
addCodeBlock(s, 0.5, 1.5, W - 1, 2.6, [
"<?php return [",
" 'base_url' => 'https://asterionvr.com/PS_Launcher',",
" 'db' => [",
" 'dsn' => 'mysql:host=xxx.mysql.db;dbname=xxx;charset=utf8mb4',",
" 'username' => '...', 'password' => '...',",
" ],",
" 'hmac_secret' => '<64 hex chars>', // bin2hex(random_bytes(32))",
" 'ed25519' => [",
" 'private_key_hex' => '<128 hex>', // tools/generate-keypair.php",
" 'public_key_hex' => '<64 hex>', // → idem + Resources/server-pubkey.txt",
" ],",
" 'jwt_secret' => '<64 hex>',",
" 'admin_password_hash' => '$2y$10$...' // password_hash('...')",
"];",
]);
// Tip card
addAccentCard(s, 0.5, 4.3, W - 1, 0.85, COL.amber);
s.addText("⚠ Point critique", {
x: 0.7, y: 4.4, w: 3, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.amber, margin: 0
});
s.addText("public_key_hex doit être recopié à l'identique dans Resources/server-pubkey.txt côté projet C# avant un nouveau publish — sinon le launcher refusera la signature serveur.", {
x: 0.7, y: 4.7, w: W - 1.4, h: 0.5,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 6: GENERATE KEYS =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Génération des secrets", "section 2");
addFooter(s, 6);
// Two columns
const c1 = 0.5, c2 = 5.2, cw = 4.3;
// Col 1 : Ed25519
s.addText("🔐 Keypair Ed25519", {
x: c1, y: 1.4, w: cw, h: 0.4,
fontFace: F_TITLE, fontSize: 15, bold: true, color: COL.text, margin: 0
});
s.addText("Signe le manifest et la réponse de validation license.", {
x: c1, y: 1.7, w: cw, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
addCodeBlock(s, c1, 2.1, cw, 0.7, [
"$ cd ~/www/PS_Launcher",
"$ php tools/generate-keypair.php",
]);
addBullets(s, c1, 2.95, cw, 1.6, [
"private_key_hex → api/config.php uniquement",
"public_key_hex → api/config.php ET",
" Resources/server-pubkey.txt",
], { fontSize: 11 });
// Col 2 : Admin password
s.addText("🔑 Mot de passe admin", {
x: c2, y: 1.4, w: cw, h: 0.4,
fontFace: F_TITLE, fontSize: 15, bold: true, color: COL.text, margin: 0
});
s.addText("Bcrypt — pour la connexion à /admin/.", {
x: c2, y: 1.7, w: cw, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
addCodeBlock(s, c2, 2.1, cw, 1.3, [
"$ php -r 'echo password_hash(",
" \"<motdepasse>\",",
" PASSWORD_DEFAULT) . \"\\n\";'",
]);
addBullets(s, c2, 3.55, cw, 1.0, [
"Hash dans api/config.php",
" → admin_password_hash",
], { fontSize: 11 });
// hmac/jwt
addCard(s, 0.5, 4.7, W - 1, 0.65);
s.addText("🎲 HMAC & JWT secrets", {
x: 0.7, y: 4.8, w: 4, h: 0.25,
fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
});
s.addText("Générer 64 hex chars : php -r \"echo bin2hex(random_bytes(32));\"", {
x: 0.7, y: 5.0, w: W - 1.4, h: 0.3,
fontFace: F_CODE, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 7: BACKOFFICE - DASHBOARD =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Backoffice — Dashboard", "section 3 · vue d'ensemble");
addFooter(s, 7);
s.addText("URL : https://asterionvr.com/PS_Launcher/admin/", {
x: 0.5, y: 1.4, w: W - 1, h: 0.3,
fontFace: F_CODE, fontSize: 12, color: COL.accent, margin: 0
});
// KPI grid 5 cells
const kpiW = 1.85, kpiH = 1.0, kpiY = 1.95, gap = 0.05;
const kpis = [
["Licenses actives", "•", COL.green],
["Expirées", "•", COL.amber],
["Révoquées", "•", COL.red],
["Machines vues 30j", "•", COL.text],
["Validations 24h", "•", COL.text],
];
kpis.forEach((kpi, i) => {
const [label, val, color] = kpi;
const x = 0.5 + i * (kpiW + gap);
addCard(s, x, kpiY, kpiW, kpiH);
s.addText(label.toUpperCase(), {
x: x + 0.1, y: kpiY + 0.1, w: kpiW - 0.2, h: 0.3,
fontFace: F_BODY, fontSize: 9, bold: true, color: COL.muted, charSpacing: 2, margin: 0
});
s.addText(val, {
x: x + 0.1, y: kpiY + 0.35, w: kpiW - 0.2, h: 0.55,
fontFace: F_TITLE, fontSize: 32, bold: true, color, margin: 0
});
});
// Liste features
s.addText("Ce que tu y vois", {
x: 0.5, y: 3.2, w: W - 1, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
addBullets(s, 0.5, 3.5, W - 1, 1.3, [
"Compteurs licenses + machines + validations en temps réel",
"État du dépôt de versions (manifest signé / non signé, version latest)",
"10 dernières lignes de l'audit log avec leur sévérité (validate_ok, expired, revoked, …)",
], { fontSize: 12 });
// Bottom callout
addAccentCard(s, 0.5, 4.85, W - 1, 0.5, COL.accent);
s.addText("✓ Page d'entrée — pas d'action destructive ici, c'est le tableau de bord lecture seule.", {
x: 0.7, y: 4.9, w: W - 1.4, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
});
}
// ===================== SLIDE 8: BACKOFFICE - LICENSES =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Backoffice — Licenses", "section 3 · gestion clients");
addFooter(s, 8);
s.addText("Émettre, prolonger, révoquer, libérer les machines occupées.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 13, italic: true, color: COL.muted, margin: 0
});
// Workflow boxes
const flow = [
["Émettre", "Formulaire en haut\nNom client, date d'expiration, max machines, notes\n→ Clé PRSRV-XXXX-XXXX-XXXX-XXXX affichée UNE FOIS", COL.green],
["Prolonger", "Bouton Prolonger / ligne\n→ Champ date → la nouvelle date d'expiration prend effet immédiatement côté client à la prochaine validation", COL.accent],
["Révoquer", "Bouton rouge / ligne\n→ revoked_at = NOW()\nLe client perd ses droits de DL au prochain check, le launcher reste utilisable sur les versions déjà installées", COL.red],
["Libérer machines", "Si un client change de PC, ses anciennes machines occupent un slot\n→ Bouton Libérer machines vide la table license_machines pour cette license", COL.amber],
];
let y = 1.85;
flow.forEach(([title, desc, color]) => {
addAccentCard(s, 0.5, y, W - 1, 0.75, color);
s.addText(title, {
x: 0.75, y: y + 0.1, w: 2, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color, margin: 0
});
s.addText(desc, {
x: 2.7, y: y + 0.05, w: W - 3.2, h: 0.7,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.85;
});
}
// ===================== SLIDE 9: BACKOFFICE - VERSIONS =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Backoffice — Versions Proserve", "section 3 · catalogue des builds");
addFooter(s, 9);
s.addText("Édite manifest/versions.json depuis le web (sans toucher au JSON à la main).", {
x: 0.5, y: 1.4, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 13, italic: true, color: COL.muted, margin: 0
});
// Workflow numbered
const steps = [
["1", "Ajouter version", "Formulaire haut → Numéro X.Y.Z, date min de license requise, release notes en Markdown."],
["2", "Upload SFTP du ZIP", "Dans builds/proserve-X.Y.Z.zip — taille typique 14 Go par build UE5."],
["3", "Hasher + signer", "Bouton bleu 🔁 Hasher les versions + signer.\nCalcule sizeBytes + sha256, bump latest, signe Ed25519."],
["4", "Édition fine", "Méta (date, minLicenseDate), Notes (Markdown), toggle disponibilité, retirer du manifest."],
];
let y = 1.85;
steps.forEach(([num, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.75);
s.addShape(pres.shapes.OVAL, {
x: 0.7, y: y + 0.18, w: 0.4, h: 0.4,
fill: { color: COL.accent }, line: { type: "none" }
});
s.addText(num, {
x: 0.7, y: y + 0.18, w: 0.4, h: 0.4,
fontFace: F_TITLE, fontSize: 14, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
s.addText(title, {
x: 1.3, y: y + 0.1, w: 8, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.3, y: y + 0.4, w: 8, h: 0.35,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.85;
});
}
// ===================== SLIDE 10: BACKOFFICE - LAUNCHER =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Backoffice — Launcher (auto-update)", "section 3 · MAJ du launcher lui-même");
addFooter(s, 10);
s.addText("Quand tu pousses une nouvelle version de PSLauncher.exe, les launchers déjà déployés se mettent à jour seuls. Cette page pilote ce flux.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.45,
fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
});
// Two columns: Status & Workflow
const colY = 2.0;
// Status card
addCard(s, 0.5, colY, 4.3, 2.6);
s.addText("État affiché en haut", {
x: 0.7, y: colY + 0.15, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
addBullets(s, 0.7, colY + 0.55, 4, 2, [
"Version annoncée (ex : v0.8.0)",
"minRequired (plancher de compatibilité)",
"Exe présent / absent dans builds/launcher/",
"Hash calculé / à calculer",
"Manifest signé / non signé",
], { fontSize: 11 });
// Workflow card
addCard(s, 5.2, colY, 4.3, 2.6);
s.addText("Workflow nouvelle version", {
x: 5.4, y: colY + 0.15, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
addBullets(s, 5.4, colY + 0.55, 4, 2, [
"build-launcher.bat (publie l'exe)",
"Renommer en PSLauncher-X.Y.Z.exe",
"Upload SFTP dans builds/launcher/",
"Définir version dans le formulaire",
"Bouton bleu : 🔁 Hasher le launcher + signer",
], { fontSize: 11 });
// Bottom note
addAccentCard(s, 0.5, 4.7, W - 1, 0.55, COL.accent);
s.addText(" Les clients en Program Files passeront un UAC à la mise à jour ; les nouveaux installs (user-mode) n'en demanderont jamais.", {
x: 0.7, y: 4.78, w: W - 1.4, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
});
}
// ===================== SLIDE 11: BACKOFFICE - AUDIT =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Backoffice — Audit log", "section 3 · traçabilité");
addFooter(s, 11);
s.addText("Toutes les validations license + émissions d'URL signées sont enregistrées avec horodatage, IP et détail JSON.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
});
// Event types table
addCard(s, 0.5, 1.95, W - 1, 3);
s.addText("Événements possibles", {
x: 0.7, y: 2.05, w: W - 1.4, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
const events = [
["validate_ok", "License validée avec succès", COL.green],
["validate_expired", "Clé connue mais date dépassée", COL.amber],
["validate_revoked", "License révoquée par l'admin", COL.red],
["validate_invalid", "Clé inconnue (faute de frappe)", COL.red],
["machine_limit", "Slot machines max atteint", COL.red],
["download_url_issued", "URL HMAC signée pour DL d'un ZIP", COL.accent],
];
let y = 2.45;
events.forEach(([code, desc, color]) => {
// Pill
addPill(s, 0.7, y, 2.0, 0.28, code, color);
// Description
s.addText(desc, {
x: 2.85, y: y - 0.04, w: 6, h: 0.35,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
y += 0.4;
});
}
// ===================== SLIDE 12: WORKFLOW PROSERVE =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Workflow release — Proserve", "section 4");
addFooter(s, 12);
// Big timeline
const stepW = 1.7, stepGap = 0.18, sy = 1.7;
const steps = [
["1", "Package", "Unreal\n→ ZIP de\nla version"],
["2", "SFTP", "Upload dans\nbuilds/\n(14 Go)"],
["3", "Backoffice", "Onglet Versions\nAjouter X.Y.Z\nRelease notes"],
["4", "Sign", "Bouton bleu\n🔁 Hasher\n+ signer"],
["5", "✓", "Manifest signé\nClients voient\nla MAJ"],
];
steps.forEach((step, i) => {
const x = 0.5 + i * (stepW + stepGap);
const [num, title, desc] = step;
const isLast = i === steps.length - 1;
addCard(s, x, sy, stepW, 2.6, {
fill: isLast ? COL.greenDk : COL.card,
border: isLast ? COL.green : COL.border
});
s.addShape(pres.shapes.OVAL, {
x: x + (stepW - 0.55) / 2, y: sy + 0.2, w: 0.55, h: 0.55,
fill: { color: isLast ? COL.green : COL.accent }, line: { type: "none" }
});
s.addText(num, {
x: x + (stepW - 0.55) / 2, y: sy + 0.2, w: 0.55, h: 0.55,
fontFace: F_TITLE, fontSize: 18, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
s.addText(title, {
x: x + 0.1, y: sy + 0.85, w: stepW - 0.2, h: 0.35,
fontFace: F_TITLE, fontSize: 13, bold: true,
color: COL.text, align: "center", margin: 0
});
s.addText(desc, {
x: x + 0.1, y: sy + 1.25, w: stepW - 0.2, h: 1.2,
fontFace: F_BODY, fontSize: 11, color: COL.muted, align: "center", margin: 0
});
// Connecting arrow
if (i < steps.length - 1) {
s.addShape(pres.shapes.LINE, {
x: x + stepW, y: sy + 0.475, w: stepGap, h: 0,
line: { color: COL.border, width: 1.5 }
});
}
});
// Notes
addAccentCard(s, 0.5, 4.6, W - 1, 0.65, COL.amber);
s.addText("⚠ Tant que le ZIP n'est pas hashé/signé, les clients ne le verront pas — la signature Ed25519 prouve l'intégrité.", {
x: 0.7, y: 4.7, w: W - 1.4, h: 0.5,
fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
});
}
// ===================== SLIDE 13: WORKFLOW LAUNCHER =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Workflow release — Launcher", "section 4");
addFooter(s, 13);
s.addText("Pour pousser une nouvelle version du launcher elle-même (auto-update transparent côté client).", {
x: 0.5, y: 1.4, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
});
// Two columns: scripts / steps
addCard(s, 0.5, 1.85, 4.3, 3.3);
s.addText("Scripts disponibles à la racine", {
x: 0.7, y: 1.95, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
const scripts = [
["build-launcher.bat", "Publie PSLauncher.exe (~78 Mo)"],
["build-updater.bat", "Publie PSLauncher.Updater.exe"],
["build-all.bat", "Les deux d'un coup"],
["build-installer.bat","Tout + Inno Setup .exe"],
];
let y = 2.35;
scripts.forEach(([cmd, desc]) => {
s.addText(cmd, {
x: 0.7, y, w: 4, h: 0.25,
fontFace: F_CODE, fontSize: 11, color: COL.accent, margin: 0
});
s.addText(desc, {
x: 0.7, y: y + 0.25, w: 4, h: 0.3,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.65;
});
// Right: workflow
addCard(s, 5.2, 1.85, 4.3, 3.3);
s.addText("Étapes d'une release", {
x: 5.4, y: 1.95, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
const wsteps = [
"Bumper <Version> dans PSLauncher.App.csproj",
"build-launcher.bat",
"Renommer PSLauncher.exe → PSLauncher-X.Y.Z.exe",
"SFTP → builds/launcher/",
"Backoffice → Launcher → Définir X.Y.Z",
"Bouton bleu 🔁 Hasher le launcher + signer",
];
addBullets(s, 5.4, 2.4, 4, 2.5, wsteps, { fontSize: 11 });
}
// ===================== SLIDE 14: SECURITY =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Sécurité", "section 6");
addFooter(s, 14);
const blocks = [
["🔏 Ed25519", "Signe le manifest et la réponse de validation license. La clé publique est embarquée dans le launcher, donc même un MITM sur HTTPS ne peut pas falsifier. Sodium PHP natif côté serveur, NSec.Cryptography côté C#.", COL.accent],
["🔗 HMAC-SHA256", "URLs de download présignées par le serveur (path|exp|licId|secret). Apache route via gate.php qui vérifie en constant-time avant de servir le ZIP. Validité 1h, lié à la license.", COL.amber],
["💾 DPAPI local", "La clé license est stockée chiffrée dans %LocalAppData%/PSLauncher/config.json via ProtectedData.Protect (scope CurrentUser). Copier ce fichier sur une autre machine ou un autre user → ne marche pas.", COL.green],
];
let y = 1.55;
blocks.forEach(([title, desc, color]) => {
addAccentCard(s, 0.5, y, W - 1, 1.05, color);
s.addText(title, {
x: 0.75, y: y + 0.15, w: 3, h: 0.35,
fontFace: F_TITLE, fontSize: 15, bold: true, color, margin: 0
});
s.addText(desc, {
x: 0.75, y: y + 0.5, w: W - 1.2, h: 0.55,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 1.15;
});
}
// ===================== SLIDE 15: FILES & LOGS =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Fichiers & logs", "section 7 · localisation");
addFooter(s, 15);
// Server side
s.addText("Côté serveur OVH", {
x: 0.5, y: 1.4, w: 4.3, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.accent, margin: 0
});
addCodeBlock(s, 0.5, 1.75, 4.3, 3.3, [
"www/PS_Launcher/",
"├── api/",
"│ ├── config.php (gitignored)",
"│ ├── index.php",
"│ ├── lib/",
"│ └── routes/",
"├── admin/ (backoffice)",
"├── manifest/versions.json",
"├── releasenotes/X.Y.Z.md",
"├── builds/",
"│ ├── proserve-X.Y.Z.zip",
"│ └── launcher/",
"│ └── PSLauncher-X.Y.Z.exe",
"├── tools/sign-manifest.php",
"└── migrations/001_init.sql",
]);
// Client side
s.addText("Côté client Windows", {
x: 5.2, y: 1.4, w: 4.3, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.accent, margin: 0
});
addCodeBlock(s, 5.2, 1.75, 4.3, 3.3, [
"%LocalAppData%/PSLauncher/",
"├── config.json",
"│ (license chiffrée DPAPI)",
"├── logs/",
"│ ├── app-YYYYMMDD.log",
"│ └── updater.log",
"├── downloads/",
"│ ├── *.partial",
"│ └── *.state.json",
"└── selfupdate/",
"",
"C:\\ASTERION_VR/",
" (default installRoot)",
" ├── Proserve v1.4.5/",
" ├── Proserve v1.4.6/",
" └── ...",
]);
}
// ===================== SLIDE 16: TROUBLESHOOTING =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Troubleshooting", "section 7 · pannes courantes");
addFooter(s, 16);
const issues = [
["⛔", "500 sur l'API", "Vérifier api/config.php existe + valeurs renseignées (debug endpoint /api/debug). Logs PHP : Manager OVH → Logs.", COL.red],
["⚠", "Hash incorrect côté client", "Le ZIP a changé sans resync. Backoffice → Versions → bouton bleu 🔁 pour resigner.", COL.amber],
["⚠", "License invalide après désactivation", "Cache local toujours valide 7 jours. Désactiver/réactiver depuis Settings → ⚙ ou supprimer config.json.", COL.amber],
["⛔", "Updater ne relance pas", "Vérifier %LocalAppData%/PSLauncher/logs/updater.log. Si UAC refusé en Program Files : copier manuellement le binaire.", COL.red],
["✓", "Tout marche mais pas d'auto-update", "Section 'launcher' absente du manifest. Onglet Launcher → Définir + 🔁.", COL.green],
];
let y = 1.55;
issues.forEach(([icon, title, desc, color]) => {
addCard(s, 0.5, y, W - 1, 0.65);
s.addText(icon, {
x: 0.7, y: y + 0.1, w: 0.5, h: 0.45,
fontFace: F_TITLE, fontSize: 18, bold: true, color, align: "center", margin: 0
});
s.addText(title, {
x: 1.2, y: y + 0.07, w: 3, h: 0.3,
fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.2, y: y + 0.32, w: W - 1.7, h: 0.32,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.72;
});
}
// ===================== SLIDE 17: CLOSING =====================
{
const s = pres.addSlide();
fillBg(s);
// Diagonal band
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 1.3, w: W, h: 1.5, fill: { color: COL.accentDk }, line: { type: "none" }
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 1.3, w: W, h: 0.05, fill: { color: COL.accent }, line: { type: "none" }
});
s.addText("Fin de la documentation interne", {
x: 0.5, y: 1.55, w: W - 1, h: 0.6,
fontFace: F_TITLE, fontSize: 30, bold: true, color: COL.text, margin: 0
});
s.addText("Pour le guide utilisateur destiné aux clients : voir document séparé.", {
x: 0.5, y: 2.1, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 14, italic: true, color: COL.muted, margin: 0
});
// Three quick-reference cards
const ix = 0.5, iy = 3.4, iw = 3.0, gap = 0.15;
const refs = [
["📂 Repo", "C:\\ASTERION\\GIT\\PS_Launcher", COL.accent],
["🌐 Backoffice", "asterionvr.com/PS_Launcher/admin/", COL.green],
["📋 Logs", "%LocalAppData%\\PSLauncher\\logs\\", COL.amber],
];
refs.forEach(([title, path, color], i) => {
const x = ix + i * (iw + gap);
addAccentCard(s, x, iy, iw, 1.1, color);
s.addText(title, {
x: x + 0.2, y: iy + 0.15, w: iw - 0.4, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color, margin: 0
});
s.addText(path, {
x: x + 0.2, y: iy + 0.5, w: iw - 0.4, h: 0.55,
fontFace: F_CODE, fontSize: 10, color: COL.text, margin: 0
});
});
// Footer brand
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
});
s.addText("© 2026 ASTERION VR — All rights reserved", {
x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, align: "center", margin: 0
});
}
pres.writeFile({ fileName: "C:\\ASTERION\\GIT\\PS_Launcher\\docs\\PS_Launcher-Documentation-Interne.pptx" })
.then(f => console.log("Generated:", f));

793
docs/_build_user.js Normal file
View File

@@ -0,0 +1,793 @@
// Generates PS_Launcher-Guide-Utilisateur.pptx
const pptxgen = require("pptxgenjs");
const COL = {
bg: "000000",
card: "161B23",
elev: "1E2632",
border: "2A2F3A",
text: "F2F2F2",
muted: "A0A0A8",
accent: "3B82F6",
accentDk: "1F3A66",
green: "16A34A",
greenDk: "0E2A1B",
amber: "F59E0B",
amberDk: "3A2A0E",
red: "EF4444",
redDk: "2A1414",
codeBg: "0A0E14",
};
const F_TITLE = "Calibri";
const F_BODY = "Calibri";
const F_CODE = "Consolas";
const pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "ASTERION VR";
pres.title = "PROSERVE Launcher — Guide Utilisateur";
pres.company = "ASTERION VR";
const W = 10, H = 5.625;
// ---- helpers (same as internal but kept local to avoid module fuss) ----
function fillBg(slide) { slide.background = { color: COL.bg }; }
function addHeader(slide, title, kicker) {
slide.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: W, h: 0.06, fill: { color: COL.green }, line: { type: "none" }
});
if (kicker) {
slide.addText(kicker.toUpperCase(), {
x: 0.5, y: 0.25, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true,
color: COL.green, charSpacing: 4, margin: 0
});
}
slide.addText(title, {
x: 0.5, y: kicker ? 0.55 : 0.35, w: W - 1, h: 0.7,
fontFace: F_TITLE, fontSize: 32, bold: true,
color: COL.text, margin: 0
});
slide.addShape(pres.shapes.LINE, {
x: 0.5, y: kicker ? 1.3 : 1.15, w: 1.2, h: 0,
line: { color: COL.green, width: 2 }
});
}
function addFooter(slide, page) {
slide.addText("PROSERVE Launcher • Guide Utilisateur • ASTERION VR", {
x: 0.5, y: H - 0.35, w: W - 2, h: 0.25,
fontFace: F_BODY, fontSize: 9, color: COL.muted, margin: 0
});
if (page) {
slide.addText(String(page), {
x: W - 1, y: H - 0.35, w: 0.5, h: 0.25,
fontFace: F_BODY, fontSize: 9, color: COL.muted, align: "right", margin: 0
});
}
}
function addCard(slide, x, y, w, h, options = {}) {
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w, h,
fill: { color: options.fill || COL.card },
line: { color: options.border || COL.border, width: 1 },
});
}
function addAccentCard(slide, x, y, w, h, accentColor) {
addCard(slide, x, y, w, h);
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w: 0.08, h,
fill: { color: accentColor }, line: { type: "none" }
});
}
function addCodeBlock(slide, x, y, w, h, lines) {
slide.addShape(pres.shapes.RECTANGLE, {
x, y, w, h, fill: { color: COL.codeBg }, line: { color: COL.border, width: 1 }
});
const text = (Array.isArray(lines) ? lines : [lines]).map((line, i, arr) => ({
text: line,
options: { breakLine: i < arr.length - 1 }
}));
slide.addText(text, {
x: x + 0.15, y: y + 0.1, w: w - 0.3, h: h - 0.2,
fontFace: F_CODE, fontSize: 11, color: COL.text, margin: 0,
valign: "top"
});
}
function addBullets(slide, x, y, w, h, items, opts = {}) {
const fontSize = opts.fontSize || 14;
const color = opts.color || COL.text;
const arr = items.map((it, i) => ({
text: it,
options: { bullet: { code: "25A0" }, breakLine: i < items.length - 1, color }
}));
slide.addText(arr, {
x, y, w, h, fontFace: F_BODY, fontSize, color, margin: 0,
paraSpaceAfter: 6
});
}
// ===================== SLIDE 1: COVER =====================
{
const s = pres.addSlide();
fillBg(s);
// Big band on left
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 0, w: 3.5, h: H, fill: { color: COL.greenDk }, line: { type: "none" }
});
s.addShape(pres.shapes.RECTANGLE, {
x: 3.45, y: 0, w: 0.05, h: H, fill: { color: COL.green }, line: { type: "none" }
});
// Brand
s.addText("ASTERION VR", {
x: 0.4, y: 0.5, w: 3, h: 0.4,
fontFace: F_BODY, fontSize: 13, bold: true,
color: COL.green, charSpacing: 8, margin: 0
});
// Big right title
s.addText("PROSERVE", {
x: 3.9, y: 1.3, w: 6, h: 1,
fontFace: F_TITLE, fontSize: 60, bold: true,
color: COL.text, margin: 0
});
s.addText("Launcher", {
x: 3.9, y: 2.1, w: 6, h: 0.7,
fontFace: F_TITLE, fontSize: 40, bold: true,
color: COL.muted, margin: 0
});
s.addText("Guide utilisateur", {
x: 3.9, y: 3.0, w: 6, h: 0.5,
fontFace: F_TITLE, fontSize: 22,
color: COL.green, margin: 0
});
s.addText("Installation, activation et utilisation", {
x: 3.9, y: 3.5, w: 6, h: 0.4,
fontFace: F_BODY, fontSize: 14, italic: true,
color: COL.muted, margin: 0
});
// Left band content
s.addText("v0.8", {
x: 0.4, y: 2, w: 3, h: 1,
fontFace: F_TITLE, fontSize: 56, bold: true,
color: COL.text, align: "center", margin: 0
});
s.addText("Mai 2026", {
x: 0.4, y: 2.95, w: 3, h: 0.3,
fontFace: F_BODY, fontSize: 13,
color: COL.muted, align: "center", margin: 0
});
// Bottom strip
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
});
s.addText("© 2026 ASTERION VR — Tous droits réservés", {
x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 2: WHAT IS IT =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Qu'est-ce que PROSERVE Launcher ?", "introduction");
addFooter(s, 2);
s.addText("Une application Windows qui te permet de :", {
x: 0.5, y: 1.4, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 16, color: COL.text, margin: 0
});
const features = [
["📦", "Installer", "Télécharge et déploie chaque nouvelle version de Proserve sur ta machine en quelques clics."],
["▶", "Lancer", "Démarre la version de ton choix. Plusieurs versions peuvent cohabiter sur le même PC."],
["🔄", "Mettre à jour", "Détecte automatiquement les nouvelles versions disponibles côté serveur ASTERION et propose la mise à jour."],
["🔑", "License", "Vérifie ta license en ligne (ou via cache local 7 jours en cas de coupure réseau)."],
];
let y = 2.0;
features.forEach(([icon, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.7);
s.addText(icon, {
x: 0.7, y: y + 0.13, w: 0.6, h: 0.45,
fontFace: F_TITLE, fontSize: 20, color: COL.green, align: "center", margin: 0
});
s.addText(title, {
x: 1.4, y: y + 0.07, w: 2, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.4, y: y + 0.34, w: W - 1.9, h: 0.35,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
y += 0.78;
});
}
// ===================== SLIDE 3: PREREQUISITES =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Pré-requis", "avant de commencer");
addFooter(s, 3);
const prereqs = [
["💻", "Windows 10 ou 11", "Édition 64-bit. Pas de version macOS / Linux pour l'instant."],
["🌐", "Connexion internet", "Requise pour activer la license et télécharger les versions. Le launcher fonctionne hors-ligne pendant 7 jours après la dernière validation."],
["💾", "Espace disque", "Prévoir ~14 Go par version installée + ~14 Go de cache temporaire pendant le téléchargement."],
["🔑", "Une clé license", "Format PRSRV-XXXX-XXXX-XXXX-XXXX, fournie par ASTERION VR."],
];
let y = 1.5;
prereqs.forEach(([icon, title, desc]) => {
addAccentCard(s, 0.5, y, W - 1, 0.85, COL.green);
s.addText(icon, {
x: 0.75, y: y + 0.18, w: 0.7, h: 0.5,
fontFace: F_TITLE, fontSize: 22, color: COL.green, align: "center", margin: 0
});
s.addText(title, {
x: 1.5, y: y + 0.12, w: 6, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.5, y: y + 0.42, w: W - 2, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
y += 0.95;
});
}
// ===================== SLIDE 4: INSTALLATION =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Installation", "première fois");
addFooter(s, 4);
const steps = [
["1", "Télécharge le setup", "Tu reçois un fichier nommé PSLauncher-Setup-X.Y.Z.exe (~80 Mo) — par email, lien WeTransfer, ou via le site ASTERION."],
["2", "Double-clique pour lancer", "Windows peut afficher un avertissement SmartScreen « Éditeur inconnu ».\nClique « Informations complémentaires » → « Exécuter quand même »."],
["3", "Suis l'assistant", "Choisis la langue, valide les chemins par défaut. Le launcher s'installe dans ton profil utilisateur — pas besoin de droits administrateur."],
["4", "Lance PROSERVE Launcher", "Un raccourci apparaît sur le bureau et dans le menu Démarrer."],
];
let y = 1.5;
steps.forEach(([num, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.85);
s.addShape(pres.shapes.OVAL, {
x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
fill: { color: COL.green }, line: { type: "none" }
});
s.addText(num, {
x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
fontFace: F_TITLE, fontSize: 16, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
s.addText(title, {
x: 1.4, y: y + 0.1, w: W - 2, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.4, y: y + 0.4, w: W - 2, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
y += 0.95;
});
}
// ===================== SLIDE 5: ACTIVATE LICENSE =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Activer ta license", "premier démarrage");
addFooter(s, 5);
s.addText("Au premier lancement, le badge en haut au centre de la fenêtre indique « 🔒 Aucune license » en rouge.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.45,
fontFace: F_BODY, fontSize: 13, color: COL.text, margin: 0
});
// Badge mockup
addAccentCard(s, 0.5, 1.95, 4.5, 1.6, COL.red);
s.addText("🔒 Aucune license", {
x: 0.7, y: 2.05, w: 4, h: 0.4,
fontFace: F_TITLE, fontSize: 16, bold: true, color: COL.red, margin: 0
});
s.addText("Visible dans la barre du haut au centre", {
x: 0.7, y: 2.4, w: 4, h: 0.3,
fontFace: F_BODY, fontSize: 11, italic: true, color: COL.muted, margin: 0
});
s.addText("👉 Clique dessus !", {
x: 0.7, y: 2.85, w: 4, h: 0.5,
fontFace: F_TITLE, fontSize: 16, bold: true, color: COL.green, margin: 0
});
// Then
addAccentCard(s, 5.2, 1.95, 4.3, 1.6, COL.green);
s.addText("Dialog d'activation", {
x: 5.4, y: 2.05, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
addBullets(s, 5.4, 2.4, 4, 1.1, [
"Colle ta clé PRSRV-…",
"Clique « Activer »",
"Le badge passe en vert ✓",
], { fontSize: 11 });
// Result
addCard(s, 0.5, 3.85, W - 1, 1.0);
s.addText("Une fois activée :", {
x: 0.7, y: 3.95, w: 4, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.green, margin: 0
});
s.addText("La barre indique le nom du client + la date de validité. Tu peux maintenant télécharger les versions de Proserve auxquelles ta license te donne accès.", {
x: 0.7, y: 4.25, w: W - 1.4, h: 0.6,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 6: LIBRARY OVERVIEW =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "La fenêtre principale", "ta bibliothèque");
addFooter(s, 6);
// Hero card simulated
addAccentCard(s, 0.5, 1.4, W - 1, 1.2, COL.green);
s.addText("VERSION COURANTE", {
x: 0.75, y: 1.5, w: 4, h: 0.3,
fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
});
s.addText("Proserve v1.4.7", {
x: 0.75, y: 1.78, w: 5, h: 0.5,
fontFace: F_TITLE, fontSize: 24, bold: true, color: COL.text, margin: 0
});
s.addText("● Installée • Sortie le 29/04/2026 • 14 Go", {
x: 0.75, y: 2.25, w: 5, h: 0.3,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
// Big launcher button mockup
s.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 7.4, y: 1.7, w: 1.8, h: 0.6,
fill: { color: COL.green }, line: { type: "none" }, rectRadius: 0.05
});
s.addText("▶ LANCER", {
x: 7.4, y: 1.7, w: 1.8, h: 0.6,
fontFace: F_TITLE, fontSize: 16, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
// Section header
s.addText("AUTRES VERSIONS", {
x: 0.5, y: 2.85, w: 3, h: 0.3,
fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
});
// Other rows
const rowY1 = 3.2, rowY2 = 4.0;
addAccentCard(s, 0.5, rowY1, W - 1, 0.7, COL.green);
s.addText("v1.4.6 ● Installée", {
x: 0.75, y: rowY1 + 0.1, w: 5, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
s.addText("14 Go • 15/04/2026", {
x: 0.75, y: rowY1 + 0.4, w: 5, h: 0.3,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
s.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 8.3, y: rowY1 + 0.18, w: 1.05, h: 0.4,
fill: { color: COL.green }, line: { type: "none" }, rectRadius: 0.05
});
s.addText("▶ Lancer", {
x: 8.3, y: rowY1 + 0.18, w: 1.05, h: 0.4,
fontFace: F_BODY, fontSize: 11, bold: true, color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
addAccentCard(s, 0.5, rowY2, W - 1, 0.7, COL.accent);
s.addText("v1.4.5 ○ Disponible", {
x: 0.75, y: rowY2 + 0.1, w: 5, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
s.addText("448 Mo • 15/04/2026", {
x: 0.75, y: rowY2 + 0.4, w: 5, h: 0.3,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
s.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 8.1, y: rowY2 + 0.18, w: 1.25, h: 0.4,
fill: { color: COL.accent }, line: { type: "none" }, rectRadius: 0.05
});
s.addText("⬇ Installer", {
x: 8.1, y: rowY2 + 0.18, w: 1.25, h: 0.4,
fontFace: F_BODY, fontSize: 11, bold: true, color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
}
// ===================== SLIDE 7: LAUNCH A VERSION =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Lancer une version", "▶ jouer");
addFooter(s, 7);
s.addText("Deux possibilités :", {
x: 0.5, y: 1.4, w: W - 1, h: 0.3,
fontFace: F_BODY, fontSize: 14, color: COL.text, margin: 0
});
addAccentCard(s, 0.5, 1.85, 4.5, 2.5, COL.green);
s.addText("LA PLUS RÉCENTE", {
x: 0.7, y: 1.95, w: 4, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true, color: COL.green, charSpacing: 3, margin: 0
});
s.addText("Le gros bouton vert dans la carte « Version courante » lance la version la plus récente que tu as installée.", {
x: 0.7, y: 2.3, w: 4, h: 1.2,
fontFace: F_BODY, fontSize: 12, color: COL.text, margin: 0
});
s.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 0.7, y: 3.55, w: 4, h: 0.6,
fill: { color: COL.green }, line: { type: "none" }, rectRadius: 0.05
});
s.addText("▶ LANCER", {
x: 0.7, y: 3.55, w: 4, h: 0.6,
fontFace: F_TITLE, fontSize: 16, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
addAccentCard(s, 5.2, 1.85, 4.3, 2.5, COL.green);
s.addText("UNE VERSION ANCIENNE", {
x: 5.4, y: 1.95, w: 4, h: 0.3,
fontFace: F_BODY, fontSize: 11, bold: true, color: COL.green, charSpacing: 3, margin: 0
});
s.addText("Dans la section « Autres versions », chaque ligne installée a son propre bouton ▶ Lancer pour démarrer cette version précisément.", {
x: 5.4, y: 2.3, w: 4, h: 1.5,
fontFace: F_BODY, fontSize: 12, color: COL.text, margin: 0
});
// Behavior
addCard(s, 0.5, 4.55, W - 1, 0.7);
s.addText("", {
x: 0.7, y: 4.65, w: 0.5, h: 0.5,
fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.accent, align: "center", margin: 0
});
s.addText("La fenêtre du launcher se réduit automatiquement dans la barre des tâches pour laisser la place à Proserve.", {
x: 1.2, y: 4.7, w: W - 1.7, h: 0.5,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 8: INSTALL NEW VERSION =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Installer une nouvelle version", "📦 télécharger");
addFooter(s, 8);
const steps = [
["1", "Vérifie les MAJ", "Bouton « 🔄 Vérifier les MAJ » en bas à gauche.\nLe launcher contacte le serveur et liste les nouvelles versions."],
["2", "Repère la ligne bleue", "Une ligne « ○ Disponible » apparaît avec un bouton bleu « ⬇ Installer X.Y.Z »."],
["3", "Clique « ⬇ Installer »", "Une fenêtre s'ouvre avec les notes de version (Markdown). Lis et clique « Télécharger » pour confirmer."],
["4", "Patience pendant le DL", "La barre de progression en bas montre la vitesse, l'avancement, l'ETA. Tu peux mettre en pause ou annuler."],
["5", "Installation auto", "Une fois le ZIP vérifié (SHA-256), il est extrait dans le dossier d'install à côté des autres versions."],
];
let y = 1.45;
steps.forEach(([num, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.7);
s.addShape(pres.shapes.OVAL, {
x: 0.7, y: y + 0.13, w: 0.4, h: 0.4,
fill: { color: COL.accent }, line: { type: "none" }
});
s.addText(num, {
x: 0.7, y: y + 0.13, w: 0.4, h: 0.4,
fontFace: F_TITLE, fontSize: 14, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
s.addText(title, {
x: 1.3, y: y + 0.07, w: 8, h: 0.27,
fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.3, y: y + 0.32, w: 8, h: 0.4,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.78;
});
}
// ===================== SLIDE 9: PROGRESS / RESUME =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Suivre un téléchargement", "barre du bas");
addFooter(s, 9);
// Mock footer
addCard(s, 0.5, 1.5, W - 1, 0.7);
s.addText("⬇ v1.4.7 : 4,2 / 14,0 Go • 38 Mo/s • ETA 4 min", {
x: 0.75, y: 1.6, w: 7.5, h: 0.3,
fontFace: F_CODE, fontSize: 12, color: COL.muted, margin: 0
});
// Progress bar
s.addShape(pres.shapes.RECTANGLE, {
x: 0.75, y: 1.95, w: 7.5, h: 0.1, fill: { color: "2A2A30" }, line: { type: "none" }
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0.75, y: 1.95, w: 2.25, h: 0.1, fill: { color: COL.accent }, line: { type: "none" }
});
// Cancel button
s.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 8.5, y: 1.62, w: 0.85, h: 0.4,
fill: { color: "3A3A40" }, line: { type: "none" }, rectRadius: 0.05
});
s.addText("Annuler", {
x: 8.5, y: 1.62, w: 0.85, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.text, align: "center", valign: "middle", margin: 0
});
// Features
s.addText("Ce que tu peux faire pendant le DL", {
x: 0.5, y: 2.5, w: W - 1, h: 0.3,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
});
addBullets(s, 0.5, 2.85, W - 1, 1.1, [
"Annuler à tout moment (le ZIP partiel est conservé pour reprendre plus tard)",
"Fermer le launcher : la prochaine ouverture proposera de reprendre",
"Couper le réseau : le launcher attend (retry exponentiel) puis reprend automatiquement",
], { fontSize: 12 });
// Resume callout
addAccentCard(s, 0.5, 4.15, W - 1, 0.95, COL.amber);
s.addText("↻ Reprendre", {
x: 0.75, y: 4.25, w: 2, h: 0.35,
fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.amber, margin: 0
});
s.addText("Un DL interrompu apparaît avec un bouton « ↻ Reprendre (X%) » à la place de « Installer ». Le téléchargement reprend là où il s'était arrêté — pas besoin de tout recommencer.", {
x: 0.75, y: 4.55, w: W - 1.5, h: 0.6,
fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
});
}
// ===================== SLIDE 10: VERSION MENU =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Menu d'une version", "bouton ⋯");
addFooter(s, 10);
s.addText("Le bouton « ⋯ » à droite de chaque ligne ouvre un menu contextuel.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 13, color: COL.muted, italic: true, margin: 0
});
// Menu mockup
addCard(s, 0.5, 1.95, 4.5, 2.4);
s.addText("📜 Voir les release notes", {
x: 0.75, y: 2.1, w: 4, h: 0.4,
fontFace: F_BODY, fontSize: 13, color: COL.text, margin: 0
});
s.addShape(pres.shapes.LINE, {
x: 0.75, y: 2.5, w: 4, h: 0,
line: { color: COL.border, width: 0.5 }
});
s.addText("📁 Ouvrir le dossier", {
x: 0.75, y: 2.6, w: 4, h: 0.4,
fontFace: F_BODY, fontSize: 13, color: COL.text, margin: 0
});
s.addShape(pres.shapes.LINE, {
x: 0.75, y: 3.0, w: 4, h: 0,
line: { color: COL.border, width: 0.5 }
});
s.addText("🗑 Supprimer cette version…", {
x: 0.75, y: 3.1, w: 4, h: 0.4,
fontFace: F_BODY, fontSize: 13, color: COL.text, margin: 0
});
// Description
addBullets(s, 5.3, 2.1, 4.2, 2.4, [
"Release notes : disponibles aussi sur les versions non installées",
"Ouvrir le dossier : utile pour vérifier l'installation",
"Supprimer : libère l'espace disque (≈ 14 Go) avec confirmation. Garde tes anciennes versions tant que tu veux pouvoir y revenir.",
], { fontSize: 11 });
// Tip
addAccentCard(s, 0.5, 4.55, W - 1, 0.65, COL.green);
s.addText("✓ Aucune suppression automatique : c'est toujours toi qui décides quand effacer une vieille version.", {
x: 0.75, y: 4.65, w: W - 1.5, h: 0.5,
fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
});
}
// ===================== SLIDE 11: SETTINGS =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Paramètres ⚙", "bouton en haut à droite");
addFooter(s, 11);
const sections = [
["🔑", "License", "Statut, propriétaire, date d'expiration. Identifiant machine anonyme (à communiquer à ASTERION en cas de souci de slot machine)."],
["🌐", "Serveur", "URL de l'API ASTERION. À ne changer que si tu y es invité par le support."],
["📁", "Installation", "Dossier où sont posées les versions de Proserve. Par défaut C:\\ASTERION_VR. Bouton « Parcourir » pour changer."],
["💾", "Cache", "Taille du cache de téléchargements. Bouton « Vider » pour libérer de l'espace si besoin."],
["📋", "Logs", "Dossier des logs (.log) — utile en cas de souci, à transmettre au support."],
];
let y = 1.4;
sections.forEach(([icon, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.7);
s.addText(icon, {
x: 0.7, y: y + 0.12, w: 0.5, h: 0.45,
fontFace: F_TITLE, fontSize: 18, color: COL.green, align: "center", margin: 0
});
s.addText(title, {
x: 1.25, y: y + 0.07, w: 2, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.25, y: y + 0.33, w: W - 1.7, h: 0.4,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.78;
});
}
// ===================== SLIDE 12: AUTO-UPDATE =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "Mises à jour du launcher", "🔄 automatiques");
addFooter(s, 12);
s.addText("Le launcher se met à jour tout seul quand ASTERION pousse une nouvelle version.", {
x: 0.5, y: 1.4, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 13, color: COL.text, margin: 0
});
const flow = [
["1", "Détection", "Au démarrage et à chaque clic « Vérifier les MAJ », le launcher contacte le serveur."],
["2", "Notification", "Si une nouvelle version du launcher existe, une popup t'informe et propose de mettre à jour."],
["3", "Téléchargement", "Quelques secondes (le launcher fait ~80 Mo)."],
["4", "Redémarrage automatique", "L'application se ferme, est remplacée, puis se relance avec la nouvelle version."],
];
let y = 1.95;
flow.forEach(([num, title, desc]) => {
addCard(s, 0.5, y, W - 1, 0.65);
s.addShape(pres.shapes.OVAL, {
x: 0.7, y: y + 0.12, w: 0.4, h: 0.4,
fill: { color: COL.green }, line: { type: "none" }
});
s.addText(num, {
x: 0.7, y: y + 0.12, w: 0.4, h: 0.4,
fontFace: F_TITLE, fontSize: 14, bold: true,
color: "FFFFFF", align: "center", valign: "middle", margin: 0
});
s.addText(title, {
x: 1.3, y: y + 0.07, w: 8, h: 0.27,
fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
});
s.addText(desc, {
x: 1.3, y: y + 0.32, w: 8, h: 0.32,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.73;
});
addAccentCard(s, 0.5, 4.95, W - 1, 0.5, COL.green);
s.addText("✓ Tu peux toujours refuser et reporter — clique « Plus tard ».", {
x: 0.75, y: 5.0, w: W - 1.5, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
});
}
// ===================== SLIDE 13: FAQ =====================
{
const s = pres.addSlide();
fillBg(s);
addHeader(s, "FAQ", "questions fréquentes");
addFooter(s, 13);
const qs = [
["❓", "Ma license est expirée. Que se passe-t-il ?",
"Tu peux toujours utiliser les versions déjà installées. Seul le téléchargement de nouvelles versions est bloqué jusqu'à renouvellement.",
COL.amber],
["❓", "Pas de connexion internet — le launcher marche-t-il ?",
"Oui, pendant 7 jours après la dernière validation en ligne. Au-delà, la license est considérée comme à revérifier.",
COL.accent],
["❓", "Comment changer de PC ?",
"Désactive ta license sur l'ancien (Settings → bouton Désactiver), puis active-la sur le nouveau avec la même clé.",
COL.green],
["❓", "Le téléchargement échoue avec « SHA-256 invalide » ?",
"Réseau bruité ou ZIP modifié côté serveur entre-temps. Le partial est jeté ; le DL recommence proprement au prochain clic.",
COL.red],
];
let y = 1.5;
qs.forEach(([icon, q, a, color]) => {
addAccentCard(s, 0.5, y, W - 1, 0.85, color);
s.addText(icon, {
x: 0.7, y: y + 0.18, w: 0.4, h: 0.45,
fontFace: F_TITLE, fontSize: 16, color, align: "center", margin: 0
});
s.addText(q, {
x: 1.2, y: y + 0.1, w: W - 1.7, h: 0.3,
fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
});
s.addText(a, {
x: 1.2, y: y + 0.4, w: W - 1.7, h: 0.45,
fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
});
y += 0.95;
});
}
// ===================== SLIDE 14: SUPPORT =====================
{
const s = pres.addSlide();
fillBg(s);
// Big band
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 1.3, w: W, h: 1.3, fill: { color: COL.greenDk }, line: { type: "none" }
});
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: 1.3, w: W, h: 0.05, fill: { color: COL.green }, line: { type: "none" }
});
s.addText("Besoin d'aide ?", {
x: 0.5, y: 1.55, w: W - 1, h: 0.6,
fontFace: F_TITLE, fontSize: 32, bold: true, color: COL.text, margin: 0
});
s.addText("Contacte ASTERION VR avec ces infos pour un support efficace :", {
x: 0.5, y: 2.1, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 14, italic: true, color: COL.muted, margin: 0
});
// Three cards
const ix = 0.5, iy = 3.05, iw = 3.0, gap = 0.15;
const refs = [
["🔑", "Ta clé license", "PRSRV-XXXX-XXXX-XXXX-XXXX", COL.green],
["💻", "Ton ID machine", "Settings → License\nbouton 📋 Copier", COL.accent],
["📋", "Tes logs", "Settings → Logs\nbouton 📁 Ouvrir", COL.amber],
];
refs.forEach(([icon, title, body, color], i) => {
const x = ix + i * (iw + gap);
addAccentCard(s, x, iy, iw, 1.7, color);
s.addText(icon, {
x: x + 0.2, y: iy + 0.15, w: iw - 0.4, h: 0.45,
fontFace: F_TITLE, fontSize: 22, color, margin: 0
});
s.addText(title, {
x: x + 0.2, y: iy + 0.65, w: iw - 0.4, h: 0.3,
fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
});
s.addText(body, {
x: x + 0.2, y: iy + 0.95, w: iw - 0.4, h: 0.7,
fontFace: F_CODE, fontSize: 10, color: COL.muted, margin: 0
});
});
// Footer brand
s.addShape(pres.shapes.RECTANGLE, {
x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
});
s.addText("© 2026 ASTERION VR — Tous droits réservés", {
x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
fontFace: F_BODY, fontSize: 11, color: COL.muted, align: "center", margin: 0
});
}
pres.writeFile({ fileName: "C:\\ASTERION\\GIT\\PS_Launcher\\docs\\PS_Launcher-Guide-Utilisateur.pptx" })
.then(f => console.log("Generated:", f));

View File

@@ -60,9 +60,11 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription
[Files] [Files]
Source: "..\src\PSLauncher.App\bin\Release\net8.0-windows10.0.17763.0\win-x64\publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\src\PSLauncher.App\bin\Release\net8.0-windows10.0.17763.0\win-x64\publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\src\PSLauncher.Updater\bin\Release\net8.0-windows\win-x64\publish\{#MyUpdaterExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "..\src\PSLauncher.Updater\bin\Release\net8.0-windows\win-x64\publish\{#MyUpdaterExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\docs\PS_Launcher-Guide-Utilisateur.pptx"; DestDir: "{app}\docs"; Flags: ignoreversion
[Icons] [Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\Guide utilisateur"; Filename: "{app}\docs\PS_Launcher-Guide-Utilisateur.pptx"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

View File

@@ -1,6 +1,7 @@
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@@ -24,6 +25,20 @@ namespace PSLauncher.App;
public partial class App : Application public partial class App : Application
{ {
private IHost? _host; private IHost? _host;
private static System.Threading.Mutex? _singleInstanceMutex;
private const string SingleInstanceMutexName = "Global\\PSLauncher-3F8E2C1A-9B47-4D5E-A0F8-2E9D1B6C7A3F";
private const string BringToFrontMessage = "PSLAUNCHER_BRING_TO_FRONT";
private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage(BringToFrontMessage);
[DllImport("user32.dll")]
private static extern int RegisterWindowMessage(string lpString);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
private const int HWND_BROADCAST = 0xFFFF;
public static string LogsDirectory { get; private set; } = string.Empty; public static string LogsDirectory { get; private set; } = string.Empty;
@@ -31,6 +46,17 @@ public partial class App : Application
{ {
base.OnStartup(e); base.OnStartup(e);
// Single-instance : si une autre instance tourne déjà, on lui demande de
// se mettre au premier plan via un broadcast Windows et on quitte tout de suite.
_singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true,
name: SingleInstanceMutexName, out var createdNew);
if (!createdNew)
{
PostMessage((IntPtr)HWND_BROADCAST, WM_PSLAUNCHER_BRING, IntPtr.Zero, IntPtr.Zero);
Shutdown();
return;
}
// Logs avant tout — pour ne rien perdre des erreurs au boot // Logs avant tout — pour ne rien perdre des erreurs au boot
LogsDirectory = Path.Combine( LogsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),

View File

@@ -55,8 +55,9 @@
</ItemGroup> </ItemGroup>
<!-- Après chaque `dotnet publish -c Release`, copie le single-file exe à la racine du repo <!-- Après chaque `dotnet publish -c Release`, copie le single-file exe à la racine du repo
(C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe) pour avoir un binaire prêt à tester (C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe). ContinueOnError pour ne pas casser le
sans aller chercher dans bin\Release\... --> publish si une instance du launcher tourne et verrouille la cible — l'opérateur peut
killer et réessayer, ou simplement utiliser le binaire depuis bin\Release\... -->
<Target Name="CopyPublishedToRepoRoot" AfterTargets="Publish" <Target Name="CopyPublishedToRepoRoot" AfterTargets="Publish"
Condition=" '$(Configuration)' == 'Release' "> Condition=" '$(Configuration)' == 'Release' ">
<PropertyGroup> <PropertyGroup>
@@ -65,7 +66,8 @@
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe" <Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
DestinationFolder="$(RepoRoot)" DestinationFolder="$(RepoRoot)"
SkipUnchangedFiles="false" SkipUnchangedFiles="false"
OverwriteReadOnlyFiles="true" /> OverwriteReadOnlyFiles="true"
ContinueOnError="WarnAndContinue" />
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" /> <Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
</Target> </Target>

View File

@@ -1,14 +1,58 @@
using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Interop;
namespace PSLauncher.App.Views; namespace PSLauncher.App.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage("PSLAUNCHER_BRING_TO_FRONT");
[DllImport("user32.dll")]
private static extern int RegisterWindowMessage(string lpString);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
private const int SW_RESTORE = 9;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
SourceInitialized += MainWindow_SourceInitialized;
}
/// <summary>
/// Hook le pump de messages Windows pour réagir au broadcast envoyé par une
/// seconde instance qui veut nous mettre au premier plan.
/// </summary>
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
var helper = new WindowInteropHelper(this);
var source = HwndSource.FromHwnd(helper.Handle);
source?.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_PSLAUNCHER_BRING)
{
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
else
ShowWindow(hwnd, SW_RESTORE);
Activate();
SetForegroundWindow(hwnd);
handled = true;
}
return IntPtr.Zero;
} }
/// <summary> /// <summary>

View File

@@ -32,7 +32,8 @@
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe" <Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
DestinationFolder="$(RepoRoot)" DestinationFolder="$(RepoRoot)"
SkipUnchangedFiles="false" SkipUnchangedFiles="false"
OverwriteReadOnlyFiles="true" /> OverwriteReadOnlyFiles="true"
ContinueOnError="WarnAndContinue" />
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" /> <Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
</Target> </Target>