; ============================================================ ; Lea.iss — Script Inno Setup pour l'installeur Lea ; ------------------------------------------------------------ ; Compile avec Inno Setup 6.2+ (ISCC.exe Lea.iss) ; ; Ce script produit Lea-Setup-v{VERSION}.exe dans ..\releases\ ; ; Fonctions principales : ; - Page de bienvenue + licence (CGU) ; - Page custom d'enrollment (nom, email, ID AIVANOV, URL, token) ; - Generation d'un machine_id unique par poste ; - Generation automatique de config.txt ; - Installation silencieuse de Python 3.12 embedded (optionnelle) ; - Raccourci demarrage automatique (checkbox) ; - Installation silencieuse : /VERYSILENT /CONFIG=path\to\config.txt ; - Desinstallation propre (kill process, cleanup, export logs) ; ; Pre-requis staging : ; Le dossier ..\build\installer_staging\ doit contenir : ; - Le package Lea complet (agent_v1/, lea_ui/, run_agent_v1.py, Lea.bat, ...) ; - Optionnel : python-3.12-embed\ (runtime Python embedded pre-configure) ; build_installer.sh s'occupe de preparer ce staging. ; ============================================================ #define MyAppName "Lea" #define MyAppVersion "1.0.1" #define MyAppPublisher "AIVANOV" #define MyAppURL "https://lea.labs.laurinebazin.design" #define MyAppExeName "Lea.bat" #define MyAppDescription "Lea - Assistante IA pour l'automatisation" ; Chemin du staging (peut etre surcharge via ISCC /DSourceDir=...) #ifndef SourceDir #define SourceDir "..\build\installer_staging" #endif ; Chemin de sortie des installeurs #ifndef OutputDir #define OutputDir "..\releases" #endif ; Activer le bundle Python embedded si present dans le staging #define PythonEmbedDir "python-3.12-embed" [Setup] AppId={{B3F9A1E2-5C4D-4E7F-9A1B-2C3D4E5F6789} AppName={#MyAppName} AppVersion={#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes OutputDir={#OutputDir} OutputBaseFilename=Lea-Setup-v{#MyAppVersion} ; Compression correcte (pas trop aggressive pour que l'install reste rapide) Compression=lzma2 SolidCompression=yes ; Support HiDPI WizardStyle=modern ; Langue FR par defaut ShowLanguageDialog=no ; Autorise l'install en mode user si pas admin (bascule sur LOCALAPPDATA) PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog ; Icone de l'installeur (decommenter si disponible) ; SetupIconFile=lea.ico ; Uninstall UninstallDisplayName={#MyAppName} {#MyAppVersion} ; UninstallDisplayIcon={app}\lea.ico ; decommenter quand l'icone sera fournie ; Architecture : 64-bit uniquement (Windows 10+ / 11) ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible ; Version minimale Windows : 10 MinVersion=10.0 ; Informations legales VersionInfoVersion={#MyAppVersion} VersionInfoCompany={#MyAppPublisher} VersionInfoDescription={#MyAppDescription} VersionInfoCopyright=Copyright (C) 2026 {#MyAppPublisher} ; Licence CGU affichee avant le choix du repertoire LicenseFile=LICENSE.txt [Languages] Name: "french"; MessagesFile: "compiler:Languages\French.isl" [Files] ; Package complet (code Python + .bat + requirements) ; Note : install.bat est EXCLU du staging (runtime 100% embedded, plus de venv/pip) ; Note : config.txt n'est PAS copie depuis le staging (il est genere par [Code]) Source: "{#SourceDir}\*"; \ DestDir: "{app}"; \ Flags: ignoreversion recursesubdirs createallsubdirs; \ Excludes: "{#PythonEmbedDir}\*,config.txt,*.log,sessions\*,__pycache__\*" ; Python 3.12 embedded (OBLIGATOIRE — runtime 100% autonome, aucune dependance Python systeme) Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \ DestDir: "{app}\python-embed"; \ Flags: ignoreversion recursesubdirs createallsubdirs ; Script de desinstallation custom (kill + export logs) Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion ; Script de configuration du runtime Python embedded (toujours installe) Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion ; Licence CGU (affichee dans la page licence ET conservee dans {app}) Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme ; Template de config pour installation silencieuse (reference) Source: "config_template.txt"; DestDir: "{app}"; Flags: ignoreversion [Components] ; Composant unique fixe : pas de choix utilisateur (runtime embedded toujours inclus). ; Inno masque la page Composants quand il n'y a aucun composant selectionnable. Name: "core"; Description: "Lea"; Types: full compact custom; Flags: fixed [Tasks] Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; GroupDescription: "Options :" Name: "desktopicon"; Description: "Creer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"; Flags: unchecked Name: "startmenuicon"; Description: "Creer un raccourci dans le menu Demarrer"; GroupDescription: "Raccourcis :" [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startmenuicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon ; Raccourci autostart (shell:startup) — cree si tache autostart selectionnee Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \ WorkingDir: "{app}"; Tasks: autostart [Run] ; Configuration du runtime embedded : reecrit Lea.bat pour pointer sur python-embed. ; TOUJOURS execute — runtime 100% autonome, aucune branche venv/pip/Python systeme. Filename: "{cmd}"; \ Parameters: "/c copy /y ""{app}\Lea.bat"" ""{app}\Lea.bat.bak"" && powershell -NoProfile -ExecutionPolicy Bypass -File ""{app}\configure_embed.ps1"""; \ WorkingDir: "{app}"; \ StatusMsg: "Configuration de Lea..."; \ Flags: runhidden waituntilterminated ; Lancer Lea a la fin de l'installation (optionnel) Filename: "{app}\{#MyAppExeName}"; \ Description: "Lancer {#MyAppName} maintenant"; \ Flags: postinstall skipifsilent nowait shellexec [UninstallRun] ; Tuer le process via PID du lock avant suppression des fichiers Filename: "powershell.exe"; \ Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\uninstall_lea.ps1"" -AppDir ""{app}"""; \ RunOnceId: "KillLeaProcess"; \ Flags: runhidden waituntilterminated [UninstallDelete] Type: filesandordirs; Name: "{app}\.venv" Type: filesandordirs; Name: "{app}\python-embed" Type: filesandordirs; Name: "{app}\__pycache__" Type: filesandordirs; Name: "{app}\agent_v1\__pycache__" Type: filesandordirs; Name: "{app}\agent_v1\sessions" Type: filesandordirs; Name: "{app}\agent_v1\logs" Type: files; Name: "{app}\lea_agent.lock" Type: files; Name: "{app}\config.txt" Type: files; Name: "{app}\config.txt.bak.*" Type: files; Name: "{app}\machine_id.txt" Type: files; Name: "{app}\Lea.bat.bak" Type: files; Name: "{app}\install.bat" ; Filet de securite : supprime tout residu genere au runtime (caches, *.pyc, logs) ; afin que le dossier applicatif soit entierement supprime (exigence desinstall propre). Type: filesandordirs; Name: "{app}" ; ============================================================ ; Code Pascal : pages custom + generation config.txt + helpers ; ============================================================ [Code] const SERVER_URL_DEFAULT = 'https://lea.labs.laurinebazin.design/api/v1'; SERVER_HOST_DEFAULT = 'lea.labs.laurinebazin.design'; DEFAULT_TOKEN = 'o3_LHqV_7_Gc6OVPHndhsBbvG6HJ5PCgl8yIBhGUIz8'; var EnrollmentPage: TInputQueryWizardPage; TokenPage: TInputQueryWizardPage; MachineIdValue: string; ConfigFilePath: string; ExistingMachineId: string; // -------------------------------------------------------------------- // Helper : ajoute des guillemets autour d'une chaine // -------------------------------------------------------------------- function AddQuotes(const S: string): string; begin Result := '"' + S + '"'; end; // -------------------------------------------------------------------- // Wrapper CreateGUIDString (via PowerShell, fallback par defaut) // -------------------------------------------------------------------- function CreateGUIDString(var Guid: string): Boolean; var ResultCode: Integer; TmpFile: string; Lines: TArrayOfString; begin Result := False; TmpFile := ExpandConstant('{tmp}\guid.txt'); // powershell : genere un GUID if Exec('powershell.exe', '-NoProfile -Command "[guid]::NewGuid().ToString() | Out-File -Encoding ASCII ' + AddQuotes(TmpFile) + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then begin if LoadStringsFromFile(TmpFile, Lines) and (GetArrayLength(Lines) > 0) then begin Guid := Trim(Lines[0]); Result := Length(Guid) > 0; end; DeleteFile(TmpFile); end; end; // -------------------------------------------------------------------- // Recupere le hostname de la machine // -------------------------------------------------------------------- function GetComputerNameString(): string; var Buffer: string; begin Buffer := ExpandConstant('{computername}'); if Length(Buffer) = 0 then Buffer := 'unknown-host'; Result := Buffer; end; // -------------------------------------------------------------------- // Genere un identifiant machine unique : UUID4 + hostname hashe // -------------------------------------------------------------------- function GenerateMachineId(): string; var Guid: string; Hostname: string; I: Integer; Hash: Cardinal; begin // Essaye d'utiliser le GUID genere par Windows (via PowerShell) Guid := ''; if CreateGUIDString(Guid) then begin StringChange(Guid, '{', ''); StringChange(Guid, '}', ''); StringChange(Guid, '-', ''); Result := LowerCase(Guid); end else Result := GetDateTimeString('yyyymmddhhnnss', #0, #0); // Ajoute un hash du hostname pour stabilite Hostname := GetComputerNameString(); Hash := 0; for I := 1 to Length(Hostname) do Hash := (Hash * 31 + Ord(Hostname[I])) and $FFFFFFFF; Result := Copy(Result, 1, 16) + '-' + Format('%08x', [Hash]); end; // -------------------------------------------------------------------- // Charge une configuration depuis /CONFIG=path (installation silencieuse) // Format du fichier : NOM=valeur, une ligne par parametre // Cles attendues : USER_NAME, USER_EMAIL, USER_ID, SERVER_URL, API_TOKEN // -------------------------------------------------------------------- procedure LoadConfigFromCommandLine(); forward; // -------------------------------------------------------------------- // UPGRADE — trouve le dossier d'une install Lea existante (config.txt present) // -------------------------------------------------------------------- function FindExistingInstallDir(): string; var Candidates: array[0..1] of string; I: Integer; begin Result := ''; Candidates[0] := ExpandConstant('{localappdata}\Programs\Lea'); Candidates[1] := ExpandConstant('{autopf}\Lea'); for I := 0 to 1 do begin if FileExists(Candidates[I] + '\config.txt') then begin Result := Candidates[I]; Exit; end; end; end; // -------------------------------------------------------------------- // UPGRADE — lit le config.txt existant : pre-remplit le wizard avec la // VRAIE conf du poste (serveur/token/user) et MEMORISE le machine_id pour // le PRESERVER (ne pas regenerer une nouvelle identite fleet). // -------------------------------------------------------------------- procedure LoadExistingConfig(); var Dir, ConfPath: string; Lines: TArrayOfString; I, EqPos: Integer; Line, Key, Value: string; begin ExistingMachineId := ''; Dir := FindExistingInstallDir(); if Dir = '' then Exit; // install neuve -> comportement par defaut ConfPath := Dir + '\config.txt'; if LoadStringsFromFile(ConfPath, Lines) then begin for I := 0 to GetArrayLength(Lines) - 1 do begin Line := Trim(Lines[I]); if (Length(Line) = 0) or (Line[1] = '#') then Continue; EqPos := Pos('=', Line); if EqPos = 0 then Continue; Key := Trim(Copy(Line, 1, EqPos - 1)); Value := Trim(Copy(Line, EqPos + 1, Length(Line))); if Key = 'RPA_SERVER_URL' then TokenPage.Values[0] := Value else if Key = 'RPA_API_TOKEN' then TokenPage.Values[1] := Value else if Key = 'RPA_USER_NAME' then EnrollmentPage.Values[0] := Value else if Key = 'RPA_USER_EMAIL' then EnrollmentPage.Values[1] := Value else if Key = 'RPA_USER_ID' then EnrollmentPage.Values[2] := Value else if Key = 'RPA_MACHINE_ID' then ExistingMachineId := Value; end; end; // Fallback : machine_id.txt si absent du config.txt if (ExistingMachineId = '') and FileExists(Dir + '\machine_id.txt') then begin if LoadStringsFromFile(Dir + '\machine_id.txt', Lines) and (GetArrayLength(Lines) > 0) then ExistingMachineId := Trim(Lines[0]); end; end; // -------------------------------------------------------------------- // Initialisation : cree les pages custom d'enrollment // -------------------------------------------------------------------- procedure InitializeWizard(); begin // Page 1 : informations collaborateur EnrollmentPage := CreateInputQueryPage(wpSelectTasks, 'Identification du collaborateur', 'Veuillez renseigner vos informations pour l''enrollment', 'Ces informations sont envoyees au serveur Lea pour identifier votre poste. ' + 'Elles sont stockees de maniere securisee et ne sont jamais partagees avec des tiers.'); EnrollmentPage.Add('Nom et prenom :', False); EnrollmentPage.Add('Email professionnel :', False); EnrollmentPage.Add('ID interne AIVANOV (optionnel) :', False); EnrollmentPage.Values[0] := ''; EnrollmentPage.Values[1] := ''; EnrollmentPage.Values[2] := ''; // Page 2 : configuration serveur (URL + token) TokenPage := CreateInputQueryPage(EnrollmentPage.ID, 'Connexion au serveur Lea', 'Configuration de la connexion au serveur central', 'L''URL du serveur est pre-remplie par defaut. Le token d''authentification ' + 'vous est fourni par votre administrateur AIVANOV. Laissez la valeur par defaut ' + 'si vous ne savez pas quoi mettre.'); TokenPage.Add('URL du serveur (avec /api/v1) :', False); TokenPage.Add('Token d''authentification :', False); TokenPage.Values[0] := SERVER_URL_DEFAULT; TokenPage.Values[1] := DEFAULT_TOKEN; // UPGRADE : si une install existe, pre-remplir avec SA config (pas les // defauts) et memoriser son machine_id pour le preserver. LoadExistingConfig(); // Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir (prioritaire) LoadConfigFromCommandLine(); end; // -------------------------------------------------------------------- // Implementation de LoadConfigFromCommandLine (declare en forward ci-dessus) // -------------------------------------------------------------------- procedure LoadConfigFromCommandLine(); var ConfigParam: string; Lines: TArrayOfString; I: Integer; Line, Key, Value: string; EqPos: Integer; begin ConfigParam := ExpandConstant('{param:CONFIG}'); if Length(ConfigParam) = 0 then Exit; if not FileExists(ConfigParam) then Exit; if not LoadStringsFromFile(ConfigParam, Lines) then Exit; for I := 0 to GetArrayLength(Lines) - 1 do begin Line := Trim(Lines[I]); if (Length(Line) = 0) or (Line[1] = '#') then Continue; EqPos := Pos('=', Line); if EqPos = 0 then Continue; Key := Trim(Copy(Line, 1, EqPos - 1)); Value := Trim(Copy(Line, EqPos + 1, Length(Line))); if Key = 'USER_NAME' then EnrollmentPage.Values[0] := Value else if Key = 'USER_EMAIL' then EnrollmentPage.Values[1] := Value else if Key = 'USER_ID' then EnrollmentPage.Values[2] := Value else if Key = 'SERVER_URL' then TokenPage.Values[0] := Value else if Key = 'API_TOKEN' then TokenPage.Values[1] := Value; end; end; // -------------------------------------------------------------------- // Validation des pages custom (Nom/Email obligatoires, token non vide) // -------------------------------------------------------------------- function NextButtonClick(CurPageID: Integer): Boolean; var Email: string; begin Result := True; if CurPageID = EnrollmentPage.ID then begin if Length(Trim(EnrollmentPage.Values[0])) = 0 then begin MsgBox('Le nom est obligatoire.', mbError, MB_OK); Result := False; Exit; end; Email := Trim(EnrollmentPage.Values[1]); if (Length(Email) = 0) or (Pos('@', Email) = 0) then begin MsgBox('Un email valide est obligatoire.', mbError, MB_OK); Result := False; Exit; end; end; if CurPageID = TokenPage.ID then begin if Length(Trim(TokenPage.Values[0])) = 0 then begin MsgBox('L''URL du serveur est obligatoire.', mbError, MB_OK); Result := False; Exit; end; if Length(Trim(TokenPage.Values[1])) < 16 then begin if MsgBox('Le token parait court (< 16 caracteres). Continuer quand meme ?', mbConfirmation, MB_YESNO) = IDNO then begin Result := False; Exit; end; end; end; end; // -------------------------------------------------------------------- // Ecrit config.txt genere dans le dossier d'installation // -------------------------------------------------------------------- procedure WriteGeneratedConfig(); var Config: string; ServerUrl, ServerHost, Token: string; UserName, UserEmail, UserId: string; SlashPos: Integer; begin ConfigFilePath := ExpandConstant('{app}\config.txt'); ServerUrl := Trim(TokenPage.Values[0]); Token := Trim(TokenPage.Values[1]); UserName := Trim(EnrollmentPage.Values[0]); UserEmail := Trim(EnrollmentPage.Values[1]); UserId := Trim(EnrollmentPage.Values[2]); // Derive ServerHost depuis ServerUrl : https://host/api/v1 -> host ServerHost := ServerUrl; StringChange(ServerHost, 'https://', ''); StringChange(ServerHost, 'http://', ''); SlashPos := Pos('/', ServerHost); if SlashPos > 0 then ServerHost := Copy(ServerHost, 1, SlashPos - 1); Config := '# ============================================================' + #13#10 + '# Configuration Lea (genere par l''installeur)' + #13#10 + '# ============================================================' + #13#10 + '# Genere le ' + GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':') + #13#10 + '# Installe par : ' + UserName + ' <' + UserEmail + '>' + #13#10 + '# ID interne : ' + UserId + #13#10 + '# Machine ID : ' + MachineIdValue + #13#10 + '# ============================================================' + #13#10 + '' + #13#10 + '# Adresse du serveur Lea (URL complete avec /api/v1)' + #13#10 + 'RPA_SERVER_URL=' + ServerUrl + #13#10 + '' + #13#10 + '# Cle d''authentification (fournie par l''administrateur)' + #13#10 + 'RPA_API_TOKEN=' + Token + #13#10 + '' + #13#10 + '# Nom du serveur (sans https://, sans /api/v1)' + #13#10 + 'RPA_SERVER_HOST=' + ServerHost + #13#10 + '' + #13#10 + '# Identifiant unique de cette machine (genere a l''install)' + #13#10 + 'RPA_MACHINE_ID=' + MachineIdValue + #13#10 + '' + #13#10 + '# Informations collaborateur (utilisees pour l''audit cote serveur)' + #13#10 + 'RPA_USER_NAME=' + UserName + #13#10 + 'RPA_USER_EMAIL=' + UserEmail + #13#10; if Length(UserId) > 0 then Config := Config + 'RPA_USER_ID=' + UserId + #13#10; Config := Config + '' + #13#10 + '# ============================================================' + #13#10 + '# Parametres avances (ne pas modifier sauf indication)' + #13#10 + '# ============================================================' + #13#10 + '' + #13#10 + '# Flouter les zones de texte dans les captures (securite donnees)' + #13#10 + 'RPA_BLUR_SENSITIVE=true' + #13#10 + '' + #13#10 + '# Duree de conservation des logs en jours (minimum 180 pour conformite)' + #13#10 + 'RPA_LOG_RETENTION_DAYS=180' + #13#10; if not SaveStringToFile(ConfigFilePath, Config, False) then MsgBox('Echec de l''ecriture de config.txt dans ' + ConfigFilePath, mbError, MB_OK); end; // -------------------------------------------------------------------- // Ecrit le machine_id.txt (identifiant du poste) // -------------------------------------------------------------------- procedure WriteMachineId(); var MachineIdFile: string; begin MachineIdFile := ExpandConstant('{app}\machine_id.txt'); if not SaveStringToFile(MachineIdFile, MachineIdValue, False) then MsgBox('Echec de l''ecriture de machine_id.txt', mbError, MB_OK); end; // -------------------------------------------------------------------- // Notifie le serveur de l'enrollment (best-effort, non bloquant) // POST vers {SERVER_URL}/agents/enroll avec les infos collaborateur // -------------------------------------------------------------------- procedure NotifyServerEnrollment(); var ResultCode: Integer; PsScript: string; PsFile: string; ServerUrl, Token: string; begin ServerUrl := Trim(TokenPage.Values[0]); Token := Trim(TokenPage.Values[1]); PsFile := ExpandConstant('{tmp}\enroll.ps1'); PsScript := '$ErrorActionPreference = ''SilentlyContinue''' + #13#10 + '$body = @{' + #13#10 + ' machine_id = ''' + MachineIdValue + '''' + #13#10 + ' hostname = $env:COMPUTERNAME' + #13#10 + ' user_name = ''' + EnrollmentPage.Values[0] + '''' + #13#10 + ' user_email = ''' + EnrollmentPage.Values[1] + '''' + #13#10 + ' user_id = ''' + EnrollmentPage.Values[2] + '''' + #13#10 + ' agent_version = ''' + '{#MyAppVersion}' + '''' + #13#10 + '} | ConvertTo-Json' + #13#10 + 'try {' + #13#10 + ' Invoke-RestMethod -Uri ''' + ServerUrl + '/agents/enroll'' ' + '-Method POST -Body $body -ContentType ''application/json'' ' + '-Headers @{ Authorization = ''Bearer ' + Token + ''' } -TimeoutSec 10 | Out-Null' + #13#10 + '} catch { exit 0 }' + #13#10; SaveStringToFile(PsFile, PsScript, False); Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(PsFile), '', SW_HIDE, ewWaitUntilTerminated, ResultCode); DeleteFile(PsFile); end; // -------------------------------------------------------------------- // UPGRADE — AVANT la copie des fichiers : tuer une Lea en cours (via le // PID du lock) pour liberer les DLL de python-embed. Evite une install // partielle / "reboot required". Ne tue QUE le PID du lock (jamais tous // les pythonw du poste). // -------------------------------------------------------------------- function PrepareToInstall(var NeedsRestart: Boolean): String; var AppDir, LockPath, BackupDir, SessionsDir: string; Lines: TArrayOfString; ResultCode: Integer; begin Result := ''; AppDir := ExpandConstant('{app}'); // 1) Tuer une Lea en cours (via le PID du lock) pour liberer les DLL // python-embed. Ne tue QUE ce PID, jamais tous les pythonw du poste. LockPath := AppDir + '\lea_agent.lock'; if FileExists(LockPath) then begin if LoadStringsFromFile(LockPath, Lines) and (GetArrayLength(Lines) > 0) then Exec('taskkill.exe', '/F /PID ' + Trim(Lines[0]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode); DeleteFile(LockPath); Sleep(1500); end; // UPGRADE uniquement (install existante detectee via config.txt). if FileExists(AppDir + '\config.txt') then begin // 2) BACKUP (rollback) : copie code+config vers _backup, HORS // python-embed / sessions / logs (leger, rapide). Filet si la nouvelle // version deconne : Julien restaure ce dossier. BackupDir := AppDir + '_backup'; Exec(ExpandConstant('{cmd}'), '/c rmdir /s /q "' + BackupDir + '" 2>nul & robocopy "' + AppDir + '" "' + BackupDir + '" /E /XD python-embed sessions logs __pycache__ /XF *.pyc /R:1 /W:1 /NFL /NDL /NJH /NJS /NP >nul 2>&1', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // 3) PURGE des captures accumulees (donnees d'apprentissage internes, non // exploitables cote clinique) : libere le disque. Le fix capture JPEG // evite que la saturation reprenne. Les logs (compliance 180j) restent. SessionsDir := AppDir + '\agent_v1\sessions'; if DirExists(SessionsDir) then Exec(ExpandConstant('{cmd}'), '/c rmdir /s /q "' + SessionsDir + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; end; // -------------------------------------------------------------------- // Hook : actions apres copie des fichiers (ssPostInstall) // -------------------------------------------------------------------- procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin // UPGRADE : preserver l'identite existante ; sinon en generer une neuve. if ExistingMachineId <> '' then MachineIdValue := ExistingMachineId else MachineIdValue := GenerateMachineId(); end; if CurStep = ssPostInstall then begin // Ecrit config.txt et machine_id.txt WriteGeneratedConfig(); WriteMachineId(); // Notifie le serveur (best-effort) NotifyServerEnrollment(); end; end; // -------------------------------------------------------------------- // Desinstallation : proposer d'exporter les logs avant suppression // -------------------------------------------------------------------- function InitializeUninstall(): Boolean; var LogDir, ExportDir: string; ResultCode: Integer; begin Result := True; LogDir := ExpandConstant('{app}\agent_v1\logs'); if DirExists(LogDir) then begin if MsgBox('Voulez-vous exporter les logs de Lea avant la desinstallation ?' + #13#10 + '(les logs seront copies dans votre dossier Documents)', mbConfirmation, MB_YESNO) = IDYES then begin ExportDir := ExpandConstant('{userdocs}\Lea_logs_export'); ForceDirectories(ExportDir); Exec('powershell.exe', '-NoProfile -Command "Copy-Item -Path ' + AddQuotes(LogDir + '\*') + ' -Destination ' + AddQuotes(ExportDir) + ' -Recurse -Force"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); MsgBox('Logs exportes dans : ' + ExportDir, mbInformation, MB_OK); end; end; end;