Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
502 lines (460 loc) · 22.8 KB
/
Copy pathmain.js
File metadata and controls
502 lines (460 loc) · 22.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
const { app, BrowserWindow, ipcMain, dialog, shell, session } = require('electron');
const path = require('path');
const fs = require('fs');
const fsPromises = fs.promises;
const { isAllowedPopup } = require('./lib/popup-guard');
const { Scheduler, registerSchedulerIPC } = require('./scheduler');
// On fixe le dossier de données (comptes + sessions IA) sur "ai-manager" quel que
// soit le nom du produit. Ainsi la version packagée (.exe) réutilise le profil
// existant au lieu d'en créer un vierge.
app.setPath('userData', path.join(app.getPath('appData'), 'ai-manager'));
// Nom applicatif stable : sous Linux, Electron s'en sert pour le WM_CLASS de la
// fenêtre. Le fichier .desktop déclare `StartupWMClass=IAO`, ce qui
// permet à GNOME/Ubuntu de rattacher la fenêtre ouverte à l'icône du Dock au
// lieu d'afficher une entrée « inconnue » séparée. (Sans effet sur Windows, et
// sans effet sur le dossier de données : userData est fixé juste au-dessus.)
app.setName('IAO');
// ===== Journal de démarrage et de diagnostic (issue #52) =====
// Avant : une erreur du preload ou du renderer ne laissait AUCUNE trace dans le
// terminal (seuls les messages de Chromium y défilaient) et rien du tout quand
// IAO était lancé depuis l'icône du Dock. Désormais chaque événement utile est
// écrit dans le terminal ET dans <userData>/logs/iao.log (tourné à 1 Mo).
// `--iao-debug` (ou IAO_DEBUG=1) recopie en plus TOUS les messages console de la
// fenêtre principale, pas seulement warnings et erreurs.
const diag = require('./lib/startup-diagnostics');
const DEBUG = diag.isDebugEnabled(process.argv, process.env);
const debugTrace = require('./lib/debug-trace');
const LOG_DIR = path.join(app.getPath('userData'), 'logs');
const LOG_FILE = path.join(LOG_DIR, 'iao.log');
const LOG_MAX_BYTES = 1024 * 1024;
let logFileReady = false;
function prepareLogFile() {
try {
fs.mkdirSync(LOG_DIR, { recursive: true });
try {
if (fs.statSync(LOG_FILE).size > LOG_MAX_BYTES) fs.renameSync(LOG_FILE, LOG_FILE + '.1');
} catch (_) { /* pas encore de journal */ }
logFileReady = true;
} catch (e) {
console.error('[demarrage] journal indisponible (' + LOG_FILE + ') :', e.message);
}
}
function log(scope, level, message, sourceId, line) {
const text = diag.formatLogLine(new Date(), scope, level, message, sourceId, line);
const name = diag.consoleLevelName(level);
if (name === 'error') console.error(text);
else if (name === 'warning') console.warn(text);
else console.log(text);
if (logFileReady) {
try { fs.appendFileSync(LOG_FILE, text + '\n', 'utf-8'); } catch (_) { /* disque plein : terminal seul */ }
}
}
// Issue #162 : trace réservée au mode debug (--iao-debug ou IAO_DEBUG=1).
function debug(scope, message) {
if (DEBUG) log(scope, 'debug', message);
}
// Issue #162 : en mode debug, chaque appel IPC (main.js et ordonnanceur) est
// journalisé — canal, durée, succès ou erreur, jamais les arguments. Hors
// mode debug, `ipc` est ipcMain lui-même.
const ipc = debugTrace.traceIpcMain(ipcMain, { enabled: DEBUG, log });
// ===== Instance unique (issue #52) =====
// Deux IAO lancés en même temps se disputent le même dossier de données
// (localStorage, cookies des ~70 partitions, service workers). Chromium
// n'obtient alors pas les verrous de ses bases et inonde le terminal de
// « Failed to delete the database: Database IO error », avec une interface
// qui peut sembler vide. La 2e instance passe donc la main à la 1re (mise au
// premier plan) et s'arrête immédiatement.
const gotSingleInstanceLock = app.requestSingleInstanceLock();
if (!gotSingleInstanceLock) {
console.warn('[demarrage] IAO est déjà lancé : la fenêtre existante est mise au premier plan, cette instance s\'arrête.');
app.quit();
} else {
prepareLogFile();
app.on('second-instance', () => {
log('demarrage', 'info', 'Seconde instance demandée : mise au premier plan de la fenêtre existante.');
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
// ===== Durcissement navigation / popups (chantier B) =====
// La liste blanche ALLOWED_POPUP_HOSTS et isAllowedPopup() vivent désormais
// dans lib/popup-guard.js (chantier F) : extraction pure, testable via
// `node --test` sans lancer Electron. Comportement inchangé.
// N'expose que le host dans les logs : une URL d'auth complète peut contenir des
// tokens OAuth en query string qu'on n'a pas à écrire dans la console.
function hostOf(urlStr) {
try { return new URL(urlStr).host; } catch (_) { return '(url illisible)'; }
}
// Taille max d'un fichier ouvrable dans l'éditeur intégré. Au-delà, lire le
// fichier entier en mémoire + le transférer par IPC figerait main ET renderer.
const MAX_EDITABLE_FILE_BYTES = 20 * 1024 * 1024; // 20 Mo
// ===== Nettoyage du stockage Service Worker (issue #165) =====
// Chromium peut laisser des bases de données de service workers dans un état
// incohérent (arrêt brutal, verrous non relâchés). Au démarrage suivant,
// le service de stockage tente de les supprimer, échoue en boucle et inonde
// le terminal de « Failed to delete the database: Database IO error ».
// On supprime le dossier avant que Chromium ne réouvre ses bases : il sera
// recréé proprement. Les service workers des sites IA se réenregistreront
// automatiquement à la prochaine visite.
function cleanupServiceWorkerStorage() {
try {
const swDir = path.join(app.getPath('userData'), 'Service Worker');
if (fs.existsSync(swDir)) {
fs.rmSync(swDir, { recursive: true, force: true });
log('demarrage', 'info', 'Dossier Service Worker nettoyé au démarrage (prévention Database IO error).');
}
} catch (e) {
log('demarrage', 'warning', 'Nettoyage Service Worker impossible : ' + e.message);
}
}
// ===== Ordonnanceur IA (chantier E) =====
// Instance unique, créée ici (module.getPath('userData') est déjà fixé plus
// haut) mais initialisée (watchers de téléchargement, journal de démarrage)
// seulement dans app.whenReady() : les sessions Electron par partition ne
// sont utilisables qu'une fois l'app prête. `mainWindow` sert d'ancrage aux
// boîtes de dialogue « Choisir un dossier » du panneau Ordonnanceur.
const scheduler = new Scheduler(app, session);
let mainWindow = null;
function createWindow() {
const win = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1000,
minHeight: 600,
backgroundColor: '#0a0c10',
title: 'IAO',
// Icône de fenêtre : utilisée par les environnements Linux (barre des tâches,
// Alt+Tab) quand l'app tourne depuis les sources. Dans un build packagé,
// l'icône est déjà embarquée par le packager — ce chemin reste valide.
icon: path.join(__dirname, 'build', 'icon.png'),
webPreferences: {
webviewTag: true,
// Issue #4 : contextIsolation: true + preload = le renderer n'a plus
// accès direct à Node (require, ipcRenderer, fs…). Seules les APIs
// exposées par preload.js (window.iaoAPI) sont disponibles.
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
// Issue #149 : la barre de menu native (File, Edit, View, Window) disparaît de
// l'écran. Le menu reste l'application menu d'Electron : ses raccourcis
// (Ctrl+Maj+I, Ctrl+R, zoom, copier/coller) fonctionnent toujours. Avec
// autoHideMenuBar à false (défaut), la touche Alt ne la fait pas réapparaître,
// ce qui compte ici : Alt+1..9 sert à changer d'onglet.
win.setMenuBarVisibility(false);
// --- 1. La fenêtre HÔTE est privilégiée (nodeIntegration:true). Elle ne doit
// JAMAIS naviguer ailleurs que vers son index.html local : sinon un contenu
// distant hériterait de l'accès complet à Node/OS. On bloque toute navigation
// dont l'URL n'est pas un fichier local (le rechargement dev reste un file://).
win.webContents.on('will-navigate', (event, url) => {
if (!url.startsWith('file://')) {
event.preventDefault();
console.warn('[security] navigation de la fenêtre hôte bloquée :', hostOf(url));
}
});
// --- 2. L'hôte ne doit ouvrir AUCUNE nouvelle fenêtre lui-même.
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
// --- 3. Les <webview> sont des invités sandboxés (sans nodeIntegration). On
// filtre leurs popups : seules les origines connues (services + auth OAuth)
// peuvent ouvrir une fenêtre ; tout le reste est refusé. Une popup autorisée
// s'ouvre avec les webPreferences de l'invité (donc SANS privilèges Node),
// jamais avec ceux de l'hôte.
win.webContents.on('did-attach-webview', (event, guestContents) => {
guestContents.on('render-process-gone', (_e, details) => {
log('webview', 'warning', 'onglet ' + hostOf(guestContents.getURL()) + ' arrêté : ' + details.reason);
});
// Issue #162 : en mode debug, navigations, échecs de chargement et
// avertissements/erreurs console de chaque onglet IA (hôte seulement).
debug('webview', 'onglet attaché : ' + hostOf(guestContents.getURL()));
guestContents.on('did-navigate', (_e, url) => debug('webview', 'navigation → ' + hostOf(url)));
guestContents.on('did-fail-load', (_e, code, description, url) => {
debug('webview', 'échec de chargement ' + hostOf(url) + ' : ' + description + ' (' + code + ')');
});
guestContents.on('console-message', (ev) => {
if (DEBUG && debugTrace.shouldForwardGuestConsole(ev.level)) {
log('webview ' + hostOf(guestContents.getURL()), ev.level, ev.message, ev.sourceId, ev.lineNumber);
}
});
guestContents.setWindowOpenHandler(({ url }) => {
if (isAllowedPopup(url)) return { action: 'allow' };
// Origine hors liste : jamais de fenêtre Electron. Si c'est un lien web
// classique, on le confie au navigateur système (aucun privilège Node) ;
// sinon (about:, data:, javascript:…) on refuse purement.
if (/^https?:\/\//i.test(url)) {
shell.openExternal(url);
console.warn('[security] popup hors liste -> navigateur système :', hostOf(url));
} else {
console.warn('[security] popup refusée (schéma non web) :', hostOf(url));
}
return { action: 'deny' };
});
// Ordonnanceur (lot 18/09/2026) : enregistre le webContents de chaque
// <webview> attachée pour que le scheduler puisse interagir avec Claude.ai
// via executeJavaScript et CDP (points 7-8, 10-13).
// Correctif du lot « test Electron réel du 18/09/2026 » : sur Electron 43,
// guestContents.getWebPreferences() renvoie undefined pour un guest —
// l'ancienne lecture directe de la partition échouait donc en silence et
// AUCUNE webview n'était jamais enregistrée (automatisation Claude
// inopérante). Le profil est désormais résolu par comparaison de session
// (scheduler.resolveProfileFromSession, testée en réel par le harnais
// test-electron/popups-continue.js — test B4).
try {
const profile = scheduler.resolveProfileFromSession(guestContents.session);
if (profile) {
scheduler.registerWebview(profile, guestContents);
guestContents.on('destroyed', () => {
try { scheduler.unregisterWebview(profile); } catch (_) {}
});
}
} catch (e) {
// Session non identifiable (webview sans partition connue) -> non enregistrée.
}
});
// --- 4. Diagnostic (issue #52) : tout échec du preload, du chargement ou
// du renderer de la fenêtre principale est journalisé (terminal + fichier).
const wc = win.webContents;
wc.on('preload-error', (_event, preloadPath, error) => {
log('preload', 'error', 'échec de ' + path.basename(preloadPath) + ' : ' + diag.errorText(error) +
' — window.iaoAPI sera absent, l\'interface ne peut pas afficher les comptes.');
});
wc.on('did-fail-load', (_event, code, description, url, isMainFrame) => {
if (isMainFrame) log('renderer', 'error', 'chargement impossible de ' + url + ' : ' + description + ' (' + code + ')');
});
wc.on('render-process-gone', (_event, details) => {
log('renderer', 'error', 'processus de rendu arrêté : ' + details.reason + ' (code ' + details.exitCode + ')');
});
wc.on('unresponsive', () => log('renderer', 'warning', 'la fenêtre ne répond plus'));
wc.on('console-message', (event) => {
// Electron >= 35 : un seul objet `event` (level, message, lineNumber,
// sourceId). Déclarer des arguments positionnels déclenche un avertissement
// de dépréciation : on ne lit que l'objet.
if (diag.shouldForwardConsole(event.level, DEBUG)) {
log('renderer', event.level, event.message, event.sourceId, event.lineNumber);
}
});
wc.on('did-finish-load', () => log('demarrage', 'info', 'interface chargée (' + path.basename(wc.getURL()) + ').'));
win.loadFile('index.html');
mainWindow = win;
win.on('closed', () => { if (mainWindow === win) mainWindow = null; });
}
// Issue #149 : les outils de développement passent du menu natif (masqué) à
// un bouton de la barre du workspace. Seule la fenêtre principale peut les
// ouvrir, et seulement pour elle-même : une <webview> invitée n'a pas accès
// à ce canal (preload non exposé), mais on vérifie l'émetteur par principe.
// Issue #162 : le renderer adapte ses traces (dbg) et affiche un badge.
ipc.handle('app:is-debug', () => DEBUG);
ipc.handle('app:toggle-devtools', (event) => {
if (!mainWindow || event.sender !== mainWindow.webContents) return false;
event.sender.toggleDevTools();
return true;
});
// --- IPC pour la gestion des fichiers locaux ---
ipc.handle('select-folder', async () => {
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
if (!result.canceled && result.filePaths.length > 0) return result.filePaths[0];
return null;
});
ipc.handle('read-directory', async (event, folderPath) => {
try {
const files = await fsPromises.readdir(folderPath, { withFileTypes: true });
const ignored = ['node_modules', '.git', '.next', 'dist', 'build', '.cache'];
return files
.filter(dirent => dirent.isFile() && !ignored.includes(dirent.name))
.map(dirent => ({ name: dirent.name, path: path.join(folderPath, dirent.name) }));
} catch (error) { return []; }
});
// Lot 18/09/2026 : lecture récursive (sous-dossiers) pour l'explorateur de
// fichiers. Renvoie une liste PLATE de fichiers avec chemin relatif au dossier
// racine, prête à être filtrée par filterFiles() côté renderer. Garde-fous :
// - dossiers ignorés (node_modules, .git, .next, dist, build, .cache) ;
// - pas de suivi des liens symboliques (évite les boucles) ;
// - profondeur maximale 5 (évite de scanner tout un disque) ;
// - nombre maximum de fichiers 500 (garde anti-saturation).
const MAX_RECURSION_DEPTH = 5;
const MAX_RECURSIVE_FILES = 500;
const IGNORED_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache']);
ipc.handle('read-directory-recursive', async (event, folderPath) => {
const result = [];
async function walk(dir, prefix, depth) {
if (depth > MAX_RECURSION_DEPTH) return;
// Pas de garde « result.length >= MAX » ici : la boucle ci-dessous la
// teste avant chaque entrée, donc avant chaque appel récursif (branche
// impossible à provoquer, retirée pour la couverture, #55).
let entries;
try { entries = await fsPromises.readdir(dir, { withFileTypes: true }); }
catch (_) { return; }
for (const dirent of entries) {
if (result.length >= MAX_RECURSIVE_FILES) return;
const name = dirent.name;
if (dirent.isFile()) {
const fullPath = path.join(dir, name);
const rel = prefix ? prefix + '/' + name : name;
result.push({ name, path: fullPath, relativePath: rel });
} else if (dirent.isDirectory() && !IGNORED_DIRS.has(name) && !dirent.isSymbolicLink()) {
await walk(path.join(dir, name), prefix ? prefix + '/' + name : name, depth + 1);
}
}
}
try { await walk(folderPath, '', 0); }
catch (error) { /* retourne ce qu'on a déjà */ }
return result;
});
ipc.handle('read-file', async (event, filePath) => {
try {
// Garde-fou taille : un gros fichier lu en entier figerait l'app. On renvoie
// un objet-sentinel que le renderer sait distinguer d'un contenu texte.
const stat = await fsPromises.stat(filePath);
if (stat.size > MAX_EDITABLE_FILE_BYTES) {
return { error: 'too_large', size: stat.size };
}
return await fsPromises.readFile(filePath, 'utf-8');
} catch (error) { return null; }
});
// NOUVEAU : Sauvegarder un fichier depuis l'éditeur
ipc.handle('save-file', async (event, filePath, content) => {
try {
await fsPromises.writeFile(filePath, content, 'utf-8');
return true;
} catch (error) {
console.error('Erreur sauvegarde:', error);
return false;
}
});
// --- IPC pour l'import/export JSON des comptes ---
// La sérialisation (exportAccountsJSON()/importAccountsJSON()) reste côté
// renderer (index.html, chantier C) : main.js ne fait ici que ce que le
// renderer ne peut pas faire lui-même (choix de fichier + lecture/écriture
// disque), sur le même modèle que 'read-file'/'save-file' plus haut.
ipc.handle('accounts:export', async (event, jsonContent) => {
const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Exporter les comptes',
defaultPath: `iao-comptes-${stamp}.json`,
filters: [{ name: 'JSON', extensions: ['json'] }]
});
if (result.canceled || !result.filePath) return { canceled: true };
try {
await fsPromises.writeFile(result.filePath, jsonContent, 'utf-8');
return { canceled: false, filePath: result.filePath };
} catch (error) {
console.error('Erreur export comptes:', error);
return { canceled: false, error: 'write_failed' };
}
});
ipc.handle('accounts:import', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
title: 'Importer des comptes',
properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }]
});
if (result.canceled || result.filePaths.length === 0) return { canceled: true };
try {
const content = await fsPromises.readFile(result.filePaths[0], 'utf-8');
return { canceled: false, filePath: result.filePaths[0], content };
} catch (error) {
console.error('Erreur import comptes:', error);
return { canceled: false, error: 'read_failed' };
}
});
// --- IPC pour la déconnexion d'un profil (lot « Actions sur un onglet
// ouvert », 17/09/2026) ---
// Purge cookies + stockage web de la partition Electron du compte (destructif,
// confirmé côté renderer avant l'appel — voir index.html, #disconnectModal).
// `session` n'existe que côté process principal, d'où l'IPC (comme pour les 4
// handlers fichiers ci-dessus). Un profil = un seul compte (invariant 4,
// CLAUDE.md), donc ceci ne peut jamais affecter qu'un compte à la fois.
ipc.handle('accounts:disconnect-profile', async (event, profile) => {
if (!profile || typeof profile !== 'string') return { ok: false, error: 'invalid_profile' };
try {
const ses = session.fromPartition(`persist:${profile}`);
await ses.clearStorageData();
return { ok: true };
} catch (error) {
console.error('Erreur déconnexion profil:', error);
return { ok: false, error: 'clear_failed' };
}
});
// --- IPC pour les réglages utilisateur (lot 18/09/2026, FEATURES.md P2) ---
// Persistance dans <userData>/settings.json. La normalisation/validation est
// faite côté renderer via lib/settings.js (fonctions pures testées).
const { normalizeSettings, DEFAULT_SETTINGS } = require('./lib/settings');
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
ipc.handle('settings:load', async () => {
try {
const raw = await fsPromises.readFile(settingsPath, 'utf-8');
return normalizeSettings(JSON.parse(raw));
} catch (_) {
// Fichier inexistant ou illisible -> renvoyer les valeurs par défaut
return DEFAULT_SETTINGS;
}
});
ipc.handle('settings:save', async (event, rawSettings) => {
try {
const normalized = normalizeSettings(rawSettings);
await fsPromises.writeFile(settingsPath, JSON.stringify(normalized, null, 2), 'utf-8');
return { ok: true, settings: normalized };
} catch (error) {
console.error('Erreur sauvegarde réglages:', error);
return { ok: false, error: error.message };
}
});
// --- Internationalisation (issue #51) ---
// Catalogue gettext compilé (lang/<locale>/LC_MESSAGES/iao.mo) de la langue
// demandée (--iao-lang=xx, IAO_LANG, réglage « Langue », langue du système) ;
// français (langue source) si aucune n'est livrée.
// Lu à la demande du renderer, qui traduit l'interface statique.
const i18n = require('./lib/i18n');
// Lot 3 : réglage « Langue » (settings.json, 'auto' par défaut) puis langues
// du système ; --iao-lang / IAO_LANG gardent la priorité.
function systemLocales() {
if (typeof app.getPreferredSystemLanguages === 'function') {
const list = app.getPreferredSystemLanguages();
if (Array.isArray(list) && list.length) return list;
}
return typeof app.getLocale === 'function' ? [app.getLocale()] : [];
}
function languageSetting() {
try {
return normalizeSettings(JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))).language;
} catch (_) {
return DEFAULT_SETTINGS.language; // pas de settings.json : 'auto'
}
}
ipc.handle('i18n:catalog', () => {
const setting = languageSetting();
const catalog = i18n.loadCatalog({
langDir: path.join(__dirname, 'lang'),
preferred: i18n.preferredLocales({ argv: process.argv, env: process.env, setting, system: systemLocales() }),
fs,
join: path.join
});
catalog.setting = setting;
if (catalog.error) log('i18n', 'warning', 'Catalogue de traduction illisible, repli en français : ' + catalog.error);
else if (catalog.locale !== i18n.SOURCE_LOCALE) log('i18n', 'info', 'Langue de l\'interface : ' + catalog.locale);
return catalog;
});
// --- IPC pour l'ordonnanceur IA (chantier E) ---
registerSchedulerIPC(ipc, scheduler, () => mainWindow);
app.whenReady().then(() => {
// 2e instance (voir « Instance unique ») : ne rien ouvrir, app.quit() est en cours.
if (!gotSingleInstanceLock) return;
cleanupServiceWorkerStorage();
log('demarrage', 'info', 'IAO ' + app.getVersion() + ' — Electron ' + process.versions.electron +
', ' + process.platform + '/' + process.arch + (app.isPackaged ? ', packagé' : ', sources') +
(DEBUG ? ', mode debug' : '') + '. Données : ' + app.getPath('userData') + ' — journal : ' + LOG_FILE);
debug('demarrage', debugTrace.describeRuntime(process));
// Issue #162 : le journal d'activité de l'ordonnanceur rejoint iao.log, et
// ses traces détaillées sont actives en mode debug.
scheduler.setLogger(log, DEBUG);
createWindow();
scheduler.init();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});