Files
PS_Launcher/docs/_build_internal.js
j.foucher 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

982 lines
36 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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));